From b0d2aca4ff4deb39f1ab97009ebfb8fd32a6d74c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Sun, 19 Apr 2026 22:50:32 +0200 Subject: [PATCH] perf(tableau): memoize filter+sort+postprocess pipeline (#72) Co-Authored-By: Claude Sonnet 4.6 --- src/utils/table.py | 82 +++++++++++------------- tests/test_table.py | 150 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 44 deletions(-) diff --git a/src/utils/table.py b/src/utils/table.py index 612e0a8..d7cf69b 100644 --- a/src/utils/table.py +++ b/src/utils/table.py @@ -10,7 +10,7 @@ from src.db import query_marches, schema from src.utils import logger from src.utils.data import DATA_SCHEMA from src.utils.frontend import get_button_properties -from src.utils.tracking import track_search # noqa: F401 +from src.utils.tracking import track_search def split_filter_part(filter_part): @@ -420,66 +420,60 @@ def prepare_table_data( :param source_table: :return: """ + logger.debug(" + + + + + + + + + + + + + + + + + + ") - if os.getenv("DEVELOPMENT").lower() == "true": - logger.debug(" + + + + + + + + + + + + + + + + + + ") - - trigger_cleanup = no_update - - # Récupération des données - if isinstance(data, list): - lff: pl.LazyFrame = pl.LazyFrame(data, strict=False, infer_schema_length=5000) - elif isinstance(data, pl.LazyFrame): - lff = data - else: - lff: pl.LazyFrame = query_marches().lazy() - - # Application des filtres if filter_query: - lff = filter_table_data(lff, filter_query, source_table) - trigger_cleanup = no_update if source_table == "tableau" else str(uuid.uuid4()) + track_search(filter_query, source_table) - # Application des tris - if sort_by and len(sort_by) > 0: - lff = sort_table_data(lff, sort_by) + trigger_cleanup = no_update if source_table == "tableau" else str(uuid.uuid4()) + + if data is None: + sort_by_key = normalize_sort_by(sort_by) + dff: pl.DataFrame = _load_filter_sort_postprocess( + filter_query=filter_query, sort_by_key=sort_by_key + ) + else: + if isinstance(data, list): + lff: pl.LazyFrame = pl.LazyFrame( + data, strict=False, infer_schema_length=5000 + ) + elif isinstance(data, pl.LazyFrame): + lff = data + else: + lff = query_marches().lazy() + + if filter_query: + lff = filter_table_data(lff, filter_query, source_table) + + if sort_by and len(sort_by) > 0: + lff = sort_table_data(lff, sort_by) + + dff = lff.collect() + dff = dff.cast(pl.String) + dff = dff.fill_null("") + dff = add_links(dff) + if "sourceFile" in dff.columns: + dff = add_resource_link(dff) + if dff.height > 0: + dff = format_values(dff) - # Matérialisation des filtres - dff: pl.DataFrame = lff.collect() height = dff.height if height > 0: - nb_rows = f"{format_number(height)} lignes ({format_number(dff.select('uid').unique().height)} marchés)" + nb_rows = ( + f"{format_number(height)} lignes " + f"({format_number(dff.select('uid').unique().height)} marchés)" + ) else: nb_rows = "0 lignes (0 marchés)" - # Pagination des données start_row = page_current * page_size - # end_row = (page_current + 1) * page_size dff = dff.slice(start_row, page_size) - # Tout devient string - dff = dff.cast(pl.String) - - # Remplace les strings null par "", mais pas les numeric null - dff = dff.fill_null("") - - # Ajout des liens vers les pages de détails - dff = add_links(dff) - - # Ajout des liens vers les fichiers Open Data - if "sourceFile" in dff.columns: - dff = add_resource_link(dff) - - # Formatage des montants - if height > 0: - dff = format_values(dff) - - # Récupération des colonnes et tooltip table_columns, tooltip = setup_table_columns(dff) dicts = dff.to_dicts() - # Propriétés du bouton de téléchargement download_disabled, download_text, download_title = get_button_properties(height) return ( diff --git a/tests/test_table.py b/tests/test_table.py index e67676f..d15daa7 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -155,3 +155,153 @@ def test_load_filter_sort_postprocess_adds_links(flask_app, monkeypatch, sample_ assert "= 32 # uuid4 hex string + + +def test_prepare_table_data_with_external_data_does_not_use_cache( + monkeypatch, flask_app, sample_lff +): + """When a caller passes data (acheteur/titulaire/observatoire path), + bypass the memoized helper entirely.""" + from src.utils import table + + sentinel = {"called": False} + + def should_not_be_called(*a, **kw): + sentinel["called"] = True + raise AssertionError("Memoized helper must not be called when data is provided") + + monkeypatch.setattr(table, "_load_filter_sort_postprocess", should_not_be_called) + + with flask_app.app_context(): + table.prepare_table_data( + data=sample_lff, # external LazyFrame + data_timestamp=0, + filter_query=None, + page_current=0, + page_size=20, + sort_by=[], + source_table="acheteur", + ) + + assert sentinel["called"] is False