diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95783a1..350ef4d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,6 +37,7 @@ jobs: - name: Start API run: | + mkdir -p output uvicorn main:app --host 0.0.0.0 --port 8000 & sleep 5 - name: Check API health diff --git a/.gitignore b/.gitignore index 3b13e98..1a74220 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ report-generator-demo/frontend/dist/ .env node_modules/ frontend/dist/ +frontend/node_modules report/ssr-dist/ report/node_modules/ *.lock diff --git a/README.md b/README.md index 2ccfd66..669b4b9 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,151 @@ -# report-generator-demo +# Automatic Reporting -Projeto com duas partes: +Sistema de geração de relatórios PDF para o Data Nordeste. Combina dados de CSVs com textos descritivos do Google Docs e renderiza relatórios visuais em PDF. -- **API em FastAPI** para gerar relatórios do Data Nordeste. -- **Frontend em React + Vite** para escolher macrotema e cidade e disparar o relatório. +## Arquitetura -## Estrutura +``` +Browser (React SPA) + │ + ▼ +FastAPI (Python) ──────► Node.js (React SSR) + │ │ + │ ▼ + │ HTML renderizado + ▼ │ +WeasyPrint │ + │ │ + ▼ │ + PDF ◄─────────────────────┘ +``` + +- **Python/FastAPI**: processa dados, gerencia CSV, Docs, gráficos +- **React SSR**: renderiza HTML da capa e conteúdo via `renderToStaticMarkup` +- **WeasyPrint**: converte HTML em PDF -- `main.py` — API FastAPI -- `citys.txt` — lista de cidades usadas no frontend -- `demografia.csv` — base de dados do relatório -- `output/` — arquivos HTML/PDF e gráficos gerados -- `frontend/` — interface web +## Stack + +- **Backend**: FastAPI (Python 3.11+) +- **Frontend**: React + Vite +- **PDF**: WeasyPrint + React SSR +- **Build**: npm workspaces (monorepo) ## Requisitos -- Python 3.10+ +- Python 3.11+ - Node.js 18+ +- npm -## Instalação da API - -Dentro da pasta `report-generator-demo`: +## Quick start ```bash -python -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt +# Ativar ambiente virtual e rodar +source .venv/bin/activate && ./build.sh ``` -## Como executar a API +Isso instala dependências Python e JS, builda o SSR e o frontend, e sobe os servidores. + +## Componentes do build ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +pip install -r requirements.txt # Python deps +npm install # Node deps +npm run build -w report # Build React SSR → report/ssr-dist/ +npm run build -w frontend # Build SPA → frontend/dist/ ``` -Se quiser usar um Google Docs específico como texto-base do relatório: +## Como rodar ```bash -export DATANE_DOCS_URL="https://docs.google.com/document/d/SEU_DOC_ID/edit" +source .venv/bin/activate && ./build.sh ``` -Se essa variável não for definida, a API usa o documento padrão já configurado no `main.py`. +O `build.sh` inicia: +- Servidor SSR (Node.js) na porta 3001 +- API FastAPI na porta 8000 -## Instalação do frontend +## Endpoints da API -Dentro da pasta `report-generator-demo/frontend`: +| Método | URL | Descrição | +|--------|-----|-----------| +| GET | `/cities` | Lista de cidades | +| GET | `/macrotemas` | Lista de macrotemas | +| GET | `/relatorios` | Relatórios gerados | +| GET | `/relatorio/{cidade}?macrotema={tema}` | Gera relatório | +| DELETE | `/relatorios/{arquivo}` | Remove relatório | -```bash -yarn install -``` +## Macrotemas -## Como executar o frontend +- `todos` — Todos os temas concatenados +- `demografia` — Demografia +- `educacao` — Educação +- `saude` — Saúde +- `economia-renda` — Economia e Renda +- `saneamento` — Saneamento +- `hidraulica` — Segurança Hídrica -```bash -yarn dev -``` - -O frontend abre normalmente em `http://localhost:5173`. +## Estrutura de diretórios -Se quiser, também funciona: - -```bash -yarn start +``` +/ +├── main.py # FastAPI app +├── reports.py # Lógica de geração (orquestrador) +├── config.py # Configurações e variáveis de ambiente +├── plotting.py # Gráficos matplotlib +│ +├── frontend/ # SPA React (interface) +│ └── src/App.jsx +│ +├── report/ # React SSR (renderização PDF) +│ └── src/components/ # Cover, Report, Brand, etc. +│ +├── utils/ +│ ├── cover.py # Monta objeto "cover" para o React +│ ├── renderer.py # Converte markdown Docs → HTML +│ ├── ssr.py # Chama Node.js para renderizar +│ ├── docs.py # Baixa Google Docs +│ ├── cities.py # Filtro e список cidades +│ ├── macrotemas.py # Config dos temas +│ └── maps.py # Mapas (Contentful ou gerado) +│ +├── docs/ +│ └── ARCHITECTURE.md # Documentação da arquitetura +│ +└── output/ # Relatórios gerados (.html + .pdf) ``` -## Como usar -1. Rode a API. -2. Rode o frontend. -3. Abra o frontend no navegador. -4. Clique em **Gerar relatório**. -5. Escolha o macrotema e a cidade. -6. Clique em **Gerar relatório** novamente para abrir o relatório. +### Resumo das etapas -Por enquanto, o macrotema é apenas visual no formulário; a API recebe apenas a cidade. +| Etapa | O que acontece | +|-------|----------------| +| 1 | Carrega CSV do tema selecionado (pandas) | +| 2 | Filtra linhas pela cidade informada | +| 3 | Gera gráficos matplotlib (só se demografia) | +| 4 | Busca texto no Google Docs (com cache) | +| 5 | Extrai seções marcadas (##resumo_tema, etc.) | +| 6 | Preenche cover com métricas, score, indicadores | +| 7 | Converte markdown dos Docs em HTML | +| 8 | React SSR renderiza HTML final | +| 9 | Salva HTML, PDF gerado em background | -## O que a API faz -Quando você acessa `/relatorio/{cidade}`: +## Variáveis de ambiente -1. lê o CSV `demografia.csv`; -2. procura a cidade informada, com ou sem UF; -3. busca o texto-base no Google Docs; -4. renderiza o HTML do relatório; -5. gera gráfico de população por sexo; -6. salva os arquivos em `output/`; -7. devolve o HTML no navegador. +```bash +# URLs dos CSVs (por tema) +DEMOGRAFIA_CSV_URL, EDUCACAO_CSV_URL, SAUDE_CSV_URL, etc. -## Exemplo de uso direto da API +# URLs dos Docs (por tema) +DEMOGRAFIA_DOCS_URL, EDUCACAO_DOCS_URL, etc. -- `http://127.0.0.1:8000/docs` -- `http://127.0.0.1:8000/relatorio/Caruaru%20(PE)` +# Contentful (mapas) +CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN +``` -## Arquivos importantes do frontend +## Docker -- `frontend/src/App.jsx` — tela principal -- `frontend/src/styles.css` — estilos da interface -- `frontend/package.json` — scripts do frontend +```bash +docker build -t automatic-reporting . +docker run -p 8000:8000 automatic-reporting +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4d204f8 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +pip install -r requirements.txt +npm install +npm run build -w report +npm run build -w frontend +pkill -f "node report/ssr-dist/server.js" +node report/ssr-dist/server.js & uvicorn main:app --reload diff --git a/config.py b/config.py index 82bf05e..d70191d 100644 --- a/config.py +++ b/config.py @@ -35,86 +35,7 @@ def get_config_value(name: str) -> str | None: SANEAMENTO_DOCS_URL = get_config_value("SANEAMENTO_DOCS_URL") HIDRAULICA_DOCS_URL = get_config_value("HIDRAULICA_DOCS_URL") -MACROTEMAS = { - "demografia": { - "nome": "Demografia", - "docs_url": DEMOGRAFIA_DOCS_URL, - "docs_env": "DEMOGRAFIA_DOCS_URL", - "csv_url": DEMOGRAFIA_CSV_URL, - "csv_env": "DEMOGRAFIA_CSV_URL", - }, - "educacao": { - "nome": "Educação", - "docs_url": EDUCACAO_DOCS_URL, - "docs_env": "EDUCACAO_DOCS_URL", - "csv_url": EDUCACAO_CSV_URL, - "csv_env": "EDUCACAO_CSV_URL", - }, - "saude": { - "nome": "Saúde", - "docs_url": SAUDE_DOCS_URL, - "docs_env": "SAUDE_DOCS_URL", - "csv_url": SAUDE_CSV_URL, - "csv_env": "SAUDE_CSV_URL", - }, - "economia-renda": { - "nome": "Economia e Renda", - "docs_url": ECONOMIA_RENDA_DOCS_URL, - "docs_env": "ECONOMIA_RENDA_DOCS_URL", - "csv_url": ECONOMIA_RENDA_CSV_URL, - "csv_env": "ECONOMIA_RENDA_CSV_URL", - }, - "saneamento": { - "nome": "Saneamento", - "docs_url": SANEAMENTO_DOCS_URL, - "docs_env": "SANEAMENTO_DOCS_URL", - "csv_url": SANEAMENTO_CSV_URL, - "csv_env": "SANEAMENTO_CSV_URL", - }, - "hidraulica": { - "nome": "Hidráulica", - "docs_url": HIDRAULICA_DOCS_URL, - "docs_env": "HIDRAULICA_DOCS_URL", - "csv_url": HIDRAULICA_CSV_URL, - "csv_env": "HIDRAULICA_CSV_URL", - }, -} - -MACROTEMA_SECOES = { - "demografia": { - "numero": "01", - "titulo": "Demografia", - "aliases": ["demografia"], - }, - "educacao": { - "numero": "02", - "titulo": "Educação", - "aliases": ["educacao", "educação"], - }, - "saude": { - "numero": "03", - "titulo": "Saúde", - "aliases": ["saude", "saúde"], - }, - "economia-renda": { - "numero": "04", - "titulo": "Economia e Renda", - "aliases": ["economia", "economia e renda"], - }, - "saneamento": { - "numero": "05", - "titulo": "Infraestrutura e Saneamento", - "aliases": ["saneamento", "infraestrutura e saneamento"], - }, - "hidraulica": { - "numero": "06", - "titulo": "Segurança Hídrica", - "aliases": ["hidraulica", "hidráulica", "seguranca hidrica", "segurança hídrica"], - }, -} -OUTPUT_DIR = BASE_DIR / "output" -CITIES_FILE = BASE_DIR / "citys.txt" def resolve_csv_source(source: str | None, env_name: str = "CSV_URL") -> str | Path: if not source: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 413130e..facd165 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,104 +12,173 @@ Cada camada tem uma responsabilidade única: Python processa dados, React render --- -## Fluxo detalhado +## Fluxo de execução + +### Visão geral em 5 passos ``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Browser (React SPA) │ -│ frontend/src/App.jsx │ -│ Interface pra selecionar cidade + macrotema │ -│ Comunica com FastAPI via fetch() │ -└───────────────────────────┬─────────────────────────────────────────┘ - │ GET /relatorio/{cidade}?macrotema={tema} - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ FastAPI (Python) — main.py :8000 │ -│ │ -│ reports.py — gerar_relatorio_handler(): │ -│ │ -│ 1. Lê CSV do Google Drive (pandas) │ -│ └── utils/macrotemas.py — URLs de cada tema (demografia, │ -│ educação, saúde, etc.) │ -│ │ -│ 2. Filtra por cidade │ -│ └── utils/cities.py — filtrar_linhas_por_cidade() │ -│ │ -│ 3. Lê texto descritivo do Google Docs │ -│ └── utils/docs.py — carregar_texto_do_docs() │ -│ └── extrai resumo_tema / descricao_tema via marcadores │ -│ │ -│ 4. Gera gráficos matplotlib │ -│ └── plotting.py — gerar_grafico_sexo(), _porte(), _top_cidades()│ -│ │ -│ 5. Monta objeto "cover" (dicionário Python) │ -│ └── utils/cover.py — montar_capa_relatorio() │ -│ ├── metricas (4 cards: área, população, IDH, PIB) │ -│ ├── score (valor, max, status, descricao) │ -│ ├── macrotema (nome, icone, status, resumo, indicadores) │ -│ └── data_extenso, cidade_nome, uf │ -│ │ -│ 6. Converte texto markdown dos Docs → HTML │ -│ └── utils/renderer.py — texto_para_html() │ -│ ├── processa %%chart, ##componente, $placeholders │ -│ ├── gera

