From 6af60b0f46c505193e7b6d201b9b582fc341627e Mon Sep 17 00:00:00 2001 From: rayaneBSilva Date: Sat, 25 Jul 2026 21:39:24 -0300 Subject: [PATCH 1/2] feat: change map --- utils/maps.py | 292 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 236 insertions(+), 56 deletions(-) diff --git a/utils/maps.py b/utils/maps.py index bf26b89..418cdd2 100644 --- a/utils/maps.py +++ b/utils/maps.py @@ -258,16 +258,71 @@ def aplicar_grade_coordenadas(ax, bounds, x_ticks=None, y_ticks=None, fontsize: ) -def desenhar_escala(ax, km_total: int, pos=(0.70, 0.055), largura_frac: float = 0.16, fontsize: float = 5.5) -> None: +def _escala_arredondada(km_maximo: float) -> float: + """Retorna uma distância cartograficamente legível menor que km_maximo.""" + if km_maximo <= 0: + return 1 + expoente = 10 ** int(__import__("math").floor(__import__("math").log10(km_maximo))) + for fator in (5, 2, 1): + candidato = fator * expoente + if candidato <= km_maximo: + return candidato + return expoente / 2 + + +def desenhar_escala( + ax, + pos=(0.62, 0.055), + largura_maxima_frac: float = 0.28, + fontsize: float = 5.5, + km_total: float | None = None, + altura: float = 0.012, +) -> None: + """Desenha uma escala dinâmica compatível com eixos em longitude/latitude.""" + import math + from matplotlib.patches import Rectangle + + minx, maxx = ax.get_xlim() + miny, maxy = ax.get_ylim() + latitude_media = (miny + maxy) / 2 + km_por_grau_lon = 111.32 * max(math.cos(math.radians(latitude_media)), 0.15) + largura_km = abs(maxx - minx) * km_por_grau_lon + km_total = km_total or _escala_arredondada(largura_km * largura_maxima_frac) + largura_frac = km_total / largura_km + x0, y0 = pos - x1 = x0 + largura_frac - x_mid = x0 + largura_frac / 2 - ax.plot([x0, x1], [y0, y0], transform=ax.transAxes, color="#111", lw=0.9, clip_on=False) - for x in [x0, x_mid, x1]: - ax.plot([x, x], [y0, y0 - 0.018], transform=ax.transAxes, color="#111", lw=0.9, clip_on=False) - ax.text(x0, y0 + 0.012, "0", transform=ax.transAxes, ha="center", va="bottom", fontsize=fontsize, color="#111") - ax.text(x_mid, y0 + 0.012, f"{km_total // 2}", transform=ax.transAxes, ha="center", va="bottom", fontsize=fontsize, color="#111") - ax.text(x1 + 0.012, y0 + 0.012, f"{km_total} km", transform=ax.transAxes, ha="left", va="bottom", fontsize=fontsize, color="#111") + metade = largura_frac / 2 + for indice, cor in enumerate(("#111111", "#ffffff")): + ax.add_patch( + Rectangle( + (x0 + indice * metade, y0), + metade, + altura, + transform=ax.transAxes, + facecolor=cor, + edgecolor="#111111", + linewidth=0.7, + clip_on=False, + zorder=20, + ) + ) + + valores = (0, km_total / 2, km_total) + for indice, (x, valor) in enumerate(zip((x0, x0 + metade, x0 + largura_frac), valores)): + rotulo = f"{valor:g}" + if indice == 2: + rotulo += " km" + ax.text( + x, + y0 - 0.012, + rotulo, + transform=ax.transAxes, + ha="center", + va="top", + fontsize=fontsize, + color="#111111", + zorder=20, + ) def desenhar_nomes_ufs(ax, ufs, bounds, alvo_uf: str, fontsize: float = 7.0) -> None: @@ -297,40 +352,106 @@ def desenhar_nomes_ufs(ax, ufs, bounds, alvo_uf: str, fontsize: float = 7.0) -> ) -def anotar_municipios_vizinhos(ax, municipios, alvo, limite: int = 9) -> None: - municipios_metrico = municipios.to_crs(epsg=3857) - alvo_metrico = municipios.loc[[alvo.name]].to_crs(epsg=3857).iloc[0] - centro_alvo = alvo_metrico.geometry.representative_point() - municipios = municipios.copy() - municipios["_distancia"] = municipios_metrico.geometry.representative_point().distance(centro_alvo).values - vizinhos = municipios.sort_values("_distancia").head(limite) +def anotar_municipios_vizinhos(ax, municipios, alvo, bounds) -> None: + """Nomeia o alvo e todos os municípios com área relevante no quadro.""" + import textwrap + + from shapely.geometry import box + + minx, maxx, miny, maxy = bounds + janela = box(minx, miny, maxx, maxy) + margem_x = (maxx - minx) * 0.025 + margem_y = (maxy - miny) * 0.025 + janela_texto = box( + minx + margem_x, + miny + margem_y, + maxx - margem_x, + maxy - margem_y, + ) + + visiveis = municipios[municipios.geometry.intersects(janela)].copy() + for indice, municipio in visiveis.iterrows(): + geometria_visivel = municipio.geometry.intersection(janela_texto) + if geometria_visivel.is_empty: + continue + + # Evita nomear apenas uma pequena ponta de um município cortado na borda. + proporcao_visivel = geometria_visivel.area / max(municipio.geometry.area, 1e-12) + if proporcao_visivel < 0.12 and not municipio.geometry.representative_point().within(janela_texto): + continue - for _, municipio in vizinhos.iterrows(): - ponto = municipio.geometry.representative_point() + ponto = geometria_visivel.representative_point() + eh_alvo = indice == alvo.name + nome = str(municipio["NM_MUN"]) + linhas = textwrap.wrap( + nome, + width=18 if eh_alvo else 15, + break_long_words=False, + break_on_hyphens=False, + ) + rotulo = "\n".join(linhas) + maior_linha = max(map(len, linhas), default=len(nome)) + # Reserva espaço horizontal de acordo com o comprimento do rótulo. + # Assim nomes posicionados perto das bordas não ficam cortados. + meia_largura_texto = min( + (maxx - minx) * max(maior_linha, 5) * (0.0032 if eh_alvo else 0.00265), + (maxx - minx) * 0.18, + ) + texto_x = min( + max(ponto.x, minx + margem_x + meia_largura_texto), + maxx - margem_x - meia_largura_texto, + ) + texto_y = min( + max(ponto.y, miny + margem_y * 1.4), + maxy - margem_y * 1.4, + ) ax.text( - ponto.x, - ponto.y, - str(municipio["NM_MUN"]), - fontsize=4.8, - color="#5e5245", + texto_x, + texto_y, + rotulo, + fontsize=5.0 if eh_alvo else 3.9, + color="#4b4035", + fontweight="bold" if eh_alvo else "normal", ha="center", va="center", + linespacing=0.9, + clip_on=True, + zorder=12, ) -def desenhar_norte(ax, fontsize: float = 8.0, pos=(0.94, 0.9)) -> None: +def desenhar_norte(ax, fontsize: float = 8.0, pos=(0.92, 0.12), tamanho: float = 0.065) -> None: + from matplotlib.patches import Polygon + x, y = pos - ax.annotate( + ax.text( + x, + y + tamanho * 1.25, "N", - xy=(x, y), - xytext=(x, y - 0.105), - xycoords="axes fraction", - textcoords="axes fraction", + transform=ax.transAxes, ha="center", - va="center", + va="bottom", fontsize=fontsize, fontweight="bold", - arrowprops={"arrowstyle": "-|>", "color": "#262626", "lw": 1}, + color="#111111", + zorder=22, + ) + ax.add_patch( + Polygon( + [ + (x, y + tamanho), + (x - tamanho * 0.42, y - tamanho), + (x, y - tamanho * 0.38), + (x + tamanho * 0.42, y - tamanho), + ], + closed=True, + transform=ax.transAxes, + facecolor="white", + edgecolor="#111111", + linewidth=1, + clip_on=False, + zorder=22, + ) ) @@ -348,7 +469,7 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: os.environ.setdefault("MPLCONFIGDIR", str(OUTPUT_DIR / "matplotlib-cache")) import matplotlib.pyplot as plt - from matplotlib.patches import Patch + from matplotlib.patches import Patch, Rectangle OUTPUT_DIR.mkdir(parents=True, exist_ok=True) chart_file = OUTPUT_DIR / f"mapa_regiao_{safe_report}.png" @@ -372,7 +493,7 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: ax_cidade = fig.add_subplot(grid[1]) estado_bounds = ampliar_altura_bounds( - bounds_por_pontos_principais(municipios_uf, municipio, 0.42), + expandir_bounds(uf_alvo.total_bounds, 0.28), 1.25, ) ax_estado.set_facecolor("#d8e9f7") @@ -382,20 +503,67 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: ufs.boundary.plot(ax=ax_estado, color="#202020", linewidth=0.55) uf_alvo.boundary.plot(ax=ax_estado, color="#202020", linewidth=0.65) municipio_gdf.plot(ax=ax_estado, color=municipio_cor, edgecolor="#8a3d12", linewidth=0.7) - aplicar_grade_coordenadas(ax_estado, estado_bounds, fontsize=5.4) + estado_minx, estado_maxx, estado_miny, estado_maxy = estado_bounds + estado_altura = estado_maxy - estado_miny + aplicar_grade_coordenadas( + ax_estado, + estado_bounds, + x_ticks=[(estado_minx + estado_maxx) / 2], + y_ticks=[ + estado_miny + estado_altura * 0.08, + (estado_miny + estado_maxy) / 2, + estado_maxy - estado_altura * 0.08, + ], + fontsize=5.4, + ) desenhar_nomes_ufs(ax_estado, ufs, estado_bounds, uf, fontsize=7.0) - desenhar_norte(ax_estado, fontsize=7.5, pos=(0.94, 0.91)) - desenhar_escala(ax_estado, 200, pos=(0.61, 0.06), largura_frac=0.28, fontsize=5.1) + desenhar_norte(ax_estado, fontsize=7.5, pos=(0.93, 0.075), tamanho=0.035) + desenhar_escala(ax_estado, pos=(0.61, 0.06), largura_maxima_frac=0.28, fontsize=5.1) ax_cidade.set_facecolor(fundo_cor) municipios_uf.plot(ax=ax_cidade, color="#ffd79d", edgecolor="#c2955f", linewidth=0.35) municipio_gdf.plot(ax=ax_cidade, color=municipio_cor, edgecolor="#8a3d12", linewidth=1) - bounds_cidade = expandir_bounds(municipio.geometry.bounds, 0.75) + bounds_cidade = ampliar_altura_bounds( + expandir_bounds(municipio.geometry.bounds, 0.18), + 1.25, + ) configurar_eixo_mapa(ax_cidade, bounds_cidade) - aplicar_grade_coordenadas(ax_cidade, bounds_cidade, fontsize=5.4) - anotar_municipios_vizinhos(ax_cidade, municipios_uf, municipio) - desenhar_norte(ax_cidade, fontsize=8.5, pos=(0.96, 0.93)) - desenhar_escala(ax_cidade, 6, pos=(0.845, 0.055), largura_frac=0.13, fontsize=5.2) + cidade_minx, cidade_maxx, cidade_miny, cidade_maxy = bounds_cidade + cidade_largura = cidade_maxx - cidade_minx + aplicar_grade_coordenadas( + ax_cidade, + bounds_cidade, + x_ticks=[ + cidade_minx + cidade_largura / 3, + cidade_minx + cidade_largura * 2 / 3, + ], + y_ticks=[(cidade_miny + cidade_maxy) / 2], + fontsize=5.4, + ) + anotar_municipios_vizinhos(ax_cidade, municipios_uf, municipio, bounds_cidade) + # Quadro cartográfico inferior, seguindo o padrão da referência. + ax_cidade.add_patch( + Rectangle( + (0.015, 0.012), + 0.97, + 0.157, + transform=ax_cidade.transAxes, + facecolor="white", + edgecolor=limite_cor, + linewidth=0.8, + alpha=0.96, + zorder=15, + ) + ) + desenhar_norte(ax_cidade, fontsize=6.2, pos=(0.82, 0.112), tamanho=0.024) + desenhar_escala( + ax_cidade, + pos=(0.77, 0.062), + largura_maxima_frac=0.18, + fontsize=4.5, + km_total=6, + altura=0.008, + ) for ax in [ax_estado, ax_cidade]: for spine in ax.spines.values(): @@ -404,30 +572,42 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: spine.set_linewidth(0.45) legendas = [ - Patch(facecolor=municipio_cor, edgecolor="#8a3d12", label=nome_cidade), - Patch(facecolor="#f6f6f6", edgecolor="#8a8a8a", label="Limites municipais"), - Patch(facecolor=uf_cor, edgecolor="#424242", label=nome_uf), - Patch(facecolor=cinza, edgecolor="#424242", label="Brasil"), + Patch(facecolor="#d0d0d0", edgecolor="#555555", linewidth=0.8, label="Limite estadual"), + Patch(facecolor="#ffffff", edgecolor="#c2955f", linewidth=0.9, label="Limite municipal"), ] - ax_cidade.legend( + legenda = ax_cidade.legend( handles=legendas, - loc="lower left", - bbox_to_anchor=(0.01, 0.075), - fontsize=5.4, - frameon=True, - framealpha=0.92, - borderpad=0.28, - handlelength=1.2, + loc="upper left", + bbox_to_anchor=(0.035, 0.135), + fontsize=5.2, + frameon=False, + borderpad=0, + handlelength=1.8, + labelspacing=0.35, + ) + legenda.set_zorder(21) + ax_cidade.text( + 0.043, + 0.155, + "Legenda", + transform=ax_cidade.transAxes, + fontsize=6.2, + fontweight="bold", + color="#111111", + ha="left", + va="top", + zorder=21, ) ax_cidade.text( - 0.015, - 0.012, + 0.04, + 0.042, "Sistema de Coordenadas: Geográfico\nSistema Geodésico de Referência: SIRGAS 2000", transform=ax_cidade.transAxes, - fontsize=5.0, + fontsize=4.7, color="#5d5146", ha="left", va="bottom", + zorder=21, ) fig.subplots_adjust(left=0.015, right=0.995, top=0.965, bottom=0.018) fig.savefig(chart_file, dpi=190, bbox_inches="tight", pad_inches=0.02) From f2d6bebe32459828ee6cfba344f34bd9a9b01881 Mon Sep 17 00:00:00 2001 From: rayaneBSilva Date: Sat, 25 Jul 2026 21:49:58 -0300 Subject: [PATCH 2/2] fix: fix error --- config.py | 3 ++- generate_report.py | 6 +++--- main.py | 11 ++++++----- plotting.py | 4 +++- reports.py | 35 ++++++++++++++++++---------------- scripts/download_map_shapes.py | 1 - scripts/generate_all_maps.py | 12 ++++++++---- utils/cities.py | 1 + utils/contentful.py | 6 ++++-- utils/docs.py | 12 +++++++----- utils/macrotemas.py | 16 ++++++++-------- utils/maps.py | 11 +++++------ utils/renderer.py | 4 ++-- utils/tables.py | 1 - 14 files changed, 68 insertions(+), 55 deletions(-) mode change 100644 => 100755 scripts/download_map_shapes.py mode change 100644 => 100755 scripts/generate_all_maps.py diff --git a/config.py b/config.py index 82bf05e..5e16aa4 100644 --- a/config.py +++ b/config.py @@ -1,8 +1,9 @@ import os from pathlib import Path +from urllib.parse import urlparse + from dotenv import load_dotenv from fastapi import HTTPException -from urllib.parse import urlparse BASE_DIR = Path(__file__).resolve().parent diff --git a/generate_report.py b/generate_report.py index 87f0313..282627b 100644 --- a/generate_report.py +++ b/generate_report.py @@ -2,9 +2,9 @@ import argparse import csv -from datetime import datetime import html import io +from datetime import datetime from pathlib import Path from typing import Any from urllib.parse import parse_qs, urlparse @@ -20,7 +20,7 @@ def parse_drive_file_id(value: str) -> str | None: parsed = urlparse(value) query = parse_qs(parsed.query) - if "id" in query and query["id"]: + if query.get("id"): return query["id"][0] if parsed.netloc and "drive.google.com" in parsed.netloc and parsed.path: @@ -269,7 +269,7 @@ def main() -> None: args = parser.parse_args() rows = fetch_csv_rows(args.source) - generated_at = datetime.now().strftime("%d/%m/%Y %H:%M") + generated_at = datetime.now().astimezone().strftime("%d/%m/%Y %H:%M") if args.city: if not rows: diff --git a/main.py b/main.py index 1a36e67..6de723a 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,18 @@ from fastapi import BackgroundTasks, FastAPI -from fastapi.responses import HTMLResponse, FileResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from config import BASE_DIR -from utils.macrotemas import MACROTEMAS, TODOS_MACROTEMAS_NOME, TODOS_MACROTEMAS_SLUG -from utils.cities import carregar_cidades -from utils.ssr import start_server as start_ssr_server, stop_server as stop_ssr_server from reports import ( - listar_relatorios_handler, apagar_relatorio_handler, gerar_relatorio_handler, + listar_relatorios_handler, ) +from utils.cities import carregar_cidades +from utils.macrotemas import MACROTEMAS, TODOS_MACROTEMAS_NOME, TODOS_MACROTEMAS_SLUG +from utils.ssr import start_server as start_ssr_server +from utils.ssr import stop_server as stop_ssr_server app = FastAPI(on_startup=[start_ssr_server], on_shutdown=[stop_ssr_server]) diff --git a/plotting.py b/plotting.py index aaa7fd6..0c1f488 100644 --- a/plotting.py +++ b/plotting.py @@ -1,6 +1,8 @@ -import matplotlib.pyplot as plt import pathlib +import matplotlib.pyplot as plt + + def gerar_grafico_sexo(cidade, OUTPUT_DIR: pathlib.Path, safe_city: str): mulheres = int(str(cidade["pop_mulher"]).replace('.', '')) homens = int(str(cidade["pop_homem"]).replace('.', '')) diff --git a/reports.py b/reports.py index e284c37..e151720 100644 --- a/reports.py +++ b/reports.py @@ -1,29 +1,32 @@ import logging import re +from datetime import datetime, timezone +from pathlib import Path + import pandas as pd -from datetime import datetime from fastapi import BackgroundTasks, HTTPException from fastapi.responses import HTMLResponse from weasyprint import HTML -from pathlib import Path -from config import OUTPUT_DIR, resolve_csv_source, require_config_value -from utils.macrotemas import MACROTEMAS, TODOS_MACROTEMAS_NOME, TODOS_MACROTEMAS_SLUG +from config import OUTPUT_DIR, require_config_value, resolve_csv_source +from plotting import gerar_grafico_porte, gerar_grafico_sexo, gerar_grafico_top_cidades from utils.cities import filtrar_linhas_por_cidade +from utils.cover import montar_capa_relatorio from utils.docs import ( carregar_texto_do_docs, extrair_descricao_tema, - extrair_resumo_tema, - extrair_resumo_relatorio, extrair_resumo_cidade, + extrair_resumo_relatorio, + extrair_resumo_tema, +) +from utils.macrotemas import MACROTEMAS, TODOS_MACROTEMAS_NOME, TODOS_MACROTEMAS_SLUG +from utils.renderer import ( + render_descricao_tema_html, + render_mapa_marker, + substituir_placeholders, + texto_para_html, ) -from utils.cover import montar_capa_relatorio -from utils.renderer import render_descricao_tema_html, render_mapa_marker, substituir_placeholders, texto_para_html from utils.ssr import render_react_ssr -from plotting import gerar_grafico_sexo -from plotting import gerar_grafico_porte -from plotting import gerar_grafico_top_cidades - logger = logging.getLogger(__name__) @@ -34,7 +37,7 @@ def _gerar_pdf(html_content: str, pdf_file: Path) -> None: try: pdf_html = re.sub(r'src="/output/', 'src="', html_content) HTML(string=pdf_html, base_url=str(OUTPUT_DIR.resolve())).write_pdf(str(pdf_file)) - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: logger.warning("Falha ao gerar PDF %s: %s", pdf_file, e) @@ -106,7 +109,7 @@ async def listar_relatorios_handler(): mapa_file = OUTPUT_DIR / f"mapa_regiao_{slug_completo}.png" stat = pdf_file.stat() - criado_em = datetime.fromtimestamp(stat.st_mtime) + criado_em = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).astimezone() macrotema = "Demografia" if "__" in slug_completo: @@ -140,7 +143,7 @@ async def listar_relatorios_handler(): relatorios.sort( key=lambda item: datetime.strptime( f"{item['data']} {item['hora']}", "%d/%m/%Y %H:%M:%S" - ), + ).replace(tzinfo=timezone.utc), reverse=True ) @@ -182,7 +185,7 @@ async def apagar_relatorio_handler(arquivo_pdf: str): async def gerar_relatorio_handler(cidade: str, macrotema: str = "demografia", charts: str = "all", *, background_tasks: BackgroundTasks): macrotema_slugs = get_macrotema_slugs_para_relatorio(macrotema) - gerado_em = datetime.now() + gerado_em = datetime.now().astimezone() allowed = set(CHART_TYPES.keys()) requested_charts = list(CHART_TYPES.keys()) if charts == "all" else [c.strip() for c in charts.split(",")] invalid = set(requested_charts) - allowed diff --git a/scripts/download_map_shapes.py b/scripts/download_map_shapes.py old mode 100644 new mode 100755 index 812c0e2..d4d44fb --- a/scripts/download_map_shapes.py +++ b/scripts/download_map_shapes.py @@ -9,7 +9,6 @@ from urllib.parse import urlparse from urllib.request import Request, urlopen - ROOT_DIR = Path(__file__).resolve().parents[1] DEFAULT_OUTPUT_DIR = ROOT_DIR / "map_shape" REQUIRED_FILES = ( diff --git a/scripts/generate_all_maps.py b/scripts/generate_all_maps.py old mode 100644 new mode 100755 index d06d9e0..245a54d --- a/scripts/generate_all_maps.py +++ b/scripts/generate_all_maps.py @@ -6,13 +6,17 @@ import unicodedata from pathlib import Path - ROOT_DIR = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT_DIR)) -from config import OUTPUT_DIR # noqa: E402 -from utils.cities import carregar_cidades # noqa: E402 -from utils.maps import carregar_malhas, gerar_mapa_regiao, localizar_municipio, separar_cidade_uf # noqa: E402 +from config import OUTPUT_DIR +from utils.cities import carregar_cidades +from utils.maps import ( + carregar_malhas, + gerar_mapa_regiao, + localizar_municipio, + separar_cidade_uf, +) def slugify(value: object) -> str: diff --git a/utils/cities.py b/utils/cities.py index 868a05a..4244d1e 100644 --- a/utils/cities.py +++ b/utils/cities.py @@ -1,5 +1,6 @@ import ast import re + import pandas as pd from config import CITIES_FILE diff --git a/utils/contentful.py b/utils/contentful.py index c96dd1d..b267c07 100644 --- a/utils/contentful.py +++ b/utils/contentful.py @@ -1,9 +1,11 @@ +import logging import re import httpx from config import get_config_value +logger = logging.getLogger(__name__) CONTENTFUL_SPACE_ID = get_config_value("CONTENTFUL_SPACE_ID") CONTENTFUL_ACCESS_TOKEN = get_config_value("CONTENTFUL_ACCESS_TOKEN") @@ -50,7 +52,7 @@ def obter_url_mapa_contentful(nome_municipio: str) -> str | None: file_url = items[0].get("fields", {}).get("file", {}).get("url", "") if file_url: return f"https:{file_url}" if file_url.startswith("//") else file_url - except Exception: - pass + except (httpx.HTTPError, KeyError, TypeError, AttributeError, ValueError) as e: + logger.debug("Conteúdo inesperado ao extrair URL do Contentful: %s", e) return None diff --git a/utils/docs.py b/utils/docs.py index cae917e..df2756d 100644 --- a/utils/docs.py +++ b/utils/docs.py @@ -1,13 +1,15 @@ import json +import logging import re import time from pathlib import Path +from urllib.error import HTTPError, URLError from urllib.parse import urlparse from urllib.request import urlopen -from urllib.error import URLError, HTTPError from config import BASE_DIR +logger = logging.getLogger(__name__) DOCS_CACHE_DIR = BASE_DIR / "output" / "docs_cache" DOCS_CACHE_TTL = 3600 # 1 hour @@ -25,8 +27,8 @@ def _carregar_do_cache(doc_id: str) -> str | None: dados = json.loads(cache_path.read_text(encoding="utf-8")) if time.time() - dados["timestamp"] < DOCS_CACHE_TTL: return dados["texto"] - except Exception: - pass + except (OSError, json.JSONDecodeError, KeyError, TypeError) as e: + logger.debug("Falha ao ler cache de docs (%s): %s", cache_path, e) return None @@ -37,8 +39,8 @@ def _salvar_no_cache(doc_id: str, texto: str) -> None: _cache_path(doc_id).write_text( json.dumps(dados, ensure_ascii=False), encoding="utf-8" ) - except Exception: - pass + except (OSError, TypeError, ValueError) as e: + logger.debug("Falha ao salvar cache de docs (%s): %s", _cache_path(doc_id), e) def extrair_doc_id(link_ou_id: str) -> str: diff --git a/utils/macrotemas.py b/utils/macrotemas.py index 460fbce..c15baf9 100644 --- a/utils/macrotemas.py +++ b/utils/macrotemas.py @@ -1,16 +1,16 @@ from config import ( DEMOGRAFIA_CSV_URL, - EDUCACAO_CSV_URL, - SAUDE_CSV_URL, - ECONOMIA_RENDA_CSV_URL, - SANEAMENTO_CSV_URL, - HIDRAULICA_CSV_URL, DEMOGRAFIA_DOCS_URL, - EDUCACAO_DOCS_URL, - SAUDE_DOCS_URL, + ECONOMIA_RENDA_CSV_URL, ECONOMIA_RENDA_DOCS_URL, - SANEAMENTO_DOCS_URL, + EDUCACAO_CSV_URL, + EDUCACAO_DOCS_URL, + HIDRAULICA_CSV_URL, HIDRAULICA_DOCS_URL, + SANEAMENTO_CSV_URL, + SANEAMENTO_DOCS_URL, + SAUDE_CSV_URL, + SAUDE_DOCS_URL, ) MACROTEMAS = { diff --git a/utils/maps.py b/utils/maps.py index 418cdd2..fb0e621 100644 --- a/utils/maps.py +++ b/utils/maps.py @@ -4,7 +4,7 @@ import os import re import unicodedata -from urllib.error import URLError, HTTPError +from urllib.error import HTTPError, URLError from urllib.parse import quote, urlencode from urllib.request import Request, urlopen @@ -213,7 +213,7 @@ def formatar_grau_decimal(valor: float, eixo: str) -> str: hemisferio = "W" if eixo == "lon" and valor < 0 else "E" if eixo == "lon" else "S" if valor < 0 else "N" absoluto = abs(valor) graus = int(absoluto) - minutos = int(round((absoluto - graus) * 60)) + minutos = round((absoluto - graus) * 60) if minutos == 60: graus += 1 minutos = 0 @@ -280,6 +280,7 @@ def desenhar_escala( ) -> None: """Desenha uma escala dinâmica compatível com eixos em longitude/latitude.""" import math + from matplotlib.patches import Rectangle minx, maxx = ax.get_xlim() @@ -459,7 +460,7 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: try: municipios, ufs = carregar_malhas() municipio = localizar_municipio(nome_municipio) - except Exception as e: + except (ImportError, OSError, ValueError, KeyError, AttributeError) as e: logger.warning("Falha ao carregar malhas ou localizar município '%s': %s", nome_municipio, e) return None @@ -475,8 +476,6 @@ def gerar_mapa_regiao(nome_municipio: str, safe_report: str) -> str | None: chart_file = OUTPUT_DIR / f"mapa_regiao_{safe_report}.png" uf = str(municipio["SIGLA_UF"]).upper() - nome_uf = UF_NOMES.get(uf, uf) - nome_cidade = str(municipio["NM_MUN"]) municipios_uf = municipios[municipios["SIGLA_UF"].astype(str).str.upper() == uf] uf_alvo = ufs[ufs["SIGLA_UF"].astype(str).str.upper() == uf] municipio_gdf = municipios.loc[[municipio.name]] @@ -797,7 +796,7 @@ def montar_url_busca_mapa(nome_municipio: str) -> str: def render_mapa_geografico(contexto: dict) -> str: nome_municipio = str(contexto.get("nm_mun", "")).strip() - cidade, uf = separar_cidade_uf(nome_municipio) + cidade, _uf = separar_cidade_uf(nome_municipio) nome_seguro = html.escape(nome_municipio) cidade_segura = html.escape(cidade) mapa = geocodificar_municipio(nome_municipio) diff --git a/utils/renderer.py b/utils/renderer.py index 9d6f628..0c861ac 100644 --- a/utils/renderer.py +++ b/utils/renderer.py @@ -1,9 +1,9 @@ -import re import html as html_module +import re +from utils.contentful import obter_url_mapa_contentful from utils.macrotemas import MACROTEMA_SECOES from utils.maps import gerar_mapa_regiao, render_mapa_geografico -from utils.contentful import obter_url_mapa_contentful from utils.tables import render_tabela_resumo diff --git a/utils/tables.py b/utils/tables.py index 4012382..aa037d3 100644 --- a/utils/tables.py +++ b/utils/tables.py @@ -1,6 +1,5 @@ import html as html_module - STATUS_MAP = { "critico": { "label": "Atenção prioritária",