feat(backend): ranking 100% Elasticsearch e critérios do PDF
This commit is contained in:
@@ -2,7 +2,7 @@ ES_URL=http://seu-elasticsearch:9200
|
|||||||
ES_INDEX=atuacapes
|
ES_INDEX=atuacapes
|
||||||
ES_USER=seu_usuario_elastic
|
ES_USER=seu_usuario_elastic
|
||||||
ES_PASSWORD=sua_senha_elastic
|
ES_PASSWORD=sua_senha_elastic
|
||||||
|
ES_VERIFY_SSL=true
|
||||||
|
|
||||||
ORACLE_USER=FREDERICOAC
|
SCHEDULER_ENABLED=false
|
||||||
ORACLE_PASSWORD=FREDEricoac
|
SCHEDULER_HOUR=3
|
||||||
ORACLE_DSN=oracledhtsrv02.hom.capes.gov.br:1521/hom_dr
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ ES_URL=http://localhost:9200
|
|||||||
ES_INDEX=atuacapes__1763197236
|
ES_INDEX=atuacapes__1763197236
|
||||||
ES_USER=seu_usuario_elastic
|
ES_USER=seu_usuario_elastic
|
||||||
ES_PASSWORD=sua_senha_elastic
|
ES_PASSWORD=sua_senha_elastic
|
||||||
ES_DEFAULT_SOURCE_FIELDS=id,dadosPessoais.nome,dadosPessoais.cpf,atuacoes
|
ES_VERIFY_SSL=true
|
||||||
|
|
||||||
ORACLE_USER=seu_usuario
|
|
||||||
ORACLE_PASSWORD=sua_senha
|
|
||||||
ORACLE_DSN=host:1521/service_name
|
|
||||||
|
|
||||||
API_HOST=0.0.0.0
|
API_HOST=0.0.0.0
|
||||||
API_PORT=8000
|
API_PORT=8000
|
||||||
@@ -14,3 +10,5 @@ API_RELOAD=true
|
|||||||
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||||
|
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
SCHEDULER_ENABLED=false
|
||||||
|
SCHEDULER_HOUR=3
|
||||||
|
|||||||
@@ -2,23 +2,6 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gcc \
|
|
||||||
wget \
|
|
||||||
unzip \
|
|
||||||
&& (apt-get install -y libaio1t64 || apt-get install -y libaio1 || true) \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN wget https://download.oracle.com/otn_software/linux/instantclient/2115000/instantclient-basic-linux.x64-21.15.0.0.0dbru.zip \
|
|
||||||
&& unzip instantclient-basic-linux.x64-21.15.0.0.0dbru.zip -d /opt/oracle \
|
|
||||||
&& rm instantclient-basic-linux.x64-21.15.0.0.0dbru.zip \
|
|
||||||
&& sh -c "echo /opt/oracle/instantclient_21_15 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
|
|
||||||
&& ln -sf /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1 || true \
|
|
||||||
&& ldconfig
|
|
||||||
|
|
||||||
ENV LD_LIBRARY_PATH=/opt/oracle/instantclient_21_15:$LD_LIBRARY_PATH
|
|
||||||
ENV PATH=/opt/oracle/instantclient_21_15:$PATH
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ fastapi = "^0.109.0"
|
|||||||
uvicorn = {extras = ["standard"], version = "^0.27.0"}
|
uvicorn = {extras = ["standard"], version = "^0.27.0"}
|
||||||
pydantic = "^2.5.3"
|
pydantic = "^2.5.3"
|
||||||
pydantic-settings = "^2.1.0"
|
pydantic-settings = "^2.1.0"
|
||||||
elasticsearch = "^8.11.1"
|
|
||||||
cx-Oracle = "^8.3.0"
|
|
||||||
python-dateutil = "^2.8.2"
|
python-dateutil = "^2.8.2"
|
||||||
httpx = "^0.26.0"
|
httpx = "^0.26.0"
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ fastapi==0.109.0
|
|||||||
uvicorn[standard]==0.27.0
|
uvicorn[standard]==0.27.0
|
||||||
pydantic==2.5.3
|
pydantic==2.5.3
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
cx-Oracle==8.3.0
|
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
aiohttp==3.9.1
|
rich==13.7.0
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class CoordenacaoCapesDTO:
|
|||||||
periodo: PeriodoDTO
|
periodo: PeriodoDTO
|
||||||
areas_adicionais: List[str]
|
areas_adicionais: List[str]
|
||||||
ja_coordenou_antes: bool
|
ja_coordenou_antes: bool
|
||||||
|
presidente: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -54,6 +55,7 @@ class PremiacaoDTO:
|
|||||||
tipo: str
|
tipo: str
|
||||||
nome_premio: str
|
nome_premio: str
|
||||||
ano: int
|
ano: int
|
||||||
|
papel: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -77,6 +79,9 @@ class OrientacaoDTO:
|
|||||||
tipo: str
|
tipo: str
|
||||||
nivel: str
|
nivel: str
|
||||||
ano: Optional[int]
|
ano: Optional[int]
|
||||||
|
coorientacao: bool = False
|
||||||
|
premiada: bool = False
|
||||||
|
premiacao_tipo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -130,10 +135,10 @@ class ConsultorResumoDTO:
|
|||||||
class ConsultorDetalhadoDTO:
|
class ConsultorDetalhadoDTO:
|
||||||
id_pessoa: int
|
id_pessoa: int
|
||||||
nome: str
|
nome: str
|
||||||
cpf: Optional[str]
|
|
||||||
anos_atuacao: float
|
anos_atuacao: float
|
||||||
ativo: bool
|
ativo: bool
|
||||||
veterano: bool
|
veterano: bool
|
||||||
|
coordenador_ppg: bool
|
||||||
coordenacoes_capes: List[CoordenacaoCapesDTO]
|
coordenacoes_capes: List[CoordenacaoCapesDTO]
|
||||||
consultoria: Optional[ConsultoriaDTO]
|
consultoria: Optional[ConsultoriaDTO]
|
||||||
inscricoes: List[InscricaoDTO]
|
inscricoes: List[InscricaoDTO]
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from typing import Dict, Any, List
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
|
|
||||||
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
|
from ...infrastructure.ranking_store import RankingEntry, RankingStore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
from ...domain.services.calculador_pontuacao import CalculadorPontuacao
|
|
||||||
from .job_status import job_status
|
from .job_status import job_status
|
||||||
|
|
||||||
|
|
||||||
@@ -17,16 +13,12 @@ class ProcessarRankingJob:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
es_client: ElasticsearchClient,
|
es_client: ElasticsearchClient,
|
||||||
oracle_remote_client: OracleClient,
|
ranking_store: RankingStore,
|
||||||
oracle_local_client: OracleClient,
|
|
||||||
ranking_repo: RankingOracleRepository,
|
|
||||||
):
|
):
|
||||||
self.es_client = es_client
|
self.es_client = es_client
|
||||||
self.oracle_remote_client = oracle_remote_client
|
self.ranking_store = ranking_store
|
||||||
self.oracle_local_client = oracle_local_client
|
|
||||||
self.ranking_repo = ranking_repo
|
|
||||||
self.consultor_repo = ConsultorRepositoryImpl(es_client, oracle_client=None)
|
self.consultor_repo = ConsultorRepositoryImpl(es_client, oracle_client=None)
|
||||||
self.calculador = CalculadorPontuacao()
|
self._consultores: List[dict] = []
|
||||||
|
|
||||||
async def executar(self, limpar_antes: bool = True) -> Dict[str, Any]:
|
async def executar(self, limpar_antes: bool = True) -> Dict[str, Any]:
|
||||||
if job_status.is_running:
|
if job_status.is_running:
|
||||||
@@ -36,28 +28,26 @@ class ProcessarRankingJob:
|
|||||||
total = await self.es_client.contar_com_atuacoes()
|
total = await self.es_client.contar_com_atuacoes()
|
||||||
job_status.iniciar(total_esperado=total)
|
job_status.iniciar(total_esperado=total)
|
||||||
|
|
||||||
if limpar_antes:
|
self._consultores = []
|
||||||
job_status.mensagem = "Limpando tabela de ranking..."
|
job_status.mensagem = "Iniciando processamento do ranking via Scroll API (Elasticsearch)..."
|
||||||
self.ranking_repo.limpar_tabela()
|
|
||||||
|
|
||||||
job_status.mensagem = "Iniciando processamento via Scroll API..."
|
|
||||||
|
|
||||||
resultado = await self.es_client.buscar_todos_consultores(
|
resultado = await self.es_client.buscar_todos_consultores(
|
||||||
callback=self._processar_batch,
|
callback=self._processar_batch,
|
||||||
batch_size=5000
|
batch_size=5000
|
||||||
)
|
)
|
||||||
|
|
||||||
job_status.mensagem = "Atualizando posições no ranking..."
|
job_status.mensagem = "Ordenando e gerando posições..."
|
||||||
self.ranking_repo.atualizar_posicoes()
|
entries = self._gerar_entries_ordenadas(self._consultores)
|
||||||
|
await self.ranking_store.set_entries(entries)
|
||||||
|
|
||||||
estatisticas = self.ranking_repo.obter_estatisticas()
|
estatisticas = self._obter_estatisticas(entries)
|
||||||
|
|
||||||
job_status.finalizar(sucesso=True)
|
job_status.finalizar(sucesso=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"sucesso": True,
|
"sucesso": True,
|
||||||
"total_processados": resultado["processados"],
|
"total_processados": resultado.get("processados", len(entries)),
|
||||||
"total_batches": resultado["batches"],
|
"total_batches": resultado.get("batches", 0),
|
||||||
"tempo_decorrido": job_status.tempo_decorrido,
|
"tempo_decorrido": job_status.tempo_decorrido,
|
||||||
"estatisticas": estatisticas
|
"estatisticas": estatisticas
|
||||||
}
|
}
|
||||||
@@ -67,26 +57,11 @@ class ProcessarRankingJob:
|
|||||||
raise RuntimeError(f"Erro ao processar ranking: {e}")
|
raise RuntimeError(f"Erro ao processar ranking: {e}")
|
||||||
|
|
||||||
async def _processar_batch(self, docs: list, progress: dict) -> None:
|
async def _processar_batch(self, docs: list, progress: dict) -> None:
|
||||||
consultores_para_inserir = []
|
|
||||||
|
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
try:
|
try:
|
||||||
consultor = await self.consultor_repo._construir_consultor(doc)
|
consultor = await self.consultor_repo._construir_consultor(doc)
|
||||||
|
|
||||||
consultor_dict = {
|
self._consultores.append(self._gerar_json_detalhes(consultor))
|
||||||
"id_pessoa": consultor.id_pessoa,
|
|
||||||
"nome": consultor.nome,
|
|
||||||
"pontuacao_total": consultor.pontuacao_total,
|
|
||||||
"componente_a": consultor.pontuacao_bloco_a,
|
|
||||||
"componente_b": 0,
|
|
||||||
"componente_c": consultor.pontuacao_bloco_c,
|
|
||||||
"componente_d": consultor.pontuacao_bloco_d,
|
|
||||||
"ativo": consultor.ativo,
|
|
||||||
"anos_atuacao": consultor.anos_atuacao,
|
|
||||||
"detalhes": self._gerar_json_detalhes(consultor)
|
|
||||||
}
|
|
||||||
|
|
||||||
consultores_para_inserir.append(consultor_dict)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
@@ -94,9 +69,6 @@ class ProcessarRankingJob:
|
|||||||
logger.debug(f"Traceback: {traceback.format_exc()}")
|
logger.debug(f"Traceback: {traceback.format_exc()}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if consultores_para_inserir:
|
|
||||||
self.ranking_repo.inserir_batch(consultores_para_inserir)
|
|
||||||
|
|
||||||
job_status.atualizar_progresso(
|
job_status.atualizar_progresso(
|
||||||
processados=progress["processados"],
|
processados=progress["processados"],
|
||||||
batch_atual=progress["batch_atual"],
|
batch_atual=progress["batch_atual"],
|
||||||
@@ -104,10 +76,29 @@ class ProcessarRankingJob:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _gerar_json_detalhes(self, consultor) -> dict:
|
def _gerar_json_detalhes(self, consultor) -> dict:
|
||||||
|
bloco_b = 0 # reservado no V1 (dados incompletos)
|
||||||
|
pontuacao = consultor.pontuacao.to_dict() if consultor.pontuacao else None
|
||||||
|
if isinstance(pontuacao, dict):
|
||||||
|
pontuacao = dict(pontuacao)
|
||||||
|
pontuacao["bloco_b"] = {"bloco": "B", "total": bloco_b, "atuacoes": []}
|
||||||
|
pontuacao["pontuacao_total"] = (
|
||||||
|
pontuacao.get("pontuacao_total", 0) + bloco_b
|
||||||
|
if isinstance(pontuacao.get("pontuacao_total"), (int, float))
|
||||||
|
else consultor.pontuacao_total + bloco_b
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id_pessoa": consultor.id_pessoa,
|
"id_pessoa": consultor.id_pessoa,
|
||||||
"nome": consultor.nome,
|
"nome": consultor.nome,
|
||||||
"cpf": consultor.cpf,
|
"posicao": None,
|
||||||
|
"pontuacao_total": consultor.pontuacao_total + bloco_b,
|
||||||
|
"bloco_a": consultor.pontuacao_bloco_a,
|
||||||
|
"bloco_b": bloco_b,
|
||||||
|
"bloco_c": consultor.pontuacao_bloco_c,
|
||||||
|
"bloco_d": consultor.pontuacao_bloco_d,
|
||||||
|
"ativo": consultor.ativo,
|
||||||
|
"anos_atuacao": consultor.anos_atuacao,
|
||||||
|
"coordenador_ppg": consultor.coordenador_ppg,
|
||||||
"coordenacoes_capes": [
|
"coordenacoes_capes": [
|
||||||
{
|
{
|
||||||
"codigo": c.codigo,
|
"codigo": c.codigo,
|
||||||
@@ -115,7 +106,8 @@ class ProcessarRankingJob:
|
|||||||
"area_avaliacao": c.area_avaliacao,
|
"area_avaliacao": c.area_avaliacao,
|
||||||
"inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None,
|
"inicio": c.periodo.inicio.isoformat() if c.periodo.inicio else None,
|
||||||
"fim": c.periodo.fim.isoformat() if c.periodo.fim else None,
|
"fim": c.periodo.fim.isoformat() if c.periodo.fim else None,
|
||||||
"ativo": c.periodo.ativo
|
"ativo": c.periodo.ativo,
|
||||||
|
"presidente": c.presidente,
|
||||||
}
|
}
|
||||||
for c in consultor.coordenacoes_capes
|
for c in consultor.coordenacoes_capes
|
||||||
],
|
],
|
||||||
@@ -153,7 +145,8 @@ class ProcessarRankingJob:
|
|||||||
"codigo": p.codigo,
|
"codigo": p.codigo,
|
||||||
"tipo": p.tipo,
|
"tipo": p.tipo,
|
||||||
"nome_premio": p.nome_premio,
|
"nome_premio": p.nome_premio,
|
||||||
"ano": p.ano
|
"ano": p.ano,
|
||||||
|
"papel": p.papel,
|
||||||
}
|
}
|
||||||
for p in consultor.premiacoes
|
for p in consultor.premiacoes
|
||||||
],
|
],
|
||||||
@@ -179,7 +172,10 @@ class ProcessarRankingJob:
|
|||||||
"codigo": o.codigo,
|
"codigo": o.codigo,
|
||||||
"tipo": o.tipo,
|
"tipo": o.tipo,
|
||||||
"nivel": o.nivel,
|
"nivel": o.nivel,
|
||||||
"ano": o.ano
|
"ano": o.ano,
|
||||||
|
"coorientacao": o.coorientacao,
|
||||||
|
"premiada": o.premiada,
|
||||||
|
"premiacao_tipo": o.premiacao_tipo,
|
||||||
}
|
}
|
||||||
for o in consultor.orientacoes
|
for o in consultor.orientacoes
|
||||||
],
|
],
|
||||||
@@ -192,5 +188,68 @@ class ProcessarRankingJob:
|
|||||||
}
|
}
|
||||||
for m in consultor.membros_banca
|
for m in consultor.membros_banca
|
||||||
],
|
],
|
||||||
"pontuacao": consultor.pontuacao.to_dict() if consultor.pontuacao else None
|
"pontuacao": pontuacao,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _gerar_entries_ordenadas(consultores: List[dict]) -> List[RankingEntry]:
|
||||||
|
consultores_ordenados = sorted(
|
||||||
|
consultores,
|
||||||
|
key=lambda c: (int(c.get("pontuacao_total", 0)), -int(c.get("id_pessoa", 0))),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
entries: List[RankingEntry] = []
|
||||||
|
for idx, c in enumerate(consultores_ordenados, start=1):
|
||||||
|
c["posicao"] = idx
|
||||||
|
entries.append(
|
||||||
|
RankingEntry(
|
||||||
|
id_pessoa=int(c["id_pessoa"]),
|
||||||
|
nome=str(c.get("nome", "")),
|
||||||
|
posicao=idx,
|
||||||
|
pontuacao_total=int(c.get("pontuacao_total", 0)),
|
||||||
|
bloco_a=int(c.get("bloco_a", 0)),
|
||||||
|
bloco_b=int(c.get("bloco_b", 0)),
|
||||||
|
bloco_c=int(c.get("bloco_c", 0)),
|
||||||
|
bloco_d=int(c.get("bloco_d", 0)),
|
||||||
|
ativo=bool(c.get("ativo", False)),
|
||||||
|
anos_atuacao=float(c.get("anos_atuacao", 0) or 0),
|
||||||
|
detalhes=c,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _obter_estatisticas(entries: List[RankingEntry]) -> Dict[str, Any]:
|
||||||
|
if not entries:
|
||||||
|
return {
|
||||||
|
"total_consultores": 0,
|
||||||
|
"total_ativos": 0,
|
||||||
|
"total_inativos": 0,
|
||||||
|
"ultima_atualizacao": None,
|
||||||
|
"pontuacao_media": 0,
|
||||||
|
"pontuacao_maxima": 0,
|
||||||
|
"pontuacao_minima": 0,
|
||||||
|
"media_componentes": {"a": 0, "b": 0, "c": 0, "d": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
total = len(entries)
|
||||||
|
ativos = sum(1 for e in entries if e.ativo)
|
||||||
|
inativos = total - ativos
|
||||||
|
totais = [e.pontuacao_total for e in entries]
|
||||||
|
media_total = sum(totais) / total if total else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_consultores": total,
|
||||||
|
"total_ativos": ativos,
|
||||||
|
"total_inativos": inativos,
|
||||||
|
"ultima_atualizacao": None,
|
||||||
|
"pontuacao_media": float(round(media_total, 2)),
|
||||||
|
"pontuacao_maxima": float(max(totais) if totais else 0),
|
||||||
|
"pontuacao_minima": float(min(totais) if totais else 0),
|
||||||
|
"media_componentes": {
|
||||||
|
"a": float(round(sum(e.bloco_a for e in entries) / total, 2)),
|
||||||
|
"b": float(round(sum(e.bloco_b for e in entries) / total, 2)),
|
||||||
|
"c": float(round(sum(e.bloco_c for e in entries) / total, 2)),
|
||||||
|
"d": float(round(sum(e.bloco_d for e in entries) / total, 2)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ 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
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RankingScheduler:
|
class RankingScheduler:
|
||||||
def __init__(self, job: ProcessarRankingJob, job_componente_b: PopularComponenteBJob | None = None):
|
def __init__(self, job: ProcessarRankingJob):
|
||||||
self.job = job
|
self.job = job
|
||||||
self.job_componente_b = job_componente_b
|
|
||||||
self.task: Optional[asyncio.Task] = None
|
self.task: Optional[asyncio.Task] = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
@@ -45,10 +43,6 @@ class RankingScheduler:
|
|||||||
logger.info("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:
|
|
||||||
logger.info("Executando popular_componente_b após ranking")
|
|
||||||
await asyncio.to_thread(self.job_componente_b.executar)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info("Scheduler cancelado")
|
logger.info("Scheduler cancelado")
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class ObterConsultorUseCase:
|
|||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.ranking_use_case = ObterRankingUseCase(repository)
|
self.ranking_use_case = ObterRankingUseCase(repository)
|
||||||
|
|
||||||
async def executar(self, id_pessoa: int) -> Optional[ConsultorDetalhadoDTO]:
|
async def executar(self, id_pessoa: int, rank: Optional[int] = None) -> Optional[ConsultorDetalhadoDTO]:
|
||||||
consultor = await self.repository.buscar_por_id(id_pessoa)
|
consultor = await self.repository.buscar_por_id(id_pessoa)
|
||||||
if not consultor:
|
if not consultor:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if rank is None:
|
||||||
|
# Fallback legado: pode ser incompleto em grandes bases.
|
||||||
ranking_completo = await self.repository.buscar_ranking(limite=1000)
|
ranking_completo = await self.repository.buscar_ranking(limite=1000)
|
||||||
rank = next(
|
rank = next(
|
||||||
(idx + 1 for idx, c in enumerate(ranking_completo) if c.id_pessoa == id_pessoa), None
|
(idx + 1 for idx, c in enumerate(ranking_completo) if c.id_pessoa == id_pessoa), None
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ class ObterRankingUseCase:
|
|||||||
return ConsultorDetalhadoDTO(
|
return ConsultorDetalhadoDTO(
|
||||||
id_pessoa=consultor.id_pessoa,
|
id_pessoa=consultor.id_pessoa,
|
||||||
nome=consultor.nome,
|
nome=consultor.nome,
|
||||||
cpf=consultor.cpf,
|
|
||||||
anos_atuacao=consultor.anos_atuacao,
|
anos_atuacao=consultor.anos_atuacao,
|
||||||
ativo=consultor.ativo,
|
ativo=consultor.ativo,
|
||||||
veterano=consultor.veterano,
|
veterano=consultor.veterano,
|
||||||
|
coordenador_ppg=consultor.coordenador_ppg,
|
||||||
coordenacoes_capes=[
|
coordenacoes_capes=[
|
||||||
CoordenacaoCapesDTO(
|
CoordenacaoCapesDTO(
|
||||||
codigo=cc.codigo,
|
codigo=cc.codigo,
|
||||||
@@ -75,6 +75,7 @@ class ObterRankingUseCase:
|
|||||||
),
|
),
|
||||||
areas_adicionais=cc.areas_adicionais,
|
areas_adicionais=cc.areas_adicionais,
|
||||||
ja_coordenou_antes=cc.ja_coordenou_antes,
|
ja_coordenou_antes=cc.ja_coordenou_antes,
|
||||||
|
presidente=cc.presidente,
|
||||||
)
|
)
|
||||||
for cc in consultor.coordenacoes_capes
|
for cc in consultor.coordenacoes_capes
|
||||||
],
|
],
|
||||||
@@ -117,6 +118,7 @@ class ObterRankingUseCase:
|
|||||||
tipo=p.tipo,
|
tipo=p.tipo,
|
||||||
nome_premio=p.nome_premio,
|
nome_premio=p.nome_premio,
|
||||||
ano=p.ano,
|
ano=p.ano,
|
||||||
|
papel=p.papel,
|
||||||
)
|
)
|
||||||
for p in consultor.premiacoes
|
for p in consultor.premiacoes
|
||||||
],
|
],
|
||||||
@@ -143,6 +145,9 @@ class ObterRankingUseCase:
|
|||||||
tipo=o.tipo,
|
tipo=o.tipo,
|
||||||
nivel=o.nivel,
|
nivel=o.nivel,
|
||||||
ano=o.ano,
|
ano=o.ano,
|
||||||
|
coorientacao=o.coorientacao,
|
||||||
|
premiada=o.premiada,
|
||||||
|
premiacao_tipo=o.premiacao_tipo,
|
||||||
)
|
)
|
||||||
for o in consultor.orientacoes
|
for o in consultor.orientacoes
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class CoordenacaoCapes:
|
|||||||
periodo: Periodo
|
periodo: Periodo
|
||||||
areas_adicionais: List[str] = field(default_factory=list)
|
areas_adicionais: List[str] = field(default_factory=list)
|
||||||
ja_coordenou_antes: bool = False
|
ja_coordenou_antes: bool = False
|
||||||
|
presidente: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -51,6 +52,7 @@ class Premiacao:
|
|||||||
tipo: str
|
tipo: str
|
||||||
nome_premio: str
|
nome_premio: str
|
||||||
ano: int
|
ano: int
|
||||||
|
papel: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -74,6 +76,9 @@ class Orientacao:
|
|||||||
tipo: str
|
tipo: str
|
||||||
nivel: str
|
nivel: str
|
||||||
ano: Optional[int] = None
|
ano: Optional[int] = None
|
||||||
|
coorientacao: bool = False
|
||||||
|
premiada: bool = False
|
||||||
|
premiacao_tipo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -89,6 +94,7 @@ class Consultor:
|
|||||||
id_pessoa: int
|
id_pessoa: int
|
||||||
nome: str
|
nome: str
|
||||||
cpf: Optional[str] = None
|
cpf: Optional[str] = None
|
||||||
|
coordenador_ppg: bool = False
|
||||||
coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list)
|
coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list)
|
||||||
consultoria: Optional[Consultoria] = None
|
consultoria: Optional[Consultoria] = None
|
||||||
inscricoes: List[Inscricao] = field(default_factory=list)
|
inscricoes: List[Inscricao] = field(default_factory=list)
|
||||||
|
|||||||
@@ -89,9 +89,18 @@ class CalculadorPontuacao:
|
|||||||
tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo)
|
tempo = min(anos_total * criterio.multiplicador_tempo, criterio.teto_tempo)
|
||||||
|
|
||||||
bonus = 0
|
bonus = 0
|
||||||
if consultoria.anos_consecutivos >= 8 and criterio.bonus_continuidade_8anos:
|
|
||||||
bonus += criterio.bonus_continuidade_8anos
|
# Bônus de continuidade (escalonado, não cumulativo) - apenas CONS_ATIVO
|
||||||
if codigo == "CONS_ATIVO" and consultoria.retornos > 0 and criterio.bonus_retorno:
|
if codigo == "CONS_ATIVO":
|
||||||
|
if consultoria.anos_consecutivos >= 8:
|
||||||
|
bonus += 15
|
||||||
|
elif consultoria.anos_consecutivos >= 5:
|
||||||
|
bonus += 10
|
||||||
|
elif consultoria.anos_consecutivos >= 3:
|
||||||
|
bonus += 5
|
||||||
|
|
||||||
|
# Bônus de retorno (uma vez) - apenas CONS_ATIVO
|
||||||
|
if consultoria.retornos > 0 and criterio.bonus_retorno:
|
||||||
bonus += criterio.bonus_retorno
|
bonus += criterio.bonus_retorno
|
||||||
|
|
||||||
total_bruto = base + tempo + bonus
|
total_bruto = base + tempo + bonus
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
pontua_tempo=True,
|
pontua_tempo=True,
|
||||||
multiplicador_tempo=5,
|
multiplicador_tempo=5,
|
||||||
teto_tempo=50,
|
teto_tempo=50,
|
||||||
bonus_continuidade_8anos=20,
|
|
||||||
bonus_retorno=15,
|
bonus_retorno=15,
|
||||||
),
|
),
|
||||||
"CONS_HIST": CriterioPontuacao(
|
"CONS_HIST": CriterioPontuacao(
|
||||||
@@ -113,7 +112,6 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
pontua_tempo=True,
|
pontua_tempo=True,
|
||||||
multiplicador_tempo=5,
|
multiplicador_tempo=5,
|
||||||
teto_tempo=50,
|
teto_tempo=50,
|
||||||
bonus_continuidade_8anos=20,
|
|
||||||
),
|
),
|
||||||
"CONS_FALECIDO": CriterioPontuacao(
|
"CONS_FALECIDO": CriterioPontuacao(
|
||||||
codigo="CONS_FALECIDO",
|
codigo="CONS_FALECIDO",
|
||||||
@@ -124,7 +122,6 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
pontua_tempo=True,
|
pontua_tempo=True,
|
||||||
multiplicador_tempo=5,
|
multiplicador_tempo=5,
|
||||||
teto_tempo=50,
|
teto_tempo=50,
|
||||||
bonus_continuidade_8anos=20,
|
|
||||||
),
|
),
|
||||||
"INSC_AUTOR": CriterioPontuacao(
|
"INSC_AUTOR": CriterioPontuacao(
|
||||||
codigo="INSC_AUTOR",
|
codigo="INSC_AUTOR",
|
||||||
@@ -154,7 +151,7 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PAPEL,
|
tipo=TipoAtuacao.PAPEL,
|
||||||
base=50,
|
base=50,
|
||||||
teto=80,
|
teto=100,
|
||||||
bonus_recorrencia_anual=3,
|
bonus_recorrencia_anual=3,
|
||||||
teto_recorrencia=20,
|
teto_recorrencia=20,
|
||||||
),
|
),
|
||||||
@@ -187,9 +184,23 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
codigo="BOL_BPQ_INTERMEDIARIO",
|
codigo="BOL_BPQ_INTERMEDIARIO",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
||||||
|
base=50,
|
||||||
|
teto=100,
|
||||||
|
),
|
||||||
|
"BOL_BPQ_SUP": CriterioPontuacao(
|
||||||
|
codigo="BOL_BPQ_SUP",
|
||||||
|
bloco=Bloco.D,
|
||||||
|
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
||||||
base=30,
|
base=30,
|
||||||
teto=60,
|
teto=60,
|
||||||
),
|
),
|
||||||
|
"BOL_BPQ_INT": CriterioPontuacao(
|
||||||
|
codigo="BOL_BPQ_INT",
|
||||||
|
bloco=Bloco.D,
|
||||||
|
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
||||||
|
base=50,
|
||||||
|
teto=100,
|
||||||
|
),
|
||||||
"PREMIACAO": CriterioPontuacao(
|
"PREMIACAO": CriterioPontuacao(
|
||||||
codigo="PREMIACAO",
|
codigo="PREMIACAO",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
@@ -201,14 +212,14 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
codigo="PREMIACAO_GP",
|
codigo="PREMIACAO_GP",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
||||||
base=50,
|
base=30,
|
||||||
teto=60,
|
teto=60,
|
||||||
),
|
),
|
||||||
"MENCAO": CriterioPontuacao(
|
"MENCAO": CriterioPontuacao(
|
||||||
codigo="MENCAO",
|
codigo="MENCAO",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
tipo=TipoAtuacao.COMPETENCIA_RECONHECIMENTO,
|
||||||
base=30,
|
base=10,
|
||||||
teto=20,
|
teto=20,
|
||||||
),
|
),
|
||||||
"EVENTO": CriterioPontuacao(
|
"EVENTO": CriterioPontuacao(
|
||||||
@@ -230,63 +241,63 @@ CRITERIOS: Dict[str, CriterioPontuacao] = {
|
|||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=15,
|
base=15,
|
||||||
teto=0,
|
teto=100,
|
||||||
),
|
),
|
||||||
"ORIENT_TESE": CriterioPontuacao(
|
"ORIENT_TESE": CriterioPontuacao(
|
||||||
codigo="ORIENT_TESE",
|
codigo="ORIENT_TESE",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=10,
|
base=10,
|
||||||
teto=0,
|
teto=50,
|
||||||
),
|
),
|
||||||
"ORIENT_DISS": CriterioPontuacao(
|
"ORIENT_DISS": CriterioPontuacao(
|
||||||
codigo="ORIENT_DISS",
|
codigo="ORIENT_DISS",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=5,
|
base=5,
|
||||||
teto=0,
|
teto=25,
|
||||||
),
|
),
|
||||||
"CO_ORIENT_POS_DOC": CriterioPontuacao(
|
"CO_ORIENT_POS_DOC": CriterioPontuacao(
|
||||||
codigo="CO_ORIENT_POS_DOC",
|
codigo="CO_ORIENT_POS_DOC",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=7,
|
base=7,
|
||||||
teto=0,
|
teto=35,
|
||||||
),
|
),
|
||||||
"CO_ORIENT_TESE": CriterioPontuacao(
|
"CO_ORIENT_TESE": CriterioPontuacao(
|
||||||
codigo="CO_ORIENT_TESE",
|
codigo="CO_ORIENT_TESE",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=5,
|
base=5,
|
||||||
teto=0,
|
teto=25,
|
||||||
),
|
),
|
||||||
"CO_ORIENT_DISS": CriterioPontuacao(
|
"CO_ORIENT_DISS": CriterioPontuacao(
|
||||||
codigo="CO_ORIENT_DISS",
|
codigo="CO_ORIENT_DISS",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=3,
|
base=3,
|
||||||
teto=0,
|
teto=15,
|
||||||
),
|
),
|
||||||
"MB_BANCA_POS_DOC": CriterioPontuacao(
|
"MB_BANCA_POS_DOC": CriterioPontuacao(
|
||||||
codigo="MB_BANCA_POS_DOC",
|
codigo="MB_BANCA_POS_DOC",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=3,
|
base=3,
|
||||||
teto=0,
|
teto=15,
|
||||||
),
|
),
|
||||||
"MB_BANCA_TESE": CriterioPontuacao(
|
"MB_BANCA_TESE": CriterioPontuacao(
|
||||||
codigo="MB_BANCA_TESE",
|
codigo="MB_BANCA_TESE",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=3,
|
base=3,
|
||||||
teto=0,
|
teto=15,
|
||||||
),
|
),
|
||||||
"MB_BANCA_DISS": CriterioPontuacao(
|
"MB_BANCA_DISS": CriterioPontuacao(
|
||||||
codigo="MB_BANCA_DISS",
|
codigo="MB_BANCA_DISS",
|
||||||
bloco=Bloco.D,
|
bloco=Bloco.D,
|
||||||
tipo=TipoAtuacao.PARTICIPACAO,
|
tipo=TipoAtuacao.PARTICIPACAO,
|
||||||
base=2,
|
base=2,
|
||||||
teto=0,
|
teto=10,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ from typing import Optional, Dict, Any
|
|||||||
|
|
||||||
|
|
||||||
class ElasticsearchClient:
|
class ElasticsearchClient:
|
||||||
def __init__(self, url: str, index: str, user: str = "", password: str = ""):
|
def __init__(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
index: str,
|
||||||
|
user: str = "",
|
||||||
|
password: str = "",
|
||||||
|
verify_ssl: bool = True,
|
||||||
|
):
|
||||||
self.url = url.rstrip("/")
|
self.url = url.rstrip("/")
|
||||||
self.index = index
|
self.index = index
|
||||||
self.user = user
|
self.user = user
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.verify_ssl = verify_ssl
|
||||||
self._client: Optional[httpx.AsyncClient] = None
|
self._client: Optional[httpx.AsyncClient] = None
|
||||||
|
|
||||||
async def connect(self) -> None:
|
async def connect(self) -> None:
|
||||||
@@ -21,7 +29,7 @@ class ElasticsearchClient:
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
},
|
},
|
||||||
verify=False,
|
verify=self.verify_ssl,
|
||||||
timeout=120.0
|
timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
115
backend/src/infrastructure/ranking_store.py
Normal file
115
backend/src/infrastructure/ranking_store.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RankingEntry:
|
||||||
|
id_pessoa: int
|
||||||
|
nome: str
|
||||||
|
posicao: int
|
||||||
|
pontuacao_total: int
|
||||||
|
bloco_a: int
|
||||||
|
bloco_b: int
|
||||||
|
bloco_c: int
|
||||||
|
bloco_d: int
|
||||||
|
ativo: bool
|
||||||
|
anos_atuacao: float
|
||||||
|
detalhes: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class RankingStore:
|
||||||
|
"""
|
||||||
|
Armazena o ranking pré-calculado em memória.
|
||||||
|
|
||||||
|
Fonte única de dados: Elasticsearch (AtuaCAPES).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._entries: List[RankingEntry] = []
|
||||||
|
self._last_update: Optional[datetime] = None
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_update(self) -> Optional[datetime]:
|
||||||
|
return self._last_update
|
||||||
|
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
return bool(self._entries)
|
||||||
|
|
||||||
|
async def set_entries(self, entries: List[RankingEntry]) -> None:
|
||||||
|
async with self._lock:
|
||||||
|
self._entries = entries
|
||||||
|
self._last_update = datetime.now()
|
||||||
|
|
||||||
|
def total(self, filtro_ativo: Optional[bool] = None) -> int:
|
||||||
|
if filtro_ativo is None:
|
||||||
|
return len(self._entries)
|
||||||
|
return sum(1 for e in self._entries if e.ativo == filtro_ativo)
|
||||||
|
|
||||||
|
def get_page(
|
||||||
|
self, page: int, size: int, filtro_ativo: Optional[bool] = None
|
||||||
|
) -> Tuple[int, List[RankingEntry]]:
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if size < 1:
|
||||||
|
size = 1
|
||||||
|
|
||||||
|
if filtro_ativo is None:
|
||||||
|
entries = self._entries
|
||||||
|
else:
|
||||||
|
entries = [e for e in self._entries if e.ativo == filtro_ativo]
|
||||||
|
|
||||||
|
total = len(entries)
|
||||||
|
start = (page - 1) * size
|
||||||
|
end = start + size
|
||||||
|
return total, entries[start:end]
|
||||||
|
|
||||||
|
def get_slice(
|
||||||
|
self, offset: int, limit: int, filtro_ativo: Optional[bool] = None
|
||||||
|
) -> Tuple[int, List[RankingEntry]]:
|
||||||
|
if offset < 0:
|
||||||
|
offset = 0
|
||||||
|
if limit < 1:
|
||||||
|
limit = 1
|
||||||
|
|
||||||
|
if filtro_ativo is None:
|
||||||
|
entries = self._entries
|
||||||
|
else:
|
||||||
|
entries = [e for e in self._entries if e.ativo == filtro_ativo]
|
||||||
|
|
||||||
|
total = len(entries)
|
||||||
|
return total, entries[offset : offset + limit]
|
||||||
|
|
||||||
|
def buscar_por_nome(self, nome: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||||
|
palavras = [p.strip() for p in (nome or "").upper().split() if len(p.strip()) >= 2]
|
||||||
|
if not palavras:
|
||||||
|
return []
|
||||||
|
|
||||||
|
results: List[Dict[str, Any]] = []
|
||||||
|
for e in self._entries:
|
||||||
|
nome_e = (e.nome or "").upper()
|
||||||
|
if all(p in nome_e for p in palavras):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"ID_PESSOA": e.id_pessoa,
|
||||||
|
"NOME": e.nome,
|
||||||
|
"POSICAO": e.posicao,
|
||||||
|
"PONTUACAO_TOTAL": float(e.pontuacao_total),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_by_id(self, id_pessoa: int) -> Optional[RankingEntry]:
|
||||||
|
for e in self._entries:
|
||||||
|
if e.id_pessoa == id_pessoa:
|
||||||
|
return e
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
ranking_store = RankingStore()
|
||||||
@@ -21,13 +21,12 @@ from ...domain.services.calculador_pontuacao import CalculadorPontuacao
|
|||||||
from ...domain.value_objects.periodo import Periodo, mesclar_periodos
|
from ...domain.value_objects.periodo import Periodo, mesclar_periodos
|
||||||
from ..cache import ranking_cache
|
from ..cache import ranking_cache
|
||||||
from ..elasticsearch.client import ElasticsearchClient
|
from ..elasticsearch.client import ElasticsearchClient
|
||||||
from ..oracle.client import OracleClient
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConsultorRepositoryImpl(ConsultorRepository):
|
class ConsultorRepositoryImpl(ConsultorRepository):
|
||||||
def __init__(self, es_client: ElasticsearchClient, oracle_client: OracleClient = None):
|
def __init__(self, es_client: ElasticsearchClient, oracle_client=None):
|
||||||
self.es_client = es_client
|
self.es_client = es_client
|
||||||
self.oracle_client = oracle_client
|
self.oracle_client = oracle_client
|
||||||
self.calculador = CalculadorPontuacao()
|
self.calculador = CalculadorPontuacao()
|
||||||
@@ -75,6 +74,8 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
resultado = []
|
resultado = []
|
||||||
for coord in coordenacoes:
|
for coord in coordenacoes:
|
||||||
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
|
dados_coord = coord.get("dadosCoordenacaoArea", {}) or {}
|
||||||
|
tipo_coord_raw = str(dados_coord.get("tipo", "") or coord.get("nome", "") or "")
|
||||||
|
presidente = "presidente" in tipo_coord_raw.lower()
|
||||||
inicio = self._parse_date(dados_coord.get("inicioVinculacao")) or self._parse_date(coord.get("inicio"))
|
inicio = self._parse_date(dados_coord.get("inicioVinculacao")) or self._parse_date(coord.get("inicio"))
|
||||||
if not inicio:
|
if not inicio:
|
||||||
continue
|
continue
|
||||||
@@ -97,10 +98,38 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
periodo=Periodo(inicio=inicio, fim=fim),
|
periodo=Periodo(inicio=inicio, fim=fim),
|
||||||
areas_adicionais=[],
|
areas_adicionais=[],
|
||||||
ja_coordenou_antes=len(resultado) > 0,
|
ja_coordenou_antes=len(resultado) > 0,
|
||||||
|
presidente=presidente,
|
||||||
))
|
))
|
||||||
|
|
||||||
return resultado
|
return resultado
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _inferir_premiacao_tipo(texto: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Retorna o tipo de premiação em forma normalizada para uso em selos/hints.
|
||||||
|
"""
|
||||||
|
t = (texto or "").lower()
|
||||||
|
if "grande prêmio" in t or "grande premio" in t:
|
||||||
|
return "GP"
|
||||||
|
if "menção" in t or "mencao" in t or "honrosa" in t:
|
||||||
|
return "MENCAO"
|
||||||
|
if "prêmio" in t or "premio" in t:
|
||||||
|
return "PREMIO"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _tem_coordenacao_ppg(self, atuacoes: List[Dict[str, Any]]) -> bool:
|
||||||
|
"""
|
||||||
|
Selo PPG_COORD: marca consultor que possui atuação de gestão/coordenação de programa no ATUACAPES.
|
||||||
|
A pontuação do PPG é reservada (V1), mas o selo é exibido.
|
||||||
|
"""
|
||||||
|
for a in atuacoes:
|
||||||
|
if a.get("dadosGestaoPrograma"):
|
||||||
|
return True
|
||||||
|
tipo = str(a.get("tipo", "") or "").lower()
|
||||||
|
if "programa" in tipo and ("coord" in tipo or "gest" in tipo):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]:
|
def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]:
|
||||||
consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]]
|
consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]]
|
||||||
if not consultorias:
|
if not consultorias:
|
||||||
@@ -248,17 +277,19 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
|
dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
|
||||||
tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
|
tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
|
||||||
nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "")
|
nome_premio = dados.get("nomePremio") or dados.get("evento") or a.get("descricao", "")
|
||||||
|
papel = dados.get("papelPessoa") or dados.get("papel") or None
|
||||||
ano = dados.get("ano")
|
ano = dados.get("ano")
|
||||||
if not ano:
|
if not ano:
|
||||||
inicio = self._parse_date(a.get("inicio"))
|
inicio = self._parse_date(a.get("inicio"))
|
||||||
ano = inicio.year if inicio else datetime.now().year
|
ano = inicio.year if inicio else datetime.now().year
|
||||||
|
|
||||||
tipo_lower = tipo_premiacao.lower()
|
tipo_lower = str(tipo_premiacao).lower()
|
||||||
nome_lower = nome_premio.lower()
|
nome_lower = str(nome_premio).lower()
|
||||||
|
tipo_norm = self._inferir_premiacao_tipo(f"{tipo_lower} {nome_lower}")
|
||||||
|
|
||||||
if "grande" in nome_lower or "grande" in tipo_lower:
|
if tipo_norm == "GP":
|
||||||
codigo = "PREMIACAO"
|
codigo = "PREMIACAO"
|
||||||
elif "menção" in tipo_lower or "mencao" in tipo_lower or "honrosa" in tipo_lower:
|
elif tipo_norm == "MENCAO":
|
||||||
codigo = "MENCAO"
|
codigo = "MENCAO"
|
||||||
else:
|
else:
|
||||||
codigo = "PREMIACAO_GP"
|
codigo = "PREMIACAO_GP"
|
||||||
@@ -268,6 +299,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
tipo=tipo_premiacao,
|
tipo=tipo_premiacao,
|
||||||
nome_premio=nome_premio,
|
nome_premio=nome_premio,
|
||||||
ano=ano,
|
ano=ano,
|
||||||
|
papel=papel,
|
||||||
))
|
))
|
||||||
|
|
||||||
return premiacoes
|
return premiacoes
|
||||||
@@ -283,10 +315,10 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
area = dados.get("areaConhecimento", "") or ""
|
area = dados.get("areaConhecimento", "") or ""
|
||||||
|
|
||||||
nivel_lower = nivel.lower()
|
nivel_lower = nivel.lower()
|
||||||
if "1a" in nivel_lower or "1b" in nivel_lower or "1c" in nivel_lower or "1d" in nivel_lower:
|
if "1d" in nivel_lower or "2" in nivel_lower:
|
||||||
codigo = "BOL_BPQ_SUPERIOR"
|
codigo = "BOL_BPQ_INT"
|
||||||
else:
|
else:
|
||||||
codigo = "BOL_BPQ_INTERMEDIARIO"
|
codigo = "BOL_BPQ_SUP"
|
||||||
|
|
||||||
bolsas.append(BolsaCNPQ(
|
bolsas.append(BolsaCNPQ(
|
||||||
codigo=codigo,
|
codigo=codigo,
|
||||||
@@ -331,6 +363,15 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
|
|
||||||
dados = a.get("dadosOrientacao", {}) or {}
|
dados = a.get("dadosOrientacao", {}) or {}
|
||||||
nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
|
nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
|
||||||
|
premio_texto = (
|
||||||
|
dados.get("premiacao")
|
||||||
|
or dados.get("tipoPremiacao")
|
||||||
|
or dados.get("premio")
|
||||||
|
or dados.get("resultado")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
tipo_prem = self._inferir_premiacao_tipo(str(premio_texto))
|
||||||
|
premiada = tipo_prem is not None
|
||||||
ano = dados.get("ano")
|
ano = dados.get("ano")
|
||||||
if not ano:
|
if not ano:
|
||||||
inicio = self._parse_date(a.get("inicio"))
|
inicio = self._parse_date(a.get("inicio"))
|
||||||
@@ -349,6 +390,9 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
tipo=tipo,
|
tipo=tipo,
|
||||||
nivel=nivel,
|
nivel=nivel,
|
||||||
ano=ano,
|
ano=ano,
|
||||||
|
coorientacao=False,
|
||||||
|
premiada=premiada,
|
||||||
|
premiacao_tipo=tipo_prem,
|
||||||
))
|
))
|
||||||
|
|
||||||
return orientacoes
|
return orientacoes
|
||||||
@@ -363,6 +407,15 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
|
|
||||||
dados = a.get("dadosOrientacao", {}) or a.get("dadosCoorientacao", {}) or {}
|
dados = a.get("dadosOrientacao", {}) or a.get("dadosCoorientacao", {}) or {}
|
||||||
nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
|
nivel = dados.get("nivel", "") or dados.get("tipo", "") or ""
|
||||||
|
premio_texto = (
|
||||||
|
dados.get("premiacao")
|
||||||
|
or dados.get("tipoPremiacao")
|
||||||
|
or dados.get("premio")
|
||||||
|
or dados.get("resultado")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
tipo_prem = self._inferir_premiacao_tipo(str(premio_texto))
|
||||||
|
premiada = tipo_prem is not None
|
||||||
ano = dados.get("ano")
|
ano = dados.get("ano")
|
||||||
if not ano:
|
if not ano:
|
||||||
inicio = self._parse_date(a.get("inicio"))
|
inicio = self._parse_date(a.get("inicio"))
|
||||||
@@ -381,6 +434,9 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
tipo=tipo,
|
tipo=tipo,
|
||||||
nivel=nivel,
|
nivel=nivel,
|
||||||
ano=ano,
|
ano=ano,
|
||||||
|
coorientacao=True,
|
||||||
|
premiada=premiada,
|
||||||
|
premiacao_tipo=tipo_prem,
|
||||||
))
|
))
|
||||||
|
|
||||||
return coorientacoes
|
return coorientacoes
|
||||||
@@ -432,11 +488,13 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
orientacoes = self._extrair_orientacoes(atuacoes)
|
orientacoes = self._extrair_orientacoes(atuacoes)
|
||||||
coorientacoes = self._extrair_coorientacoes(atuacoes)
|
coorientacoes = self._extrair_coorientacoes(atuacoes)
|
||||||
membros_banca = self._extrair_membros_banca(atuacoes)
|
membros_banca = self._extrair_membros_banca(atuacoes)
|
||||||
|
coordenador_ppg = self._tem_coordenacao_ppg(atuacoes)
|
||||||
|
|
||||||
consultor = Consultor(
|
consultor = Consultor(
|
||||||
id_pessoa=id_pessoa,
|
id_pessoa=id_pessoa,
|
||||||
nome=dados_pessoais.get("nome", "N/A"),
|
nome=dados_pessoais.get("nome", "N/A"),
|
||||||
cpf=dados_pessoais.get("cpf"),
|
cpf=dados_pessoais.get("cpf"),
|
||||||
|
coordenador_ppg=coordenador_ppg,
|
||||||
coordenacoes_capes=coordenacoes_capes,
|
coordenacoes_capes=coordenacoes_capes,
|
||||||
consultoria=consultoria,
|
consultoria=consultoria,
|
||||||
inscricoes=inscricoes,
|
inscricoes=inscricoes,
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ from .routes import router
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .dependencies import (
|
from .dependencies import es_client, get_processar_job
|
||||||
es_client,
|
|
||||||
oracle_local_client,
|
|
||||||
oracle_remote_client,
|
|
||||||
get_processar_job,
|
|
||||||
get_popular_componente_b_job,
|
|
||||||
)
|
|
||||||
from ...application.jobs.scheduler import RankingScheduler
|
from ...application.jobs.scheduler import RankingScheduler
|
||||||
|
|
||||||
|
|
||||||
@@ -22,24 +16,12 @@ from ...application.jobs.scheduler import RankingScheduler
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
await es_client.connect()
|
await es_client.connect()
|
||||||
|
|
||||||
try:
|
|
||||||
oracle_local_client.connect()
|
|
||||||
logger.info("Oracle LOCAL conectado (Docker)")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Oracle LOCAL não conectou: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
oracle_remote_client.connect()
|
|
||||||
logger.info("Oracle REMOTO conectado (CAPES)")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Oracle REMOTO não conectou: {e}. Sistema rodando sem Componente B (PPG).")
|
|
||||||
|
|
||||||
scheduler = None
|
scheduler = None
|
||||||
try:
|
try:
|
||||||
|
if settings.SCHEDULER_ENABLED:
|
||||||
job = get_processar_job()
|
job = get_processar_job()
|
||||||
job_b = get_popular_componente_b_job()
|
scheduler = RankingScheduler(job)
|
||||||
scheduler = RankingScheduler(job, job_componente_b=job_b)
|
await scheduler.iniciar(hora_alvo=settings.SCHEDULER_HOUR)
|
||||||
await scheduler.iniciar()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Scheduler não iniciou: {e}")
|
logger.warning(f"Scheduler não iniciou: {e}")
|
||||||
|
|
||||||
@@ -53,16 +35,6 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
await es_client.close()
|
await es_client.close()
|
||||||
|
|
||||||
try:
|
|
||||||
oracle_local_client.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
oracle_remote_client.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Ranking de Consultores CAPES",
|
title="Ranking de Consultores CAPES",
|
||||||
|
|||||||
@@ -10,16 +10,7 @@ class Settings(BaseSettings):
|
|||||||
ES_INDEX: str = "atuacapes"
|
ES_INDEX: str = "atuacapes"
|
||||||
ES_USER: str = ""
|
ES_USER: str = ""
|
||||||
ES_PASSWORD: str = ""
|
ES_PASSWORD: str = ""
|
||||||
|
ES_VERIFY_SSL: bool = True
|
||||||
# Oracle LOCAL (Docker) - Para salvar ranking
|
|
||||||
ORACLE_LOCAL_USER: str
|
|
||||||
ORACLE_LOCAL_PASSWORD: str
|
|
||||||
ORACLE_LOCAL_DSN: str
|
|
||||||
|
|
||||||
# Oracle REMOTO (CAPES) - Para ler dados de programas
|
|
||||||
ORACLE_REMOTE_USER: str
|
|
||||||
ORACLE_REMOTE_PASSWORD: str
|
|
||||||
ORACLE_REMOTE_DSN: str
|
|
||||||
|
|
||||||
API_HOST: str = "0.0.0.0"
|
API_HOST: str = "0.0.0.0"
|
||||||
API_PORT: int = 8000
|
API_PORT: int = 8000
|
||||||
@@ -28,6 +19,8 @@ class Settings(BaseSettings):
|
|||||||
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
||||||
|
|
||||||
LOG_LEVEL: str = "INFO"
|
LOG_LEVEL: str = "INFO"
|
||||||
|
SCHEDULER_ENABLED: bool = False
|
||||||
|
SCHEDULER_HOUR: int = 3
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cors_origins_list(self) -> List[str]:
|
def cors_origins_list(self) -> List[str]:
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
||||||
from ...infrastructure.oracle.client import OracleClient
|
|
||||||
from ...infrastructure.oracle.ranking_repository import RankingOracleRepository
|
|
||||||
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
|
||||||
from ...application.jobs.processar_ranking import ProcessarRankingJob
|
from ...application.jobs.processar_ranking import ProcessarRankingJob
|
||||||
from ...application.jobs.popular_componente_b_job import PopularComponenteBJob
|
from ...infrastructure.ranking_store import ranking_store, RankingStore
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -11,41 +9,23 @@ es_client = ElasticsearchClient(
|
|||||||
url=settings.ES_URL,
|
url=settings.ES_URL,
|
||||||
index=settings.ES_INDEX,
|
index=settings.ES_INDEX,
|
||||||
user=settings.ES_USER,
|
user=settings.ES_USER,
|
||||||
password=settings.ES_PASSWORD
|
password=settings.ES_PASSWORD,
|
||||||
)
|
verify_ssl=settings.ES_VERIFY_SSL,
|
||||||
|
|
||||||
# Oracle LOCAL (Docker) - Para salvar ranking
|
|
||||||
oracle_local_client = OracleClient(
|
|
||||||
user=settings.ORACLE_LOCAL_USER,
|
|
||||||
password=settings.ORACLE_LOCAL_PASSWORD,
|
|
||||||
dsn=settings.ORACLE_LOCAL_DSN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Oracle REMOTO (CAPES) - Para ler dados de programas
|
|
||||||
oracle_remote_client = OracleClient(
|
|
||||||
user=settings.ORACLE_REMOTE_USER,
|
|
||||||
password=settings.ORACLE_REMOTE_PASSWORD,
|
|
||||||
dsn=settings.ORACLE_REMOTE_DSN
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_repository: ConsultorRepositoryImpl = None
|
_repository: ConsultorRepositoryImpl = None
|
||||||
_ranking_repository: RankingOracleRepository = None
|
|
||||||
_processar_job: ProcessarRankingJob = None
|
_processar_job: ProcessarRankingJob = None
|
||||||
_popular_b_job: PopularComponenteBJob = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_repository() -> ConsultorRepositoryImpl:
|
def get_repository() -> ConsultorRepositoryImpl:
|
||||||
global _repository
|
global _repository
|
||||||
if _repository is None:
|
if _repository is None:
|
||||||
_repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_remote_client)
|
_repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=None)
|
||||||
return _repository
|
return _repository
|
||||||
|
|
||||||
|
|
||||||
def get_ranking_repository() -> RankingOracleRepository:
|
def get_ranking_store() -> RankingStore:
|
||||||
global _ranking_repository
|
return ranking_store
|
||||||
if _ranking_repository is None:
|
|
||||||
_ranking_repository = RankingOracleRepository(oracle_client=oracle_local_client)
|
|
||||||
return _ranking_repository
|
|
||||||
|
|
||||||
|
|
||||||
def get_processar_job() -> ProcessarRankingJob:
|
def get_processar_job() -> ProcessarRankingJob:
|
||||||
@@ -53,18 +33,6 @@ def get_processar_job() -> ProcessarRankingJob:
|
|||||||
if _processar_job is None:
|
if _processar_job is None:
|
||||||
_processar_job = ProcessarRankingJob(
|
_processar_job = ProcessarRankingJob(
|
||||||
es_client=es_client,
|
es_client=es_client,
|
||||||
oracle_remote_client=oracle_remote_client,
|
ranking_store=ranking_store,
|
||||||
oracle_local_client=oracle_local_client,
|
|
||||||
ranking_repo=get_ranking_repository()
|
|
||||||
)
|
)
|
||||||
return _processar_job
|
return _processar_job
|
||||||
|
|
||||||
|
|
||||||
def get_popular_componente_b_job() -> PopularComponenteBJob:
|
|
||||||
global _popular_b_job
|
|
||||||
if _popular_b_job is None:
|
|
||||||
_popular_b_job = PopularComponenteBJob(
|
|
||||||
oracle_local_client=oracle_local_client,
|
|
||||||
oracle_remote_client=oracle_remote_client
|
|
||||||
)
|
|
||||||
return _popular_b_job
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from typing import Optional, List
|
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,
|
||||||
@@ -20,7 +21,7 @@ from ..schemas.ranking_schema import (
|
|||||||
ProcessarRankingResponseSchema,
|
ProcessarRankingResponseSchema,
|
||||||
ConsultaNomeSchema,
|
ConsultaNomeSchema,
|
||||||
)
|
)
|
||||||
from .dependencies import get_repository, get_ranking_repository, get_processar_job
|
from .dependencies import get_repository, get_ranking_store, get_processar_job
|
||||||
from ...application.jobs.job_status import job_status
|
from ...application.jobs.job_status import job_status
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
||||||
@@ -34,20 +35,35 @@ async def obter_ranking(
|
|||||||
default=None, description="Filtrar por bloco (a, c, d)"
|
default=None, description="Filtrar por bloco (a, c, d)"
|
||||||
),
|
),
|
||||||
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
||||||
|
store = Depends(get_ranking_store),
|
||||||
):
|
):
|
||||||
use_case = ObterRankingUseCase(repository=repository)
|
if store.is_ready():
|
||||||
consultores_dto = await use_case.executar(limite=limite, componente=componente)
|
total, entries = store.get_slice(offset=offset, limit=limite)
|
||||||
|
|
||||||
total = await repository.contar_total()
|
|
||||||
|
|
||||||
consultores_schema = [
|
consultores_schema = [
|
||||||
ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto
|
ConsultorResumoSchema(
|
||||||
|
id_pessoa=e.id_pessoa,
|
||||||
|
nome=e.nome,
|
||||||
|
anos_atuacao=e.anos_atuacao,
|
||||||
|
ativo=e.ativo,
|
||||||
|
veterano=e.anos_atuacao >= 10,
|
||||||
|
pontuacao_total=e.pontuacao_total,
|
||||||
|
bloco_a=e.bloco_a,
|
||||||
|
bloco_c=e.bloco_c,
|
||||||
|
bloco_d=e.bloco_d,
|
||||||
|
rank=e.posicao,
|
||||||
|
)
|
||||||
|
for e in entries
|
||||||
]
|
]
|
||||||
|
|
||||||
return RankingResponseSchema(
|
return RankingResponseSchema(
|
||||||
total=total, limite=limite, offset=offset, consultores=consultores_schema
|
total=total, limite=limite, offset=offset, consultores=consultores_schema
|
||||||
)
|
)
|
||||||
|
|
||||||
|
use_case = ObterRankingUseCase(repository=repository)
|
||||||
|
consultores_dto = await use_case.executar(limite=limite, componente=componente)
|
||||||
|
total = await repository.contar_total()
|
||||||
|
consultores_schema = [ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto]
|
||||||
|
return RankingResponseSchema(total=total, limite=limite, offset=offset, consultores=consultores_schema)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema)
|
@router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema)
|
||||||
async def obter_ranking_detalhado(
|
async def obter_ranking_detalhado(
|
||||||
@@ -73,9 +89,15 @@ async def obter_ranking_detalhado(
|
|||||||
async def obter_consultor(
|
async def obter_consultor(
|
||||||
id_pessoa: int,
|
id_pessoa: int,
|
||||||
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
||||||
|
store = Depends(get_ranking_store),
|
||||||
):
|
):
|
||||||
use_case = ObterConsultorUseCase(repository=repository)
|
use_case = ObterConsultorUseCase(repository=repository)
|
||||||
consultor = await use_case.executar(id_pessoa=id_pessoa)
|
rank = None
|
||||||
|
if store.is_ready():
|
||||||
|
found = store.get_by_id(id_pessoa)
|
||||||
|
rank = found.posicao if found else None
|
||||||
|
|
||||||
|
consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank)
|
||||||
|
|
||||||
if not consultor:
|
if not consultor:
|
||||||
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
|
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
|
||||||
@@ -93,14 +115,46 @@ async def ranking_paginado(
|
|||||||
page: int = Query(default=1, ge=1, description="Número da página"),
|
page: int = Query(default=1, ge=1, description="Número da página"),
|
||||||
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
|
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
|
||||||
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
|
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
|
||||||
ranking_repo = Depends(get_ranking_repository),
|
store = Depends(get_ranking_store),
|
||||||
):
|
):
|
||||||
total = ranking_repo.contar_total(filtro_ativo=ativo)
|
if not store.is_ready():
|
||||||
consultores = ranking_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo)
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||||
|
)
|
||||||
|
|
||||||
|
total, entries = store.get_page(page=page, size=size, filtro_ativo=ativo)
|
||||||
|
|
||||||
total_pages = (total + size - 1) // size
|
total_pages = (total + size - 1) // size
|
||||||
|
|
||||||
consultores_schema = [RankingMapper.consultor_ranking_to_schema(c) for c in consultores]
|
consultores_schema = []
|
||||||
|
for e in entries:
|
||||||
|
d = e.detalhes
|
||||||
|
consultores_schema.append(
|
||||||
|
ConsultorRankingResumoSchema(
|
||||||
|
id_pessoa=e.id_pessoa,
|
||||||
|
nome=e.nome,
|
||||||
|
posicao=e.posicao,
|
||||||
|
pontuacao_total=float(e.pontuacao_total),
|
||||||
|
bloco_a=float(e.bloco_a),
|
||||||
|
bloco_b=float(e.bloco_b),
|
||||||
|
bloco_c=float(e.bloco_c),
|
||||||
|
bloco_d=float(e.bloco_d),
|
||||||
|
ativo=e.ativo,
|
||||||
|
anos_atuacao=float(e.anos_atuacao),
|
||||||
|
coordenador_ppg=bool(d.get("coordenador_ppg", False)),
|
||||||
|
consultoria=d.get("consultoria"),
|
||||||
|
coordenacoes_capes=d.get("coordenacoes_capes"),
|
||||||
|
inscricoes=d.get("inscricoes"),
|
||||||
|
avaliacoes_comissao=d.get("avaliacoes_comissao"),
|
||||||
|
premiacoes=d.get("premiacoes"),
|
||||||
|
bolsas_cnpq=d.get("bolsas_cnpq"),
|
||||||
|
participacoes=d.get("participacoes"),
|
||||||
|
orientacoes=d.get("orientacoes"),
|
||||||
|
membros_banca=d.get("membros_banca"),
|
||||||
|
pontuacao=d.get("pontuacao"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return RankingPaginadoResponseSchema(
|
return RankingPaginadoResponseSchema(
|
||||||
total=total,
|
total=total,
|
||||||
@@ -115,9 +169,15 @@ async def ranking_paginado(
|
|||||||
async def buscar_por_nome(
|
async def buscar_por_nome(
|
||||||
nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"),
|
nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"),
|
||||||
limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"),
|
limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"),
|
||||||
ranking_repo = Depends(get_ranking_repository),
|
store = Depends(get_ranking_store),
|
||||||
):
|
):
|
||||||
resultados = ranking_repo.buscar_por_nome(nome=nome, limit=limit)
|
if not store.is_ready():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||||
|
)
|
||||||
|
|
||||||
|
resultados = store.buscar_por_nome(nome=nome, limit=limit)
|
||||||
return [
|
return [
|
||||||
ConsultaNomeSchema(
|
ConsultaNomeSchema(
|
||||||
id_pessoa=r["ID_PESSOA"],
|
id_pessoa=r["ID_PESSOA"],
|
||||||
@@ -131,10 +191,53 @@ async def buscar_por_nome(
|
|||||||
|
|
||||||
@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),
|
store = Depends(get_ranking_store),
|
||||||
):
|
):
|
||||||
estatisticas = ranking_repo.obter_estatisticas()
|
if not store.is_ready():
|
||||||
distribuicao = ranking_repo.obter_distribuicao()
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||||
|
)
|
||||||
|
|
||||||
|
total = store.total()
|
||||||
|
ativos = store.total(filtro_ativo=True)
|
||||||
|
inativos = total - ativos
|
||||||
|
entries = store.get_page(page=1, size=total)[1] if total else []
|
||||||
|
totais = [e.pontuacao_total for e in entries]
|
||||||
|
distribuicao = []
|
||||||
|
if total:
|
||||||
|
buckets = [
|
||||||
|
("800+", lambda x: x >= 800),
|
||||||
|
("600-799", lambda x: 600 <= x < 800),
|
||||||
|
("400-599", lambda x: 400 <= x < 600),
|
||||||
|
("200-399", lambda x: 200 <= x < 400),
|
||||||
|
("0-199", lambda x: x < 200),
|
||||||
|
]
|
||||||
|
for faixa, pred in buckets:
|
||||||
|
qtd = sum(1 for x in totais if pred(x))
|
||||||
|
distribuicao.append(
|
||||||
|
{
|
||||||
|
"faixa": faixa,
|
||||||
|
"quantidade": qtd,
|
||||||
|
"percentual": round((qtd * 100.0 / total), 2) if total else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
estatisticas = {
|
||||||
|
"total_consultores": total,
|
||||||
|
"total_ativos": ativos,
|
||||||
|
"total_inativos": inativos,
|
||||||
|
"ultima_atualizacao": store.last_update.isoformat() if store.last_update else None,
|
||||||
|
"pontuacao_media": (sum(totais) / total) if total else 0,
|
||||||
|
"pontuacao_maxima": max(totais) if totais else 0,
|
||||||
|
"pontuacao_minima": min(totais) if totais else 0,
|
||||||
|
"media_componentes": {
|
||||||
|
"a": (sum(e.bloco_a for e in entries) / total) if total else 0,
|
||||||
|
"b": (sum(e.bloco_b for e in entries) / total) if total else 0,
|
||||||
|
"c": (sum(e.bloco_c for e in entries) / total) if total else 0,
|
||||||
|
"d": (sum(e.bloco_d for e in entries) / total) if total else 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return EstatisticasRankingSchema(
|
return EstatisticasRankingSchema(
|
||||||
total_consultores=estatisticas.get("total_consultores", 0),
|
total_consultores=estatisticas.get("total_consultores", 0),
|
||||||
@@ -156,14 +259,13 @@ async def status_processamento():
|
|||||||
|
|
||||||
@router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema)
|
@router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema)
|
||||||
async def processar_ranking(
|
async def processar_ranking(
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
|
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
|
||||||
job = Depends(get_processar_job),
|
job = Depends(get_processar_job),
|
||||||
):
|
):
|
||||||
if job_status.is_running:
|
if job_status.is_running:
|
||||||
raise HTTPException(status_code=409, detail="Job já está em execução")
|
raise HTTPException(status_code=409, detail="Job já está em execução")
|
||||||
|
|
||||||
background_tasks.add_task(job.executar, limpar_antes=request.limpar_antes)
|
asyncio.create_task(job.executar(limpar_antes=request.limpar_antes))
|
||||||
|
|
||||||
return ProcessarRankingResponseSchema(
|
return ProcessarRankingResponseSchema(
|
||||||
sucesso=True,
|
sucesso=True,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class CoordenacaoCapesSchema(BaseModel):
|
|||||||
periodo: PeriodoSchema
|
periodo: PeriodoSchema
|
||||||
areas_adicionais: List[str]
|
areas_adicionais: List[str]
|
||||||
ja_coordenou_antes: bool
|
ja_coordenou_antes: bool
|
||||||
|
presidente: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ConsultoriaSchema(BaseModel):
|
class ConsultoriaSchema(BaseModel):
|
||||||
@@ -48,6 +49,7 @@ class PremiacaoSchema(BaseModel):
|
|||||||
tipo: str
|
tipo: str
|
||||||
nome_premio: str
|
nome_premio: str
|
||||||
ano: int
|
ano: int
|
||||||
|
papel: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class BolsaCNPQSchema(BaseModel):
|
class BolsaCNPQSchema(BaseModel):
|
||||||
@@ -68,6 +70,9 @@ class OrientacaoSchema(BaseModel):
|
|||||||
tipo: str
|
tipo: str
|
||||||
nivel: str
|
nivel: str
|
||||||
ano: Optional[int] = None
|
ano: Optional[int] = None
|
||||||
|
coorientacao: bool = False
|
||||||
|
premiada: bool = False
|
||||||
|
premiacao_tipo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class MembroBancaSchema(BaseModel):
|
class MembroBancaSchema(BaseModel):
|
||||||
@@ -115,10 +120,10 @@ class ConsultorResumoSchema(BaseModel):
|
|||||||
class ConsultorDetalhadoSchema(BaseModel):
|
class ConsultorDetalhadoSchema(BaseModel):
|
||||||
id_pessoa: int
|
id_pessoa: int
|
||||||
nome: str
|
nome: str
|
||||||
cpf: Optional[str] = None
|
|
||||||
anos_atuacao: float
|
anos_atuacao: float
|
||||||
ativo: bool
|
ativo: bool
|
||||||
veterano: bool
|
veterano: bool
|
||||||
|
coordenador_ppg: bool = False
|
||||||
coordenacoes_capes: List[CoordenacaoCapesSchema]
|
coordenacoes_capes: List[CoordenacaoCapesSchema]
|
||||||
consultoria: Optional[ConsultoriaSchema] = None
|
consultoria: Optional[ConsultoriaSchema] = None
|
||||||
inscricoes: List[InscricaoSchema]
|
inscricoes: List[InscricaoSchema]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ConsultorRankingResumoSchema(BaseModel):
|
|||||||
bloco_d: float
|
bloco_d: float
|
||||||
ativo: bool
|
ativo: bool
|
||||||
anos_atuacao: float
|
anos_atuacao: float
|
||||||
|
coordenador_ppg: Optional[bool] = None
|
||||||
consultoria: Optional[dict] = None
|
consultoria: Optional[dict] = None
|
||||||
coordenacoes_capes: Optional[list] = None
|
coordenacoes_capes: Optional[list] = None
|
||||||
inscricoes: Optional[list] = None
|
inscricoes: Optional[list] = None
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ services:
|
|||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
networks:
|
networks:
|
||||||
- shared_network
|
- shared_network
|
||||||
depends_on:
|
|
||||||
oracle18c:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
@@ -47,32 +44,6 @@ services:
|
|||||||
- shared_network
|
- shared_network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
oracle18c:
|
|
||||||
container_name: mqapilc_oracle18c
|
|
||||||
image: gvenzl/oracle-xe:18-slim
|
|
||||||
environment:
|
|
||||||
- ORACLE_PASSWORD=local123
|
|
||||||
- ORACLE_CHARACTERSET=AL32UTF8
|
|
||||||
- APP_USER=local123
|
|
||||||
- APP_USER_PASSWORD=local123
|
|
||||||
- TZ=America/Sao_Paulo
|
|
||||||
ports:
|
|
||||||
- "1521:1521"
|
|
||||||
- "5500:5500"
|
|
||||||
volumes:
|
|
||||||
- oracle_data:/opt/oracle/oradata
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "bash", "-c", "echo 'SELECT 1 FROM DUAL;' | sqlplus -s SYSTEM/\"$${ORACLE_PASSWORD}\"@localhost:1521/XEPDB1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 20
|
|
||||||
networks:
|
|
||||||
- shared_network
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
shared_network:
|
shared_network:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
|
||||||
oracle_data:
|
|
||||||
driver: local
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ for i in {1..30}; do
|
|||||||
if docker compose exec backend python - <<'PY' >/dev/null 2>&1; then
|
if docker compose exec backend python - <<'PY' >/dev/null 2>&1; then
|
||||||
import httpx, sys
|
import httpx, sys
|
||||||
try:
|
try:
|
||||||
r = httpx.get("http://localhost:8000/api/v1/health", verify=False, timeout=15)
|
r = httpx.get("http://localhost:8000/api/v1/health", timeout=15)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -36,7 +36,7 @@ done
|
|||||||
echo "[4/5] Disparando job do ranking (limpar_antes=true)..."
|
echo "[4/5] Disparando job do ranking (limpar_antes=true)..."
|
||||||
docker compose exec backend python - <<'PY'
|
docker compose exec backend python - <<'PY'
|
||||||
import httpx
|
import httpx
|
||||||
client = httpx.Client(verify=False, timeout=120)
|
client = httpx.Client(timeout=120)
|
||||||
resp = client.post("http://localhost:8000/api/v1/ranking/processar", json={"limpar_antes": True})
|
resp = client.post("http://localhost:8000/api/v1/ranking/processar", json={"limpar_antes": True})
|
||||||
print("POST /api/v1/ranking/processar ->", resp.status_code, resp.text)
|
print("POST /api/v1/ranking/processar ->", resp.status_code, resp.text)
|
||||||
PY
|
PY
|
||||||
@@ -44,7 +44,7 @@ PY
|
|||||||
echo "[5/5] Acompanhando status até finalizar..."
|
echo "[5/5] Acompanhando status até finalizar..."
|
||||||
docker compose exec backend python - <<'PY'
|
docker compose exec backend python - <<'PY'
|
||||||
import httpx, time
|
import httpx, time
|
||||||
client = httpx.Client(verify=False, timeout=120)
|
client = httpx.Client(timeout=120)
|
||||||
while True:
|
while True:
|
||||||
r = client.get("http://localhost:8000/api/v1/ranking/status")
|
r = client.get("http://localhost:8000/api/v1/ranking/status")
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pkill -f "ngrok http 5173" 2>/dev/null
|
|||||||
echo "[4/5] Criando rede e subindo containers..."
|
echo "[4/5] Criando rede e subindo containers..."
|
||||||
docker network create shared_network 2>/dev/null
|
docker network create shared_network 2>/dev/null
|
||||||
docker compose up -d backend frontend
|
docker compose up -d backend frontend
|
||||||
echo "Aguardando backend subir (e Oracle ficar healthy)..."
|
echo "Aguardando backend subir..."
|
||||||
sleep 10
|
sleep 10
|
||||||
docker compose up -d backend frontend >/dev/null
|
docker compose up -d backend frontend >/dev/null
|
||||||
|
|
||||||
@@ -33,9 +33,6 @@ if ! docker ps | grep -q ranking_frontend; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[4b/5] Rodando bootstrap do ranking dentro do backend..."
|
|
||||||
docker exec ranking_backend bash -lc "ORACLE_LOCAL_DSN=oracle18c:1521/XEPDB1 /app/scripts/bootstrap_ranking.sh"
|
|
||||||
|
|
||||||
echo "[5/5] Iniciando ngrok..."
|
echo "[5/5] Iniciando ngrok..."
|
||||||
ngrok http 5173 --log=stdout > /tmp/ngrok-ranking.log 2>&1 &
|
ngrok http 5173 --log=stdout > /tmp/ngrok-ranking.log 2>&1 &
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user