Refatoracao de qualidade de codigo

- Mover logica de routes.py para RankingMapper na camada de aplicacao
- Consolidar funcoes mesclar_periodos e anos_completos_periodos em periodo.py
- Extrair RankingCache para modulo separado em infrastructure/cache
- Substituir todos os print() por logging adequado
- Corrigir exception handlers genericos para tipos especificos
- Remover classe Atuacao e atributo atuacoes_raw nao utilizados
- Documentar status dos scripts utilitarios
This commit is contained in:
Frederico Castro
2025-12-14 21:47:00 -03:00
parent 4a98e8b38c
commit f91651056a
15 changed files with 284 additions and 218 deletions

View File

@@ -0,0 +1,46 @@
# Status dos Scripts - Backend
## Resumo
Scripts utilitários para operações manuais e debug do sistema de ranking.
## Scripts e Status
### auditar_ranking.py
**Status:** PARCIALMENTE INCOMPATIVEL
**Problema:** Usa propriedades antigas (`componente_a`, `componente_b`, `componente_c`, `componente_d`) que agora são `bloco_a`, `bloco_c`, `bloco_d`.
**Acao:** Atualizar referencias de propriedades.
### popular_componente_b.py
**Status:** OK
**Descricao:** Script standalone para popular COMPONENTE_B manualmente. Duplica logica do `PopularComponenteBJob` mas e util para execucao independente.
**Nota:** Usa variaveis de ambiente do `.env`.
### top10_ranking.py
**Status:** INCOMPATIVEL
**Problemas:**
- Credenciais hardcoded
- Usa estrutura antiga de `Consultoria` (total_eventos, eventos_recentes, primeiro_evento, ultimo_evento)
- Usa estrutura antiga de `Premiacao` (campo pontos)
- `CoordenacaoCapes` sem campo `codigo`
- Referencias a `componente_a/b/c/d` em vez de `bloco_a/c/d`
- Referencias a `coordenacoes_programas` que nao existe mais
**Acao:** Reescrever usando `ConsultorRepositoryImpl` do sistema.
### buscar_consultores_especificos.py
**Status:** INCOMPATIVEL
**Problemas:** Mesmos problemas do top10_ranking.py.
**Acao:** Reescrever usando infraestrutura do sistema.
### analise_detalhada.py
**Status:** FUNCIONAL COM RESSALVAS
**Descricao:** Script de analise que apenas le dados brutos do Elasticsearch.
**Problema:** Credenciais hardcoded.
**Acao:** Mover credenciais para .env.
## Recomendacoes
1. Scripts que precisam usar entidades do sistema devem usar os repositorios existentes em vez de reimplementar extracoes
2. Credenciais devem sempre vir de variaveis de ambiente
3. Considerar consolidar scripts repetitivos em comandos CLI do sistema

View File

