feat: add memoized _load_filter_sort_postprocess helper (#72)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import polars as pl
|
|||||||
from dash import no_update
|
from dash import no_update
|
||||||
from polars import selectors as cs
|
from polars import selectors as cs
|
||||||
|
|
||||||
|
from src.cache import cache
|
||||||
from src.db import query_marches, schema
|
from src.db import query_marches, schema
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
from src.utils.data import DATA_SCHEMA
|
from src.utils.data import DATA_SCHEMA
|
||||||
@@ -373,6 +374,37 @@ def get_default_hidden_columns(page):
|
|||||||
return hidden_columns
|
return hidden_columns
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize()
|
||||||
|
def _load_filter_sort_postprocess(filter_query, sort_by_key):
|
||||||
|
logger.debug(
|
||||||
|
f"Cache miss — recomputing for filter={filter_query!r} sort={sort_by_key!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
lff: pl.LazyFrame = query_marches().lazy()
|
||||||
|
|
||||||
|
if filter_query:
|
||||||
|
lff = filter_table_data(lff, filter_query, "tableau")
|
||||||
|
|
||||||
|
if sort_by_key:
|
||||||
|
sort_by = [
|
||||||
|
{"column_id": col, "direction": direction} for col, direction in sort_by_key
|
||||||
|
]
|
||||||
|
lff = sort_table_data(lff, sort_by)
|
||||||
|
|
||||||
|
lff = lff.cast(pl.String)
|
||||||
|
lff = lff.fill_null("")
|
||||||
|
|
||||||
|
dff: pl.DataFrame = lff.collect()
|
||||||
|
|
||||||
|
dff = add_links(dff)
|
||||||
|
if "sourceFile" in dff.columns:
|
||||||
|
dff = add_resource_link(dff)
|
||||||
|
if dff.height > 0:
|
||||||
|
dff = format_values(dff)
|
||||||
|
|
||||||
|
return dff
|
||||||
|
|
||||||
|
|
||||||
def prepare_table_data(
|
def prepare_table_data(
|
||||||
data, data_timestamp, filter_query, page_current, page_size, sort_by, source_table
|
data, data_timestamp, filter_query, page_current, page_size, sort_by, source_table
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -78,3 +78,80 @@ def test_normalize_sort_by_preserves_order():
|
|||||||
[{"column_id": "b", "direction": "asc"}, {"column_id": "a", "direction": "asc"}]
|
[{"column_id": "b", "direction": "asc"}, {"column_id": "a", "direction": "asc"}]
|
||||||
)
|
)
|
||||||
assert a_then_b != b_then_a
|
assert a_then_b != b_then_a
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def flask_app():
|
||||||
|
"""Minimal Flask app with SimpleCache so @cache.memoize() works in tests."""
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
from src.cache import cache
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
cache.init_app(app, config={"CACHE_TYPE": "SimpleCache"})
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_cache(flask_app):
|
||||||
|
"""Ensure the flask-caching backend is empty between tests so that
|
||||||
|
cache-hit assertions are meaningful. Falls back to no-op when no
|
||||||
|
Flask app context is active (NullCache)."""
|
||||||
|
from src.cache import cache
|
||||||
|
|
||||||
|
with flask_app.app_context():
|
||||||
|
try:
|
||||||
|
cache.clear()
|
||||||
|
except (RuntimeError, AttributeError):
|
||||||
|
# No app context — cache is NullCache, nothing to clear
|
||||||
|
pass
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_filter_sort_postprocess_returns_dataframe(
|
||||||
|
flask_app, monkeypatch, sample_lff
|
||||||
|
):
|
||||||
|
from src.utils import table
|
||||||
|
|
||||||
|
monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect())
|
||||||
|
|
||||||
|
with flask_app.app_context():
|
||||||
|
df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=())
|
||||||
|
|
||||||
|
assert isinstance(df, pl.DataFrame)
|
||||||
|
assert df.height == 1
|
||||||
|
# All values must be strings after post-processing
|
||||||
|
for col in df.columns:
|
||||||
|
assert df.schema[col] == pl.String
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_filter_sort_postprocess_applies_filter(
|
||||||
|
flask_app, monkeypatch, sample_lff
|
||||||
|
):
|
||||||
|
from src.utils import table
|
||||||
|
|
||||||
|
monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect())
|
||||||
|
|
||||||
|
with flask_app.app_context():
|
||||||
|
df = table._load_filter_sort_postprocess(
|
||||||
|
filter_query="{objet} icontains travaux", sort_by_key=()
|
||||||
|
)
|
||||||
|
assert df.height == 1
|
||||||
|
|
||||||
|
df_empty = table._load_filter_sort_postprocess(
|
||||||
|
filter_query="{objet} icontains nonexistent", sort_by_key=()
|
||||||
|
)
|
||||||
|
assert df_empty.height == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_filter_sort_postprocess_adds_links(flask_app, monkeypatch, sample_lff):
|
||||||
|
from src.utils import table
|
||||||
|
|
||||||
|
monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect())
|
||||||
|
|
||||||
|
with flask_app.app_context():
|
||||||
|
df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=())
|
||||||
|
# add_links injects an <a href> wrapper around uid, acheteur_nom, titulaire_nom
|
||||||
|
assert "<a href" in df["uid"][0]
|
||||||
|
assert "<a href" in df["acheteur_nom"][0]
|
||||||
|
assert "<a href" in df["titulaire_nom"][0]
|
||||||
|
|||||||
Reference in New Issue
Block a user