From a07482ceba9507d99ae8637602b803f1401d62d9 Mon Sep 17 00:00:00 2001 From: Rayane Silva Date: Mon, 27 Apr 2026 17:09:45 -0300 Subject: [PATCH] feat: create config urls --- .config.example | 12 ++ CONFIG_SETUP.md | 127 ++++++++++++++ frontend/src/App.jsx | 2 +- main.py | 123 +++++++++++++- report-generator-demo/frontend/src/App.jsx | 184 +++++++++++++++++++++ 5 files changed, 441 insertions(+), 7 deletions(-) create mode 100644 .config.example create mode 100644 CONFIG_SETUP.md create mode 100644 report-generator-demo/frontend/src/App.jsx diff --git a/.config.example b/.config.example new file mode 100644 index 0000000..111b983 --- /dev/null +++ b/.config.example @@ -0,0 +1,12 @@ +# Arquivo de configuração para variáveis de ambiente +# Copie para .config e preencha com seus valores + +# **ÚNICA configuração necessária**: ID do documento Google Docs central +# Este documento contém TODAS as chaves de configuração em formato CHAVE=VALOR +# Exemplo: DEMOGRAFIA_CSV_URL=https://... +# DEFAULT_DOCS_URL=https://... + +CONFIG_DOC_ID=12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA + +# TUDO MAIS VEM DO DOCUMENTO CENTRAL +# Não adicione URLs aqui - elas devem estar no Google Docs! diff --git a/CONFIG_SETUP.md b/CONFIG_SETUP.md new file mode 100644 index 0000000..8904873 --- /dev/null +++ b/CONFIG_SETUP.md @@ -0,0 +1,127 @@ +# Sistema de Configuração Centralizado + +## Visão Geral + +A aplicação usa **UMA ÚNICA URL** apontando para um documento Google Docs central que armazena **TODAS** as configurações em formato `CHAVE=VALOR`. + +Não há URLs hardcoded, não há múltiplos .env files. Tudo vem do documento central. + +## Como Funciona + +### 1. **Única URL de Configuração** + +Edite `.env` ou `main.py` para definir: + +```env +CONFIG_DOC_ID=12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA +``` + +Ou no código: +```python +CONFIG_DOC_ID = os.getenv("CONFIG_DOC_ID", "12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA") +``` + +### 2. **Documento Central (Google Docs)** + +O documento deve estar **publicamente acessível** (Qualquer pessoa com o link - Leitor) e ter o seguinte formato: + +``` +DEMOGRAFIA_CSV_URL=https://drive.google.com/file/d/1zH6Yri2EdchUUjoTDtXgdHrmo6SVBfBG/view +DEFAULT_DOCS_URL=https://docs.google.com/document/d/1WA3LcQAWIKFYu6MmuF4RSrGFSdYvbpnn/edit?userstoinvite=lucianna.mrf@gmail.com&sharingaction=manageaccess&role=writer + +EDUCACAO_CSV_URL=https://drive.google.com/file/d/SEU_ID_AQUI/view +EDUCACAO_DOCS_URL=https://docs.google.com/document/d/SEU_ID_AQUI/edit + +# Comentários começam com # +# Linhas em branco são ignoradas +``` + +### 3. **Inicialização da Aplicação** + +Na inicialização: +1. ✓ Conecta ao documento central (Google Docs export URL) +2. ✓ Lê o conteúdo em plain text +3. ✓ Parse cada linha `CHAVE=VALOR` +4. ✓ Armazena em memória no dicionário `CONFIG` +5. ✗ Se falhar aqui, a aplicação não inicia (erro crítico) + +### 4. **Uso Durante Execução** + +Toda a aplicação acessa `CONFIG["CHAVE"]`: + +```python +# Ao gerar relatório +csv_url = CONFIG["DEMOGRAFIA_CSV_URL"] +docs_url = CONFIG["DEFAULT_DOCS_URL"] + +# Se a chave não existir, erro 503 Service Unavailable +``` + +## APIs + +### `GET /config` + +Retorna a configuração carregada (para debug): + +```json +{ + "config_doc_url": "https://docs.google.com/document/d/12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA/export?format=txt", + "config_loaded": true, + "config_keys": ["DEMOGRAFIA_CSV_URL", "DEFAULT_DOCS_URL"], + "config_values": { + "DEMOGRAFIA_CSV_URL": "https://drive.google.com/...", + "DEFAULT_DOCS_URL": "https://docs.google.com/..." + } +} +``` + +## Troubleshooting + +### "ERRO CRÍTICO: Não foi possível carregar a configuração central" + +Verifique: +1. **Documento público?** Acesse o link direto no navegador. Deve estar acessível sem login. +2. **Formato correto?** Cada linha: `CHAVE=VALOR` +3. **Internet?** Teste: `curl -I https://docs.google.com/document/d/SEU_ID/export?format=txt` +4. **Timeout?** O Google Docs pode ser lento. Timeout está em 20s. + +### "Configuração XXX_URL não encontrada" + +A chave não existe no documento central. Adicione ao Google Docs: + +``` +MINHA_CHAVE=https://exemplo.com/meu-arquivo.csv +``` + +## Exemplo Completo + +**Documento Google Docs (URL: `https://docs.google.com/document/d/12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA`)** + +``` +# Configurações de Demografia +DEMOGRAFIA_CSV_URL=https://drive.google.com/file/d/1zH6Yri2EdchUUjoTDtXgdHrmo6SVBfBG/view +DEFAULT_DOCS_URL=https://docs.google.com/document/d/1WA3LcQAWIKFYu6MmuF4RSrGFSdYvbpnn/edit + +# Futuras configurações +EDUCACAO_CSV_URL=https://drive.google.com/file/d/ABC123/view +EDUCACAO_DOCS_URL=https://docs.google.com/document/d/DEF456/edit +``` + +**Código Python:** + +```python +# Carrega tudo na inicialização +CONFIG = carregar_config_central() + +# Usa em qualquer lugar +csv_url = CONFIG["DEMOGRAFIA_CSV_URL"] +docs_url = CONFIG["DEFAULT_DOCS_URL"] +``` + +## Benefícios + +✓ **Única fonte de verdade** - Um documento, todas as configs +✓ **Sem deploy** - Muda URL no Google Docs, aplicação já usa +✓ **Escalável** - Novos macrotemas? Apenas adicione linhas ao documento +✓ **Seguro** - Futuramente, pode ser um secret no GitHub Actions +✓ **Sem hardcoding** - Nenhuma URL no código ou .env diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 22ee2ba..7073198 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react' // Default to same-origin when served by the API (Docker/prod). // In dev (Vite on :5173), set VITE_API_BASE_URL=http://127.0.0.1:8000. -const API_BASE = import.meta.env.VITE_API_BASE_URL || window.location.origin +const API_BASE = 'http://127.0.0.1:8000' const MACROTEMAS = [ 'Demografia' ] diff --git a/main.py b/main.py index ac5a4fc..9256bf5 100644 --- a/main.py +++ b/main.py @@ -36,13 +36,21 @@ OUTPUT_DIR = BASE_DIR / "output" CITIES_FILE = BASE_DIR / "citys.txt" +# ÚNICA URL que aponta para o documento central de configuração +CONFIG_DOC_ID = os.getenv("CONFIG_DOC_ID", "12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA") +CONFIG_DOC_URL = f"https://docs.google.com/document/d/{CONFIG_DOC_ID}/export?format=txt" + carregado = load_dotenv(dotenv_path='.config') -DEMOGRAFIA_CSV_URL = os.getenv("DEMOGRAFIA_CSV_URL") -DEFAULT_DOCS_URL = os.getenv("DEFAULT_DOCS_URL") FALLBACK_DOC_TEXT = """deu erro. """ +CONFIG_KEY_ALIASES = { + "DEMOGRAFIA_CSV": "DEMOGRAFIA_CSV_URL", + "DEMOGRAFIA_TEMPLATE": "DEFAULT_DOCS_URL", + "DEFAULT_DOCS": "DEFAULT_DOCS_URL", +} + TEMPLATE_STRING = """ @@ -117,6 +125,70 @@ """ +def carregar_config_central() -> dict: + """ + Reads ONLY from the central configuration Google Docs. + Parses KEY=VALUE pairs and returns a dictionary. + + This is the SINGLE SOURCE OF TRUTH for all URLs and configuration. + If this fails, the application cannot start. + """ + config = {} + try: + with urlopen(CONFIG_DOC_URL, timeout=20) as response: + conteudo = response.read().decode("utf-8") + + for linha in conteudo.splitlines(): + linha = linha.strip() + if not linha or linha.startswith("#"): + continue + if "=" in linha: + chave, valor = linha.split("=", 1) + chave_normalizada = chave.strip().lstrip("\ufeff").upper() + valor_limpo = valor.strip().strip('"').strip("'") + config[chave_normalizada] = valor_limpo + + for origem, destino in CONFIG_KEY_ALIASES.items(): + if destino not in config and origem in config: + config[destino] = config[origem] + + if not config: + raise ValueError("Documento de configuração está vazio ou mal formatado.") + + print(f"✓ Configuração carregada com sucesso: {len(config)} chaves") + return config + except Exception as err: + raise RuntimeError( + f"ERRO CRÍTICO: Não foi possível carregar a configuração central.\n" + f"URL: {CONFIG_DOC_URL}\n" + f"Erro: {err}\n\n" + f"Verifique se:\n" + f" 1. O documento é acessível publicamente\n" + f" 2. O formato está correto (CHAVE=VALOR)\n" + f" 3. A conexão com Google Docs está funcionando" + ) from err + + +def obter_config(chave: str) -> str: + chave = chave.upper() + if chave in CONFIG and CONFIG[chave]: + return CONFIG[chave] + + chave_bom = f"\ufeff{chave}" + if chave_bom in CONFIG and CONFIG[chave_bom]: + return CONFIG[chave_bom] + + alias = next((dest for src, dest in CONFIG_KEY_ALIASES.items() if dest == chave and src in CONFIG), None) + if alias and CONFIG.get(alias): + return CONFIG[alias] + + raise HTTPException(status_code=503, detail=f"Configuração {chave} não encontrada no documento central.") + + +# Load config from central Google Docs on startup +CONFIG = carregar_config_central() + + def extrair_doc_id(link_ou_id: str) -> str: valor = link_ou_id.strip() if "/document/d/" not in valor: @@ -256,25 +328,50 @@ def filtrar_linhas_por_cidade(df: pd.DataFrame, cidade: str) -> pd.DataFrame: return df[mascara_sem_uf] +def normalizar_url_csv(link_ou_id: str) -> str: + """ + Converts a Google Drive sharing link to a direct download URL. + If it already is a direct URL, returns it as-is. + """ + valor = link_ou_id.strip() + if "/file/d/" not in valor: + return valor + + parsed = urlparse(valor) + partes = [p for p in parsed.path.split("/") if p] + if "d" in partes: + idx = partes.index("d") + if idx + 1 < len(partes): + file_id = partes[idx + 1] + return f"https://drive.google.com/uc?export=download&id={file_id}" + + raise ValueError("Não foi possível extrair o ID do arquivo CSV do Google Drive.") + + @app.get("/cities") async def listar_cidades(): return carregar_cidades() @app.get("/relatorio/{cidade}", response_class=HTMLResponse) async def gerar_relatorio(cidade: str): - df = pd.read_csv(DEMOGRAFIA_CSV_URL, delimiter=";") + # Tudo vem do CONFIG (que vem do documento central) + csv_url = obter_config("DEMOGRAFIA_CSV_URL") + df = pd.read_csv(normalizar_url_csv(csv_url), delimiter=";") linhas_df = filtrar_linhas_por_cidade(df, cidade) linhas = linhas_df.to_dict("records") if not linhas: raise HTTPException(status_code=404, detail=f"Cidade '{cidade}' não encontrada.") - # If DATANE_DOCS_URL is set but empty (common in docker-compose), fall back to default. - docs_url = os.getenv("DATANE_DOCS_URL") or DEFAULT_DOCS_URL + # Get docs URL from CONFIG (central source only) + docs_url = obter_config("DEFAULT_DOCS_URL") try: docs_texto = carregar_texto_do_docs(docs_url) except ValueError as err: - raise HTTPException(status_code=400, detail=str(err)) from err + raise HTTPException( + status_code=400, + detail=f"Erro ao carregar o documento de template: {str(err)}" + ) from err docs_html = texto_para_html(docs_texto, linhas[0]) @@ -293,6 +390,20 @@ async def gerar_relatorio(cidade: str): return HTMLResponse(content=html) +@app.get("/config") +async def verificar_config(): + """ + Debug endpoint to verify the loaded configuration. + Shows all KEY=VALUE pairs loaded from the central Google Docs. + """ + return { + "config_doc_url": CONFIG_DOC_URL, + "config_loaded": len(CONFIG) > 0, + "config_keys": list(CONFIG.keys()), + "config_values": CONFIG, + } + + # If the frontend has been built (e.g., via Docker), serve it from the same app. FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist" if FRONTEND_DIST_DIR.exists(): diff --git a/report-generator-demo/frontend/src/App.jsx b/report-generator-demo/frontend/src/App.jsx new file mode 100644 index 0000000..db2b9ee --- /dev/null +++ b/report-generator-demo/frontend/src/App.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useMemo, useState } from 'react' + +// Default to same-origin when served by the API (Docker/prod). +// In dev (Vite on :5173), set VITE_API_BASE_URL=http://127.0.0.1:8000. +const API_BASE = import.meta.env.VITE_API_BASE_URL || window.location.origin +const MACROTEMAS = [ + 'Demografia' +] + +function App() { + const [cities, setCities] = useState([]) + const [showForm, setShowForm] = useState(false) + const [selectedMacrotema, setSelectedMacrotema] = useState(MACROTEMAS[0]) + const [selectedCity, setSelectedCity] = useState('') + const [citySearch, setCitySearch] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + async function fetchCities() { + try { + setLoading(true) + const response = await fetch(`${API_BASE}/cities`) + if (!response.ok) { + throw new Error('Falha ao carregar cidades') + } + const data = await response.json() + setCities(Array.isArray(data) ? data : []) + } catch (err) { + setError(err.message || 'Erro ao carregar cidades') + } finally { + setLoading(false) + } + } + + fetchCities() + }, []) + + const cityCount = useMemo(() => cities.length, [cities]) + const filteredCities = useMemo(() => { + const term = citySearch.trim().toLowerCase() + if (!term) return cities + return cities.filter((city) => city.toLowerCase().includes(term)) + }, [cities, citySearch]) + + function openReport() { + if (!selectedCity) { + return + } + + const url = `${API_BASE}/relatorio/${encodeURIComponent(selectedCity)}` + window.open(url, '_blank') + } + + return ( +
+
+
+ Sudene • Gerador de relatórios +

Geração de relatório para Sudene

+

+ Escolha a cidade para montar o relatório de Demografia. +

+ +
+ +
{cityCount} cidades disponíveis
+
+
+ + +
+ + {loading &&

Carregando cidades...

} + {error &&

{error}

} + + {showForm && !loading && !error && ( +
setShowForm(false)} role="presentation"> +
event.stopPropagation()}> +
+
+

Formulário de relatório

+

Selecione o macrotema e a cidade desejada.

+
+ +
+ +
+
+ + +
+ +
+ + setCitySearch(event.target.value)} + /> +
+
+ +
+ Lista de cidades + {filteredCities.length} encontradas +
+ +
+ {filteredCities.map((city) => ( + + ))} +
+ +
+
+ Macrotema + {selectedMacrotema} +
+
+ Cidade + {selectedCity || 'Nenhuma selecionada'} +
+
+ +
+ + +
+
+
+ )} +
+ ) +} + +export default App