feat(backend): ranking 100% Elasticsearch e critérios do PDF
This commit is contained in:
@@ -8,13 +8,7 @@ from .routes import router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .config import settings
|
||||
from .dependencies import (
|
||||
es_client,
|
||||
oracle_local_client,
|
||||
oracle_remote_client,
|
||||
get_processar_job,
|
||||
get_popular_componente_b_job,
|
||||
)
|
||||
from .dependencies import es_client, get_processar_job
|
||||
from ...application.jobs.scheduler import RankingScheduler
|
||||
|
||||
|
||||
@@ -22,24 +16,12 @@ from ...application.jobs.scheduler import RankingScheduler
|
||||
async def lifespan(app: FastAPI):
|
||||
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
|
||||
try:
|
||||
job = get_processar_job()
|
||||
job_b = get_popular_componente_b_job()
|
||||
scheduler = RankingScheduler(job, job_componente_b=job_b)
|
||||
await scheduler.iniciar()
|
||||
if settings.SCHEDULER_ENABLED:
|
||||
job = get_processar_job()
|
||||
scheduler = RankingScheduler(job)
|
||||
await scheduler.iniciar(hora_alvo=settings.SCHEDULER_HOUR)
|
||||
except Exception as e:
|
||||
logger.warning(f"Scheduler não iniciou: {e}")
|
||||
|
||||
@@ -53,16 +35,6 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
await es_client.close()
|
||||
|
||||
try:
|
||||
oracle_local_client.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
oracle_remote_client.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Ranking de Consultores CAPES",
|
||||
|
||||
@@ -10,16 +10,7 @@ class Settings(BaseSettings):
|
||||
ES_INDEX: str = "atuacapes"
|
||||
ES_USER: str = ""
|
||||
ES_PASSWORD: str = ""
|
||||
|
||||
# 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
|
||||
ES_VERIFY_SSL: bool = True
|
||||
|
||||
API_HOST: str = "0.0.0.0"
|
||||
API_PORT: int = 8000
|
||||
@@ -28,6 +19,8 @@ class Settings(BaseSettings):
|
||||
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
||||
|
||||
LOG_LEVEL: str = "INFO"
|
||||
SCHEDULER_ENABLED: bool = False
|
||||
SCHEDULER_HOUR: int = 3
|
||||
|
||||
@property
|
||||
def cors_origins_list(self) -> List[str]:
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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 ...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
|
||||
|
||||
|
||||
@@ -11,41 +9,23 @@ es_client = ElasticsearchClient(
|
||||
url=settings.ES_URL,
|
||||
index=settings.ES_INDEX,
|
||||
user=settings.ES_USER,
|
||||
password=settings.ES_PASSWORD
|
||||
)
|
||||
|
||||
# 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
|
||||
password=settings.ES_PASSWORD,
|
||||
verify_ssl=settings.ES_VERIFY_SSL,
|
||||
)
|
||||
|
||||
_repository: ConsultorRepositoryImpl = None
|
||||
_ranking_repository: RankingOracleRepository = None
|
||||
_processar_job: ProcessarRankingJob = None
|
||||
_popular_b_job: PopularComponenteBJob = None
|
||||
|
||||
|
||||
def get_repository() -> ConsultorRepositoryImpl:
|
||||
global _repository
|
||||
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
|
||||
|
||||
|
||||
def get_ranking_repository() -> RankingOracleRepository:
|
||||
global _ranking_repository
|
||||
if _ranking_repository is None:
|
||||
_ranking_repository = RankingOracleRepository(oracle_client=oracle_local_client)
|
||||
return _ranking_repository
|
||||
def get_ranking_store() -> RankingStore:
|
||||
return ranking_store
|
||||
|
||||
|
||||
def get_processar_job() -> ProcessarRankingJob:
|
||||
@@ -53,18 +33,6 @@ def get_processar_job() -> ProcessarRankingJob:
|
||||
if _processar_job is None:
|
||||
_processar_job = ProcessarRankingJob(
|
||||
es_client=es_client,
|
||||
oracle_remote_client=oracle_remote_client,
|
||||
oracle_local_client=oracle_local_client,
|
||||
ranking_repo=get_ranking_repository()
|
||||
ranking_store=ranking_store,
|
||||
)
|
||||
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 ...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,
|
||||
@@ -20,7 +21,7 @@ from ..schemas.ranking_schema import (
|
||||
ProcessarRankingResponseSchema,
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["ranking"])
|
||||
@@ -34,19 +35,34 @@ async def obter_ranking(
|
||||
default=None, description="Filtrar por bloco (a, c, d)"
|
||||
),
|
||||
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
||||
store = Depends(get_ranking_store),
|
||||
):
|
||||
if store.is_ready():
|
||||
total, entries = store.get_slice(offset=offset, limit=limite)
|
||||
consultores_schema = [
|
||||
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(
|
||||
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
|
||||
)
|
||||
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)
|
||||
@@ -73,9 +89,15 @@ async def obter_ranking_detalhado(
|
||||
async def obter_consultor(
|
||||
id_pessoa: int,
|
||||
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
||||
store = Depends(get_ranking_store),
|
||||
):
|
||||
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:
|
||||
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"),
|
||||
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"),
|
||||
ranking_repo = Depends(get_ranking_repository),
|
||||
store = Depends(get_ranking_store),
|
||||
):
|
||||
total = ranking_repo.contar_total(filtro_ativo=ativo)
|
||||
consultores = ranking_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo)
|
||||
if not store.is_ready():
|
||||
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
|
||||
|
||||
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(
|
||||
total=total,
|
||||
@@ -115,9 +169,15 @@ async def ranking_paginado(
|
||||
async def buscar_por_nome(
|
||||
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"),
|
||||
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 [
|
||||
ConsultaNomeSchema(
|
||||
id_pessoa=r["ID_PESSOA"],
|
||||
@@ -131,10 +191,53 @@ async def buscar_por_nome(
|
||||
|
||||
@router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema)
|
||||
async def ranking_estatisticas(
|
||||
ranking_repo = Depends(get_ranking_repository),
|
||||
store = Depends(get_ranking_store),
|
||||
):
|
||||
estatisticas = ranking_repo.obter_estatisticas()
|
||||
distribuicao = ranking_repo.obter_distribuicao()
|
||||
if not store.is_ready():
|
||||
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(
|
||||
total_consultores=estatisticas.get("total_consultores", 0),
|
||||
@@ -156,14 +259,13 @@ async def status_processamento():
|
||||
|
||||
@router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema)
|
||||
async def processar_ranking(
|
||||
background_tasks: BackgroundTasks,
|
||||
request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(),
|
||||
job = Depends(get_processar_job),
|
||||
):
|
||||
if job_status.is_running:
|
||||
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(
|
||||
sucesso=True,
|
||||
|
||||
@@ -16,6 +16,7 @@ class CoordenacaoCapesSchema(BaseModel):
|
||||
periodo: PeriodoSchema
|
||||
areas_adicionais: List[str]
|
||||
ja_coordenou_antes: bool
|
||||
presidente: bool = False
|
||||
|
||||
|
||||
class ConsultoriaSchema(BaseModel):
|
||||
@@ -48,6 +49,7 @@ class PremiacaoSchema(BaseModel):
|
||||
tipo: str
|
||||
nome_premio: str
|
||||
ano: int
|
||||
papel: Optional[str] = None
|
||||
|
||||
|
||||
class BolsaCNPQSchema(BaseModel):
|
||||
@@ -68,6 +70,9 @@ class OrientacaoSchema(BaseModel):
|
||||
tipo: str
|
||||
nivel: str
|
||||
ano: Optional[int] = None
|
||||
coorientacao: bool = False
|
||||
premiada: bool = False
|
||||
premiacao_tipo: Optional[str] = None
|
||||
|
||||
|
||||
class MembroBancaSchema(BaseModel):
|
||||
@@ -115,10 +120,10 @@ class ConsultorResumoSchema(BaseModel):
|
||||
class ConsultorDetalhadoSchema(BaseModel):
|
||||
id_pessoa: int
|
||||
nome: str
|
||||
cpf: Optional[str] = None
|
||||
anos_atuacao: float
|
||||
ativo: bool
|
||||
veterano: bool
|
||||
coordenador_ppg: bool = False
|
||||
coordenacoes_capes: List[CoordenacaoCapesSchema]
|
||||
consultoria: Optional[ConsultoriaSchema] = None
|
||||
inscricoes: List[InscricaoSchema]
|
||||
|
||||
@@ -14,6 +14,7 @@ class ConsultorRankingResumoSchema(BaseModel):
|
||||
bloco_d: float
|
||||
ativo: bool
|
||||
anos_atuacao: float
|
||||
coordenador_ppg: Optional[bool] = None
|
||||
consultoria: Optional[dict] = None
|
||||
coordenacoes_capes: Optional[list] = None
|
||||
inscricoes: Optional[list] = None
|
||||
|
||||
Reference in New Issue
Block a user