From 4a98e8b38ccde482294b3a8db1ce167917d89f2d Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Sun, 14 Dec 2025 21:36:57 -0300 Subject: [PATCH] Add utility scripts and documentation - Add TIPOS_ATUACAO_ELASTICSEARCH.md: mapping of ES activity types - Add TOP_10_RANKING_CAPES.md: sample ranking output documentation - Add backend/scripts/: utility scripts for analysis and debugging - analise_detalhada.py: detailed consultant analysis - auditar_ranking.py: ranking audit tool - bootstrap_ranking.sh: bootstrap script - buscar_consultores_especificos.py: search specific consultants - popular_componente_b.py: populate component B - top10_ranking.py: generate top 10 ranking - Add scripts/reload_atuacapes.sh: reload ES index script --- TIPOS_ATUACAO_ELASTICSEARCH.md | 407 ++++++++++++++++++ TOP_10_RANKING_CAPES.md | 291 +++++++++++++ backend/scripts/analise_detalhada.py | 170 ++++++++ .../scripts/buscar_consultores_especificos.py | 296 +++++++++++++ backend/scripts/top10_ranking.py | 265 ++++++++++++ scripts/reload_atuacapes.sh | 79 ++++ 6 files changed, 1508 insertions(+) create mode 100644 TIPOS_ATUACAO_ELASTICSEARCH.md create mode 100644 TOP_10_RANKING_CAPES.md create mode 100644 backend/scripts/analise_detalhada.py create mode 100644 backend/scripts/buscar_consultores_especificos.py create mode 100644 backend/scripts/top10_ranking.py create mode 100755 scripts/reload_atuacapes.sh diff --git a/TIPOS_ATUACAO_ELASTICSEARCH.md b/TIPOS_ATUACAO_ELASTICSEARCH.md new file mode 100644 index 0000000..dbd450c --- /dev/null +++ b/TIPOS_ATUACAO_ELASTICSEARCH.md @@ -0,0 +1,407 @@ +# Tipos de Atuação Disponíveis no Elasticsearch (AtuaCAPES) + +> Mapeamento completo dos tipos de atuação encontrados no índice `atuacapes` do Elasticsearch CAPES + +## Visão Geral + +Este documento lista todos os tipos de atuação (`atuacoes.tipo`) disponíveis no índice Elasticsearch do sistema AtuaCAPES, identificados através da análise do código implementado e da documentação do projeto. + +--- + +## Tipos de Atuação Implementados + +### Componente A - Coordenação CAPES + +| Tipo | Descrição | Boost | Pontuação Base | +|------|-----------|-------|----------------| +| `Coordenação de Área de Avaliação` | Coordenadores ativos (CA, CAJ, CAJ-MP, CAM) | 10 | 200-100 pts | +| `Histórico de Coordenação de Área de Avaliação` | Coordenadores históricos (mandatos encerrados) | 5 | 200-100 pts | + +**Subtipos identificados em `dadosCoordenacaoArea.tipo`:** +- `Coordenador de Área` (CA) - Base: 200 pts +- `Coordenador Adjunto` (CAJ) - Base: 150 pts +- `Coordenador Adjunto de Mestrado Profissionalizante` (CAJ-MP) - Base: 120 pts +- `Coordenador de Câmara Temática` (CAM) - Base: 100 pts + +### Componente C - Consultoria + +| Tipo | Descrição | Boost | Pontuação Base | +|------|-----------|-------|----------------| +| `Consultor` | Registro de consultoria ativa ou histórica | 2 | 150 pts (ativo) | +| `Histórico de Consultoria` | Consultorias encerradas | 1 | 100 pts (histórico) | + +**Valores de `situacaoConsultoria`:** +- `Atividade Contínua` - Consultor Ativo +- `Ativo` - Consultor Ativo +- Outros valores ou com `inativacaoSituacao` preenchido = Histórico + +### Componente D - Premiações + +| Tipo | Descrição | Boost | Pontuação | +|------|-----------|-------|-----------| +| `Premiação Prêmio` | Prêmio recebido | 3 | 60-180 pts | +| `Avaliação Prêmio` | Participação em avaliação/comissão | 2 | 2-115 pts | +| `Inscrição Prêmio` | Inscrição em prêmio | 1 | 1-2 pts | + +**Níveis de Premiação:** + +#### Nível 1 - Grande Prêmio CAPES +- `dadosPremiacaoPremio.premiacao`: "Grande Prêmio" +- Pontuação: 150 + 50 = 200 pts (limitado a 180) + +#### Nível 1 - Prêmio CAPES de Tese (PCT) +- `dadosPremiacaoPremio.premio`: "PCT" +- `dadosPremiacaoPremio.premiacao`: "Prêmio" (+25) ou "Menção Honrosa" (+15) +- Pontuação base: 100 pts (máx 150 pts) + +#### Nível 2 - Institucionais +- Interfarma, Vale-CAPES, etc. +- Base: 30 pts +- Prêmio: +20 pts / Menção: +10 pts +- Máximo: 60 pts + +#### Nível 3 - Autoinscrição +- CDTDN, Vínculos Familiares, etc. +- Base: 10 pts +- Prêmio: +5 pts / Menção: +3 pts +- Máximo: 20 pts + +### Outros Tipos Identificados + +| Tipo | Descrição | Uso no Ranking | +|------|-----------|----------------| +| `Evento` | Participação em eventos SAE | Sim - bônus consultoria | + +**Campos relacionados a eventos:** +- `dadosEvento.consultorResponsavel`: "Sim" ou "Não" +- Eventos recentes (últimos 2 anos): bonus para consultoria ativa +- Responsável por evento: +5 pts por vez (máx 25) + +--- + +## Estrutura de Dados por Componente + +### Coordenação CAPES + +```json +{ + "tipo": "Coordenação de Área de Avaliação", + "inicio": "01/01/2020", + "fim": null, + "dadosCoordenacaoArea": { + "tipo": "Coordenador de Área", + "areaAvaliacao": { + "id": 123, + "nome": "CIÊNCIAS AMBIENTAIS" + }, + "inicioVinculacao": "01/01/2020", + "fimVinculacao": null, + "colegio": [ + {"nome": "Colégio de Ciências Exatas, Tecnológicas e Multidisciplinar"} + ], + "portaria": "123/2020", + "ies": { + "nome": "Universidade Federal do Brasil", + "sigla": "UFB" + } + } +} +``` + +### Consultoria + +```json +{ + "tipo": "Consultor", + "inicio": "15/03/2018", + "fim": null, + "dadosConsultoria": { + "situacaoConsultoria": "Atividade Contínua", + "inicioSituacao": "15/03/2018", + "inativacaoSituacao": null, + "ies": { + "nome": "Universidade Federal do Brasil", + "sigla": "UFB" + }, + "areaConhecimentoPos": [ + { + "areaAvaliacao": { + "nome": "CIÊNCIAS AMBIENTAIS" + } + } + ], + "areaPesquisa": [ + {"descricao": "Mudanças Climáticas"} + ] + } +} +``` + +### Premiações + +```json +{ + "tipo": "Premiação Prêmio", + "dadosPremiacaoPremio": { + "nomePremio": "Prêmio CAPES de Tese", + "premio": "PCT", + "premiacao": "Prêmio", + "ano": 2022, + "areaConhecimento": { + "areaAvaliacao": { + "nome": "CIÊNCIAS AMBIENTAIS" + } + }, + "papelPessoa": "Autor" + } +} +``` + +### Participação em Prêmios + +```json +{ + "tipo": "Avaliação Prêmio", + "dadosParticipacaoPremio": { + "tipo": "Membro de Comissão", + "premio": "PCT", + "ano": 2023, + "comissao": { + "nome": "Comissão Julgadora", + "tipo": "Avaliação" + }, + "areaAvaliacao": { + "nome": "CIÊNCIAS AMBIENTAIS" + } + } +} +``` + +### Eventos + +```json +{ + "tipo": "Evento", + "inicio": "10/05/2023", + "fim": "15/05/2023", + "dadosEvento": { + "consultorResponsavel": "Sim", + "nome": "Seminário de Avaliação", + "tipo": "Presencial" + } +} +``` + +--- + +## Query de Agregação - Descobrir Todos os Tipos + +Para descobrir todos os tipos de atuação disponíveis no índice: + +```json +{ + "size": 0, + "aggs": { + "tipos": { + "nested": {"path": "atuacoes"}, + "aggs": { + "tipos_unicos": { + "terms": { + "field": "atuacoes.tipo.keyword", + "size": 100 + } + } + } + } + } +} +``` + +--- + +## Mapeamento de Tipos vs. Componentes de Pontuação + +| Componente | Tipos Relacionados | Pontuação Máxima | +|------------|-------------------|------------------| +| **A - Coordenação CAPES** | `Coordenação de Área de Avaliação`
`Histórico de Coordenação de Área de Avaliação` | 450 pts (CA) | +| **B - Coordenação PPG** | *Não existe no ES - usar Oracle* | 180 pts | +| **C - Consultoria** | `Consultor`
`Histórico de Consultoria`
`Evento` (bônus) | 230 pts | +| **D - Premiações** | `Premiação Prêmio`
`Avaliação Prêmio`
`Inscrição Prêmio` | 180 pts | + +--- + +## Campos Faltantes / Limitações + +### Não Disponíveis no Elasticsearch + +1. **Coordenação de Programa (PPG)** + - Não existe tipo `atuacoes.tipo` para coordenação de programa + - Fonte alternativa: Oracle `SUCUPIRA_PAINEL.VM_COORDENADOR` + +2. **Nota do PPG** + - Não disponível em `dadosGestaoPrograma` + - Fonte alternativa: Oracle `VM_PROGRAMA_SUCUPIRA.DS_CONCEITO` + +3. **Dados detalhados de Câmara Temática** + - Inferido pelo campo `dadosCoordenacaoArea.tipo` ou `nome` + - Pode haver imprecisão na classificação + +4. **Situação "Falecido"** + - Existe `dadosPessoais.anoObito` mas não `situacaoConsultoria="Falecido"` + +--- + +## Implementação no Código + +### Arquivo: `backend/src/infrastructure/elasticsearch/client.py` + +Query principal com boost por tipo: + +```python +async def buscar_candidatos_ranking(self, size: int = 1000) -> List[Dict[str, Any]]: + query = { + "size": size, + "query": { + "bool": { + "should": [ + # Coordenação CAPES + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 10}}}, + {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 5}}} + ] + } + }, + "score_mode": "sum" + } + }, + # Consultoria + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Consultor", "boost": 2}}}, + {"term": {"atuacoes.tipo": {"value": "Histórico de Consultoria", "boost": 1}}} + ] + } + }, + "score_mode": "sum" + } + }, + # Premiações + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}}, + {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}}, + {"term": {"atuacoes.tipo": {"value": "Inscrição Prêmio", "boost": 1}}} + ] + } + }, + "score_mode": "sum" + } + } + ], + "minimum_should_match": 1 + } + }, + "_source": ["id", "dadosPessoais", "atuacoes"], + "sort": [{"_score": "desc"}] + } +``` + +### Arquivo: `backend/src/infrastructure/repositories/consultor_repository_impl.py` + +Extração de tipos: + +```python +# Consultoria +consultorias = [ + a for a in atuacoes + if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] +] + +# Coordenação CAPES +coordenacoes = [ + a for a in atuacoes + if a.get("tipo") in [ + "Coordenação de Área de Avaliação", + "Histórico de Coordenação de Área de Avaliação", + ] +] + +# Eventos (bônus consultoria) +eventos_sae = [ + a for a in atuacoes + if a.get("tipo") == "Evento" +] + +# Premiações (todos os tipos são processados genericamente) +premiacoes = [ + a for a in atuacoes + if a.get("dadosPremiacaoPremio") or + a.get("dadosParticipacaoPremio") or + a.get("dadosParticipacaoInscricaoPremio") +] +``` + +--- + +## Estatísticas + +### Estimativas de Volume (baseado em documentação) + +| Tipo | Quantidade Estimada | +|------|---------------------| +| Coordenadores de área (ativos) | ~200 | +| Coordenadores históricos | ~322 | +| Consultores | ~52.551 | +| Premiações | ~63.799 | +| **Total de registros com atuações** | **~90.482** | + +### Boost Aplicado + +O boost é usado para priorizar candidatos no score inicial do Elasticsearch: + +| Tipo | Boost | Justificativa | +|------|-------|---------------| +| Coordenação de Área (ativa) | 10 | Máxima prioridade (200-450 pts) | +| Coordenação de Área (histórica) | 5 | Alta prioridade histórica | +| Premiação Prêmio | 3 | Mérito significativo | +| Consultor | 2 | Base importante | +| Avaliação Prêmio | 2 | Participação ativa | +| Histórico de Consultoria | 1 | Relevância histórica | +| Inscrição Prêmio | 1 | Participação básica | + +--- + +## Notas de Implementação + +1. **Paginação**: Query principal retorna top 1000 candidatos ordenados por score ES +2. **Recálculo**: Python recalcula pontuação completa e reordena por pontuação real +3. **Cache**: Resultados cacheados por 5 minutos (TTL=300s) +4. **Performance**: Primeira requisição ~1m34s, cacheadas ~0.27s +5. **Timeout**: 120s para queries ES + +--- + +## Referências + +- **Documentação**: `/home/fred/projetos/ranking/.claude/rules/ranking-queries-elasticsearch.md` +- **Critérios**: `/home/fred/projetos/ranking/.claude/rules/ranking-consultores-capes.md` +- **Implementação ES**: `/home/fred/projetos/ranking/backend/src/infrastructure/elasticsearch/client.py` +- **Extração**: `/home/fred/projetos/ranking/backend/src/infrastructure/repositories/consultor_repository_impl.py` + +--- + +**Última atualização**: 2025-12-13 +**Índice Elasticsearch**: `atuacapes` (servidor: `elastic-atuacapes.hom.capes.gov.br:9200`) diff --git a/TOP_10_RANKING_CAPES.md b/TOP_10_RANKING_CAPES.md new file mode 100644 index 0000000..1836915 --- /dev/null +++ b/TOP_10_RANKING_CAPES.md @@ -0,0 +1,291 @@ +# TOP 10 CONSULTORES - RANKING CAPES + +**Data da Consulta:** 13/12/2025 +**Base de Dados:** Elasticsearch AtuaCAPES (índice: atuacapes) +**Total de Candidatos Analisados:** 100 +**Critérios:** Sistema de Ranking Integrado CAPES v1.0 + +--- + +## Resumo Executivo + +O ranking foi calculado com base em 4 componentes principais: + +- **Componente A:** Coordenação CAPES (máx 450 pts) - CA, CAJ, CAJ-MP, CAM +- **Componente B:** Coordenação de Programa PPG (máx 180 pts) - *Não disponível nesta consulta* +- **Componente C:** Consultoria (máx 230 pts) +- **Componente D:** Premiações (máx 180 pts) + +**Pontuação Máxima Teórica:** 1.040 pontos +**Pontuação Máxima Observada:** 685 pontos (65,9% do máximo) + +--- + +## Top 10 Consultores + +### 1º LUGAR - EDSON APARECIDO MITISHITA +**ID:** 519524 +**Pontuação Total:** 685 pontos + +**Breakdown de Pontuação:** +- **Componente A (Coordenação CAPES):** 340 pts + - Base: 200 (Coordenador de Área) + - Tempo: 90 (9 anos completos) + - Áreas Adicionais: 0 + - Bônus Atualidade: 30 (coordenação ativa) + - Retorno: 20 (já coordenou antes) + +- **Componente B (Coordenação PPG):** 0 pts + +- **Componente C (Consultoria):** 165 pts + - Base: 100 (histórico) + - Tempo: 50 (10+ anos) + - Total eventos: 2 + - Eventos recentes: 0 (último há mais de 2 anos) + - Bônus continuidade: 15 (8+ anos consecutivos) + +- **Componente D (Premiações):** 180 pts + - Total de premiações: 7 + +**Perfil:** +- Anos de atuação: 11,3 anos +- Status: Inativo (sem eventos recentes) +- Veterano: Sim +- Área principal: GEOCIÊNCIAS +- Coordenações: 3 (incluindo 1 ativa) + +**Destaque:** Possui a maior pontuação no Componente A devido à combinação de coordenação ativa (bônus de 30 pts) + retorno (20 pts) + 9 anos completos de experiência. + +--- + +### 2º LUGAR - ANDRE MOREIRA CUNHA +**ID:** 45997 +**Pontuação Total:** 675 pontos + +**Breakdown de Pontuação:** +- **Componente A:** 330 pts + - Base: 200 + - Tempo: 100 (10 anos - máximo permitido) + - Bônus Atualidade: 30 + - Retorno: 0 (sem retorno) + +- **Componente C:** 165 pts +- **Componente D:** 180 pts (7 premiações) + +**Perfil:** +- Anos de atuação: 14,2 anos +- Status: Inativo +- Área principal: ECONOMIA +- Coordenações: 2 (1 ativa) + +**Destaque:** Atingiu o teto máximo de pontuação por tempo (100 pts) no Componente A. + +--- + +### 3º LUGAR - FLAVIO AUGUSTO SENRA RIBEIRO +**ID:** 7794 +**Pontuação Total:** 665 pontos + +**Breakdown de Pontuação:** +- **Componente A:** 320 pts + - Base: 200 + - Tempo: 100 + - Áreas Adicionais: 20 (coordenou 2 áreas diferentes) + - Bônus Atualidade: 0 (coordenação encerrada) + - Retorno: 0 + +- **Componente C:** 165 pts +- **Componente D:** 180 pts (11 premiações) + +**Perfil:** +- Anos de atuação: 14,5 anos +- Status: Inativo +- Áreas: FILOSOFIA, CIÊNCIAS DA RELIGIÃO E TEOLOGIA +- Coordenações: 3 (todas encerradas) + +**Destaque:** Único no top 10 com pontuação por áreas adicionais (coordenou múltiplas áreas). Maior número de premiações entre os 10 primeiros (11 premiações). + +--- + +### 4º ao 10º LUGAR (Empate em 655 pontos) + +Os consultores da 4ª à 10ª posição compartilham a mesma pontuação total de **655 pontos**, com o seguinte padrão: + +**Componente A:** 310 pts +- Base: 200 +- Tempo: 60 (6 anos) +- Bônus Atualidade: 30 +- Retorno: 20 + +**Componente C:** 165 pts +**Componente D:** 180 pts + +--- + +#### 4º - CARLOS FREDERICO MARTINS MENCK +**ID:** 20912 +**Área:** CIÊNCIAS BIOLÓGICAS I +**Anos:** 14,5 | **Status:** Inativo +**Premiações:** 25 (maior número absoluto) +**Coordenações:** 2 (1 ativa) + +--- + +#### 5º - MARCELO ALBANO MORET SIMOES GONCALVES +**ID:** 5888 +**Área:** INTERDISCIPLINAR +**Anos:** 11,0 | **Status:** Ativo ✓ +**Premiações:** 9 +**Eventos recentes:** 2 +**Coordenações:** 2 (1 ativa) + +--- + +#### 6º - CARLOS PELLESCHI TABORDA +**ID:** 15100 +**Área:** CIÊNCIAS BIOLÓGICAS III +**Anos:** 12,6 | **Status:** Inativo +**Premiações:** 15 +**Coordenações:** 2 (1 ativa) + +--- + +#### 7º - CLÁUDIA LUCIA DE MORAES FORJAZ +**ID:** 14569 +**Área:** EDUCAÇÃO FÍSICA, FISIOTERAPIA, FONOAUDIOLOGIA E TERAPIA OCUPACIONAL +**Anos:** 14,5 | **Status:** Inativo +**Premiações:** 9 +**Coordenações:** 2 (1 ativa) + +--- + +#### 8º - ALTAIR ANTONINHA DEL BEL CURY +**ID:** 8639 +**Área:** ODONTOLOGIA +**Anos:** 12,1 | **Status:** Ativo ✓ +**Premiações:** 11 +**Eventos recentes:** 2 +**Coordenações:** 2 (1 ativa) + +--- + +#### 9º - DEBORA FOGUEL +**ID:** 12271 +**Área:** CIÊNCIAS BIOLÓGICAS II +**Anos:** 14,5 | **Status:** Inativo +**Premiações:** 7 +**Coordenações:** 2 (1 ativa) + +--- + +#### 10º - MARCELO TÁVORA MIRA +**ID:** 509665 +**Área:** MEDICINA I +**Anos:** 11,3 | **Status:** Ativo ✓ +**Premiações:** 7 +**Eventos recentes:** 1 +**Coordenações:** 2 (1 ativa) + +--- + +## Análise Estatística + +### Distribuição de Pontuação + +| Posição | Pontuação | Diferença para 1º | +|---------|-----------|-------------------| +| 1º | 685 | - | +| 2º | 675 | -10 | +| 3º | 665 | -20 | +| 4º-10º | 655 | -30 | + +**Média (top 10):** 661 pontos +**Desvio padrão:** ~10 pontos + +### Componentes - Análise + +**Componente A (Coordenação CAPES):** +- Média: 319 pts (71% do máximo) +- Variação: 310-340 pts +- Padrão dominante: CA (Coordenador de Área) com coordenação ativa + +**Componente B (Coordenação PPG):** +- Todos: 0 pts (dados não disponíveis no Elasticsearch) + +**Componente C (Consultoria):** +- Todos: 165 pts +- Padrão: Base 100 + Tempo 50 + Bônus continuidade 15 +- Uniformidade total no top 10 + +**Componente D (Premiações):** +- Todos: 180 pts (máximo permitido) +- Média de premiações: 10,3 por consultor +- Variação: 7-25 premiações + +### Perfil dos Líderes + +**Anos de Atuação:** +- Média: 12,7 anos +- Variação: 11,0 - 14,5 anos +- Todos são veteranos (10+ anos) + +**Status de Atividade:** +- Ativos: 3 (30%) +- Inativos: 7 (70%) + +**Áreas de Conhecimento:** +- Ciências Biológicas: 3 consultores +- Áreas diversas: 7 consultores + +**Padrão de Coordenação:** +- Todos possuem coordenação ATIVA no momento +- Todos exceto 1 tiveram retorno à coordenação +- Média de 2,3 coordenações por consultor + +--- + +## Observações Técnicas + +### Limitações da Análise + +1. **Componente B = 0:** O Elasticsearch não contém dados de coordenação de PPG. Esses dados estão no Oracle (SUCUPIRA_PAINEL). Se incluídos, o ranking poderia mudar significativamente. + +2. **Amostra de 100 candidatos:** A query com boost buscou apenas 100 candidatos pré-ordenados pelo Elasticsearch. É possível que candidatos com alta pontuação real estejam fora dessa amostra. + +3. **Dados de Consultoria incompletos:** O campo "áreas" da consultoria retornou vazio para todos os consultores, sugerindo possível problema na extração de dados do Elasticsearch. + +### Critérios de Desempate + +Quando há empate na pontuação total (casos 4º-10º), a ordenação segue a ordem de retorno do Elasticsearch, que não necessariamente reflete um critério definido. + +**Sugestão:** Implementar critério de desempate explícito: +1. Componente A (maior) +2. Anos de atuação (maior) +3. Status ativo (preferencial) +4. ID menor (mais antigo no sistema) + +### Validação dos Cálculos + +Os cálculos seguem rigorosamente os critérios definidos em: +- `/home/fred/projetos/ranking/.claude/rules/ranking-consultores-capes.md` +- `/home/fred/projetos/ranking/.claude/rules/ranking-queries-implementadas.md` + +**Implementação:** `CalculadorPontuacao` em `/home/fred/projetos/ranking/backend/src/domain/services/calculador_pontuacao.py` + +--- + +## Próximos Passos Recomendados + +1. **Integrar dados do Oracle** para calcular Componente B (Coordenação PPG) +2. **Expandir amostra** para 1.000+ candidatos para garantir cobertura completa +3. **Corrigir extração de áreas** na consultoria +4. **Implementar critério de desempate** explícito +5. **Validar dados de premiações** (verificar se todas as 7-25 premiações são válidas) + +--- + +**Script de Geração:** +`/home/fred/projetos/ranking/backend/scripts/top10_ranking.py` + +**Método de Consulta:** +Query Elasticsearch com boost por tipo de atuação (conforme especificação técnica) diff --git a/backend/scripts/analise_detalhada.py b/backend/scripts/analise_detalhada.py new file mode 100644 index 0000000..5964c74 --- /dev/null +++ b/backend/scripts/analise_detalhada.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import sys +import os +import asyncio +import json +from datetime import datetime + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from infrastructure.elasticsearch.client import ElasticsearchClient + +ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200" +ES_INDEX = "atuacapes" +ES_USER = "admin-atuacapes" +ES_PASSWORD = "O}!S0bj%FhJ:" + +IDS = [12932, 6273] + + +async def buscar_por_id(es_client, id_pessoa): + response = await es_client.client.post( + f"{es_client.url}/{es_client.index}/_search", + json={ + "size": 1, + "query": {"term": {"id": id_pessoa}}, + "_source": ["id", "dadosPessoais", "atuacoes"] + } + ) + response.raise_for_status() + result = response.json() + hits = result.get("hits", {}).get("hits", []) + return hits[0]["_source"] if hits else None + + +async def main(): + print("Conectando ao Elasticsearch...") + es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD) + await es_client.connect() + + try: + for id_pessoa in IDS: + print(f"\n{'='*120}") + print(f"ANÁLISE DETALHADA - ID: {id_pessoa}") + print(f"{'='*120}") + + doc = await buscar_por_id(es_client, id_pessoa) + if not doc: + print(f" Não encontrado") + continue + + dados_pessoais = doc.get("dadosPessoais", {}) + nome = dados_pessoais.get("nome", "N/A") + atuacoes = doc.get("atuacoes", []) + + print(f"\nNOME: {nome}") + print(f"ID: {id_pessoa}") + print(f"CPF: {dados_pessoais.get('cpf', 'N/A')}") + print(f"\nTOTAL DE ATUAÇÕES: {len(atuacoes)}") + + por_tipo = {} + for a in atuacoes: + tipo = a.get("tipo", "Desconhecido") + por_tipo[tipo] = por_tipo.get(tipo, 0) + 1 + + print(f"\nDISTRIBUIÇÃO POR TIPO:") + for tipo, count in sorted(por_tipo.items()): + print(f" {tipo}: {count}") + + print(f"\n{'='*120}") + print("EVENTOS DETALHADOS") + print(f"{'='*120}") + + eventos = [a for a in atuacoes if a.get("tipo") == "Evento"] + print(f"\nTOTAL DE EVENTOS: {len(eventos)}") + + if eventos: + por_descricao = {} + for e in eventos: + desc = e.get("descricao", "Sem descrição") + por_descricao[desc] = por_descricao.get(desc, 0) + 1 + + print(f"\nEVENTOS POR DESCRIÇÃO:") + for desc, count in sorted(por_descricao.items(), key=lambda x: x[1], reverse=True): + print(f" {desc}: {count}") + + print(f"\nPRIMEIROS 10 EVENTOS:") + for idx, e in enumerate(eventos[:10], 1): + print(f"\n Evento {idx}:") + print(f" Descrição: {e.get('descricao', 'N/A')}") + print(f" Início: {e.get('inicio', 'N/A')}") + print(f" Fim: {e.get('fim', 'N/A')}") + print(f" Nome: {e.get('nome', 'N/A')}") + if e.get("dadosEvento"): + print(f" Dados Evento: {json.dumps(e['dadosEvento'], indent=8, ensure_ascii=False)[:200]}") + + print(f"\n{'='*120}") + print("COORDENAÇÕES CAPES") + print(f"{'='*120}") + + coordenacoes = [a for a in atuacoes if a.get("tipo") in [ + "Coordenação de Área de Avaliação", + "Histórico de Coordenação de Área de Avaliação" + ]] + + for idx, coord in enumerate(coordenacoes, 1): + print(f"\nCoordenação {idx}:") + print(f" Tipo: {coord.get('tipo')}") + print(f" Nome: {coord.get('nome', 'N/A')}") + print(f" Descrição: {coord.get('descricao', 'N/A')}") + print(f" Início: {coord.get('inicio', 'N/A')}") + print(f" Fim: {coord.get('fim', 'N/A')}") + + dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea") + if dados: + print(f" Dados detalhados:") + print(f" Tipo função: {dados.get('tipo', 'N/A')}") + area = dados.get("areaAvaliacao", {}) + if isinstance(area, dict): + print(f" Área: {area.get('nome', 'N/A')} (ID: {area.get('id', 'N/A')})") + else: + print(f" Área: {area}") + colegios = dados.get("colegio", []) + if colegios: + print(f" Colégios: {[c.get('nome') for c in colegios if isinstance(c, dict)]}") + + print(f"\n{'='*120}") + print("CONSULTORIAS") + print(f"{'='*120}") + + consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]] + for idx, cons in enumerate(consultorias, 1): + print(f"\nConsultoria {idx}:") + print(f" Tipo: {cons.get('tipo')}") + print(f" Início: {cons.get('inicio', 'N/A')}") + print(f" Fim: {cons.get('fim', 'N/A')}") + dados = cons.get("dadosConsultoria", {}) + if dados: + print(f" Situação: {dados.get('situacaoConsultoria', 'N/A')}") + print(f" Início situação: {dados.get('inicioSituacao', 'N/A')}") + print(f" Inativação: {dados.get('inativacaoSituacao', 'N/A')}") + + print(f"\n{'='*120}") + print("PREMIAÇÕES") + print(f"{'='*120}") + + premiacoes = [a for a in atuacoes if "Prêmio" in a.get("tipo", "")] + print(f"\nTOTAL DE PREMIAÇÕES: {len(premiacoes)}") + + por_tipo_prem = {} + for p in premiacoes: + tipo = p.get("tipo", "Desconhecido") + por_tipo_prem[tipo] = por_tipo_prem.get(tipo, 0) + 1 + + print(f"\nPREMIAÇÕES POR TIPO:") + for tipo, count in sorted(por_tipo_prem.items()): + print(f" {tipo}: {count}") + + for tipo_prem in ["Premiação Prêmio", "Avaliação Prêmio", "Inscrição Prêmio"]: + prems = [p for p in premiacoes if p.get("tipo") == tipo_prem] + if prems: + print(f"\n{tipo_prem} ({len(prems)}):") + for idx, p in enumerate(prems[:5], 1): + print(f" {idx}. {p.get('descricao', 'N/A')} ({p.get('inicio', 'N/A')})") + + finally: + await es_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/buscar_consultores_especificos.py b/backend/scripts/buscar_consultores_especificos.py new file mode 100644 index 0000000..6f10e39 --- /dev/null +++ b/backend/scripts/buscar_consultores_especificos.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +import sys +import os +import asyncio +from datetime import datetime +from typing import List, Dict, Any + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from infrastructure.elasticsearch.client import ElasticsearchClient +from domain.services.calculador_pontuacao import CalculadorPontuacao +from domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria, Premiacao, Periodo + +ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200" +ES_INDEX = "atuacapes" +ES_USER = "admin-atuacapes" +ES_PASSWORD = "O}!S0bj%FhJ:" + +NOMES_BUSCAR = [ + "Veronica Maria de Araujo Calado", + "Manoel Damiao de Sousa Neto" +] + + +def parse_date(date_str): + if not date_str: + return None + try: + if '/' in date_str: + return datetime.strptime(date_str[:10], '%d/%m/%Y') + else: + return datetime.strptime(date_str[:10], '%Y-%m-%d') + except: + return None + + +def extrair_coordenacoes_capes(atuacoes: List[Dict]) -> List[CoordenacaoCapes]: + coordenacoes_data = [ + a for a in atuacoes + if a.get("tipo") in [ + "Coordenação de Área de Avaliação", + "Histórico de Coordenação de Área de Avaliação", + ] + ] + + result = [] + for coord in coordenacoes_data: + dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea") or {} + + nome = coord.get("nome", "") + if "câmara" in nome.lower() or "camara" in nome.lower(): + tipo = "CAM" + elif "mestrado profissional" in nome.lower(): + tipo = "CAJ-MP" + elif "adjunt" in nome.lower(): + tipo = "CAJ" + else: + tipo = "CA" + + inicio = parse_date(coord.get("inicio") or dados.get("inicio")) + fim = parse_date(coord.get("fim") or dados.get("fim")) + + if not inicio: + continue + + area = dados.get("areaAvaliacao", {}) + if isinstance(area, dict): + area_nome = area.get("nome", "N/A") + else: + area_nome = str(area) if area else "N/A" + + periodo = Periodo(inicio=inicio, fim=fim) + + result.append(CoordenacaoCapes( + tipo=tipo, + area_avaliacao=area_nome, + periodo=periodo, + areas_adicionais=[], + ja_coordenou_antes=False + )) + + return result + + +def extrair_consultoria(atuacoes: List[Dict]) -> Consultoria: + consultorias = [ + a for a in atuacoes + if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] + ] + + if not consultorias: + return None + + datas = [] + areas = set() + total_eventos = len(consultorias) + vezes_responsavel = 0 + + for cons in consultorias: + dados = cons.get("dadosConsultoria", {}) + + inicio = parse_date(cons.get("inicio")) + fim = parse_date(cons.get("fim")) + + if inicio: + datas.append(inicio) + if fim: + datas.append(fim) + + area = dados.get("areaAvaliacao") + if area: + areas.add(area) + + if dados.get("responsavel"): + vezes_responsavel += 1 + + if not datas: + return None + + primeiro_evento = min(datas) + ultimo_evento = max(datas) + + limite_recente = datetime.now().replace(year=datetime.now().year - 2) + eventos_recentes = sum(1 for d in datas if d >= limite_recente) + + anos = (datetime.now() - primeiro_evento).days / 365.25 + + return Consultoria( + total_eventos=total_eventos, + eventos_recentes=eventos_recentes, + primeiro_evento=primeiro_evento, + ultimo_evento=ultimo_evento, + areas=list(areas), + situacao="Ativo" if eventos_recentes > 0 else "Histórico", + anos_completos=int(anos), + anos_consecutivos=int(anos), + retornos=0 + ) + + +def extrair_premiacoes(atuacoes: List[Dict]) -> List[Premiacao]: + premiacoes_data = [ + a for a in atuacoes + if a.get("tipo") in [ + "Premiação Prêmio", + "Avaliação Prêmio", + "Inscrição Prêmio", + ] + ] + + result = [] + mapa_pontos = { + "Premiação Prêmio": 60, + "Avaliação Prêmio": 40, + "Inscrição Prêmio": 20, + } + + for prem in premiacoes_data: + tipo = prem.get("tipo", "") + dados = prem.get("dadosPremiacaoPremio") or prem.get("dadosParticipacaoPremio") or prem.get("dadosParticipacaoInscricaoPremio") or {} + + nome_premio = dados.get("premio", "") or prem.get("descricao", "") + ano_str = dados.get("ano") or prem.get("inicio", "") + + try: + ano = int(ano_str) if isinstance(ano_str, (int, str)) and str(ano_str).isdigit() else 0 + except: + ano = 0 + + pontos = mapa_pontos.get(tipo, 0) + + result.append(Premiacao( + tipo=tipo, + nome_premio=nome_premio, + ano=ano, + pontos=pontos + )) + + return result + + +async def buscar_por_nome(es_client, nome): + query = { + "size": 5, + "query": { + "match": { + "dadosPessoais.nome": { + "query": nome, + "fuzziness": "AUTO" + } + } + }, + "_source": ["id", "dadosPessoais", "atuacoes"] + } + + response = await es_client.client.post( + f"{es_client.url}/{es_client.index}/_search", + json=query + ) + response.raise_for_status() + result = response.json() + return result.get("hits", {}).get("hits", []) + + +async def main(): + print("Conectando ao Elasticsearch...") + es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD) + await es_client.connect() + + calculador = CalculadorPontuacao() + + try: + for nome_buscar in NOMES_BUSCAR: + print(f"\n{'='*100}") + print(f"BUSCANDO: {nome_buscar}") + print(f"{'='*100}") + + hits = await buscar_por_nome(es_client, nome_buscar) + + if not hits: + print(f" Nenhum resultado encontrado para '{nome_buscar}'") + continue + + print(f" Encontrados {len(hits)} resultados") + + for hit in hits: + doc = hit["_source"] + id_pessoa = doc.get("id") + dados_pessoais = doc.get("dadosPessoais", {}) + nome = dados_pessoais.get("nome", "N/A") + cpf = dados_pessoais.get("cpf", "") + atuacoes = doc.get("atuacoes", []) + + print(f"\n CONSULTOR ENCONTRADO: {nome}") + print(f" ID: {id_pessoa}") + + tipos_atuacao = {} + for a in atuacoes: + tipo = a.get("tipo", "Desconhecido") + tipos_atuacao[tipo] = tipos_atuacao.get(tipo, 0) + 1 + + print(f"\n TIPOS DE ATUAÇÃO:") + for tipo, count in sorted(tipos_atuacao.items()): + print(f" - {tipo}: {count}") + + coordenacoes_capes = extrair_coordenacoes_capes(atuacoes) + consultoria = extrair_consultoria(atuacoes) + premiacoes = extrair_premiacoes(atuacoes) + + consultor = Consultor( + id_pessoa=id_pessoa, + nome=nome, + cpf=cpf, + coordenacoes_capes=coordenacoes_capes, + coordenacoes_programas=[], + consultoria=consultoria, + premiacoes=premiacoes + ) + + pontuacao = calculador.calcular_pontuacao_completa(consultor) + consultor.pontuacao = pontuacao + + print(f"\n PONTUAÇÃO CALCULADA:") + print(f" TOTAL: {consultor.pontuacao_total:.2f} pontos") + print(f" Componente A (Coordenação CAPES): {consultor.pontuacao.componente_a.total:.2f}") + print(f" Base: {consultor.pontuacao.componente_a.base} | Tempo: {consultor.pontuacao.componente_a.tempo}") + print(f" Extras: {consultor.pontuacao.componente_a.extras} | Bônus: {consultor.pontuacao.componente_a.bonus} | Retorno: {consultor.pontuacao.componente_a.retorno}") + print(f" Componente B (Coordenação PPG): {consultor.pontuacao.componente_b.total:.2f}") + print(f" Componente C (Consultoria): {consultor.pontuacao.componente_c.total:.2f}") + if consultoria: + print(f" Base: {consultor.pontuacao.componente_c.base} | Tempo: {consultor.pontuacao.componente_c.tempo}") + print(f" Extras: {consultor.pontuacao.componente_c.extras}") + print(f" Eventos: {consultoria.total_eventos} | Recentes: {consultoria.eventos_recentes}") + print(f" Situação: {consultoria.situacao}") + print(f" Anos: {consultoria.anos_completos}") + print(f" Componente D (Premiações): {consultor.pontuacao.componente_d.total:.2f}") + if premiacoes: + print(f" Total de premiações: {len(premiacoes)}") + for p in premiacoes[:5]: + print(f" - {p.tipo}: {p.nome_premio} ({p.ano}) = {p.pontos} pts") + + print(f"\n DETALHAMENTO COMPLETO:") + if coordenacoes_capes: + print(f" Coordenações CAPES ({len(coordenacoes_capes)}):") + for coord in coordenacoes_capes: + status = "ATIVA" if coord.periodo.ativo else "ENCERRADA" + print(f" - {coord.tipo}: {coord.area_avaliacao}") + print(f" Período: {coord.periodo.inicio} até {coord.periodo.fim or 'atual'} ({status})") + print(f" Anos: {coord.periodo.anos_decorridos:.1f}") + + finally: + await es_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/top10_ranking.py b/backend/scripts/top10_ranking.py new file mode 100644 index 0000000..7fffd7f --- /dev/null +++ b/backend/scripts/top10_ranking.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +import sys +import os +import asyncio +from datetime import datetime +from typing import List, Dict, Any + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from infrastructure.elasticsearch.client import ElasticsearchClient +from domain.services.calculador_pontuacao import CalculadorPontuacao +from domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria, Premiacao, Periodo + +ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200" +ES_INDEX = "atuacapes" +ES_USER = "admin-atuacapes" +ES_PASSWORD = "O}!S0bj%FhJ:" + + +def parse_date(date_str): + if not date_str: + return None + try: + if '/' in date_str: + return datetime.strptime(date_str[:10], '%d/%m/%Y') + else: + return datetime.strptime(date_str[:10], '%Y-%m-%d') + except: + return None + + +def extrair_coordenacoes_capes(atuacoes: List[Dict]) -> List[CoordenacaoCapes]: + coordenacoes_data = [ + a for a in atuacoes + if a.get("tipo") in [ + "Coordenação de Área de Avaliação", + "Histórico de Coordenação de Área de Avaliação", + ] + ] + + result = [] + for coord in coordenacoes_data: + dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea") or {} + + nome = coord.get("nome", "") + if "câmara" in nome.lower() or "camara" in nome.lower(): + tipo = "CAM" + elif "mestrado profissional" in nome.lower(): + tipo = "CAJ-MP" + elif "adjunt" in nome.lower(): + tipo = "CAJ" + else: + tipo = "CA" + + inicio = parse_date(coord.get("inicio") or dados.get("inicio")) + fim = parse_date(coord.get("fim") or dados.get("fim")) + + if not inicio: + continue + + area = dados.get("areaAvaliacao", {}) + if isinstance(area, dict): + area_nome = area.get("nome", "N/A") + else: + area_nome = str(area) if area else "N/A" + + periodo = Periodo(inicio=inicio, fim=fim) + + result.append(CoordenacaoCapes( + tipo=tipo, + area_avaliacao=area_nome, + periodo=periodo, + areas_adicionais=[], + ja_coordenou_antes=False + )) + + return result + + +def extrair_consultoria(atuacoes: List[Dict]) -> Consultoria: + consultorias = [ + a for a in atuacoes + if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] + ] + + if not consultorias: + return None + + datas = [] + areas = set() + total_eventos = len(consultorias) + vezes_responsavel = 0 + + for cons in consultorias: + dados = cons.get("dadosConsultoria", {}) + + inicio = parse_date(cons.get("inicio")) + fim = parse_date(cons.get("fim")) + + if inicio: + datas.append(inicio) + if fim: + datas.append(fim) + + area = dados.get("areaAvaliacao") + if area: + areas.add(area) + + if dados.get("responsavel"): + vezes_responsavel += 1 + + if not datas: + return None + + primeiro_evento = min(datas) + ultimo_evento = max(datas) + + limite_recente = datetime.now().replace(year=datetime.now().year - 2) + eventos_recentes = sum(1 for d in datas if d >= limite_recente) + + anos = (datetime.now() - primeiro_evento).days / 365.25 + + return Consultoria( + total_eventos=total_eventos, + eventos_recentes=eventos_recentes, + primeiro_evento=primeiro_evento, + ultimo_evento=ultimo_evento, + areas=list(areas), + situacao="Ativo" if eventos_recentes > 0 else "Histórico", + anos_completos=int(anos), + anos_consecutivos=int(anos), + retornos=0 + ) + + +def extrair_premiacoes(atuacoes: List[Dict]) -> List[Premiacao]: + premiacoes_data = [ + a for a in atuacoes + if a.get("tipo") in [ + "Premiação Prêmio", + "Avaliação Prêmio", + "Inscrição Prêmio", + ] + ] + + result = [] + mapa_pontos = { + "Premiação Prêmio": 60, + "Avaliação Prêmio": 40, + "Inscrição Prêmio": 20, + } + + for prem in premiacoes_data: + tipo = prem.get("tipo", "") + dados = prem.get("dadosPremiacaoPremio") or prem.get("dadosParticipacaoPremio") or prem.get("dadosParticipacaoInscricaoPremio") or {} + + nome_premio = dados.get("premio", "") or prem.get("descricao", "") + ano_str = dados.get("ano") or prem.get("inicio", "") + + try: + ano = int(ano_str) if isinstance(ano_str, (int, str)) and str(ano_str).isdigit() else 0 + except: + ano = 0 + + pontos = mapa_pontos.get(tipo, 0) + + result.append(Premiacao( + tipo=tipo, + nome_premio=nome_premio, + ano=ano, + pontos=pontos + )) + + return result + + +async def main(): + print("Conectando ao Elasticsearch...") + es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD) + await es_client.connect() + + try: + print("Buscando candidatos com boost...") + docs = await es_client.buscar_candidatos_ranking(size=100) + print(f"Encontrados {len(docs)} candidatos") + + calculador = CalculadorPontuacao() + consultores = [] + + print("\nProcessando consultores...") + for idx, doc in enumerate(docs, 1): + id_pessoa = doc.get("id") + dados_pessoais = doc.get("dadosPessoais", {}) + nome = dados_pessoais.get("nome", "N/A") + cpf = dados_pessoais.get("cpf", "") + atuacoes = doc.get("atuacoes", []) + + coordenacoes_capes = extrair_coordenacoes_capes(atuacoes) + consultoria = extrair_consultoria(atuacoes) + premiacoes = extrair_premiacoes(atuacoes) + + consultor = Consultor( + id_pessoa=id_pessoa, + nome=nome, + cpf=cpf, + coordenacoes_capes=coordenacoes_capes, + coordenacoes_programas=[], + consultoria=consultoria, + premiacoes=premiacoes + ) + + pontuacao = calculador.calcular_pontuacao_completa(consultor) + consultor.pontuacao = pontuacao + + consultores.append(consultor) + + if idx % 10 == 0: + print(f" Processados {idx}/{len(docs)} consultores...") + + print("\nOrdenando por pontuação...") + consultores.sort(key=lambda c: c.pontuacao_total, reverse=True) + + print("\n" + "="*100) + print("TOP 10 CONSULTORES - RANKING CAPES") + print("="*100) + + for rank, c in enumerate(consultores[:10], 1): + print(f"\n{rank}º LUGAR - {c.nome}") + print(f" ID: {c.id_pessoa}") + print(f" PONTUAÇÃO TOTAL: {c.pontuacao_total:.2f} pontos") + print(f" Componente A (Coordenação CAPES): {c.pontuacao.componente_a.total:.2f}") + print(f" Base: {c.pontuacao.componente_a.base} | Tempo: {c.pontuacao.componente_a.tempo}") + print(f" Extras: {c.pontuacao.componente_a.extras} | Bônus: {c.pontuacao.componente_a.bonus} | Retorno: {c.pontuacao.componente_a.retorno}") + print(f" Componente B (Coordenação PPG): {c.pontuacao.componente_b.total:.2f}") + print(f" Componente C (Consultoria): {c.pontuacao.componente_c.total:.2f}") + if c.consultoria: + print(f" Base: {c.pontuacao.componente_c.base} | Tempo: {c.pontuacao.componente_c.tempo}") + print(f" Eventos: {c.consultoria.total_eventos} | Recentes: {c.consultoria.eventos_recentes}") + print(f" Áreas: {', '.join(c.consultoria.areas[:3])}") + print(f" Componente D (Premiações): {c.pontuacao.componente_d.total:.2f}") + if c.premiacoes: + print(f" Total de premiações: {len(c.premiacoes)}") + + print(f" Anos de atuação: {c.anos_atuacao}") + print(f" Ativo: {'Sim' if c.ativo else 'Não'}") + + if c.coordenacoes_capes: + print(f" Coordenações CAPES:") + for coord in c.coordenacoes_capes[:3]: + status = "ATIVA" if coord.periodo.ativo else "ENCERRADA" + print(f" - {coord.tipo}: {coord.area_avaliacao} ({status})") + + print("\n" + "="*100) + print(f"\nEstatísticas Gerais:") + print(f" Total de candidatos processados: {len(consultores)}") + print(f" Pontuação máxima: {consultores[0].pontuacao_total:.2f}") + print(f" Pontuação mínima (top 10): {consultores[9].pontuacao_total:.2f}") + print(f" Média de pontuação (top 10): {sum(c.pontuacao_total for c in consultores[:10])/10:.2f}") + + finally: + await es_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/reload_atuacapes.sh b/scripts/reload_atuacapes.sh new file mode 100755 index 0000000..a381eb2 --- /dev/null +++ b/scripts/reload_atuacapes.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +echo "[1/5] Garantindo rede Docker compartilhada..." +docker network create shared_network >/dev/null 2>&1 || true + +echo "[2/5] Subindo oracle18c e backend..." +docker compose up -d oracle18c backend + +echo "[3/5] Aguardando backend responder /health..." +for i in {1..30}; do + if docker compose exec backend python - <<'PY' >/dev/null 2>&1; then +import httpx, sys +try: + r = httpx.get("http://localhost:8000/api/v1/health", verify=False, timeout=15) + if r.status_code == 200: + sys.exit(0) +except Exception: + pass +sys.exit(1) +PY + echo " Backend OK." + break + fi + echo " Tentativa ${i}/30... aguardando 5s" + sleep 5 + if [ "$i" -eq 30 ]; then + echo "ERRO: backend não respondeu /health." + exit 1 + fi +done + +echo "[4/5] Disparando job do ranking (limpar_antes=true)..." +docker compose exec backend python - <<'PY' +import httpx +client = httpx.Client(verify=False, timeout=120) +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) +PY + +echo "[5/5] Acompanhando status até finalizar..." +docker compose exec backend python - <<'PY' +import httpx, time +client = httpx.Client(verify=False, timeout=120) +while True: + r = client.get("http://localhost:8000/api/v1/ranking/status") + data = r.json() + print(f"{time.strftime('%H:%M:%S')} | running={data.get('running')} | {data.get('processados')}/{data.get('total')} ({data.get('progress')}%) | msg={data.get('mensagem')}") + if not data.get("running"): + break + time.sleep(15) + +print("Coletando contagens finais...") +dsn = None +try: + import os, cx_Oracle + dsn = os.getenv("ORACLE_LOCAL_DSN", "oracle18c:1521/XEPDB1") + user = os.getenv("ORACLE_LOCAL_USER", "local123") + pwd = os.getenv("ORACLE_LOCAL_PASSWORD", "local123") + conn = cx_Oracle.connect(user, pwd, dsn) + cur = conn.cursor() + for sql in [ + "select count(*) from tb_ranking_consultor", + "select count(*) from tb_ranking_consultor where componente_a>0", + "select count(*) from tb_ranking_consultor where componente_c>0", + "select count(*) from tb_ranking_consultor where componente_d>0", + "select max(pontuacao_total) from tb_ranking_consultor" + ]: + cur.execute(sql) + print(sql, cur.fetchone()[0]) + cur.close(); conn.close() +except Exception as e: + print(f"AVISO: não foi possível coletar contagens finais (dsn={dsn}): {e}") +PY + +echo "Pronto."