@@ -1,9 +1,12 @@
import logging
from datetime import datetime from datetime import datetime
from itertools import islice from itertools import islice
from typing import Dict, List, Optional from typing import Dict, List, Optional
from ...infrastructure.oracle.client import OracleClient from ...infrastructure.oracle.client import OracleClient
logger = logging.getLogger(__name__)
class PopularComponenteBJob: class PopularComponenteBJob:
""" """
@@ -89,15 +92,15 @@ class PopularComponenteBJob:
Este método é síncrono; use asyncio.to_thread quando chamá-lo em corrotina. Este método é síncrono; use asyncio.to_thread quando chamá-lo em corrotina.
""" """
if not self.oracle_local.is_connected: if not self.oracle_local.is_connected:
print("PopularComponenteB: Oracle LOCAL não conectado, abortando.") logger.warning("PopularComponenteB: Oracle LOCAL não conectado, abortando.")
return return
if not self.oracle_remote.is_connected: if not self.oracle_remote.is_connected:
print("PopularComponenteB: Oracle REMOTO não conectado, abortando.") logger.warning("PopularComponenteB: Oracle REMOTO não conectado, abortando.")
return return
ids_pessoas = self._buscar_ids_pendentes() ids_pessoas = self._buscar_ids_pendentes()
total_ids = len(ids_pessoas) total_ids = len(ids_pessoas)
print(f"PopularComponenteB: {total_ids} consultores pendentes para COMPONENTE_B") logger.info(f"PopularComponenteB: {total_ids} consultores pendentes para COMPONENTE_B")
processados = 0 processados = 0
com_ppg = 0 com_ppg = 0
@@ -107,7 +110,7 @@ class PopularComponenteBJob:
try: try:
registros = self._buscar_ppg_lote(lote) registros = self._buscar_ppg_lote(lote)
except Exception as e: except Exception as e:
print(f"PopularComponenteB: erro ao buscar lote {lote[:3]}... -> {e}") logger.warning(f"PopularComponenteB: erro ao buscar lote {lote[:3]}... -> {e}")
continue continue
por_pessoa: Dict[int, List[Dict]] = {} por_pessoa: Dict[int, List[Dict]] = {}
@@ -127,14 +130,14 @@ class PopularComponenteBJob:
if len(batch) >= batch_updates: if len(batch) >= batch_updates:
self._aplicar_batch(batch) self._aplicar_batch(batch)
print(f"PopularComponenteB: Processados {processados}/{total_ids} | Com PPG: {com_ppg}") logger.info(f"PopularComponenteB: Processados {processados}/{total_ids} | Com PPG: {com_ppg}")
batch = [] batch = []
if batch: if batch:
self._aplicar_batch(batch) self._aplicar_batch(batch)
self._atualizar_posicoes() self._atualizar_posicoes()
print(f"PopularComponenteB: Finalizado. Processados={processados} Com PPG={com_ppg}") logger.info(f"PopularComponenteB: Finalizado. Processados={processados} Com PPG={com_ppg}")
def _aplicar_batch(self, batch: List[Dict[str, int]]) -> None: def _aplicar_batch(self, batch: List[Dict[str, int]]) -> None:
if not batch: if not batch:

View File

@@ -1,8 +1,11 @@
import json import json
import logging
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from ...infrastructure.elasticsearch.client import ElasticsearchClient from ...infrastructure.elasticsearch.client import ElasticsearchClient
logger = logging.getLogger(__name__)
from ...infrastructure.oracle.client import OracleClient from ...infrastructure.oracle.client import OracleClient
from ...infrastructure.oracle.ranking_repository import RankingOracleRepository from ...infrastructure.oracle.ranking_repository import RankingOracleRepository
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
@@ -87,8 +90,8 @@ class ProcessarRankingJob:
except Exception as e: except Exception as e:
import traceback import traceback
print(f"AVISO: Erro ao processar consultor {doc.get('id')}: {e}") logger.warning(f"Erro ao processar consultor {doc.get('id')}: {e}")
print(f"Traceback: {traceback.format_exc()}") logger.debug(f"Traceback: {traceback.format_exc()}")
continue continue
if consultores_para_inserir: if consultores_para_inserir:

View File

@@ -1,10 +1,13 @@
import asyncio import asyncio
import logging
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from typing import Optional from typing import Optional
from .processar_ranking import ProcessarRankingJob from .processar_ranking import ProcessarRankingJob
from .popular_componente_b_job import PopularComponenteBJob from .popular_componente_b_job import PopularComponenteBJob
logger = logging.getLogger(__name__)
class RankingScheduler: class RankingScheduler:
def __init__(self, job: ProcessarRankingJob, job_componente_b: PopularComponenteBJob | None = None): def __init__(self, job: ProcessarRankingJob, job_componente_b: PopularComponenteBJob | None = None):
@@ -24,7 +27,7 @@ class RankingScheduler:
proxima_execucao += timedelta(days=1) proxima_execucao += timedelta(days=1)
segundos_ate_proxima = (proxima_execucao - agora).total_seconds() segundos_ate_proxima = (proxima_execucao - agora).total_seconds()
print(f"Próxima execução do ranking: {proxima_execucao.strftime('%d/%m/%Y %H:%M:%S')}") logger.info(f"Próxima execução do ranking: {proxima_execucao.strftime('%d/%m/%Y %H:%M:%S')}")
await asyncio.sleep(segundos_ate_proxima) await asyncio.sleep(segundos_ate_proxima)
@@ -39,18 +42,18 @@ class RankingScheduler:
if not self.running: if not self.running:
break break
print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando job de ranking automático") logger.info("Executando job de ranking automático")
await self.job.executar(limpar_antes=True) await self.job.executar(limpar_antes=True)
if self.job_componente_b: if self.job_componente_b:
print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando popular_componente_b após ranking") logger.info("Executando popular_componente_b após ranking")
await asyncio.to_thread(self.job_componente_b.executar) await asyncio.to_thread(self.job_componente_b.executar)
except asyncio.CancelledError: except asyncio.CancelledError:
print("Scheduler cancelado") logger.info("Scheduler cancelado")
break break
except Exception as e: except Exception as e:
print(f"Erro no scheduler: {e}") logger.error(f"Erro no scheduler: {e}")
await asyncio.sleep(3600) await asyncio.sleep(3600)
async def iniciar(self, hora_alvo: int = 3) -> None: async def iniciar(self, hora_alvo: int = 3) -> None:
@@ -64,7 +67,7 @@ class RankingScheduler:
self.running = True self.running = True
self.task = asyncio.create_task(self._loop_diario(hora_alvo)) self.task = asyncio.create_task(self._loop_diario(hora_alvo))
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
print(f"Scheduler do ranking iniciado: job rodará diariamente às {hora_alvo}h") logger.info(f"Scheduler do ranking iniciado: job rodará diariamente às {hora_alvo}h")
def parar(self) -> None: def parar(self) -> None:
""" """

