feat(backend): ranking 100% Elasticsearch e critérios do PDF
This commit is contained in:
@@ -3,11 +3,19 @@ from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
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.index = index
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.verify_ssl = verify_ssl
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
async def connect(self) -> None:
|
||||
@@ -21,7 +29,7 @@ class ElasticsearchClient:
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
verify=False,
|
||||
verify=self.verify_ssl,
|
||||
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 ..cache import ranking_cache
|
||||
from ..elasticsearch.client import ElasticsearchClient
|
||||
from ..oracle.client import OracleClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.oracle_client = oracle_client
|
||||
self.calculador = CalculadorPontuacao()
|
||||
@@ -75,6 +74,8 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
resultado = []
|
||||
for coord in coordenacoes:
|
||||
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"))
|
||||
if not inicio:
|
||||
continue
|
||||
@@ -97,10 +98,38 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
periodo=Periodo(inicio=inicio, fim=fim),
|
||||
areas_adicionais=[],
|
||||
ja_coordenou_antes=len(resultado) > 0,
|
||||
presidente=presidente,
|
||||
))
|
||||
|
||||
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]:
|
||||
consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]]
|
||||
if not consultorias:
|
||||
@@ -248,17 +277,19 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
dados = a.get("dadosPremiacaoPremio", {}) or a.get("dadosPremio", {}) or {}
|
||||
tipo_premiacao = dados.get("tipoPremiacao") or dados.get("premiacao") or ""
|
||||
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")
|
||||
if not ano:
|
||||
inicio = self._parse_date(a.get("inicio"))
|
||||
ano = inicio.year if inicio else datetime.now().year
|
||||
|
||||
tipo_lower = tipo_premiacao.lower()
|
||||
nome_lower = nome_premio.lower()
|
||||
tipo_lower = str(tipo_premiacao).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"
|
||||
elif "menção" in tipo_lower or "mencao" in tipo_lower or "honrosa" in tipo_lower:
|
||||
elif tipo_norm == "MENCAO":
|
||||
codigo = "MENCAO"
|
||||
else:
|
||||
codigo = "PREMIACAO_GP"
|
||||
@@ -268,6 +299,7 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
tipo=tipo_premiacao,
|
||||
nome_premio=nome_premio,
|
||||
ano=ano,
|
||||
papel=papel,
|
||||
))
|
||||
|
||||
return premiacoes
|
||||
@@ -283,10 +315,10 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
area = dados.get("areaConhecimento", "") or ""
|
||||
|
||||
nivel_lower = nivel.lower()
|
||||
if "1a" in nivel_lower or "1b" in nivel_lower or "1c" in nivel_lower or "1d" in nivel_lower:
|
||||
codigo = "BOL_BPQ_SUPERIOR"
|
||||
if "1d" in nivel_lower or "2" in nivel_lower:
|
||||
codigo = "BOL_BPQ_INT"
|
||||
else:
|
||||
codigo = "BOL_BPQ_INTERMEDIARIO"
|
||||
codigo = "BOL_BPQ_SUP"
|
||||
|
||||
bolsas.append(BolsaCNPQ(
|
||||
codigo=codigo,
|
||||
@@ -331,6 +363,15 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
|
||||
dados = a.get("dadosOrientacao", {}) 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")
|
||||
if not ano:
|
||||
inicio = self._parse_date(a.get("inicio"))
|
||||
@@ -349,6 +390,9 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
tipo=tipo,
|
||||
nivel=nivel,
|
||||
ano=ano,
|
||||
coorientacao=False,
|
||||
premiada=premiada,
|
||||
premiacao_tipo=tipo_prem,
|
||||
))
|
||||
|
||||
return orientacoes
|
||||
@@ -363,6 +407,15 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
|
||||
dados = a.get("dadosOrientacao", {}) or a.get("dadosCoorientacao", {}) 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")
|
||||
if not ano:
|
||||
inicio = self._parse_date(a.get("inicio"))
|
||||
@@ -381,6 +434,9 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
tipo=tipo,
|
||||
nivel=nivel,
|
||||
ano=ano,
|
||||
coorientacao=True,
|
||||
premiada=premiada,
|
||||
premiacao_tipo=tipo_prem,
|
||||
))
|
||||
|
||||
return coorientacoes
|
||||
@@ -432,11 +488,13 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
||||
orientacoes = self._extrair_orientacoes(atuacoes)
|
||||
coorientacoes = self._extrair_coorientacoes(atuacoes)
|
||||
membros_banca = self._extrair_membros_banca(atuacoes)
|
||||
coordenador_ppg = self._tem_coordenacao_ppg(atuacoes)
|
||||
|
||||
consultor = Consultor(
|
||||
id_pessoa=id_pessoa,
|
||||
nome=dados_pessoais.get("nome", "N/A"),
|
||||
cpf=dados_pessoais.get("cpf"),
|
||||
coordenador_ppg=coordenador_ppg,
|
||||
coordenacoes_capes=coordenacoes_capes,
|
||||
consultoria=consultoria,
|
||||
inscricoes=inscricoes,
|
||||
|
||||
Reference in New Issue
Block a user