This commit is contained in:
kolmeasi
2026-05-31 18:57:03 -03:00
parent b90e69c1f9
commit 77ec04cc43
6 changed files with 142575 additions and 54 deletions

View File

@@ -156,6 +156,50 @@ def list_aerodromes() -> list[str]:
return result
def list_catalog() -> list[dict]:
"""Retorna catálogo completo da tabela aerodromes. Fallback: só aeros com dados."""
if _db_available():
import db as _db
conn = _db.get_connection(DB_PATH)
_db.ensure_schema(conn)
catalog = _db.list_all_aerodromes(conn)
conn.close()
if catalog:
return catalog
return [{"icao": a, "nome": "", "uf": ""} for a in list_aerodromes()]
def _fmt_aero(d: dict) -> str:
"""Formata dict de aeródromo para exibição no selectbox."""
nome = d.get("nome", "")
uf = d.get("uf", "")
if nome and uf:
return f"{d['icao']}{nome} ({uf})"
if nome:
return f"{d['icao']}{nome}"
return d["icao"]
def _save_csv_to_folder(df: pd.DataFrame, aerodrome: str,
start: str, end: str) -> str | None:
"""Abre diálogo nativo de pasta e salva o DataFrame como CSV. Retorna caminho ou None."""
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw()
root.wm_attributes("-topmost", True)
folder = filedialog.askdirectory(title="Escolha a pasta de destino")
root.destroy()
if not folder:
return None
s = start.replace("-", "")
e = end.replace("-", "")
filename = f"{aerodrome}_{s}_{e}.csv"
filepath = Path(folder) / filename
df.to_csv(filepath, index=False, encoding="utf-8-sig")
return str(filepath)
def get_period(aerodrome: str) -> tuple[str, str]:
"""Retorna (inicio, fim) dos dados disponíveis para o aeródromo."""
if _db_available():
@@ -649,7 +693,7 @@ def chart_climatology(anl: pd.DataFrame, var: str, title: str,
# ══════════════════════════════════════════════════════════════════════════════
# SIDEBAR
# ══════════════════════════════════════════════════════════════════════════════
aerodromes = list_aerodromes()
catalog = list_catalog()
with st.sidebar:
st.markdown("## ✈️ MET Aeroportuário")
@@ -657,11 +701,12 @@ with st.sidebar:
st.caption(fonte)
st.markdown("---")
if not aerodromes:
if not catalog:
st.warning("Nenhum dado encontrado.\nExecute a coleta primeiro.")
st.stop()
sel_aero = st.selectbox("Aeródromo", aerodromes)
sel_dict = st.selectbox("Aeródromo", catalog, format_func=_fmt_aero)
sel_aero = sel_dict["icao"]
p_start, p_end = get_period(sel_aero)
st.caption(f"📅 Disponível: `{p_start}` → `{p_end}`")
st.markdown("---")
@@ -781,12 +826,42 @@ with tab0:
st.markdown("### Aquisição de Dados — ICEA/DECEA")
st.markdown("Baixa dados de superfície do site da ICEA e armazena no banco SQLite local.")
_btn_col, _info_col = st.columns([1, 3])
with _btn_col:
if st.button("🔄 Atualizar catálogo ICEA",
help="Acessa o site ICEA e baixa a lista completa de aeródromos disponíveis"):
from scraper_meteorologia import fetch_aerodrome_catalog
import db as _db
with st.spinner("Acessando site ICEA…"):
_new_catalog = fetch_aerodrome_catalog(headless=True)
_conn = _db.get_connection(DB_PATH)
_db.ensure_schema(_conn)
_n = _db.upsert_aerodromes(_conn, _new_catalog)
_conn.close()
st.success(f"{_n} aeródromos gravados no catálogo.")
st.rerun()
with _info_col:
_n_cat = len(catalog)
st.caption(
f"Catálogo atual: **{_n_cat}** aeródromos. "
"Use o botão ao lado para atualizar do site ICEA."
if _n_cat else
"Catálogo vazio — clique em **Atualizar catálogo ICEA** para baixar a lista do site."
)
col_form, col_log = st.columns([1, 1.6])
with col_form:
with st.form("form_coleta"):
aero_input = st.text_input("Aeródromo (ICAO)", value="SBGR",
placeholder="Ex: SBGR, SBSP, SBGL").upper().strip()
_coleta_catalog = catalog if catalog else [{"icao": "SBGR", "nome": "", "uf": ""}]
_default_idx = next(
(i for i, d in enumerate(_coleta_catalog) if d["icao"] == sel_aero), 0
)
_sel_coleta = st.selectbox(
"Aeródromo (ICAO)", _coleta_catalog,
index=_default_idx, format_func=_fmt_aero,
)
aero_input = _sel_coleta["icao"]
modo = st.radio("Modo de coleta", [
"Todos os períodos (histórico completo)",
@@ -822,49 +897,46 @@ with tab0:
res_box = st.empty()
if submitted:
if not aero_input:
st.error("Informe o código ICAO.")
else:
all_years = (modo == "Todos os períodos (histórico completo)")
update_only= (modo == "Atualizar (apenas dados novos)")
all_years = (modo == "Todos os períodos (histórico completo)")
update_only= (modo == "Atualizar (apenas dados novos)")
log_lines: list[str] = []
q: queue.Queue = queue.Queue()
result_holder: dict = {}
log_lines: list[str] = []
q: queue.Queue = queue.Queue()
result_holder: dict = {}
def _log(msg: str):
q.put(("log", msg))
def _log(msg: str):
q.put(("log", msg))
def _prog(pct: float, msg: str):
q.put(("prog", pct, msg))
def _prog(pct: float, msg: str):
q.put(("prog", pct, msg))
def _worker():
try:
from pipeline import run_pipeline
kw: dict = dict(
aerodrome = aero_input,
dados_dir = str(DADOS_DIR),
all_years = all_years,
headless = headless,
n_samples = n_samp,
do_validate = validate,
do_cleanup = cleanup,
log = _log,
progress = _prog,
)
if modo == "Período específico":
kw["start_year"] = int(start_yr)
kw["end_year"] = int(end_yr)
elif update_only:
# Atualizar: passa all_years=False e usa o ano atual
kw["all_years"] = False
kw["start_year"] = date.today().year - 1
kw["end_year"] = date.today().year
result_holder.update(run_pipeline(**kw))
except Exception as exc:
q.put(("log", f"ERRO: {exc}"))
finally:
q.put(("done",))
def _worker():
try:
from pipeline import run_pipeline
kw: dict = dict(
aerodrome = aero_input,
dados_dir = str(DADOS_DIR),
all_years = all_years,
headless = headless,
n_samples = n_samp,
do_validate = validate,
do_cleanup = cleanup,
log = _log,
progress = _prog,
)
if modo == "Período específico":
kw["start_year"] = int(start_yr)
kw["end_year"] = int(end_yr)
elif update_only:
# Atualizar: passa all_years=False e usa o ano atual
kw["all_years"] = False
kw["start_year"] = date.today().year - 1
kw["end_year"] = date.today().year
result_holder.update(run_pipeline(**kw))
except Exception as exc:
q.put(("log", f"ERRO: {exc}"))
finally:
q.put(("done",))
t = threading.Thread(target=_worker, daemon=True)
t.start()
@@ -1029,6 +1101,39 @@ with tab7:
st.dataframe(show_df, use_container_width=True, height=480)
st.caption(f"{len(show_df):,} observações · {len(sel)} variáveis selecionadas")
csv_bytes = show_df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig")
st.download_button("⬇️ Baixar CSV filtrado", csv_bytes,
f"{sel_aero}_export.csv", "text/csv")
_exp_left, _exp_right = st.columns([1, 1])
with _exp_left:
csv_bytes = show_df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig")
st.download_button(
"⬇️ Baixar CSV (variáveis selecionadas)",
csv_bytes,
f"{sel_aero}_export.csv",
"text/csv",
use_container_width=True,
)
with _exp_right:
if st.button("📁 Salvar CSV completo em pasta…",
use_container_width=True,
help="Abre um seletor de pasta e salva todas as variáveis meteorológicas"):
# monta DataFrame completo com todos as variáveis disponíveis
_full_labels = {
"T": "Temperatura Bulbo Seco (°C)", "Td": "Ponto de Orvalho (°C)",
"UR": "Umidade Relativa (%)", "QNH": "Pressão QNH (hPa)",
"WS": "Velocidade do Vento (kt)", "WG": "Rajada Máxima (kt)",
"WD": "Direção do Vento (°)", "VIS": "Visibilidade Predominante (dam)",
"TETO": "Teto (dam)", "PREC": "Precipitação (mm)",
}
_all_cols = [c for c in _full_labels if c in anl.columns]
_export_df = anl[["_dt"] + _all_cols].copy()
_export_df["_dt"] = _export_df["_dt"].dt.strftime("%Y-%m-%d %H:%M")
_export_df = _export_df.rename(
columns={"_dt": "Data/Hora", **{c: _full_labels[c] for c in _all_cols}}
)
_path = _save_csv_to_folder(_export_df, sel_aero, sel_start, sel_end)
if _path:
st.success(f"Arquivo salvo em:\n`{_path}`")
else:
st.info("Exportação cancelada.")