View File

@@ -0,0 +1,3 @@
from .ranking_mapper import RankingMapper
__all__ = ["RankingMapper"]

View File

@@ -0,0 +1,104 @@
import json
import logging
from typing import Any, Dict, Optional
from ...domain.entities.consultor_ranking import ConsultorRanking
from ...interface.schemas.ranking_schema import ConsultorRankingResumoSchema
logger = logging.getLogger(__name__)
class RankingMapper:
@staticmethod
def consultor_ranking_to_schema(c: ConsultorRanking) -> ConsultorRankingResumoSchema:
consultoria = None
coordenacoes_capes = None
inscricoes = None
avaliacoes_comissao = None
premiacoes = None
bolsas_cnpq = None
participacoes = None
orientacoes = None
membros_banca = None
pontuacao = None
try:
jd = json.loads(c.json_detalhes) if c.json_detalhes else {}
if isinstance(jd, dict):
consultoria = jd.get("consultoria")
coordenacoes_capes = jd.get("coordenacoes_capes")
inscricoes = jd.get("inscricoes")
avaliacoes_comissao = jd.get("avaliacoes_comissao")
premiacoes = jd.get("premiacoes")
bolsas_cnpq = jd.get("bolsas_cnpq")
participacoes = jd.get("participacoes")
orientacoes = jd.get("orientacoes")
membros_banca = jd.get("membros_banca")
pontuacao = jd.get("pontuacao")
except (json.JSONDecodeError, TypeError) as e:
logger.warning(f"Erro ao parsear json_detalhes do consultor {c.id_pessoa}: {e}")
pontuacao_total = float(c.pontuacao_total or 0)
bloco_a = float(c.componente_a or 0)
bloco_b = float(c.componente_b or 0)
bloco_c = float(c.componente_c or 0)
bloco_d = float(c.componente_d or 0)
pontuacao_ajustada = RankingMapper._ajustar_pontuacao(
pontuacao, bloco_a, bloco_b, bloco_c, bloco_d, pontuacao_total
)
return ConsultorRankingResumoSchema(
id_pessoa=c.id_pessoa,
nome=c.nome,
posicao=c.posicao,
pontuacao_total=pontuacao_total,
bloco_a=bloco_a,
bloco_b=bloco_b,
bloco_c=bloco_c,
bloco_d=bloco_d,
ativo=c.ativo,
anos_atuacao=c.anos_atuacao,
consultoria=consultoria,
coordenacoes_capes=coordenacoes_capes,
inscricoes=inscricoes,
avaliacoes_comissao=avaliacoes_comissao,
premiacoes=premiacoes,
bolsas_cnpq=bolsas_cnpq,
participacoes=participacoes,
orientacoes=orientacoes,
membros_banca=membros_banca,
pontuacao=pontuacao_ajustada if pontuacao_ajustada else None,
)
@staticmethod
def _ajustar_pontuacao(
pontuacao: Optional[Dict],
bloco_a: float,
bloco_b: float,
bloco_c: float,
bloco_d: float,
pontuacao_total: float,
) -> Dict[str, Any]:
if isinstance(pontuacao, dict):
pontuacao_ajustada = dict(pontuacao)
else:
pontuacao_ajustada = {}
def ajustar_bloco(chave: str, total: float, letra: str):
b = pontuacao_ajustada.get(chave)
if isinstance(b, dict):
b2 = dict(b)
b2["bloco"] = letra
b2["total"] = total
pontuacao_ajustada[chave] = b2
else:
pontuacao_ajustada[chave] = {"bloco": letra, "total": total, "atuacoes": []}
ajustar_bloco("bloco_a", bloco_a, "A")
ajustar_bloco("bloco_b", bloco_b, "B")
ajustar_bloco("bloco_c", bloco_c, "C")
ajustar_bloco("bloco_d", bloco_d, "D")
pontuacao_ajustada["pontuacao_total"] = pontuacao_total
return pontuacao_ajustada

