V.1.0
This commit is contained in:
@@ -1281,6 +1281,342 @@ with tab0:
|
||||
with st.expander("Divergências"):
|
||||
for e in r["errors"]: st.text(e)
|
||||
|
||||
# ── Atualização Total ─────────────────────────────────────────────────────
|
||||
st.divider()
|
||||
st.markdown("### 🌐 Atualização Total")
|
||||
st.markdown(
|
||||
"**O que faz:** verifica novos aeródromos no catálogo ICEA "
|
||||
"e baixa **apenas os dados novos** para cada aeródromo que já "
|
||||
"possui observações no banco. Recomendado a cada 2 meses.\n\n"
|
||||
"> **Nota:** aeródromos que ainda **não têm dados** no banco não são "
|
||||
"coletados aqui — apenas adicionados ao catálogo. "
|
||||
"Para coletar o histórico completo de todos os aeródromos, "
|
||||
"use o script `_apps/collect_all.py` (pode levar horas/dias)."
|
||||
)
|
||||
|
||||
_at_c1, _at_c2, _ = st.columns([1, 1, 3])
|
||||
with _at_c1:
|
||||
_at_headless = not st.checkbox("Mostrar Chrome", value=False, key="at_headless")
|
||||
with _at_c2:
|
||||
_at_validate = st.checkbox("Validar amostras", value=False, key="at_validate")
|
||||
|
||||
_at_submitted = st.button(
|
||||
"🌐 Iniciar Atualização Total",
|
||||
type="primary",
|
||||
help="Atualiza catálogo + coleta dados novos para todos os aeródromos com dados no banco",
|
||||
)
|
||||
|
||||
if _at_submitted:
|
||||
import db as _db_at
|
||||
|
||||
_at_conn_check = _db_at.get_connection(DB_PATH)
|
||||
_db_at.ensure_schema(_at_conn_check)
|
||||
_at_aeros = _db_at.list_aerodromes_with_data(_at_conn_check)
|
||||
_at_conn_check.close()
|
||||
|
||||
if not _at_aeros:
|
||||
st.warning(
|
||||
"Nenhum aeródromo com dados no banco. "
|
||||
"Use o formulário acima para coletar dados primeiro."
|
||||
)
|
||||
else:
|
||||
_at_n_total = len(_at_aeros)
|
||||
_at_q: queue.Queue = queue.Queue()
|
||||
_at_results: list[dict] = []
|
||||
_at_log_lines: list[str] = []
|
||||
|
||||
_at_prog_bar = st.progress(0.0)
|
||||
_at_stat_txt = st.empty()
|
||||
_at_log_box = st.empty()
|
||||
_at_table_box = st.empty()
|
||||
|
||||
_at_aero_list = [d["icao"] for d in _at_aeros]
|
||||
_at_aero_meta = {d["icao"]: d for d in _at_aeros}
|
||||
|
||||
def _at_worker() -> None:
|
||||
try:
|
||||
# Phase 1 — refresh aerodrome catalog
|
||||
_at_q.put(("status", "Fase 1 — Atualizando catálogo ICEA…"))
|
||||
from scraper_meteorologia import fetch_aerodrome_catalog as _fat
|
||||
import db as _dbi
|
||||
_new_cat = _fat(headless=_at_headless)
|
||||
_cat_conn = _dbi.get_connection(DB_PATH)
|
||||
_dbi.ensure_schema(_cat_conn)
|
||||
_n_new = _dbi.upsert_aerodromes(_cat_conn, _new_cat)
|
||||
_cat_conn.close()
|
||||
_at_q.put(("log",
|
||||
f"[catálogo] {len(_new_cat)} aeródromos no portal "
|
||||
f"| {_n_new} registros atualizados"
|
||||
))
|
||||
|
||||
# Phase 2 — forward-only update for each aerodrome
|
||||
for _i, _icao in enumerate(_at_aero_list):
|
||||
_at_q.put(("aero_start", _icao, _i))
|
||||
try:
|
||||
from pipeline import run_pipeline as _rp
|
||||
_res = _rp(
|
||||
aerodrome = _icao,
|
||||
dados_dir = DADOS_DIR,
|
||||
db_path = DB_PATH,
|
||||
preproc_dir = PREPROC_DIR,
|
||||
all_years = False,
|
||||
update_only = True,
|
||||
headless = _at_headless,
|
||||
n_samples = 5 if _at_validate else 0,
|
||||
do_validate = _at_validate,
|
||||
do_cleanup = True,
|
||||
log = lambda m: _at_q.put(("log", m)),
|
||||
progress = lambda p, m: None,
|
||||
)
|
||||
_at_q.put(("aero_done", _icao, _res))
|
||||
except Exception as _exc:
|
||||
_at_q.put(("aero_error", _icao, str(_exc)[:120]))
|
||||
|
||||
except Exception as _exc:
|
||||
_at_q.put(("log", f"ERRO: {_exc}"))
|
||||
finally:
|
||||
_at_q.put(("done",))
|
||||
|
||||
_at_t = threading.Thread(target=_at_worker, daemon=True)
|
||||
_at_t.start()
|
||||
|
||||
_at_n_done = 0
|
||||
|
||||
while _at_t.is_alive() or not _at_q.empty():
|
||||
try:
|
||||
_item = _at_q.get(timeout=0.4)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
if _item[0] == "done":
|
||||
break
|
||||
|
||||
elif _item[0] == "status":
|
||||
_at_stat_txt.caption(_item[1])
|
||||
|
||||
elif _item[0] == "log":
|
||||
_at_log_lines.append(_item[1])
|
||||
_at_log_box.code("\n".join(_at_log_lines[-60:]))
|
||||
|
||||
elif _item[0] == "aero_start":
|
||||
_, _icao, _i = _item
|
||||
_at_prog_bar.progress(_i / max(_at_n_total, 1))
|
||||
_at_stat_txt.caption(
|
||||
f"Fase 2 — {_icao} ({_i + 1} / {_at_n_total})"
|
||||
)
|
||||
_meta = _at_aero_meta.get(_icao, {})
|
||||
_at_results.append({
|
||||
"ICAO": _icao,
|
||||
"Nome": _meta.get("nome", ""),
|
||||
"UF": _meta.get("uf", ""),
|
||||
"Status": "⏳ Coletando…",
|
||||
"Novas linhas": "—",
|
||||
})
|
||||
_at_table_box.dataframe(
|
||||
_at_results, use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
elif _item[0] == "aero_done":
|
||||
_, _icao, _res = _item
|
||||
_at_n_done += 1
|
||||
_new = _res.get("n_upserted", 0)
|
||||
for _row in _at_results:
|
||||
if _row["ICAO"] == _icao:
|
||||
_row["Status"] = "✅ Concluído"
|
||||
_row["Novas linhas"] = f"{_new:,}"
|
||||
break
|
||||
_at_table_box.dataframe(
|
||||
_at_results, use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
elif _item[0] == "aero_error":
|
||||
_, _icao, _err = _item
|
||||
for _row in _at_results:
|
||||
if _row["ICAO"] == _icao:
|
||||
_row["Status"] = "❌ Erro"
|
||||
_row["Novas linhas"] = _err[:50]
|
||||
break
|
||||
_at_table_box.dataframe(
|
||||
_at_results, use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
_at_t.join()
|
||||
_at_prog_bar.progress(1.0)
|
||||
st.cache_data.clear()
|
||||
_at_stat_txt.empty()
|
||||
st.success(
|
||||
f"✅ Atualização Total concluída — "
|
||||
f"{_at_n_done} / {_at_n_total} aeródromos atualizados."
|
||||
)
|
||||
st.rerun()
|
||||
|
||||
# ── Coleta Total — Histórico Completo ─────────────────────────────────────
|
||||
st.divider()
|
||||
st.markdown("### 🗄️ Coleta Total — Histórico Completo")
|
||||
st.markdown(
|
||||
"Coleta o **histórico completo** de todos os aeródromos do catálogo "
|
||||
"que ainda **não têm dados** no banco. "
|
||||
"Aeródromos com dados são ignorados automaticamente.\n\n"
|
||||
"> ⚠️ Esta operação pode demorar **horas ou dias** dependendo do número "
|
||||
"de aeródromos. Não feche o dashboard durante a coleta. "
|
||||
"Em caso de interrupção, basta clicar novamente — os aeródromos já "
|
||||
"coletados serão pulados automaticamente."
|
||||
)
|
||||
|
||||
# Status summary
|
||||
if _db_available():
|
||||
import db as _db_ct_info
|
||||
_ct_conn_info = _db_ct_info.get_connection(DB_PATH)
|
||||
_db_ct_info.ensure_schema(_ct_conn_info)
|
||||
_ct_catalog = _db_ct_info.list_all_aerodromes(_ct_conn_info)
|
||||
_ct_with_data = set(_db_ct_info.list_aerodromes(_ct_conn_info))
|
||||
_ct_conn_info.close()
|
||||
_ct_total = len(_ct_catalog)
|
||||
_ct_done = len(_ct_with_data)
|
||||
_ct_pending = _ct_total - _ct_done
|
||||
else:
|
||||
_ct_total = _ct_done = _ct_pending = 0
|
||||
|
||||
_ct_info_col1, _ct_info_col2, _ct_info_col3 = st.columns(3)
|
||||
_ct_info_col1.metric("No catálogo", _ct_total)
|
||||
_ct_info_col2.metric("Com dados", _ct_done)
|
||||
_ct_info_col3.metric("Aguardando coleta", _ct_pending)
|
||||
|
||||
_ct_opt1, _ct_opt2, _ct_opt3 = st.columns([1, 1, 2])
|
||||
with _ct_opt1:
|
||||
_ct_headless = not st.checkbox("Mostrar Chrome", value=False, key="ct_headless")
|
||||
with _ct_opt2:
|
||||
_ct_max = int(st.number_input(
|
||||
"Limite de aeródromos (0 = todos)",
|
||||
min_value=0, max_value=200, value=0, step=1, key="ct_max",
|
||||
help="Útil para testes. 0 processa todos os pendentes.",
|
||||
))
|
||||
|
||||
if _ct_pending == 0 and _ct_total > 0:
|
||||
st.info("✅ Todos os aeródromos do catálogo já têm dados no banco.")
|
||||
elif _ct_total == 0:
|
||||
st.warning(
|
||||
"Catálogo vazio. Clique em **Atualizar catálogo ICEA** (acima) para "
|
||||
"baixar a lista de aeródromos disponíveis."
|
||||
)
|
||||
|
||||
_ct_submitted = st.button(
|
||||
"🗄️ Iniciar Coleta Total",
|
||||
type="primary",
|
||||
disabled=(_ct_pending == 0),
|
||||
help="Coleta o histórico completo para os aeródromos sem dados no banco",
|
||||
)
|
||||
|
||||
if _ct_submitted and _ct_pending > 0:
|
||||
_ct_q: queue.Queue = queue.Queue()
|
||||
_ct_results: list[dict] = []
|
||||
_ct_log_lines: list[str] = []
|
||||
|
||||
_ct_prog_bar = st.progress(0.0)
|
||||
_ct_stat_txt = st.empty()
|
||||
_ct_log_box = st.empty()
|
||||
_ct_table_box = st.empty()
|
||||
|
||||
# Build metadata dict for display
|
||||
_ct_meta = {d["icao"]: d for d in _ct_catalog}
|
||||
_ct_max_arg = _ct_max if _ct_max > 0 else None
|
||||
|
||||
def _ct_worker() -> None:
|
||||
try:
|
||||
from collect_all import collect_all as _ca
|
||||
|
||||
def _ct_on_start(icao: str, idx: int, total: int, nome: str) -> None:
|
||||
_ct_q.put(("aero_start", icao, idx, total, nome))
|
||||
|
||||
def _ct_on_done(icao: str, event: str, result: dict) -> None:
|
||||
_ct_q.put(("aero_done", icao, event, result))
|
||||
|
||||
_ca(
|
||||
resume = True,
|
||||
max_aerodromes = _ct_max_arg,
|
||||
headless = _ct_headless,
|
||||
n_samples = 0,
|
||||
do_cleanup = True,
|
||||
log = lambda m: _ct_q.put(("log", m)),
|
||||
on_start = _ct_on_start,
|
||||
on_done = _ct_on_done,
|
||||
)
|
||||
except Exception as _exc:
|
||||
_ct_q.put(("log", f"ERRO: {_exc}"))
|
||||
finally:
|
||||
_ct_q.put(("done",))
|
||||
|
||||
_ct_t = threading.Thread(target=_ct_worker, daemon=True)
|
||||
_ct_t.start()
|
||||
|
||||
_ct_n_done = 0
|
||||
_ct_n_total_run = _ct_max_arg or _ct_pending
|
||||
|
||||
while _ct_t.is_alive() or not _ct_q.empty():
|
||||
try:
|
||||
_citem = _ct_q.get(timeout=0.4)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
if _citem[0] == "done":
|
||||
break
|
||||
|
||||
elif _citem[0] == "log":
|
||||
_ct_log_lines.append(_citem[1])
|
||||
_ct_log_box.code("\n".join(_ct_log_lines[-80:]))
|
||||
|
||||
elif _citem[0] == "aero_start":
|
||||
_, _icao, _idx, _tot, _nome = _citem
|
||||
_ct_prog_bar.progress((_idx - 1) / max(_tot, 1))
|
||||
_ct_stat_txt.caption(f"Coletando {_icao} — {_idx} / {_tot}")
|
||||
_meta = _ct_meta.get(_icao, {})
|
||||
_ct_results.append({
|
||||
"ICAO": _icao,
|
||||
"Nome": _meta.get("nome", _nome),
|
||||
"UF": _meta.get("uf", ""),
|
||||
"Status": "⏳ Coletando…",
|
||||
"Período": "—",
|
||||
"Linhas": "—",
|
||||
})
|
||||
_ct_table_box.dataframe(
|
||||
_ct_results, use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
elif _citem[0] == "aero_done":
|
||||
_, _icao, _event, _res = _citem
|
||||
_status_map = {
|
||||
"completed": "✅ Concluído",
|
||||
"skipped": "⏭️ Já tinha dados",
|
||||
"failed": "❌ Erro",
|
||||
"interrupted": "⏸️ Interrompido",
|
||||
}
|
||||
_st = _status_map.get(_event, _event)
|
||||
for _row in _ct_results:
|
||||
if _row["ICAO"] == _icao:
|
||||
_row["Status"] = _st
|
||||
if _event == "completed":
|
||||
_ct_n_done += 1
|
||||
_row["Período"] = (
|
||||
f"{_res.get('period_start','')} → "
|
||||
f"{_res.get('period_end','')}"
|
||||
)
|
||||
_row["Linhas"] = f"{_res.get('rows', 0):,}"
|
||||
elif _event == "failed":
|
||||
_row["Período"] = _res.get("error", "")[:40]
|
||||
break
|
||||
_ct_table_box.dataframe(
|
||||
_ct_results, use_container_width=True, hide_index=True
|
||||
)
|
||||
|
||||
_ct_t.join()
|
||||
_ct_prog_bar.progress(1.0)
|
||||
st.cache_data.clear()
|
||||
_ct_stat_txt.empty()
|
||||
st.success(
|
||||
f"✅ Coleta Total concluída — {_ct_n_done} aeródromos coletados."
|
||||
)
|
||||
st.rerun()
|
||||
|
||||
# ── Visão Geral ───────────────────────────────────────────────────────────────
|
||||
with tab1:
|
||||
st.plotly_chart(chart_overview(anl_r), use_container_width=True)
|
||||
|
||||
Reference in New Issue
Block a user