,

") em_lista = False - nome_componente = marcador_componente.group(1) - - if nome_componente == "tabela_resumo": - - html_lines.append( - render_tabela_resumo( - contexto=contexto, - namespace=namespace, - ) - ) - continue # GRÁFICOS @@ -345,16 +292,7 @@ def texto_para_html( em_lista = False # SEÇÕES - secao_macrotema = identificar_secao_macrotema( - linha_limpa, - namespace, - ) - - if secao_macrotema: - proximo_paragrafo_destaque = False - continue - - elif ( + if ( re.match(r"^\d+\.\s+", linha_limpa) or linha_limpa.lower() in {"apresentação", "demografia"} diff --git a/utils/tables.py b/utils/tables.py deleted file mode 100644 index 4012382..0000000 --- a/utils/tables.py +++ /dev/null @@ -1,187 +0,0 @@ -import html as html_module - - -STATUS_MAP = { - "critico": { - "label": "Atenção prioritária", - "cor": "#c0392b", - "bg": "#fbeaea", - }, - "moderado": { - "label": "Atenção moderada", - "cor": "#d4a017", - "bg": "#fdf3d7", - }, - "positivo": { - "label": "Atenção regular", - "cor": "#2e7d32", - "bg": "#e8f5e9", - }, -} - - -MACROTEMA_CONFIG = { - "demografia": { - "tema": "Demografia", - "indicadores": "População residente · Variação populacional · Sexo, idade, cor/raça", - "paineis": "Perfil Demográfico", - "calcular_status": lambda ctx: _status_demografia(ctx), - }, - "educacao": { - "tema": "Educação", - "indicadores": "Taxa de alfabetização (15+) · Distribuição por grau de instrução · Analfabetismo por faixa etária", - "paineis": "Analfabetismo\nNível de Instrução", - "calcular_status": lambda ctx: ("moderado", "Sem dados disponíveis"), - }, - "saude": { - "tema": "Saúde", - "indicadores": "Cobertura vacinal · Mortalidade infantil · Estabelecimentos de saúde", - "paineis": "Imunização\nMortalidade Infantil\nEstabelecimentos de Saúde", - "calcular_status": lambda ctx: ("positivo", "Sem dados disponíveis"), - }, - "economia-renda": { - "tema": "Economia e Renda", - "indicadores": "PIB total e per capita · Composição setorial do VAB · Comércio exterior", - "paineis": "PIB\nExportação\nImportação\nExportação vs Importação", - "calcular_status": lambda ctx: ("critico", "Sem dados disponíveis"), - }, - "saneamento": { - "tema": "Infraestrutura e Saneamento", - "indicadores": "Coleta de lixo · Esgotamento sanitário · Acesso à energia elétrica", - "paineis": "Domicílios por destino de lixo\nDomicílios por tipo de esgotamento sanitário", - "calcular_status": lambda ctx: ("critico", "Sem dados disponíveis"), - }, - "hidraulica": { - "tema": "Segurança Hídrica", - "indicadores": "Cisternas e tecnologias sociais · Distribuição por finalidade · Evolução temporal do programa", - "paineis": "Cisternas", - "calcular_status": lambda ctx: ("positivo", "Sem dados disponíveis"), - }, -} - - -def _status_demografia(ctx: dict) -> tuple[str, str]: - raw = ( - str(ctx.get("cres_pop", "0")) - .replace("%", "") - .replace(",", ".") - .strip() - ) - - try: - cres_pop = float(raw) - - except ValueError: - cres_pop = 0.0 - - if cres_pop < -5: - status_key = "critico" - - elif cres_pop < 0: - status_key = "moderado" - - else: - status_key = "positivo" - - descricao = ( - f"Variação populacional de " - f"{cres_pop:+.1f}% na década" - ) - - return status_key, descricao - - -def _render_paineis(texto: str) -> str: - - linhas = [ - linha.strip() - for linha in texto.strip().splitlines() - if linha.strip() - ] - - if not linhas: - return "" - - primeiro = html_module.escape(linhas[0]) - - resto = "".join( - f"
{html_module.escape(linha)}" - for linha in linhas[1:] - ) - - return f"{primeiro}{resto}" - - -def render_tabela_resumo( - contexto: dict, - namespace: str, -) -> str: - - config = MACROTEMA_CONFIG.get(namespace) - - if not config: - return "" - - status_key, descricao = config["calcular_status"](contexto) - - status = STATUS_MAP[status_key] - - tema = html_module.escape(config["tema"]) - - indicadores = html_module.escape( - config["indicadores"] - ) - - paineis_html = _render_paineis( - config["paineis"] - ) - - label = html_module.escape( - status["label"] - ) - - descricao_esc = html_module.escape( - descricao - ) - - cor = status["cor"] - - bg = status["bg"] - - return f''' - - - - - - - - - - - - - - - - - - - - - - - -
TEMAINDICADORES PRINCIPAISPAINÉIS DATANESTATUS
- {tema} - - {indicadores} - - {paineis_html} - -
- ● {label}
- {descricao_esc} -
-
- ''' \ No newline at end of file