View File

@@ -1,31 +1,11 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any from typing import List, Optional
from datetime import datetime from datetime import datetime
from ..value_objects.periodo import Periodo from ..value_objects.periodo import Periodo
from ..value_objects.pontuacao import PontuacaoCompleta from ..value_objects.pontuacao import PontuacaoCompleta
@dataclass
class Atuacao:
codigo: str
tipo_es: str
inicio: Optional[datetime] = None
fim: Optional[datetime] = None
dados: Dict[str, Any] = field(default_factory=dict)
@property
def ativo(self) -> bool:
return self.fim is None
@property
def anos_completos(self) -> int:
if not self.inicio:
return 0
fim = self.fim or datetime.now()
return int((fim - self.inicio).days // 365)
@dataclass @dataclass
class CoordenacaoCapes: class CoordenacaoCapes:
codigo: str codigo: str
@@ -119,7 +99,6 @@ class Consultor:
orientacoes: List[Orientacao] = field(default_factory=list) orientacoes: List[Orientacao] = field(default_factory=list)
membros_banca: List[MembroBanca] = field(default_factory=list) membros_banca: List[MembroBanca] = field(default_factory=list)
pontuacao: Optional[PontuacaoCompleta] = None pontuacao: Optional[PontuacaoCompleta] = None
atuacoes_raw: List[Atuacao] = field(default_factory=list)
@property @property
def anos_atuacao(self) -> float: def anos_atuacao(self) -> float:

View File

@@ -16,36 +16,11 @@ from ..entities.consultor import (
) )
from ..value_objects.pontuacao import PontuacaoAtuacao, PontuacaoBloco, PontuacaoCompleta from ..value_objects.pontuacao import PontuacaoAtuacao, PontuacaoBloco, PontuacaoCompleta
from ..value_objects.criterios_pontuacao import CRITERIOS, get_criterio, Bloco from ..value_objects.criterios_pontuacao import CRITERIOS, get_criterio, Bloco
from ..value_objects.periodo import Periodo from ..value_objects.periodo import Periodo, mesclar_periodos, anos_completos_periodos
class CalculadorPontuacao: class CalculadorPontuacao:
@staticmethod
def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
if not periodos:
return []
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min)
mesclados: List[Periodo] = []
for p in periodos_ordenados:
if not mesclados:
mesclados.append(p)
continue
ultimo = mesclados[-1]
ultimo_fim = ultimo.fim or datetime.now()
atual_fim = p.fim or datetime.now()
if p.inicio and p.inicio <= ultimo_fim:
novo_fim = max(ultimo_fim, atual_fim)
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
else:
mesclados.append(p)
return mesclados
@staticmethod
def _anos_completos_periodos(periodos: List[Periodo]) -> int:
ref = datetime.now()
return sum(p.anos_completos(ref) for p in periodos)
@staticmethod @staticmethod
def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco: def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco:
if not coordenacoes: if not coordenacoes:
@@ -68,9 +43,9 @@ class CalculadorPontuacao:
coords = coord_por_tipo[tipo] coords = coord_por_tipo[tipo]
periodos = [c.periodo for c in coords] periodos = [c.periodo for c in coords]
mesclados = CalculadorPontuacao._mesclar_periodos(periodos) mesclados = mesclar_periodos(periodos)
anos_total = CalculadorPontuacao._anos_completos_periodos(mesclados) anos_total = anos_completos_periodos(mesclados)
ativo = any(c.periodo.ativo for c in coords) ativo = any(c.periodo.ativo for c in coords)
tem_retorno = len(mesclados) > 1 tem_retorno = len(mesclados) > 1
@@ -109,8 +84,8 @@ class CalculadorPontuacao:
tempo = 0 tempo = 0
if criterio.pontua_tempo: if criterio.pontua_tempo:
periodos = consultoria.periodos if consultoria.periodos else [consultoria.periodo] periodos = consultoria.periodos if consultoria.periodos else [consultoria.periodo]
mesclados = CalculadorPontuacao._mesclar_periodos(periodos) mesclados = mesclar_periodos(periodos)
anos_total = CalculadorPontuacao._anos_completos_periodos(mesclados) anos_total = anos_completos_periodos(mesclados)
tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo) tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo)
bonus = 0 bonus = 0

