From f91651056ae7f689233a8042824666bef80b2e32 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Sun, 14 Dec 2025 21:47:00 -0300 Subject: [PATCH] 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 --- backend/scripts/SCRIPTS_STATUS.md | 46 ++++++++ .../jobs/popular_componente_b_job.py | 15 ++- .../src/application/jobs/processar_ranking.py | 7 +- backend/src/application/jobs/scheduler.py | 15 ++- backend/src/application/mappers/__init__.py | 3 + .../src/application/mappers/ranking_mapper.py | 104 ++++++++++++++++++ backend/src/domain/entities/consultor.py | 23 +--- .../domain/services/calculador_pontuacao.py | 35 +----- backend/src/domain/value_objects/periodo.py | 33 +++++- backend/src/infrastructure/cache/__init__.py | 3 + .../src/infrastructure/cache/ranking_cache.py | 34 ++++++ backend/src/infrastructure/oracle/client.py | 10 +- .../repositories/consultor_repository_impl.py | 73 +++--------- backend/src/interface/api/app.py | 16 +-- backend/src/interface/api/routes.py | 85 +------------- 15 files changed, 284 insertions(+), 218 deletions(-) create mode 100644 backend/scripts/SCRIPTS_STATUS.md create mode 100644 backend/src/application/mappers/__init__.py create mode 100644 backend/src/application/mappers/ranking_mapper.py create mode 100644 backend/src/infrastructure/cache/__init__.py create mode 100644 backend/src/infrastructure/cache/ranking_cache.py diff --git a/backend/scripts/SCRIPTS_STATUS.md b/backend/scripts/SCRIPTS_STATUS.md new file mode 100644 index 0000000..47c2892 --- /dev/null +++ b/backend/scripts/SCRIPTS_STATUS.md @@ -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 diff --git a/backend/src/application/jobs/popular_componente_b_job.py b/backend/src/application/jobs/popular_componente_b_job.py index 7086d93..5462242 100644 --- a/backend/src/application/jobs/popular_componente_b_job.py +++ b/backend/src/application/jobs/popular_componente_b_job.py @@ -1,9 +1,12 @@ +import logging from datetime import datetime from itertools import islice from typing import Dict, List, Optional from ...infrastructure.oracle.client import OracleClient +logger = logging.getLogger(__name__) + class PopularComponenteBJob: """ @@ -89,15 +92,15 @@ class PopularComponenteBJob: Este método é síncrono; use asyncio.to_thread quando chamá-lo em corrotina. """ 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 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 ids_pessoas = self._buscar_ids_pendentes() 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 com_ppg = 0 @@ -107,7 +110,7 @@ class PopularComponenteBJob: try: registros = self._buscar_ppg_lote(lote) 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 por_pessoa: Dict[int, List[Dict]] = {} @@ -127,14 +130,14 @@ class PopularComponenteBJob: if len(batch) >= batch_updates: 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 = [] if batch: self._aplicar_batch(batch) 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: if not batch: diff --git a/backend/src/application/jobs/processar_ranking.py b/backend/src/application/jobs/processar_ranking.py index fcc1933..9634b77 100644 --- a/backend/src/application/jobs/processar_ranking.py +++ b/backend/src/application/jobs/processar_ranking.py @@ -1,8 +1,11 @@ import json +import logging from datetime import datetime from typing import Optional, Dict, Any from ...infrastructure.elasticsearch.client import ElasticsearchClient + +logger = logging.getLogger(__name__) from ...infrastructure.oracle.client import OracleClient from ...infrastructure.oracle.ranking_repository import RankingOracleRepository from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl @@ -87,8 +90,8 @@ class ProcessarRankingJob: except Exception as e: import traceback - print(f"AVISO: Erro ao processar consultor {doc.get('id')}: {e}") - print(f"Traceback: {traceback.format_exc()}") + logger.warning(f"Erro ao processar consultor {doc.get('id')}: {e}") + logger.debug(f"Traceback: {traceback.format_exc()}") continue if consultores_para_inserir: diff --git a/backend/src/application/jobs/scheduler.py b/backend/src/application/jobs/scheduler.py index e352e8e..e1ac3c9 100644 --- a/backend/src/application/jobs/scheduler.py +++ b/backend/src/application/jobs/scheduler.py @@ -1,10 +1,13 @@ import asyncio +import logging from datetime import datetime, time, timedelta from typing import Optional from .processar_ranking import ProcessarRankingJob from .popular_componente_b_job import PopularComponenteBJob +logger = logging.getLogger(__name__) + class RankingScheduler: def __init__(self, job: ProcessarRankingJob, job_componente_b: PopularComponenteBJob | None = None): @@ -24,7 +27,7 @@ class RankingScheduler: proxima_execucao += timedelta(days=1) 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) @@ -39,18 +42,18 @@ class RankingScheduler: if not self.running: 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) 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) except asyncio.CancelledError: - print("Scheduler cancelado") + logger.info("Scheduler cancelado") break except Exception as e: - print(f"Erro no scheduler: {e}") + logger.error(f"Erro no scheduler: {e}") await asyncio.sleep(3600) async def iniciar(self, hora_alvo: int = 3) -> None: @@ -64,7 +67,7 @@ class RankingScheduler: self.running = True self.task = asyncio.create_task(self._loop_diario(hora_alvo)) 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: """ diff --git a/backend/src/application/mappers/__init__.py b/backend/src/application/mappers/__init__.py new file mode 100644 index 0000000..84a1848 --- /dev/null +++ b/backend/src/application/mappers/__init__.py @@ -0,0 +1,3 @@ +from .ranking_mapper import RankingMapper + +__all__ = ["RankingMapper"] diff --git a/backend/src/application/mappers/ranking_mapper.py b/backend/src/application/mappers/ranking_mapper.py new file mode 100644 index 0000000..455078f --- /dev/null +++ b/backend/src/application/mappers/ranking_mapper.py @@ -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 diff --git a/backend/src/domain/entities/consultor.py b/backend/src/domain/entities/consultor.py index 3ac24a8..1913e98 100644 --- a/backend/src/domain/entities/consultor.py +++ b/backend/src/domain/entities/consultor.py @@ -1,31 +1,11 @@ from dataclasses import dataclass, field -from typing import List, Optional, Dict, Any +from typing import List, Optional from datetime import datetime from ..value_objects.periodo import Periodo 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 class CoordenacaoCapes: codigo: str @@ -119,7 +99,6 @@ class Consultor: orientacoes: List[Orientacao] = field(default_factory=list) membros_banca: List[MembroBanca] = field(default_factory=list) pontuacao: Optional[PontuacaoCompleta] = None - atuacoes_raw: List[Atuacao] = field(default_factory=list) @property def anos_atuacao(self) -> float: diff --git a/backend/src/domain/services/calculador_pontuacao.py b/backend/src/domain/services/calculador_pontuacao.py index 81e5a18..102296b 100644 --- a/backend/src/domain/services/calculador_pontuacao.py +++ b/backend/src/domain/services/calculador_pontuacao.py @@ -16,36 +16,11 @@ from ..entities.consultor import ( ) from ..value_objects.pontuacao import PontuacaoAtuacao, PontuacaoBloco, PontuacaoCompleta 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: - @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 def calcular_bloco_a(coordenacoes: List[CoordenacaoCapes]) -> PontuacaoBloco: if not coordenacoes: @@ -68,9 +43,9 @@ class CalculadorPontuacao: coords = coord_por_tipo[tipo] 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) tem_retorno = len(mesclados) > 1 @@ -109,8 +84,8 @@ class CalculadorPontuacao: tempo = 0 if criterio.pontua_tempo: periodos = consultoria.periodos if consultoria.periodos else [consultoria.periodo] - mesclados = CalculadorPontuacao._mesclar_periodos(periodos) - anos_total = CalculadorPontuacao._anos_completos_periodos(mesclados) + mesclados = mesclar_periodos(periodos) + anos_total = anos_completos_periodos(mesclados) tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo) bonus = 0 diff --git a/backend/src/domain/value_objects/periodo.py b/backend/src/domain/value_objects/periodo.py index 3c02d5a..cc54c4b 100644 --- a/backend/src/domain/value_objects/periodo.py +++ b/backend/src/domain/value_objects/periodo.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import Optional +from typing import List, Optional @dataclass(frozen=True) @@ -29,6 +29,35 @@ class Periodo: return int((fim - self.inicio).days // 365) 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: 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) diff --git a/backend/src/infrastructure/cache/__init__.py b/backend/src/infrastructure/cache/__init__.py new file mode 100644 index 0000000..77c0b17 --- /dev/null +++ b/backend/src/infrastructure/cache/__init__.py @@ -0,0 +1,3 @@ +from .ranking_cache import RankingCache, ranking_cache + +__all__ = ["RankingCache", "ranking_cache"] diff --git a/backend/src/infrastructure/cache/ranking_cache.py b/backend/src/infrastructure/cache/ranking_cache.py new file mode 100644 index 0000000..bb23f52 --- /dev/null +++ b/backend/src/infrastructure/cache/ranking_cache.py @@ -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) diff --git a/backend/src/infrastructure/oracle/client.py b/backend/src/infrastructure/oracle/client.py index 3ac1076..0d69ebe 100644 --- a/backend/src/infrastructure/oracle/client.py +++ b/backend/src/infrastructure/oracle/client.py @@ -1,7 +1,11 @@ +import logging + import cx_Oracle from typing import List, Dict, Any, Optional from contextlib import contextmanager +logger = logging.getLogger(__name__) + class OracleClient: def __init__(self, user: str, password: str, dsn: str): @@ -24,14 +28,14 @@ class OracleClient: ) self._connected = True except Exception as e: - print(f"AVISO Oracle: {e}") + logger.warning(f"Oracle: {e}") self._connected = False def close(self) -> None: if self._pool: try: self._pool.close() - except: + except cx_Oracle.Error: pass @property @@ -71,7 +75,7 @@ class OracleClient: cursor.close() return results 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 return [] diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index 88e3305..793749e 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -1,7 +1,8 @@ +import asyncio +import logging from typing import List, Optional, Dict, Any from datetime import datetime, timedelta from dateutil import parser as date_parser -import asyncio from ...domain.entities.consultor import ( Consultor, @@ -17,33 +18,12 @@ from ...domain.entities.consultor import ( ) from ...domain.repositories.consultor_repository import ConsultorRepository 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 ..oracle.client import OracleClient - -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) +logger = logging.getLogger(__name__) class ConsultorRepositoryImpl(ConsultorRepository): @@ -58,28 +38,9 @@ class ConsultorRepositoryImpl(ConsultorRepository): return None try: return date_parser.parse(date_str, dayfirst=True) - except: + except (ValueError, TypeError): 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: dados_coord = coord.get("dadosCoordenacaoArea", {}) or {} tipo_coord = dados_coord.get("tipo", "").lower() @@ -182,7 +143,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): if not periodos: return None - mesclados = self._mesclar_periodos(periodos) + mesclados = mesclar_periodos(periodos) 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 retornos = max(0, len(mesclados) - 1) @@ -495,7 +456,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): try: doc = await self.es_client.buscar_por_id(id_pessoa) 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 if not doc: return None @@ -511,7 +472,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): docs = await self.es_client.buscar_com_atuacoes(size=limite, from_=offset) self.es_disponivel = True 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 return [] consultores = [await self._construir_consultor(doc) for doc in docs] @@ -524,15 +485,13 @@ class ConsultorRepositoryImpl(ConsultorRepository): async def buscar_ranking( self, limite: int = 100, componente: Optional[str] = None ) -> 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] - async with _ranking_cache._lock: - if _ranking_cache.is_valid(): - return _ranking_cache.get()[:limite] + async with ranking_cache._lock: + if ranking_cache.is_valid(): + return ranking_cache.get()[:limite] tamanho_busca = max(limite * 3, 1000) docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca) @@ -545,7 +504,7 @@ class ConsultorRepositoryImpl(ConsultorRepository): consultores_ordenados = sorted( consultores, key=lambda c: c.pontuacao_total, reverse=True ) - _ranking_cache.set(consultores_ordenados) + ranking_cache.set(consultores_ordenados) return consultores_ordenados[:limite] @@ -558,6 +517,6 @@ class ConsultorRepositoryImpl(ConsultorRepository): try: return await self.es_client.contar_com_atuacoes() 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 return 0 diff --git a/backend/src/interface/api/app.py b/backend/src/interface/api/app.py index c02e99f..e64edf4 100644 --- a/backend/src/interface/api/app.py +++ b/backend/src/interface/api/app.py @@ -1,8 +1,12 @@ +import logging + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager from .routes import router + +logger = logging.getLogger(__name__) from .config import settings from .dependencies import ( es_client, @@ -18,19 +22,17 @@ from ...application.jobs.scheduler import RankingScheduler async def lifespan(app: FastAPI): await es_client.connect() - # Conectar Oracle LOCAL (Docker) try: oracle_local_client.connect() - print("Oracle LOCAL conectado (Docker)") + logger.info("Oracle LOCAL conectado (Docker)") 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: oracle_remote_client.connect() - print("Oracle REMOTO conectado (CAPES)") + logger.info("Oracle REMOTO conectado (CAPES)") 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 try: @@ -39,7 +41,7 @@ async def lifespan(app: FastAPI): scheduler = RankingScheduler(job, job_componente_b=job_b) await scheduler.iniciar() except Exception as e: - print(f"AVISO: Scheduler não iniciou: {e}") + logger.warning(f"Scheduler não iniciou: {e}") yield diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py index 6d86cb4..e68a0be 100644 --- a/backend/src/interface/api/routes.py +++ b/backend/src/interface/api/routes.py @@ -3,6 +3,7 @@ from typing import Optional, List from ...application.use_cases.obter_ranking import ObterRankingUseCase from ...application.use_cases.obter_consultor import ObterConsultorUseCase +from ...application.mappers import RankingMapper from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl from ..schemas.consultor_schema import ( RankingResponseSchema, @@ -21,7 +22,6 @@ from ..schemas.ranking_schema import ( ) from .dependencies import get_repository, get_ranking_repository, get_processar_job from ...application.jobs.job_status import job_status -import json router = APIRouter(prefix="/api/v1", tags=["ranking"]) @@ -100,7 +100,7 @@ async def ranking_paginado( 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( 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) async def ranking_estatisticas( ranking_repo = Depends(get_ranking_repository),