From f3c8fd4481652faec576f4cab41bcc56f57675aa Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 7 Nov 2025 08:13:35 +0100 Subject: [PATCH 01/18] =?UTF-8?q?Configuration=20des=20colonnes=20affich?= =?UTF-8?q?=C3=A9es=20par=20d=C3=A9faut=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .template.env | 3 +++ src/pages/tableau.py | 2 ++ src/utils.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/.template.env b/.template.env index 0eec464..99c78a1 100644 --- a/.template.env +++ b/.template.env @@ -6,6 +6,9 @@ SOURCE_STATS_CSV_PATH="https://www.data.gouv.fr/api/1/datasets/r/8ded94de-3b80-4 # Chemin vers le schéma de données DATA_SCHEMA_PATH=https://www.data.gouv.fr/api/1/datasets/r/9a4144c0-ee44-4dec-bee5-bbef38191d9a +# Colonnes masquées par défaut +DISPLAYED_COLUMNS="uid, acheteur_id, acheteur_nom, montant, objet, titulaire_nom, titulaire_id, dateNotification, dureeMois, acheteur_departement_code, sourceDataset" + # Formulaire de contact SENDER_SERVER_DOMAIN="mail.example.com" # serveur SMTP LOGIN_PASSWORD="" # mot de passe du serveur diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 23de881..a8e31dd 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -11,6 +11,7 @@ from src.utils import ( filter_table_data, format_montant, format_number, + get_default_hidden_columns, meta_content, setup_table_columns, sort_table_data, @@ -76,6 +77,7 @@ datatable = html.Div( markdown_options={"html": True}, tooltip_duration=8000, tooltip_delay=350, + hidden_columns=get_default_hidden_columns(schema), ), ) diff --git a/src/utils.py b/src/utils.py index 85d9e43..eace5ff 100644 --- a/src/utils.py +++ b/src/utils.py @@ -6,6 +6,7 @@ from time import sleep import polars as pl import polars.selectors as cs from httpx import get +from polars import Schema from polars.exceptions import ComputeError operators = [ @@ -312,6 +313,20 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup return columns, tooltip +def get_default_hidden_columns(schema: Schema): + displayed_columns = os.getenv("DISPLAYED_COLUMNS") + hidden_columns = [] + if displayed_columns: + displayed_columns = displayed_columns.replace(" ", "").split(",") + for col in schema.names(): + if col in displayed_columns: + continue + else: + hidden_columns.append(col) + return hidden_columns + raise ValueError("DISPLAYED_COLUMNS n'est pas configuré") + + def get_data_schema() -> dict: # Récupération du schéma des données tabulaires path = os.getenv("DATA_SCHEMA_PATH") From 4c6f63f723d919bd9fe8e59d6c5990dc3f8879bc Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 7 Nov 2025 08:15:18 +0100 Subject: [PATCH 02/18] Changelog #54 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1d26aa4..8fcee21 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Ne pas oublier de mettre à jour les fichier .env. ## Notes de version +### 2.2.0 () + +- Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) + ##### 2.1.6 (15 octobre 2025) - Stabilisation de la vue marché From f6a38a29bdf998df3f0dce5feea0012bc343298c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:01:17 +0100 Subject: [PATCH 03/18] Top 10 titulaires sur les pages acheteur #55 --- README.md | 2 +- src/assets/style.css | 5 +++++ src/callbacks.py | 35 +++++++++++++++++++++++++++++++++++ src/pages/acheteur.py | 31 +++++++++++++++++++++++++------ src/utils.py | 12 +++++++----- 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 src/callbacks.py diff --git a/README.md b/README.md index 8fcee21..31c2153 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Ne pas oublier de mettre à jour les fichier .env. ## Notes de version -### 2.2.0 () +#### 2.2.0 () - Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) diff --git a/src/assets/style.css b/src/assets/style.css index bf70592..9fe5350 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -211,6 +211,11 @@ summary > h3 { grid-row: 2; } +.org_top { + grid-column: 1/3; + grid-row: 3; +} + /* Vue marché */ .marche_infos p { diff --git a/src/callbacks.py b/src/callbacks.py new file mode 100644 index 0000000..c49d77d --- /dev/null +++ b/src/callbacks.py @@ -0,0 +1,35 @@ +import polars as pl +from dash import dash_table, html + +from utils import add_links_in_dict, format_montant, setup_table_columns + + +def get_top_org_table(data, org_type: str): + dff = pl.DataFrame(data) + if dff.height == 0: + return html.Div() + + dff = dff.select(["uid", f"{org_type}_id", f"{org_type}_nom", "montant"]) + dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom").agg( + pl.len().alias("Attributions"), pl.sum("montant").alias("montant") + ) + dff_nb = dff_nb.sort(by="Attributions", descending=True) + dff_nb = dff_nb.cast(pl.String) + dff_nb = dff_nb.fill_null("") + dff_nb = format_montant(dff_nb, column="montant") + columns, tooltip = setup_table_columns( + dff_nb, hideable=False, exclude=[f"{org_type}_id"] + ) + data = dff_nb.to_dicts() + data = add_links_in_dict(data, f"{org_type}") + + print(dff_nb) + + return dash_table.DataTable( + data=data, + markdown_options={"html": True}, + page_action="native", + page_size=10, + columns=columns, + tooltip_header=tooltip, + ) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 80a258f..cb8d224 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -3,6 +3,7 @@ import datetime import polars as pl from dash import Input, Output, State, callback, dash_table, dcc, html, register_page +from src.callbacks import get_top_org_table from src.figures import point_on_map from src.utils import ( add_links_in_dict, @@ -89,6 +90,13 @@ layout = [ ], ), html.Div(className="org_map", id="acheteur_map"), + html.Div( + className="org_top", + children=[ + html.H3("Top titulaires"), + html.Div(className="marches_table", id="top10_titulaires"), + ], + ), ], ), # récupérer les données de l'acheteur sur l'api annuaire @@ -146,22 +154,22 @@ def update_acheteur_infos(url): Input(component_id="acheteur_data", component_property="data"), ) def update_acheteur_stats(data): - df = pl.DataFrame(data) - if df.height == 0: - df = pl.DataFrame(schema=df.collect_schema()) - df_marches = df.unique("id") + dff = pl.DataFrame(data) + if dff.height == 0: + dff = pl.DataFrame(schema=df.collect_schema()) + df_marches = dff.unique("id") nb_marches = format_number(df_marches.height) # somme_marches = format_number(int(df_marches.select(pl.sum("montant")).item())) marches_attribues = [html.Strong(nb_marches), " marchés et accord-cadres attribués"] # + ", pour un total de ", html.Strong(somme_marches + " €")] del df_marches - nb_titulaires = df.unique("titulaire_id").height + nb_titulaires = dff.unique("titulaire_id").height nb_titulaires = [ html.Strong(format_number(nb_titulaires)), " titulaires (SIRET) différents", ] - del df + del dff return marches_attribues, nb_titulaires @@ -203,8 +211,11 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> list[dict]: ) def get_last_marches_table(data) -> html.Div: dff = pl.DataFrame(data) + if dff.height == 0: + return html.Div(html.P("Aucun marché trouvé.")) dff = dff.cast(pl.String) dff = dff.fill_null("") + print("1", dff.columns) dff = format_montant(dff) columns, tooltip = setup_table_columns( dff, @@ -252,6 +263,14 @@ def get_last_marches_table(data) -> html.Div: return table +@callback( + Output(component_id="top10_titulaires", component_property="children"), + Input(component_id="acheteur_data", component_property="data"), +) +def get_top_titulaires(data): + return get_top_org_table(data, "titulaire") + + @callback( Output("download-acheteur-data", "data"), Input("btn-download-acheteur-data", "n_clicks"), diff --git a/src/utils.py b/src/utils.py index eace5ff..240a090 100644 --- a/src/utils.py +++ b/src/utils.py @@ -77,15 +77,16 @@ def add_links(dff: pl.DataFrame): return dff -def add_links_in_dict(data: list, org_type: str) -> list: +def add_links_in_dict(data: list[dict], org_type: str) -> list: new_data = [] for marche in data: org_id = marche[org_type + "_id"] marche[org_type + "_nom"] = ( f'{marche[org_type + "_nom"]}' ) - marche["id"] = f'{marche["id"]}' - marche["uid"] = f'{marche["uid"]}' + if marche.get("uid"): + marche["id"] = f'{marche["id"]}' + marche["uid"] = f'{marche["uid"]}' new_data.append(marche) return new_data @@ -124,7 +125,7 @@ def format_number(number) -> str: return number -def format_montant(dff: pl.DataFrame) -> pl.DataFrame: +def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: def format_function(expr, scale=None): # https://stackoverflow.com/a/78636786 expr = expr.cast(pl.String) @@ -155,7 +156,8 @@ def format_montant(dff: pl.DataFrame) -> pl.DataFrame: return montant - dff = dff.with_columns(pl.col("montant").pipe(format_function).alias("montant")) + print("3", dff.columns) + dff = dff.with_columns(pl.col(column).pipe(format_function).alias(column)) return dff From e9faf806096ad90c6ac0608f736dfdb2ebfd3235 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:30:50 +0100 Subject: [PATCH 04/18] Top 10 acheteurs sur les pages titulaire #55 --- src/assets/style.css | 5 +++-- src/callbacks.py | 31 ++++++++++++++++++++++++++++++- src/pages/titulaire.py | 18 +++++++++++++++++- src/utils.py | 2 +- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index 9fe5350..b007d9a 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -53,9 +53,10 @@ div.logo > a { /* Réduire la taille du texte de la colonne Objet */ -td[data-dash-column="objet"] { +/* +td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-column="acheteur_nom"], { font-size: 85%; -} +}*/ /* Couleur des en-têtes */ .dash-table-container diff --git a/src/callbacks.py b/src/callbacks.py index c49d77d..35534e8 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -13,7 +13,7 @@ def get_top_org_table(data, org_type: str): dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom").agg( pl.len().alias("Attributions"), pl.sum("montant").alias("montant") ) - dff_nb = dff_nb.sort(by="Attributions", descending=True) + dff_nb = dff_nb.sort(by="montant", descending=True) dff_nb = dff_nb.cast(pl.String) dff_nb = dff_nb.fill_null("") dff_nb = format_montant(dff_nb, column="montant") @@ -32,4 +32,33 @@ def get_top_org_table(data, org_type: str): page_size=10, columns=columns, tooltip_header=tooltip, + style_cell_conditional=[ + { + "if": {"column_id": "objet"}, + "minWidth": "350px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "14px", + "whiteSpace": "normal", + "fontSize": "85%", + }, + { + "if": {"column_id": "acheteur_nom"}, + "minWidth": "200px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "16px", + # "fontSize": "85%", + "whiteSpace": "normal", + }, + { + "if": {"column_id": "titulaire_nom"}, + "minWidth": "200px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "16px", + "whiteSpace": "normal", + # "fontSize": "85%", + }, + ], ) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 07f07eb..0adeed9 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -3,6 +3,7 @@ import datetime import polars as pl from dash import Input, Output, State, callback, dash_table, dcc, html, register_page +from src.callbacks import get_top_org_table from src.figures import point_on_map from src.utils import ( add_links_in_dict, @@ -91,6 +92,13 @@ layout = [ ], ), html.Div(className="org_map", id="titulaire_map"), + html.Div( + className="org_top", + children=[ + html.H3("Top acheteurs"), + html.Div(className="marches_table", id="top10_acheteurs"), + ], + ), ], ), # récupérer les données de l'acheteur sur l'api annuaire @@ -161,7 +169,7 @@ def update_titulaire_stats(data): nb_acheteurs = dff.unique("acheteur_id").height nb_acheteurs = [ html.Strong(format_number(nb_acheteurs)), - " titulaires (SIRET) différents", + " acheteurs (SIRET) différents", ] del dff @@ -266,6 +274,14 @@ def get_last_marches_table(data) -> html.Div: return table +@callback( + Output(component_id="top10_acheteurs", component_property="children"), + Input(component_id="titulaire_data", component_property="data"), +) +def get_top_acheteurs(data): + return get_top_org_table(data, "acheteur") + + @callback( Output("download-titulaire-data", "data"), Input("btn-download-titulaire-data", "n_clicks"), diff --git a/src/utils.py b/src/utils.py index 240a090..783a236 100644 --- a/src/utils.py +++ b/src/utils.py @@ -144,7 +144,7 @@ def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: frac: pl.Expr = ( pl.when(frac.is_not_null() & ~frac.is_in(["0"])) - .then("," + frac) + .then("," + frac.str.head(2)) .otherwise(pl.lit("")) ) From bbb04a4487abf8d9138723b2be916a784efe06c7 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:33:37 +0100 Subject: [PATCH 05/18] Changelog #55 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 31c2153..14b32ba 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Ne pas oublier de mettre à jour les fichier .env. #### 2.2.0 () +- Top acheteurs / titulaires par montant attribué/remporté (([#55](https://github.com/ColinMaudry/decp.info/issues/55))) - Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) ##### 2.1.6 (15 octobre 2025) From a3eb48bacb0d2846cd80fae511cd29d97a6b0a25 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 15:29:44 +0100 Subject: [PATCH 06/18] Format values inclut le formatage des distances --- src/callbacks.py | 4 ++-- src/pages/acheteur.py | 5 ++--- src/pages/marche.py | 4 ++-- src/pages/tableau.py | 4 ++-- src/pages/titulaire.py | 4 ++-- src/utils.py | 17 +++++++++++++---- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index 35534e8..689a4a6 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1,7 +1,7 @@ import polars as pl from dash import dash_table, html -from utils import add_links_in_dict, format_montant, setup_table_columns +from utils import add_links_in_dict, format_values, setup_table_columns def get_top_org_table(data, org_type: str): @@ -16,7 +16,7 @@ def get_top_org_table(data, org_type: str): dff_nb = dff_nb.sort(by="montant", descending=True) dff_nb = dff_nb.cast(pl.String) dff_nb = dff_nb.fill_null("") - dff_nb = format_montant(dff_nb, column="montant") + dff_nb = format_values(dff_nb) columns, tooltip = setup_table_columns( dff_nb, hideable=False, exclude=[f"{org_type}_id"] ) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index cb8d224..2219d40 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -8,8 +8,8 @@ from src.figures import point_on_map from src.utils import ( add_links_in_dict, df, - format_montant, format_number, + format_values, get_annuaire_data, get_departement_region, meta_content, @@ -215,8 +215,7 @@ def get_last_marches_table(data) -> html.Div: return html.Div(html.P("Aucun marché trouvé.")) dff = dff.cast(pl.String) dff = dff.fill_null("") - print("1", dff.columns) - dff = format_montant(dff) + dff = format_values(dff) columns, tooltip = setup_table_columns( dff, hideable=False, diff --git a/src/pages/marche.py b/src/pages/marche.py index bd90a50..807bb7a 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -4,7 +4,7 @@ import polars as pl from dash import Input, Output, callback, dcc, html, register_page from polars import selectors as cs -from src.utils import data_schema, df, format_montant, meta_content +from src.utils import data_schema, df, format_values, meta_content register_page( __name__, @@ -75,7 +75,7 @@ def get_marche_data(url) -> tuple[dict, list]: # Données du marché dff_marche = lff.unique("uid").collect(engine="streaming") - dff_marche = format_montant(dff_marche) + dff_marche = format_values(dff_marche) return dff_marche.to_dicts()[0], dff_titulaires.to_dicts() diff --git a/src/pages/tableau.py b/src/pages/tableau.py index a8e31dd..4b3d380 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -9,8 +9,8 @@ from src.utils import ( add_resource_link, df, filter_table_data, - format_montant, format_number, + format_values, get_default_hidden_columns, meta_content, setup_table_columns, @@ -208,7 +208,7 @@ def update_table(page_current, page_size, filter_query, sort_by, data_timestamp) dff = add_resource_link(dff) # Formatage des montants - dff = format_montant(dff) + dff = format_values(dff) columns, tooltip = setup_table_columns(dff) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 0adeed9..90b2b75 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -8,8 +8,8 @@ from src.figures import point_on_map from src.utils import ( add_links_in_dict, df, - format_montant, format_number, + format_values, get_annuaire_data, get_departement_region, meta_content, @@ -227,7 +227,7 @@ def get_last_marches_table(data) -> html.Div: dff = pl.DataFrame(data) dff = dff.cast(pl.String) dff = dff.fill_null("") - dff = format_montant(dff) + dff = format_values(dff) columns, tooltip = setup_table_columns( dff, hideable=False, exclude=["acheteur_id", "id"] ) diff --git a/src/utils.py b/src/utils.py index 783a236..23ad4fe 100644 --- a/src/utils.py +++ b/src/utils.py @@ -125,8 +125,8 @@ def format_number(number) -> str: return number -def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: - def format_function(expr, scale=None): +def format_values(dff: pl.DataFrame) -> pl.DataFrame: + def format_montant(expr, scale=None): # https://stackoverflow.com/a/78636786 expr = expr.cast(pl.String) expr = expr.str.splitn(".", 2) @@ -156,8 +156,17 @@ def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: return montant - print("3", dff.columns) - dff = dff.with_columns(pl.col(column).pipe(format_function).alias(column)) + def format_distance(expr): + expr = expr.cast(pl.String) + return pl.concat_str(expr, pl.lit(" km")) + + if "montant" in dff.columns: + dff = dff.with_columns(pl.col("montant").pipe(format_montant).alias("montant")) + if "distance" in dff.columns: + dff = dff.with_columns( + pl.col("distance").pipe(format_distance).alias("distance") + ) + return dff From 6835f328b68e7376dc4608e30717a2555a366fd1 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 15:30:47 +0100 Subject: [PATCH 07/18] Ajout de la distance dans les tableaux top #55 --- src/callbacks.py | 9 +++++---- src/pages/acheteur.py | 1 + src/pages/titulaire.py | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index 689a4a6..6bf197e 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -9,8 +9,10 @@ def get_top_org_table(data, org_type: str): if dff.height == 0: return html.Div() - dff = dff.select(["uid", f"{org_type}_id", f"{org_type}_nom", "montant"]) - dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom").agg( + dff = dff.select( + ["uid", f"{org_type}_id", f"{org_type}_nom", "distance", "montant"] + ) + dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom", "distance").agg( pl.len().alias("Attributions"), pl.sum("montant").alias("montant") ) dff_nb = dff_nb.sort(by="montant", descending=True) @@ -23,14 +25,13 @@ def get_top_org_table(data, org_type: str): data = dff_nb.to_dicts() data = add_links_in_dict(data, f"{org_type}") - print(dff_nb) - return dash_table.DataTable( data=data, markdown_options={"html": True}, page_action="native", page_size=10, columns=columns, + cell_selectable=False, tooltip_header=tooltip, style_cell_conditional=[ { diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 2219d40..faaaacc 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -191,6 +191,7 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> list[dict]: "titulaire_id", "titulaire_typeIdentifiant", "titulaire_nom", + "distance", "montant", "codeCPV", "dureeMois", diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 90b2b75..e826562 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -195,6 +195,7 @@ def get_titulaire_marches_data(url, titulaire_year: str) -> list[dict]: "dateNotification", "acheteur_id", "acheteur_nom", + "distance", "montant", "codeCPV", "dureeMois", From 1e85dbe562b00f61aaddb1559a23a3ae443cf4c4 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 11 Nov 2025 07:47:00 +0100 Subject: [PATCH 08/18] =?UTF-8?q?Champ=20de=20recherche,=20r=C3=A9cup=20de?= =?UTF-8?q?=20donn=C3=A9es=20d=C3=A9di=C3=A9e=20#58?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/style.css | 19 +++++++++++++++++++ src/pages/recherche.py | 41 +++++++++++++++++++++++++++++++++++++++++ src/pages/tableau.py | 2 +- src/utils.py | 13 +++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/pages/recherche.py diff --git a/src/assets/style.css b/src/assets/style.css index b007d9a..e754bcf 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -143,6 +143,25 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c margin: 0 0 20px 20px; } +/* Page de recherche */ + +#search { + margin: auto; + width: 450px; + font-size: 18px; + height: 30px; + display: block; +} + +.search_options { + margin: 16px auto; + width: 450px; +} + +.search_options input { + margin-right: 12px; +} + /* Menu de navigation */ .navbar { diff --git a/src/pages/recherche.py b/src/pages/recherche.py new file mode 100644 index 0000000..834dcd5 --- /dev/null +++ b/src/pages/recherche.py @@ -0,0 +1,41 @@ +from dash import Input, Output, callback, dcc, html, register_page + +from src.utils import meta_content + +name = "Recherche" + +register_page( + __name__, + path="/", + title=meta_content["title"], + name=name, + description=meta_content["description"], + image_url=meta_content["image_url"], + order=0, +) + +layout = html.Div( + className="container", + children=[ + dcc.Input( + id="search", + type="text", + placeholder="Nom d'acheteur, d'entreprise, mot de clé de marché...", + ), + html.Div( + className="search_options", + children=[dcc.RadioItems(options=["Acheteur(s)"])], + ), + html.Div(id="search_results"), + ], +) + + +@callback(Output("search_results", "children"), Input("search", "value")) +def search_results(query): + if len(query) >= 3: + # results = [] + # dff = dff.select("acheteur_id", "acheteur_nom") + return html.Div([]) + else: + return html.P("Tapez au moins 3 caractères") diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 4b3d380..83bfffe 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -25,7 +25,7 @@ schema = df.collect_schema() name = "Tableau" register_page( __name__, - path="/", + path="/tableau", title=meta_content["title"], name=name, description=meta_content["description"], diff --git a/src/utils.py b/src/utils.py index 23ad4fe..fd8816a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -208,6 +208,17 @@ def get_decp_data() -> pl.DataFrame: return lff.collect() +def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame: + lff = dff.lazy() + lff = lff.select( + cs.starts_with(org_type).exclude( + f"{org_type}_latitude", f"{org_type}_longitude" + ) + ) + lff = lff.unique(f"{org_type}_id") + return lff.collect() + + def get_departements() -> dict: with open("data/departements.json", "rb") as f: data = json.load(f) @@ -365,6 +376,8 @@ def get_data_schema() -> dict: df: pl.DataFrame = get_decp_data() +df_acheteurs = get_org_data(df, "acheteur") +df_titulaires = get_org_data(df, "titulaire") departements = get_departements() domain_name = ( "test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info" From e187cf33aaebf088dd778808ee003a379d0dea60 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 11 Nov 2025 12:42:01 +0100 Subject: [PATCH 09/18] Recherche fonctinonelle acheteurs et titulaires #58 --- pyproject.toml | 3 +- src/pages/recherche.py | 57 +++++++++++++--- src/utils.py | 145 +++++++++++++++++++++++++++++++++-------- 3 files changed, 169 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0030ed6..6d49cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ dependencies = [ "xlsxwriter", "plotly[express]", "httpx", - "pandas" # utilisé pour la création de certains graphiques + "pandas", # utilisé pour la création de certains graphiques + "unidecode" ] [project.optional-dependencies] diff --git a/src/pages/recherche.py b/src/pages/recherche.py index 834dcd5..c6a7224 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -1,6 +1,12 @@ -from dash import Input, Output, callback, dcc, html, register_page +from dash import Input, Output, callback, dash_table, dcc, html, register_page -from src.utils import meta_content +from src.utils import ( + df_acheteurs, + df_titulaires, + meta_content, + search_org, + setup_table_columns, +) name = "Recherche" @@ -31,11 +37,44 @@ layout = html.Div( ) -@callback(Output("search_results", "children"), Input("search", "value")) -def search_results(query): - if len(query) >= 3: - # results = [] - # dff = dff.select("acheteur_id", "acheteur_nom") - return html.Div([]) +@callback( + Output("search_results", "children"), + Input("search", "value"), + prevent_initial_call=True, +) +def update_search_results(query): + if len(query) >= 1: + content = [] + + for org_type in ["acheteur", "titulaire"]: + if org_type == "acheteur": + dff = df_acheteurs + elif org_type == "titulaire": + dff = df_titulaires + else: + raise ValueError(f"{org_type} is not supported") + + # Search acheteurs and titulaires using the same function + results = search_org(dff, query, org_type=org_type) + count = results.height + + # Format output + columns, tooltip = setup_table_columns(results, hideable=False) + + org_content = [ + html.H3(f"{org_type.title()}s : {count}"), + dash_table.DataTable( + columns=columns, + data=results.to_dicts(), + page_size=5, + style_table={"overflowX": "auto"}, + markdown_options={"html": True}, + ) + if count > 0 + else html.P(f"Aucun {org_type} trouvé."), + ] + content.extend(org_content) + + return content else: - return html.P("Tapez au moins 3 caractères") + return html.P("Tapez au moins 1 caractère") diff --git a/src/utils.py b/src/utils.py index fd8816a..d2c7bc4 100644 --- a/src/utils.py +++ b/src/utils.py @@ -8,6 +8,7 @@ import polars.selectors as cs from httpx import get from polars import Schema from polars.exceptions import ComputeError +from unidecode import unidecode operators = [ ["s<", "<"], @@ -50,30 +51,47 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame: return dff -def add_links(dff: pl.DataFrame): - dff = dff.with_columns( - pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET") - .then( - '' - + pl.col("titulaire_id") - + "" - ) - .otherwise(pl.col("titulaire_id")) - .alias("titulaire_id") - ) - - for column, path in [("acheteur_id", "acheteurs"), ("uid", "marches")]: - dff = dff.with_columns( - ( - f'' - + pl.col(column) - + "" - ).alias(column) - ) +def add_links(dff: pl.DataFrame, target: str = "_blank"): + for col in ["uid", "acheteur_nom", "titulaire_nom", "acheteur_id", "titulaire_id"]: + if col in dff.columns: + if col.startswith("titulaire_"): + dff = dff.with_columns( + pl.when( + pl.Expr.or_( + pl.col("titulaire_typeIdentifiant").is_null(), + pl.col("titulaire_typeIdentifiant") == "SIRET", + ) + ) + .then( + '' + + pl.col(col) + + "" + ) + .otherwise(pl.col(col)) + .alias(col) + ) + if col.startswith("acheteur_"): + dff = dff.with_columns( + ( + '' + + pl.col(col) + + "" + ).alias(col) + ) + if col == "uid": + dff = dff.with_columns( + ( + '' + + pl.col("uid") + + "" + ).alias("uid") + ) return dff @@ -211,11 +229,12 @@ def get_decp_data() -> pl.DataFrame: def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame: lff = dff.lazy() lff = lff.select( + "uid", cs.starts_with(org_type).exclude( f"{org_type}_latitude", f"{org_type}_longitude" - ) + ), ) - lff = lff.unique(f"{org_type}_id") + lff = lff.group_by(cs.starts_with(org_type)).len("len_uid") return lff.collect() @@ -375,6 +394,80 @@ def get_data_schema() -> dict: return new_schema +def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: + """ + Search in either 'acheteur' or 'titulaire' DataFrame. + + :param dff: Polars DataFrame with acheteur or titulaire columns + :param query: User search string + :param org_type: 'acheteur' or 'titulaire' + :return: Filtered DataFrame with 'matches' column + """ + if not query.strip(): + return dff.select(pl.lit(False).alias("matches")) + + # Normalize query + normalized_query = unidecode(query.strip()).upper() + tokens = [" " + t.strip() for t in normalized_query.split() if t.strip()] + print(tokens) + + # Define columns based on entity type + cols = [ + f"{org_type}_id", + f"{org_type}_nom", + f"{org_type}_departement_nom", + f"{org_type}_departement_code", + f"{org_type}_commune_nom", + ] + + # Concatenate all fields into one string per row + org_str = pl.concat_str(pl.lit(" "), pl.col(cols), separator=" ") + + # For each token, create a boolean column: True if token is found + token_matches = [] + for token in tokens: + token_match = org_str.str.contains(token).alias(f"token_{token}") + token_matches.append(token_match) + + # Count how many tokens match per row + match_score = pl.sum_horizontal(token_matches).alias("match_score") + + # For each token, create a boolean column: True if token is found + token_matches = [] + for token in tokens: + token_match = org_str.str.contains(token).alias(f"token_{token}") + token_matches.append(token_match) + + # Sélection des colonnes + if org_type == "acheteur": + dff = dff.select(cols + ["len_uid"]) + if org_type == "titulaire": + dff = dff.select(cols + ["len_uid", "titulaire_typeIdentifiant"]) + + # Apply and filter + dff = ( + dff.with_columns(token_matches + [match_score]) + .filter(pl.col("match_score") == len(tokens)) + .sort("len_uid", descending=True) + .drop([f"token_{token}" for token in tokens]) + ) + + # Format result + dff = add_links(dff, target="") + dff = dff.with_columns( + pl.concat_str( + pl.col(f"{org_type}_departement_nom"), + pl.lit(" ("), + pl.col(f"{org_type}_departement_code"), + pl.lit(")"), + ).alias("Département") + ) + + dff = dff.select(f"{org_type}_id", f"{org_type}_nom", "Département") + + return dff + + df: pl.DataFrame = get_decp_data() df_acheteurs = get_org_data(df, "acheteur") df_titulaires = get_org_data(df, "titulaire") From 8637915eb2ee3c5adb7c254165bb6f9391b0a676 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 11 Nov 2025 12:50:58 +0100 Subject: [PATCH 10/18] Cleanup HTML --- src/pages/recherche.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/recherche.py b/src/pages/recherche.py index c6a7224..6789440 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -26,12 +26,12 @@ layout = html.Div( dcc.Input( id="search", type="text", - placeholder="Nom d'acheteur, d'entreprise, mot de clé de marché...", - ), - html.Div( - className="search_options", - children=[dcc.RadioItems(options=["Acheteur(s)"])], + placeholder="Nom d'acheteur, d'entreprise, SIREN...", ), + # html.Div( + # className="search_options", + # children=[dcc.RadioItems(options=["Acheteur(s)"])], + # ), html.Div(id="search_results"), ], ) @@ -77,4 +77,4 @@ def update_search_results(query): return content else: - return html.P("Tapez au moins 1 caractère") + return html.P("") From 459ca6a846e71221170954f0ac3a5ac5b1fa2cee Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 12 Nov 2025 14:54:12 +0100 Subject: [PATCH 11/18] =?UTF-8?q?R=C3=A9arrangements=20r=C3=A9sultats=20#5?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/style.css | 17 ++++++++++++----- src/pages/recherche.py | 40 ++++++++++++++++++++++++++++++++-------- src/utils.py | 13 +++++++------ 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index e754bcf..7bde8b5 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -146,7 +146,7 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c /* Page de recherche */ #search { - margin: auto; + margin: 50px auto 0px auto; width: 450px; font-size: 18px; height: 30px; @@ -162,6 +162,16 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c margin-right: 12px; } +.results_acheteur { + grid-column: 1; + grid-row: 1; +} + +.results_titulaire { + grid-column: 2; + grid-row: 1; +} + /* Menu de navigation */ .navbar { @@ -191,7 +201,7 @@ summary > h3 { padding-top: 28px; } -/* Vue acheteur/titulaire */ +/* Vue acheteur/titulaire/recherche */ .wrapper { display: grid; grid-gap: 10px; @@ -199,9 +209,6 @@ summary > h3 { justify-content: space-between; } -.wrapper > div { -} - .org_title { grid-column: 1 / 3; grid-row: 1; diff --git a/src/pages/recherche.py b/src/pages/recherche.py index 6789440..e74a720 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -32,7 +32,7 @@ layout = html.Div( # className="search_options", # children=[dcc.RadioItems(options=["Acheteur(s)"])], # ), - html.Div(id="search_results"), + html.Div(id="search_results", className="wrapper"), ], ) @@ -62,13 +62,37 @@ def update_search_results(query): columns, tooltip = setup_table_columns(results, hideable=False) org_content = [ - html.H3(f"{org_type.title()}s : {count}"), - dash_table.DataTable( - columns=columns, - data=results.to_dicts(), - page_size=5, - style_table={"overflowX": "auto"}, - markdown_options={"html": True}, + html.Div( + className=f"results_{org_type}", + children=[ + html.H3(f"{org_type.title()}s : {count}"), + dash_table.DataTable( + columns=columns, + data=results.to_dicts(), + page_size=10, + # style_table={"overflowX": "auto"}, + markdown_options={"html": True}, + cell_selectable=False, + style_cell_conditional=[ + { + "if": {"column_id": "acheteur_nom"}, + "maxWidth": "250px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "18px", + "whiteSpace": "normal", + }, + { + "if": {"column_id": "titulaire_nom"}, + "maxWidth": "250px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "18px", + "whiteSpace": "normal", + }, + ], + ), + ], ) if count > 0 else html.P(f"Aucun {org_type} trouvé."), diff --git a/src/utils.py b/src/utils.py index d2c7bc4..9552df2 100644 --- a/src/utils.py +++ b/src/utils.py @@ -234,7 +234,7 @@ def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame: f"{org_type}_latitude", f"{org_type}_longitude" ), ) - lff = lff.group_by(cs.starts_with(org_type)).len("len_uid") + lff = lff.group_by(cs.starts_with(org_type)).len("Marchés") return lff.collect() @@ -406,10 +406,11 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: if not query.strip(): return dff.select(pl.lit(False).alias("matches")) + sleep(0.2) + # Normalize query normalized_query = unidecode(query.strip()).upper() tokens = [" " + t.strip() for t in normalized_query.split() if t.strip()] - print(tokens) # Define columns based on entity type cols = [ @@ -440,15 +441,15 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: # Sélection des colonnes if org_type == "acheteur": - dff = dff.select(cols + ["len_uid"]) + dff = dff.select(cols + ["Marchés"]) if org_type == "titulaire": - dff = dff.select(cols + ["len_uid", "titulaire_typeIdentifiant"]) + dff = dff.select(cols + ["Marchés", "titulaire_typeIdentifiant"]) # Apply and filter dff = ( dff.with_columns(token_matches + [match_score]) .filter(pl.col("match_score") == len(tokens)) - .sort("len_uid", descending=True) + .sort("Marchés", descending=True) .drop([f"token_{token}" for token in tokens]) ) @@ -463,7 +464,7 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: ).alias("Département") ) - dff = dff.select(f"{org_type}_id", f"{org_type}_nom", "Département") + dff = dff.select(f"{org_type}_id", f"{org_type}_nom", "Département", "Marchés") return dff From bb6c0dec26352117016a01b7d63f4d1425b077ad Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 12 Nov 2025 14:55:38 +0100 Subject: [PATCH 12/18] Changelog #58 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14b32ba..6f1c86d 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,9 @@ Ne pas oublier de mettre à jour les fichier .env. #### 2.2.0 () +- Moteur de recherche (acheteurs et titulaires) en page d'accueil ([#58](https://github.com/ColinMaudry/decp.info/issues/58)) - Top acheteurs / titulaires par montant attribué/remporté (([#55](https://github.com/ColinMaudry/decp.info/issues/55))) -- Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) +- Moins de colonnes affichées par défaut dans Tableau ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) ##### 2.1.6 (15 octobre 2025) From 63622d83f53ce595d865e65cf47823311abb23f1 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 12 Nov 2025 16:08:38 +0100 Subject: [PATCH 13/18] Suivi des recherches #58 --- .template.env | 5 +++++ src/pages/recherche.py | 1 + src/utils.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.template.env b/.template.env index 99c78a1..86ff67f 100644 --- a/.template.env +++ b/.template.env @@ -15,3 +15,8 @@ LOGIN_PASSWORD="" # mot de passe du serveur LOGIN_EMAIL="connect@example.fr" # adresse utilisée pour se connecter au serveur SMTP FROM_EMAIL="from@example.com" # adresse d'envoi des emails (From) TO_EMAIL="to@example.com" # adresse de destination des emails (To) + +# Matomo +MATOMO_ID_SITE= +MATOMO_BASE_URL= +MATOMO_TOKEN= diff --git a/src/pages/recherche.py b/src/pages/recherche.py index e74a720..b8b4796 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -27,6 +27,7 @@ layout = html.Div( id="search", type="text", placeholder="Nom d'acheteur, d'entreprise, SIREN...", + autoFocus=True, ), # html.Div( # className="search_options", diff --git a/src/utils.py b/src/utils.py index 9552df2..6621c64 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,11 +1,12 @@ import json import logging import os -from time import sleep +import uuid +from time import localtime, sleep import polars as pl import polars.selectors as cs -from httpx import get +from httpx import get, post from polars import Schema from polars.exceptions import ComputeError from unidecode import unidecode @@ -394,6 +395,34 @@ def get_data_schema() -> dict: return new_schema +def track_search(query): + if ( + len(query) >= 4 + and os.getenv("DEVELOPMENT").lower != "true" + and os.getenv("MATOMO_DOMAIN") + ): + if os.getenv("DEVELOPMENT").lower() == "true": + url = "https://test.decp.info" + else: + url = "https://decp.info" + params = { + "idsite": os.getenv("MATOMO_ID_SITE"), + "url": url, + "rec": "1", + "action_name": "front_page_search", + "rand": uuid.uuid4().hex, + "apiv": "1", + "h": localtime().tm_hour, + "m": localtime().tm_min, + "s": localtime().tm_sec, + "search": query, + "token_auth": os.getenv("MATOMO_TOKEN"), + } + post( + url=f"https://{os.getenv('MATOMO_DOMAIN')}/matomo.php", params=params + ).raise_for_status() + + def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: """ Search in either 'acheteur' or 'titulaire' DataFrame. @@ -407,6 +436,7 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: return dff.select(pl.lit(False).alias("matches")) sleep(0.2) + track_search(query) # Normalize query normalized_query = unidecode(query.strip()).upper() From c44ef311c3d77690e28dc37b8de2af21795b2421 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 12 Nov 2025 22:09:15 +0100 Subject: [PATCH 14/18] Suppression du temps de latence entre les recherches #58 --- src/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index 6621c64..c829f24 100644 --- a/src/utils.py +++ b/src/utils.py @@ -435,7 +435,7 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: if not query.strip(): return dff.select(pl.lit(False).alias("matches")) - sleep(0.2) + # Enregistrement des recherche dans Matomo track_search(query) # Normalize query From c19076ee85fcdcfe79daf2835c3bcce7348e2196 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 12 Nov 2025 22:18:46 +0100 Subject: [PATCH 15/18] Hauteur de lignes tableau --- pyproject.toml | 2 +- src/pages/tableau.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c81d016..bfc01c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "decp.info" description = "Interface d'exploration et d'analyse des marchés publics français." -version = "2.1.7" +version = "2.2.0" requires-python = ">= 3.10" authors = [ { name = "Colin Maudry", email = "colin+decp@maudry.com" } diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 83bfffe..972a240 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -53,7 +53,7 @@ datatable = html.Div( "minWidth": "350px", "textAlign": "left", "overflow": "hidden", - "lineHeight": "14px", + "lineHeight": "18px", "whiteSpace": "normal", }, { @@ -61,7 +61,7 @@ datatable = html.Div( "minWidth": "250px", "textAlign": "left", "overflow": "hidden", - "lineHeight": "14px", + "lineHeight": "18px", "whiteSpace": "normal", }, { @@ -69,7 +69,7 @@ datatable = html.Div( "minWidth": "250px", "textAlign": "left", "overflow": "hidden", - "lineHeight": "14px", + "lineHeight": "18px", "whiteSpace": "normal", }, ], From 783fb88d0ca37520a9b579bd56b7441bcbb93137 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 13 Nov 2025 14:13:36 +0100 Subject: [PATCH 16/18] Line height --- src/pages/acheteur.py | 2 +- src/pages/titulaire.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index faaaacc..87ddd05 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -246,7 +246,7 @@ def get_last_marches_table(data) -> html.Div: "minWidth": "300px", "textAlign": "left", "overflow": "hidden", - "lineHeight": "14px", + "lineHeight": "18px", "whiteSpace": "normal", }, { diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index e826562..3ebeaef 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -258,7 +258,7 @@ def get_last_marches_table(data) -> html.Div: "minWidth": "300px", "textAlign": "left", "overflow": "hidden", - "lineHeight": "14px", + "lineHeight": "18px", "whiteSpace": "normal", }, { From ef39be3346b82991900c970fb25447ddaa38730b Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 13 Nov 2025 14:23:08 +0100 Subject: [PATCH 17/18] =?UTF-8?q?R=C3=A9duction=20du=20logging=20httpx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/utils.py b/src/utils.py index c829f24..eaaf9a5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -11,15 +11,9 @@ from polars import Schema from polars.exceptions import ComputeError from unidecode import unidecode -operators = [ - ["s<", "<"], - ["s>", ">"], - ["i<", "<"], - ["i>", ">"], - ["icontains", "contains"], -] - logger = logging.getLogger("decp.info") +logging.getLogger("httpx").setLevel("WARNING") + logging.basicConfig( format="%(asctime)s %(levelname)-8s %(message)s", level=logging.INFO, @@ -28,6 +22,13 @@ logging.basicConfig( def split_filter_part(filter_part): + operators = [ + ["s<", "<"], + ["s>", ">"], + ["i<", "<"], + ["i>", ">"], + ["icontains", "contains"], + ] print("filter part", filter_part) for operator_group in operators: if operator_group[0] in filter_part: @@ -419,7 +420,8 @@ def track_search(query): "token_auth": os.getenv("MATOMO_TOKEN"), } post( - url=f"https://{os.getenv('MATOMO_DOMAIN')}/matomo.php", params=params + url=f"https://{os.getenv('MATOMO_DOMAIN')}/matomo.php", + params=params, ).raise_for_status() From 18f8388df5f51e468b6e11fed9a7be56c9bb60ac Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 13 Nov 2025 14:25:41 +0100 Subject: [PATCH 18/18] Changelog 2.2.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8537ef3..9f7e1de 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Ne pas oublier de mettre à jour les fichier .env. ## Notes de version -#### 2.2.0 () +#### 2.2.0 (13 novembre 2025) - Moteur de recherche (acheteurs et titulaires) en page d'accueil ([#58](https://github.com/ColinMaudry/decp.info/issues/58)) - Top acheteurs / titulaires par montant attribué/remporté (([#55](https://github.com/ColinMaudry/decp.info/issues/55)))