View File

@@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Optional from typing import List, Optional
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -29,6 +29,35 @@ class Periodo:
return int((fim - self.inicio).days // 365) return int((fim - self.inicio).days // 365)
def __post_init__(self) -> None: def __post_init__(self) -> None:
# Se houver fim anterior ao início, o período é tratado como aberto.
if self.fim and self.fim < self.inicio: if self.fim and self.fim < self.inicio:
object.__setattr__(self, "fim", None) object.__setattr__(self, "fim", None)
def mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
if not periodos:
return []
periodos_ordenados = sorted(
periodos, key=lambda p: p.inicio if p.inicio else datetime.min
)
mesclados: List[Periodo] = []
for p in periodos_ordenados:
if not mesclados:
mesclados.append(p)
continue
ultimo = mesclados[-1]
ultimo_fim = ultimo.fim or datetime.now()
atual_fim = p.fim or datetime.now()
if p.inicio and p.inicio <= ultimo_fim:
novo_fim = max(ultimo_fim, atual_fim)
mesclados[-1] = Periodo(
inicio=ultimo.inicio,
fim=novo_fim if not ultimo.ativo else None
)
else:
mesclados.append(p)
return mesclados
def anos_completos_periodos(periodos: List[Periodo], data_ref: Optional[datetime] = None) -> int:
ref = data_ref or datetime.now()
return sum(p.anos_completos(ref) for p in periodos)

View File

@@ -0,0 +1,3 @@
from .ranking_cache import RankingCache, ranking_cache
__all__ = ["RankingCache", "ranking_cache"]

View File

@@ -0,0 +1,34 @@
import asyncio
from datetime import datetime
from typing import List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from ...domain.entities.consultor import Consultor
class RankingCache:
def __init__(self, ttl_seconds: int = 300):
self.ttl = ttl_seconds
self._cache: List["Consultor"] = []
self._last_update: Optional[datetime] = None
self._loading = False
self._lock = asyncio.Lock()
def is_valid(self) -> bool:
if not self._cache or not self._last_update:
return False
return (datetime.now() - self._last_update).total_seconds() < self.ttl
def get(self) -> List["Consultor"]:
return self._cache
def set(self, consultores: List["Consultor"]) -> None:
self._cache = consultores
self._last_update = datetime.now()
def invalidate(self) -> None:
self._cache = []
self._last_update = None
ranking_cache = RankingCache(ttl_seconds=300)

View File

@@ -1,7 +1,11 @@
import logging
import cx_Oracle import cx_Oracle
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from contextlib import contextmanager from contextlib import contextmanager
logger = logging.getLogger(__name__)
class OracleClient: class OracleClient:
def __init__(self, user: str, password: str, dsn: str): def __init__(self, user: str, password: str, dsn: str):
@@ -24,14 +28,14 @@ class OracleClient:
) )
self._connected = True self._connected = True
except Exception as e: except Exception as e:
print(f"AVISO Oracle: {e}") logger.warning(f"Oracle: {e}")
self._connected = False self._connected = False
def close(self) -> None: def close(self) -> None:
if self._pool: if self._pool:
try: try:
self._pool.close() self._pool.close()
except: except cx_Oracle.Error:
pass pass
@property @property
@@ -71,7 +75,7 @@ class OracleClient:
cursor.close() cursor.close()
return results return results
except Exception as e: except Exception as e:
print(f"AVISO Oracle: falha ao executar query: {e}") logger.warning(f"Oracle: falha ao executar query: {e}")
self._connected = False self._connected = False
return [] return []

