feat: chargements de pages best-effort au boot (tableau, sources) (#78)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -1,6 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from urllib.error import HTTPError, URLError
|
|
||||||
|
|
||||||
import dash_bootstrap_components as dbc
|
import dash_bootstrap_components as dbc
|
||||||
import dash_leaflet as dl
|
import dash_leaflet as dl
|
||||||
@@ -120,9 +119,12 @@ def get_barchart_sources(lff: pl.LazyFrame, type_date: str):
|
|||||||
|
|
||||||
def get_sources_tables(source_path) -> html.Div:
|
def get_sources_tables(source_path) -> html.Div:
|
||||||
try:
|
try:
|
||||||
|
if not source_path:
|
||||||
|
raise ValueError("SOURCE_STATS_CSV_PATH non défini")
|
||||||
dff = pl.read_csv(source_path)
|
dff = pl.read_csv(source_path)
|
||||||
except (URLError, HTTPError):
|
except Exception as e:
|
||||||
return html.Div("Erreur de connexion")
|
logger.warning(f"Sources de données indisponibles ({e})")
|
||||||
|
return html.Div("Sources de données momentanément indisponibles.")
|
||||||
dff = dff.with_columns(
|
dff = dff.with_columns(
|
||||||
(
|
(
|
||||||
pl.lit('<a href = "')
|
pl.lit('<a href = "')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from dash import (
|
|||||||
|
|
||||||
from src.db import query_marches, schema
|
from src.db import query_marches, schema
|
||||||
from src.figures import DataTable, make_column_picker
|
from src.figures import DataTable, make_column_picker
|
||||||
from src.utils import get_last_modified, logger
|
from src.utils import get_data_update_timestamp, logger
|
||||||
from src.utils.seo import META_CONTENT
|
from src.utils.seo import META_CONTENT
|
||||||
from src.utils.table import (
|
from src.utils.table import (
|
||||||
COLUMNS,
|
COLUMNS,
|
||||||
@@ -33,9 +33,16 @@ from src.utils.table import (
|
|||||||
)
|
)
|
||||||
from src.utils.tracking import track_search
|
from src.utils.tracking import track_search
|
||||||
|
|
||||||
update_date_timestamp = get_last_modified(os.getenv("DATA_FILE_PARQUET_PATH", ""))
|
update_date_timestamp = get_data_update_timestamp(
|
||||||
|
os.getenv("DATA_FILE_PARQUET_PATH", ""),
|
||||||
|
os.getenv("DUCKDB_PATH", "./decp.duckdb"),
|
||||||
|
)
|
||||||
|
if update_date_timestamp is not None:
|
||||||
update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y")
|
update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y")
|
||||||
update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat()
|
update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat()
|
||||||
|
else:
|
||||||
|
update_date = "date inconnue"
|
||||||
|
update_date_iso = ""
|
||||||
|
|
||||||
|
|
||||||
NAME = "Tableau"
|
NAME = "Tableau"
|
||||||
|
|||||||
@@ -41,3 +41,19 @@ DOMAIN_NAME = (
|
|||||||
if os.getenv("DEVELOPMENT", "False").lower() == "true"
|
if os.getenv("DEVELOPMENT", "False").lower() == "true"
|
||||||
else "decp.info"
|
else "decp.info"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_update_timestamp(
|
||||||
|
parquet_path: str, fallback_path: str | None = None
|
||||||
|
) -> float | None:
|
||||||
|
"""Date de MAJ des données, best-effort, sans jamais lever (usage au boot)."""
|
||||||
|
try:
|
||||||
|
return get_last_modified(parquet_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Date de mise à jour des données indisponible ({e})")
|
||||||
|
if fallback_path:
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(fallback_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_timestamp_falls_back_to_db_mtime(tmp_path, monkeypatch):
|
||||||
|
import src.utils as u
|
||||||
|
from src.utils import get_data_update_timestamp
|
||||||
|
|
||||||
|
def boom(*a, **k):
|
||||||
|
raise RuntimeError("net down")
|
||||||
|
|
||||||
|
monkeypatch.setattr(u, "get_last_modified", boom)
|
||||||
|
fb = tmp_path / "decp.duckdb"
|
||||||
|
fb.write_bytes(b"x")
|
||||||
|
assert get_data_update_timestamp("http://x", str(fb)) == os.path.getmtime(str(fb))
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_timestamp_none_when_all_fail(monkeypatch):
|
||||||
|
import src.utils as u
|
||||||
|
from src.utils import get_data_update_timestamp
|
||||||
|
|
||||||
|
def boom(*a, **k):
|
||||||
|
raise RuntimeError("net down")
|
||||||
|
|
||||||
|
monkeypatch.setattr(u, "get_last_modified", boom)
|
||||||
|
assert get_data_update_timestamp("http://x", None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_timestamp_nominal(monkeypatch):
|
||||||
|
import src.utils as u
|
||||||
|
from src.utils import get_data_update_timestamp
|
||||||
|
|
||||||
|
monkeypatch.setattr(u, "get_last_modified", lambda p: 123.0)
|
||||||
|
assert get_data_update_timestamp("http://x", None) == 123.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_tables_none_path():
|
||||||
|
from src.figures import get_sources_tables
|
||||||
|
|
||||||
|
div = get_sources_tables(None)
|
||||||
|
assert "indisponible" in str(div.children).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_tables_missing_file():
|
||||||
|
from src.figures import get_sources_tables
|
||||||
|
|
||||||
|
div = get_sources_tables("/does/not/exist.csv")
|
||||||
|
assert "indisponible" in str(div.children).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_tables_valid_csv(tmp_path):
|
||||||
|
from dash import dash_table
|
||||||
|
|
||||||
|
from src.figures import get_sources_tables
|
||||||
|
|
||||||
|
csv = tmp_path / "s.csv"
|
||||||
|
csv.write_text(
|
||||||
|
"nom,organisation,nb_marchés,nb_acheteurs,code,url,unique\n"
|
||||||
|
"Source A,Org A,5,2,XA,http://a,1\n"
|
||||||
|
)
|
||||||
|
div = get_sources_tables(str(csv))
|
||||||
|
assert isinstance(div.children, dash_table.DataTable)
|
||||||
Reference in New Issue
Block a user