Merge branch 'hotfix/2.7.1'

This commit is contained in:
Colin Maudry
2026-03-23 13:40:01 +01:00
6 changed files with 106 additions and 216 deletions
+4
View File
@@ -1,3 +1,7 @@
#### 2.7.1 (23 mars 2026)
- Correction du partage de données filtrées entre dashboard et vue des données
#### 2.7.0 (23 mars 2026)
- Remplacement de la page Statistiques par l'observatoire
+1 -1
View File
@@ -1,6 +1,6 @@
# decp.info
> v2.7.0
> v2.7.1
> Outil d'exploration et de téléchargement des données essentielles de la commande publique.
=> [decp.info](https://decp.info)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "decp.info"
description = "Interface d'exploration et d'analyse des marchés publics français."
version = "2.7.0"
version = "2.7.1"
requires-python = ">= 3.10"
authors = [
{ name = "Colin Maudry", email = "colin@colmo.tech" }
+32 -146
View File
@@ -90,8 +90,6 @@ OBSERVATOIRE_COLUMNS = [
]
]
DF_FILTERED: pl.DataFrame = pl.DataFrame()
layout = [
dcc.Location(id="dashboard_url", refresh="callback-nav"),
dcc.Store(id="observatoire-filters", storage_type="local"),
@@ -324,7 +322,7 @@ Alors, on fait comment ?
dbc.Col("Sous-traitance :", lg=5),
dbc.Col(
dbc.RadioItems(
id="dashboard_marche_sousTraitanceDeclaree",
id="dashboard_marche_sous_traitance_declaree",
options=[
{
"label": "Tous",
@@ -380,7 +378,7 @@ Alors, on fait comment ?
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsSociales",
id="dashboard_marche_considerations_sociales",
placeholder="Considérations sociales",
options=get_enum_values_as_dict(
"considerationsSociales"
@@ -394,7 +392,7 @@ Alors, on fait comment ?
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsEnvironnementales",
id="dashboard_marche_considerations_environnementales",
placeholder="Considérations environnementales",
multi=True,
options=get_enum_values_as_dict(
@@ -544,30 +542,14 @@ FILTER_PARAMS = [
("dashboard_montant_max", "montant_max", False, None),
("dashboard_marche_techniques", "techniques", True, None),
("dashboard_marche_innovant", "innovant", False, "all"),
("dashboard_marche_sousTraitanceDeclaree", "sous_traitance", False, "all"),
("dashboard_marche_considerationsSociales", "social", True, None),
("dashboard_marche_considerationsEnvironnementales", "env", True, None),
("dashboard_marche_sous_traitance_declaree", "sous_traitance", False, "all"),
("dashboard_marche_considerations_sociales", "social", True, None),
("dashboard_marche_considerations_environnementales", "env", True, None),
]
@callback(
Output("dashboard_year", "value"),
Output("dashboard_acheteur_id", "value"),
Output("dashboard_acheteur_categorie", "value"),
Output("dashboard_acheteur_departement_code", "value"),
Output("dashboard_titulaire_id", "value"),
Output("dashboard_titulaire_categorie", "value"),
Output("dashboard_titulaire_departement_code", "value"),
Output("dashboard_marche_type", "value"),
Output("dashboard_marche_objet", "value"),
Output("dashboard_marche_code_cpv", "value"),
Output("dashboard_montant_min", "value"),
Output("dashboard_montant_max", "value"),
Output("dashboard_marche_techniques", "value"),
Output("dashboard_marche_innovant", "value"),
Output("dashboard_marche_sousTraitanceDeclaree", "value"),
Output("dashboard_marche_considerationsSociales", "value"),
Output("dashboard_marche_considerationsEnvironnementales", "value"),
*[Output(fp[0], "value") for fp in FILTER_PARAMS],
Input("dashboard_url", "search"),
Input("dashboard_url", "pathname"),
State("observatoire-filters", "data"),
@@ -670,73 +652,26 @@ def show_confirmation(n_clicks):
@callback(
Output("cards", "children"),
Input("dashboard_year", "value"),
Input("dashboard_acheteur_id", "value"),
Input("dashboard_acheteur_categorie", "value"),
Input("dashboard_acheteur_departement_code", "value"),
Input("dashboard_titulaire_id", "value"),
Input("dashboard_titulaire_categorie", "value"),
Input("dashboard_titulaire_departement_code", "value"),
Input("dashboard_marche_type", "value"),
Input("dashboard_marche_objet", "value"),
Input("dashboard_marche_code_cpv", "value"),
Input("dashboard_montant_min", "value"),
Input("dashboard_montant_max", "value"),
Input("dashboard_marche_techniques", "value"),
Input("dashboard_marche_innovant", "value"),
Input("dashboard_marche_sousTraitanceDeclaree", "value"),
Input("dashboard_marche_considerationsSociales", "value"),
Input("dashboard_marche_considerationsEnvironnementales", "value"),
Output("observatoire-filters", "data"),
*[Input(fp[0], "value") for fp in FILTER_PARAMS],
)
def udpate_dashboard_cards(
dashboard_year,
dashboard_acheteur_id,
dashboard_acheteur_categorie,
dashboard_acheteur_departement_code,
dashboard_titulaire_id,
dashboard_titulaire_categorie,
dashboard_titulaire_departement_code,
dashboard_marche_type,
dashboard_marche_objet,
dashboard_marche_code_cpv,
dashboard_montant_min,
dashboard_montant_max,
dashboard_marche_techniques,
dashboard_marche_innovant,
dashboard_marche_sous_traitance_declaree,
dashboard_marche_considerations_sociales,
dashboard_marche_considerations_environnementales,
):
def udpate_dashboard_cards(*filter_values):
lff: pl.LazyFrame = df.lazy()
# Filtrage des données
lff = prepare_dashboard_data(
lff=lff,
year=dashboard_year,
acheteur_id=dashboard_acheteur_id,
acheteur_categorie=dashboard_acheteur_categorie,
acheteur_departement_code=dashboard_acheteur_departement_code,
titulaire_id=dashboard_titulaire_id,
titulaire_categorie=dashboard_titulaire_categorie,
titulaire_departement_code=dashboard_titulaire_departement_code,
type=dashboard_marche_type,
objet=dashboard_marche_objet,
code_cpv=dashboard_marche_code_cpv,
considerations_sociales=dashboard_marche_considerations_sociales,
considerations_environnementales=dashboard_marche_considerations_environnementales,
montant_min=dashboard_montant_min,
montant_max=dashboard_montant_max,
techniques=dashboard_marche_techniques,
marche_innovant=dashboard_marche_innovant,
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
)
filter_params = {}
for (input_id, url_key, is_multi, default), value in zip(
FILTER_PARAMS, filter_values
):
filter_params[input_id] = value
print(filter_params)
lff = prepare_dashboard_data(lff=lff, **filter_params)
# Génération des métriques
dff = lff.collect(engine="streaming")
global DF_FILTERED
DF_FILTERED = dff
logger.debug("Filter data: " + str(dff.height))
df_per_uid = (
@@ -832,73 +767,18 @@ def udpate_dashboard_cards(
)
)
return dbc.Row(children=cards + geographic_maps + other_cards)
return dbc.Row(children=cards + geographic_maps + other_cards), filter_params
@callback(
Output("download-observatoire", "data"),
Input("btn-download-observatoire", "n_clicks"),
State("dashboard_year", "value"),
State("dashboard_acheteur_id", "value"),
State("dashboard_acheteur_categorie", "value"),
State("dashboard_acheteur_departement_code", "value"),
State("dashboard_titulaire_id", "value"),
State("dashboard_titulaire_categorie", "value"),
State("dashboard_titulaire_departement_code", "value"),
State("dashboard_marche_type", "value"),
State("dashboard_marche_objet", "value"),
State("dashboard_marche_code_cpv", "value"),
State("dashboard_montant_min", "value"),
State("dashboard_montant_max", "value"),
State("dashboard_marche_techniques", "value"),
State("dashboard_marche_innovant", "value"),
State("dashboard_marche_sousTraitanceDeclaree", "value"),
State("dashboard_marche_considerationsSociales", "value"),
State("dashboard_marche_considerationsEnvironnementales", "value"),
State("observatoire-filters", "data"),
State("observatoire-hidden-columns", "data"),
prevent_initial_call=True,
)
def download_observatoire(
_n_clicks,
dashboard_year,
dashboard_acheteur_id,
dashboard_acheteur_categorie,
dashboard_acheteur_departement_code,
dashboard_titulaire_id,
dashboard_titulaire_categorie,
dashboard_titulaire_departement_code,
dashboard_marche_type,
dashboard_marche_objet,
dashboard_marche_code_cpv,
dashboard_montant_min,
dashboard_montant_max,
dashboard_marche_techniques,
dashboard_marche_innovant,
dashboard_marche_sous_traitance_declaree,
dashboard_considerations_sociales,
dashboard_considerations_environnementales,
hidden_columns,
):
lff = prepare_dashboard_data(
lff=df.lazy(),
year=dashboard_year,
acheteur_id=dashboard_acheteur_id,
acheteur_categorie=dashboard_acheteur_categorie,
acheteur_departement_code=dashboard_acheteur_departement_code,
titulaire_id=dashboard_titulaire_id,
titulaire_categorie=dashboard_titulaire_categorie,
titulaire_departement_code=dashboard_titulaire_departement_code,
type=dashboard_marche_type,
objet=dashboard_marche_objet,
code_cpv=dashboard_marche_code_cpv,
considerations_sociales=dashboard_considerations_sociales,
considerations_environnementales=dashboard_considerations_environnementales,
montant_min=dashboard_montant_min,
montant_max=dashboard_montant_max,
techniques=dashboard_marche_techniques,
marche_innovant=dashboard_marche_innovant,
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
)
def download_observatoire(_n_clicks, filter_params, hidden_columns):
lff = prepare_dashboard_data(lff=df.lazy(), **(filter_params or {}))
if hidden_columns:
lff = lff.drop(hidden_columns)
@@ -974,16 +854,22 @@ def toggle_observatoire_preview(n_clicks, is_open):
Input("observatoire-preview-table", "page_size"),
Input("observatoire-preview-table", "sort_by"),
State("observatoire-preview-table", "data_timestamp"),
State("observatoire-filters", "data"),
prevent_initial_call=True,
)
def populate_preview_table(
is_open, filter_query, page_current, page_size, sort_by, data_timestamp
is_open,
filter_query,
page_current,
page_size,
sort_by,
data_timestamp,
filter_params,
):
if not is_open:
return (no_update,) * 9
global DF_FILTERED
lff = DF_FILTERED.lazy()
lff = prepare_dashboard_data(lff=df.lazy(), **(filter_params or {}))
return prepare_table_data(
lff,
+51 -51
View File
@@ -699,98 +699,98 @@ def prepare_table_data(
def prepare_dashboard_data(
lff: pl.LazyFrame,
year,
acheteur_id,
acheteur_categorie,
acheteur_departement_code,
titulaire_id,
titulaire_categorie,
titulaire_departement_code,
type,
objet,
code_cpv,
considerations_sociales,
considerations_environnementales,
techniques,
marche_innovant,
sous_traitance_declaree,
montant_min=None,
montant_max=None,
dashboard_year,
dashboard_acheteur_id,
dashboard_acheteur_categorie,
dashboard_acheteur_departement_code,
dashboard_titulaire_id,
dashboard_titulaire_categorie,
dashboard_titulaire_departement_code,
dashboard_marche_type,
dashboard_marche_objet,
dashboard_marche_code_cpv,
dashboard_marche_considerations_sociales,
dashboard_marche_considerations_environnementales,
dashboard_marche_techniques,
dashboard_marche_innovant,
dashboard_marche_sous_traitance_declaree,
dashboard_montant_min=None,
dashboard_montant_max=None,
) -> pl.LazyFrame:
if year:
lff = lff.filter(pl.col("dateNotification").dt.year() == int(year))
if dashboard_year:
lff = lff.filter(pl.col("dateNotification").dt.year() == int(dashboard_year))
else:
lff = lff.filter(
pl.col("dateNotification") > (datetime.now() - timedelta(days=365))
)
if acheteur_id:
lff = lff.filter(pl.col("acheteur_id").str.contains(acheteur_id))
if dashboard_acheteur_id:
lff = lff.filter(pl.col("acheteur_id").str.contains(dashboard_acheteur_id))
else:
if acheteur_categorie:
lff = lff.filter(pl.col("acheteur_categorie") == acheteur_categorie)
if acheteur_departement_code:
if dashboard_acheteur_categorie:
lff = lff.filter(pl.col("acheteur_categorie") == dashboard_acheteur_categorie)
if dashboard_acheteur_departement_code:
lff = lff.filter(
pl.col("acheteur_departement_code").is_in(acheteur_departement_code)
pl.col("acheteur_departement_code").is_in(dashboard_acheteur_departement_code)
)
if titulaire_id:
lff = lff.filter(pl.col("titulaire_id").str.contains(titulaire_id))
if dashboard_titulaire_id:
lff = lff.filter(pl.col("titulaire_id").str.contains(dashboard_titulaire_id))
else:
if titulaire_categorie:
lff = lff.filter(pl.col("titulaire_categorie") == titulaire_categorie)
if titulaire_departement_code:
if dashboard_titulaire_categorie:
lff = lff.filter(pl.col("titulaire_categorie") == dashboard_titulaire_categorie)
if dashboard_titulaire_departement_code:
lff = lff.filter(
pl.col("titulaire_departement_code").is_in(titulaire_departement_code)
pl.col("titulaire_departement_code").is_in(dashboard_titulaire_departement_code)
)
if type:
lff = lff.filter(pl.col("type") == type)
if dashboard_marche_type:
lff = lff.filter(pl.col("type") == dashboard_marche_type)
if objet:
lff = lff.filter(pl.col("objet").str.contains(f"(?i){objet}"))
if dashboard_marche_objet:
lff = lff.filter(pl.col("objet").str.contains(f"(?i){dashboard_marche_objet}"))
if code_cpv:
lff = lff.filter(pl.col("codeCPV").str.starts_with(code_cpv))
if dashboard_marche_code_cpv:
lff = lff.filter(pl.col("codeCPV").str.starts_with(dashboard_marche_code_cpv))
if marche_innovant and marche_innovant != "all":
lff = lff.filter(pl.col("marcheInnovant") == marche_innovant)
if dashboard_marche_innovant and dashboard_marche_innovant != "all":
lff = lff.filter(pl.col("marcheInnovant") == dashboard_marche_innovant)
if sous_traitance_declaree and sous_traitance_declaree != "all":
lff = lff.filter(pl.col("sousTraitanceDeclaree") == sous_traitance_declaree)
if dashboard_marche_sous_traitance_declaree and dashboard_marche_sous_traitance_declaree != "all":
lff = lff.filter(pl.col("sousTraitanceDeclaree") == dashboard_marche_sous_traitance_declaree)
if techniques:
if dashboard_marche_techniques:
lff = lff.filter(
pl.col("techniques")
.str.split(", ")
.list.set_intersection(techniques)
.list.set_intersection(dashboard_marche_techniques)
.list.len()
> 0
)
if considerations_sociales:
if dashboard_marche_considerations_sociales:
lff = lff.filter(
pl.col("considerationsSociales")
.str.split(", ")
.list.set_intersection(considerations_sociales)
.list.set_intersection(dashboard_marche_considerations_sociales)
.list.len()
> 0
)
if considerations_environnementales:
if dashboard_marche_considerations_environnementales:
lff = lff.filter(
pl.col("considerationsEnvironnementales")
.str.split(", ")
.list.set_intersection(considerations_environnementales)
.list.set_intersection(dashboard_marche_considerations_environnementales)
.list.len()
> 0
)
if montant_min is not None:
lff = lff.filter(pl.col("montant") >= montant_min)
if dashboard_montant_min is not None:
lff = lff.filter(pl.col("montant") >= dashboard_montant_min)
if montant_max is not None:
lff = lff.filter(pl.col("montant") <= montant_max)
if dashboard_montant_max is not None:
lff = lff.filter(pl.col("montant") <= dashboard_montant_max)
return lff
+17 -17
View File
@@ -234,23 +234,23 @@ def test_010_observatoire_montant_filter():
def apply(min_val=None, max_val=None):
return prepare_dashboard_data(
data.lazy(),
year="2025",
acheteur_id=None,
acheteur_categorie=None,
acheteur_departement_code=None,
titulaire_id=None,
titulaire_categorie=None,
titulaire_departement_code=None,
type=None,
objet=None,
code_cpv=None,
considerations_sociales=None,
considerations_environnementales=None,
techniques=None,
marche_innovant=None,
sous_traitance_declaree=None,
montant_min=min_val,
montant_max=max_val,
dashboard_year="2025",
dashboard_acheteur_id=None,
dashboard_acheteur_categorie=None,
dashboard_acheteur_departement_code=None,
dashboard_titulaire_id=None,
dashboard_titulaire_categorie=None,
dashboard_titulaire_departement_code=None,
dashboard_marche_type=None,
dashboard_marche_objet=None,
dashboard_marche_code_cpv=None,
dashboard_marche_considerations_sociales=None,
dashboard_marche_considerations_environnementales=None,
dashboard_marche_techniques=None,
dashboard_marche_innovant=None,
dashboard_marche_sous_traitance_declaree=None,
dashboard_montant_min=min_val,
dashboard_montant_max=max_val,
).collect()
assert apply().height == 3