View File

@@ -1,7 +1,8 @@
import asyncio
import logging
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil import parser as date_parser from dateutil import parser as date_parser
import asyncio
from ...domain.entities.consultor import ( from ...domain.entities.consultor import (
Consultor, Consultor,
@@ -17,33 +18,12 @@ from ...domain.entities.consultor import (
) )
from ...domain.repositories.consultor_repository import ConsultorRepository from ...domain.repositories.consultor_repository import ConsultorRepository
from ...domain.services.calculador_pontuacao import CalculadorPontuacao from ...domain.services.calculador_pontuacao import CalculadorPontuacao
from ...domain.value_objects.periodo import Periodo from ...domain.value_objects.periodo import Periodo, mesclar_periodos
from ..cache import ranking_cache
from ..elasticsearch.client import ElasticsearchClient from ..elasticsearch.client import ElasticsearchClient
from ..oracle.client import OracleClient from ..oracle.client import OracleClient
logger = logging.getLogger(__name__)
class RankingCache:
def __init__(self, ttl_seconds: int = 300):
self.ttl = ttl_seconds
self._cache: List[Consultor] = []
self._last_update: Optional[datetime] = None
self._loading = False
self._lock = asyncio.Lock()
def is_valid(self) -> bool:
if not self._cache or not self._last_update:
return False
return (datetime.now() - self._last_update).total_seconds() < self.ttl
def get(self) -> List[Consultor]:
return self._cache
def set(self, consultores: List[Consultor]) -> None:
self._cache = consultores
self._last_update = datetime.now()
_ranking_cache = RankingCache(ttl_seconds=300)
class ConsultorRepositoryImpl(ConsultorRepository): class ConsultorRepositoryImpl(ConsultorRepository):
@@ -58,28 +38,9 @@ class ConsultorRepositoryImpl(ConsultorRepository):
return None return None
try: try:
return date_parser.parse(date_str, dayfirst=True) return date_parser.parse(date_str, dayfirst=True)
except: except (ValueError, TypeError):
return None return None
def _mesclar_periodos(self, periodos: List[Periodo]) -> List[Periodo]:
if not periodos:
return []
periodos = sorted(periodos, key=lambda p: p.inicio if p.inicio else datetime.min)
mesclados: List[Periodo] = []
for p in periodos:
if not mesclados:
mesclados.append(p)
continue
ultimo = mesclados[-1]
fim_ultimo = ultimo.fim or datetime.now()
fim_atual = p.fim or datetime.now()
if p.inicio and p.inicio <= fim_ultimo:
novo_fim = max(fim_ultimo, fim_atual)
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
else:
mesclados.append(p)
return mesclados
def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str: def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str:
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
tipo_coord = dados_coord.get("tipo", "").lower() tipo_coord = dados_coord.get("tipo", "").lower()
@@ -182,7 +143,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
if not periodos: if not periodos:
return None return None
mesclados = self._mesclar_periodos(periodos) mesclados = mesclar_periodos(periodos)
periodo_ativo = next((p for p in mesclados if p.ativo), None) periodo_ativo = next((p for p in mesclados if p.ativo), None)
anos_consecutivos = periodo_ativo.anos_completos(datetime.now()) if periodo_ativo else 0 anos_consecutivos = periodo_ativo.anos_completos(datetime.now()) if periodo_ativo else 0
retornos = max(0, len(mesclados) - 1) retornos = max(0, len(mesclados) - 1)
@@ -495,7 +456,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
try: try:
doc = await self.es_client.buscar_por_id(id_pessoa) doc = await self.es_client.buscar_por_id(id_pessoa)
except Exception as e: except Exception as e:
print(f"AVISO Elasticsearch: falha ao buscar consultor {id_pessoa}: {e}") logger.warning(f"Elasticsearch: falha ao buscar consultor {id_pessoa}: {e}")
return None return None
if not doc: if not doc:
return None return None
@@ -511,7 +472,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
docs = await self.es_client.buscar_com_atuacoes(size=limite, from_=offset) docs = await self.es_client.buscar_com_atuacoes(size=limite, from_=offset)
self.es_disponivel = True self.es_disponivel = True
except Exception as e: except Exception as e:
print(f"AVISO Elasticsearch: falha ao buscar consultores: {e}") logger.warning(f"Elasticsearch: falha ao buscar consultores: {e}")
self.es_disponivel = False self.es_disponivel = False
return [] return []
consultores = [await self._construir_consultor(doc) for doc in docs] consultores = [await self._construir_consultor(doc) for doc in docs]
@@ -524,15 +485,13 @@ class ConsultorRepositoryImpl(ConsultorRepository):
async def buscar_ranking( async def buscar_ranking(
self, limite: int = 100, componente: Optional[str] = None self, limite: int = 100, componente: Optional[str] = None
) -> List[Consultor]: ) -> List[Consultor]:
global _ranking_cache if ranking_cache.is_valid():
consultores_ordenados = ranking_cache.get()
if _ranking_cache.is_valid():
consultores_ordenados = _ranking_cache.get()
return consultores_ordenados[:limite] return consultores_ordenados[:limite]
async with _ranking_cache._lock: async with ranking_cache._lock:
if _ranking_cache.is_valid(): if ranking_cache.is_valid():
return _ranking_cache.get()[:limite] return ranking_cache.get()[:limite]
tamanho_busca = max(limite * 3, 1000) tamanho_busca = max(limite * 3, 1000)
docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca) docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca)
@@ -545,7 +504,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
consultores_ordenados = sorted( consultores_ordenados = sorted(
consultores, key=lambda c: c.pontuacao_total, reverse=True consultores, key=lambda c: c.pontuacao_total, reverse=True
) )
_ranking_cache.set(consultores_ordenados) ranking_cache.set(consultores_ordenados)
return consultores_ordenados[:limite] return consultores_ordenados[:limite]
@@ -558,6 +517,6 @@ class ConsultorRepositoryImpl(ConsultorRepository):
try: try:
return await self.es_client.contar_com_atuacoes() return await self.es_client.contar_com_atuacoes()
except Exception as e: except Exception as e:
print(f"AVISO Elasticsearch: falha ao contar consultores: {e}") logger.warning(f"Elasticsearch: falha ao contar consultores: {e}")
self.es_disponivel = False self.es_disponivel = False
return 0 return 0

