V1.0
This commit is contained in:
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user