View File

@@ -1,8 +1,12 @@
import logging
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from .routes import router from .routes import router
logger = logging.getLogger(__name__)
from .config import settings from .config import settings
from .dependencies import ( from .dependencies import (
es_client, es_client,
@@ -18,19 +22,17 @@ from ...application.jobs.scheduler import RankingScheduler
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
await es_client.connect() await es_client.connect()
# Conectar Oracle LOCAL (Docker)
try: try:
oracle_local_client.connect() oracle_local_client.connect()
print("Oracle LOCAL conectado (Docker)") logger.info("Oracle LOCAL conectado (Docker)")
except Exception as e: except Exception as e:
print(f"AVISO: Oracle LOCAL não conectou: {e}") logger.warning(f"Oracle LOCAL não conectou: {e}")
# Conectar Oracle REMOTO (CAPES)
try: try:
oracle_remote_client.connect() oracle_remote_client.connect()
print("Oracle REMOTO conectado (CAPES)") logger.info("Oracle REMOTO conectado (CAPES)")
except Exception as e: except Exception as e:
print(f"AVISO: Oracle REMOTO não conectou: {e}. Sistema rodando sem Componente B (PPG).") logger.warning(f"Oracle REMOTO não conectou: {e}. Sistema rodando sem Componente B (PPG).")
scheduler = None scheduler = None
try: try:
@@ -39,7 +41,7 @@ async def lifespan(app: FastAPI):
scheduler = RankingScheduler(job, job_componente_b=job_b) scheduler = RankingScheduler(job, job_componente_b=job_b)
await scheduler.iniciar() await scheduler.iniciar()
except Exception as e: except Exception as e:
print(f"AVISO: Scheduler não iniciou: {e}") logger.warning(f"Scheduler não iniciou: {e}")
yield yield

View File

@@ -3,6 +3,7 @@ from typing import Optional, List
from ...application.use_cases.obter_ranking import ObterRankingUseCase from ...application.use_cases.obter_ranking import ObterRankingUseCase
from ...application.use_cases.obter_consultor import ObterConsultorUseCase from ...application.use_cases.obter_consultor import ObterConsultorUseCase
from ...application.mappers import RankingMapper
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
from ..schemas.consultor_schema import ( from ..schemas.consultor_schema import (
RankingResponseSchema, RankingResponseSchema,
@@ -21,7 +22,6 @@ from ..schemas.ranking_schema import (
) )
from .dependencies import get_repository, get_ranking_repository, get_processar_job from .dependencies import get_repository, get_ranking_repository, get_processar_job
from ...application.jobs.job_status import job_status from ...application.jobs.job_status import job_status
import json
router = APIRouter(prefix="/api/v1", tags=["ranking"]) router = APIRouter(prefix="/api/v1", tags=["ranking"])
@@ -100,7 +100,7 @@ async def ranking_paginado(
total_pages = (total + size - 1) // size total_pages = (total + size - 1) // size
consultores_schema = [_consultor_resumo_from_ranking(c) for c in consultores] consultores_schema = [RankingMapper.consultor_ranking_to_schema(c) for c in consultores]
return RankingPaginadoResponseSchema( return RankingPaginadoResponseSchema(
total=total, total=total,
@@ -129,87 +129,6 @@ async def buscar_por_nome(
] ]
def _consultor_resumo_from_ranking(c):
consultoria = None
coordenacoes_capes = None
inscricoes = None
avaliacoes_comissao = None
premiacoes = None
bolsas_cnpq = None
participacoes = None
orientacoes = None
membros_banca = None
pontuacao = None
try:
jd = json.loads(c.json_detalhes) if c.json_detalhes else {}
if isinstance(jd, dict):
consultoria = jd.get("consultoria")
coordenacoes_capes = jd.get("coordenacoes_capes")
inscricoes = jd.get("inscricoes")
avaliacoes_comissao = jd.get("avaliacoes_comissao")
premiacoes = jd.get("premiacoes")
bolsas_cnpq = jd.get("bolsas_cnpq")
participacoes = jd.get("participacoes")
orientacoes = jd.get("orientacoes")
membros_banca = jd.get("membros_banca")
pontuacao = jd.get("pontuacao")
except Exception:
pass
# Ajusta pontuação detalhada para refletir os valores atuais do ranking (incluindo COMPONENTE_B),
# já que o JSON pode ter sido gerado antes do job de preenchimento do Componente B.
pontuacao_total = float(c.pontuacao_total or 0)
bloco_a = float(c.componente_a or 0)
bloco_b = float(c.componente_b or 0)
bloco_c = float(c.componente_c or 0)
bloco_d = float(c.componente_d or 0)
if isinstance(pontuacao, dict):
pontuacao_ajustada = dict(pontuacao)
else:
pontuacao_ajustada = {}
def _ajustar_bloco(chave: str, total: float, letra: str):
b = pontuacao_ajustada.get(chave)
if isinstance(b, dict):
b2 = dict(b)
b2["bloco"] = letra
b2["total"] = total
pontuacao_ajustada[chave] = b2
else:
pontuacao_ajustada[chave] = {"bloco": letra, "total": total, "atuacoes": []}
_ajustar_bloco("bloco_a", bloco_a, "A")
_ajustar_bloco("bloco_b", bloco_b, "B")
_ajustar_bloco("bloco_c", bloco_c, "C")
_ajustar_bloco("bloco_d", bloco_d, "D")
pontuacao_ajustada["pontuacao_total"] = pontuacao_total
return ConsultorRankingResumoSchema(
id_pessoa=c.id_pessoa,
nome=c.nome,
posicao=c.posicao,
pontuacao_total=pontuacao_total,
bloco_a=bloco_a,
bloco_b=bloco_b,
bloco_c=bloco_c,
bloco_d=bloco_d,
ativo=c.ativo,
anos_atuacao=c.anos_atuacao,
consultoria=consultoria,
coordenacoes_capes=coordenacoes_capes,
inscricoes=inscricoes,
avaliacoes_comissao=avaliacoes_comissao,
premiacoes=premiacoes,
bolsas_cnpq=bolsas_cnpq,
participacoes=participacoes,
orientacoes=orientacoes,
membros_banca=membros_banca,
pontuacao=pontuacao_ajustada if pontuacao_ajustada else None,
)
@router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema) @router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema)
async def ranking_estatisticas( async def ranking_estatisticas(
ranking_repo = Depends(get_ranking_repository), ranking_repo = Depends(get_ranking_repository),