From d8dcb04fbfcb7fc9765761dc7345ab2ad50ea37e Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Sat, 27 Dec 2025 10:18:01 +0100 Subject: [PATCH 01/23] Fix anchor pour Sources --- src/pages/a-propos.py | 2 +- src/pages/tableau.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index d1fbc3c..8f31d49 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -77,7 +77,7 @@ Quant à l'exhaustivité, je consolide toutes les sources de données exploitabl **marches-publics.info** (AWS) publie ses données de manière assez sporadique depuis début 2023. Compte tenu de son poids dans le secteur, c'est assez dommageable pour la transparence des marchés publics. Au milieu de ces mauvaises nouvelles, je tiens à souligner la belle continuité de la publication par la DGFiP des données des marchés publics remontées via le [protocole PES](https://www.collectivites-locales.gouv.fr/finances-locales/le-protocole-dechange-standard-pes). Merci à leurs équipes."""), - html.H4("Sources de données ", id="qualite-exhausitivite"), + html.H4("Sources de données ", id="sources"), get_sources_tables(os.getenv("SOURCE_STATS_CSV_PATH")), html.H4("Mentions légales", id="mentions-legales"), dcc.Markdown(""" diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 0d685ef..cc79b70 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -66,7 +66,7 @@ layout = [ Vous pouvez appliquer un filtre pour chaque colonne en entrant du texte sous le nom de la colonne, puis en tapant sur `Entrée`. - Champs textuels : la recherche est insensible à la casse (majuscules/minuscules) et retourne les valeurs qui contiennent - le texte recherché. Exemple : `rennes` retourne "RENNES METROPOLE". Lorsque vous ouvrez une URL de vue, le format équivalent `icontains rennes` est utilisé. + le texte recherché. Exemple : `rennes` retourne "RENNES METROPOLE". Les guillemets simples (apostrophe du 4) doivent être prédédées d'une barre oblique (AltGr + 8). Exemple : `services d\\\'assurances`. Lorsque vous ouvrez une URL de vue, le format équivalent `icontains rennes` est utilisé. - Champs numériques : vous pouvez soit taper un nombre pour trouver les valeurs égales, soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres. - Champs date : vous pouvez également utiliser **>** ou **<**. Exemples : `< 2024-01-31` pour "avant le 31 janvier 2024", `2024` pour "en 2024", `> 2022` pour "à partir de 2022". Lorsque vous ouvrez une URL de vue, le format équivalent `i<` ou `i>` est utilisé. From 3c68317f9d5a7f3e01ae13a43120248185b23466 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 14 Jan 2026 18:16:50 +0100 Subject: [PATCH 02/23] Utilisation des stats produites par decp_processing --- src/figures.py | 21 +++++++++++++++++++++ src/pages/statistiques.py | 26 ++++++++++++++++++++++++-- src/utils.py | 11 +++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/figures.py b/src/figures.py index 8796e96..9a5957d 100644 --- a/src/figures.py +++ b/src/figures.py @@ -5,6 +5,8 @@ import plotly.express as px import polars as pl from dash import dash_table, dcc, html +from src.utils import format_number + def get_map_count_marches(df: pl.DataFrame): lf = df.lazy() @@ -56,6 +58,25 @@ def get_map_count_marches(df: pl.DataFrame): return fig +def get_yearly_statistics(statistics, today_str) -> html.Div: + children = [] + + for year in reversed(range(2018, int(today_str.split("/")[-1]) + 1)): + year = str(year) + stat_year = statistics[year] + md = dcc.Markdown(f""" + #### {year} + + - Nombre de marchés publics et accords-cadres : {format_number(stat_year["nb_notifications_marches"])} + - Nombre d'acheteurs publics : {format_number(stat_year["nb_acheteurs_uniques"])} + - Nombre de titulaires : {format_number(stat_year["nb_titulaires_uniques"])} + + """) + children.append(md) + + return html.Div(children=children) + + def get_barchart_sources(df: pl.DataFrame, type_date: str): lf = df.lazy() labels = { diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index 1caa4d4..44ddf78 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -1,7 +1,13 @@ +from datetime import datetime + from dash import dcc, html, register_page -from src.figures import get_barchart_sources, get_map_count_marches -from src.utils import df, meta_content +from src.figures import ( + get_barchart_sources, + get_map_count_marches, + get_yearly_statistics, +) +from src.utils import df, format_number, get_statistics, meta_content name = "Statistiques" @@ -15,6 +21,8 @@ register_page( order=3, ) +statistics: dict = get_statistics() +today_str = datetime.fromisoformat(statistics["datetime"]).strftime("%d/%m/%Y") layout = [ html.Div( @@ -38,6 +46,20 @@ layout = [ toutes les [contributions](/a-propos#contribuer) sont les bienvenues pour atteindre l'exhaustivité. """), dcc.Graph(figure=get_map_count_marches(df)), + html.H2( + f"Statistiques générales sur les marchés (au {today_str})", + id="marches", + ), + html.P( + "À noter qu'une fois un marché attribué ses données essentielles peuvent malheureusement mettre plusieurs mois à être publiées par l'acheteur." + ), + html.H4("Statistiques cumulées"), + dcc.Markdown(f""" + - Nombre de marchés publics et accords-cadres : {format_number(statistics["nb_marches"])} + - Nombre d'acheteurs publics : {format_number(statistics["nb_acheteurs_uniques"])} + - Nombre de titulaires uniques : {format_number(statistics["nb_titulaires_uniques"])} + """), + get_yearly_statistics(statistics, today_str), dcc.Graph( figure=get_barchart_sources(df, "dateNotification") ), diff --git a/src/utils.py b/src/utils.py index add2e81..f1e31f4 100644 --- a/src/utils.py +++ b/src/utils.py @@ -240,6 +240,17 @@ def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame: return lff.collect() +def get_statistics() -> dict: + return ( + get( + "https://www.data.gouv.fr/api/1/datasets/r/0ccf4a75-f3aa-4b46-8b6a-18aeb63e36df", + follow_redirects=True, + ) + .raise_for_status() + .json() + ) + + def get_departements() -> dict: with open("data/departements.json", "rb") as f: data = json.load(f) From 0bc82fae1e91fe139b83aa67c90a578127330866 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 00:31:27 +0100 Subject: [PATCH 03/23] =?UTF-8?q?m=C3=A0j=20de=20l'=C3=A9tat=20des=20sourc?= =?UTF-8?q?es=20de=20DECP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/a-propos.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index 8f31d49..fc054f6 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -69,10 +69,8 @@ les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter Quant à l'exhaustivité, je consolide toutes les sources de données exploitables que j'ai pu identifier (voir [statistiques](/statistiques)). Certains profils d'acheteurs ne publient pas leurs données malgré l'obligation réglementaire : -- marches-securises.fr - klekoon.fr (ils y travaillent) - -**marches-publics.gouv.fr** (PLACE, [AIFE](https://aife.economie.gouv.fr)), la plateforme des marchés de l'État, ne publie plus de données depuis le 1er janvier 2024 et l'entrée en vigueur de l'arrêté. Nous n'avons donc plus de données sur les marchés publics passés par les ministères, ainsi que sur ceux passés par les acheteurs clients d'[achatpublic.com](https://www.achatpublic.com), qui s'appuie sur les moyens de publication de l'AIFE. +- safetender.com (Omnikles) **marches-publics.info** (AWS) publie ses données de manière assez sporadique depuis début 2023. Compte tenu de son poids dans le secteur, c'est assez dommageable pour la transparence des marchés publics. From 16a914a98ad48ce3a88d70ad4fede4ee901d6224 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 02:17:47 +0100 Subject: [PATCH 04/23] Ajout de la matrice de doublons par source --- src/figures.py | 124 ++++++++++++++++++++++++++++++++++---- src/pages/statistiques.py | 2 + 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src/figures.py b/src/figures.py index 9a5957d..06d2481 100644 --- a/src/figures.py +++ b/src/figures.py @@ -2,6 +2,7 @@ import json from typing import Literal import plotly.express as px +import plotly.graph_objects as go import polars as pl from dash import dash_table, dcc, html @@ -59,22 +60,40 @@ def get_map_count_marches(df: pl.DataFrame): def get_yearly_statistics(statistics, today_str) -> html.Div: - children = [] + # Build DataFrame from statistics + years = list(reversed(range(2018, int(today_str.split("/")[-1]) + 1))) + data = [] + for year in years: + year_str = str(year) + stat = statistics[year_str] + data.append( + { + "Année": year_str, + "Marchés et accord-cadres": format_number( + stat["nb_notifications_marches"] + ), + "Acheteurs": format_number(stat["nb_acheteurs_uniques"]), + "Titulaires": format_number(stat["nb_titulaires_uniques"]), + } + ) - for year in reversed(range(2018, int(today_str.split("/")[-1]) + 1)): - year = str(year) - stat_year = statistics[year] - md = dcc.Markdown(f""" - #### {year} + df = pl.DataFrame(data) - - Nombre de marchés publics et accords-cadres : {format_number(stat_year["nb_notifications_marches"])} - - Nombre d'acheteurs publics : {format_number(stat_year["nb_acheteurs_uniques"])} - - Nombre de titulaires : {format_number(stat_year["nb_titulaires_uniques"])} + # Create Dash DataTable + table = dash_table.DataTable( + data=df.to_dicts(), + columns=[ + {"name": "Année", "id": "Année"}, + {"name": "Marchés et accord-cadres", "id": "Marchés et accord-cadres"}, + {"name": "Acheteurs", "id": "Acheteurs"}, + {"name": "Titulaires", "id": "Titulaires"}, + ], + page_size=10, + sort_action="none", + filter_action="none", + ) - """) - children.append(md) - - return html.Div(children=children) + return html.Div(children=table, className="marches_table") def get_barchart_sources(df: pl.DataFrame, type_date: str): @@ -280,3 +299,82 @@ class DataTable(dash_table.DataTable): hidden_columns=hidden_columns, **kwargs, # Possibilité de remplacer des arguments ) + + +def get_duplicate_matrix() -> html.Div: + """ + Fonction développée avec l'aide de la LLM Euria d'Infomaniak. + :return: + """ + result_df = pl.read_parquet( + "https://www.data.gouv.fr/api/1/datasets/r/a545bf6c-8b24-46ed-b49f-a32bf02eaffa" + ) + description = dcc.Markdown(""" + Ce graphique illustre les doublons de marchés publics entre sources. Il s'appuie sur les identifiants `uid` qui sont + pour chaque marché la concaténation du SIRET de l'acheteur et de l'identifiant interne du marché. + + **Comment lire ce graphique ?** + + On part des codes de sources de données en ordonnée. Ces jeux de données sont documentés dans [À propos](/a-propos#sources). + + La première colonne représente le pourcentage de marchés fournis par cette source qui sont uniquement disponibles dans cette source. Plus le bleu est foncé, plus important est le pourcentage. Donc plus le bleu est clair dans la colonne "unique", plus la source en ordonnée a des marchés en commun (ou en doublons selon le point de vue) avec d'autres sources, et donc plus on trouvera sur la ligne d'autres cases plus ou moins foncées qui indiqueront avec quelles autres sources elle partage des marchés. + + Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""") + + # Assuming result_df is your DataFrame with structure: + # | sourceDataset | unique | dataset1 | dataset2 | dataset3 | + # |---------------|--------|----------|----------|----------| + # | dataset1 | 0.8 | | 0.15 | 0.2 | + # | dataset2 | 0.75 | 0.15 | | 0.12 | + # | dataset3 | 0.85 | 0.2 | 0.12 | | + + # Extract data + z_data = result_df.select(pl.all().exclude("sourceDataset")).fill_null(0).to_numpy() + x_labels = result_df.columns[1:] # columns after "sourceDataset" + y_labels = result_df["sourceDataset"].to_list() + + # Create heatmap + fig = go.Figure( + data=go.Heatmap( + z=z_data, + x=x_labels, + y=y_labels, + colorscale=[ + [0, "white"], # 0% → white + [0.10, "lightblue"], # 1% → light blue (soft start) + [0.50, "steelblue"], # 50% → medium blue + [1, "darkblue"], # 100% → dark blue + ], + zmin=0, + zmax=1, + # texttemplate="%{z:.0%}", # Format as percentage + # textfont={"size": 10, "color": "black"}, # Smaller font + hoverongaps=False, + showscale=True, + hovertemplate=( + "%{z:.0%} des marchés de %{y} sont également présents dans %{x}" + ), + ) + ) + + # Update layout: make it wider and taller + fig.update_layout( + title="", + xaxis_title="Sources de données", + yaxis_title="Sources de données", + yaxis=dict(autorange="reversed"), + xaxis=dict(tickangle=45, tickfont=dict(size=10)), # Smaller x-tick labels + coloraxis_colorbar=dict(title="Percentage", tickfont=dict(size=10)), + width=1000, # Wider + height=1000, # Taller + font=dict(size=11), # Overall font size + margin=dict(l=100, r=50, t=80, b=100), # Add margin for labels + ) + + return html.Div( + children=[ + html.H2("Doublons de marchés entre les sources"), + description, + dcc.Graph(figure=fig), + ] + ) diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index 44ddf78..44af8c2 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -4,6 +4,7 @@ from dash import dcc, html, register_page from src.figures import ( get_barchart_sources, + get_duplicate_matrix, get_map_count_marches, get_yearly_statistics, ) @@ -60,6 +61,7 @@ layout = [ - Nombre de titulaires uniques : {format_number(statistics["nb_titulaires_uniques"])} """), get_yearly_statistics(statistics, today_str), + get_duplicate_matrix(), dcc.Graph( figure=get_barchart_sources(df, "dateNotification") ), From 86b445a0070a6a0c7d6b9801dda32a3415a7cd3d Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 02:18:17 +0100 Subject: [PATCH 05/23] Tableau pour les statistiques par an --- src/assets/style.css | 9 ++++++++- src/pages/statistiques.py | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index 4f91b10..24dd152 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -1,3 +1,9 @@ +/* Titres */ + +h2 { + margin-top: 40px; +} + /* Change la marge bout d'export */ .table-menu { font-size: 16px; @@ -130,7 +136,8 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c } /* Alternance des couleurs pour les lignes */ -.marches_table { +.marches_table table td, +.marches_table table th { font-family: "Open Sans", sans-serif; } diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index 44af8c2..12d19db 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -56,10 +56,11 @@ layout = [ ), html.H4("Statistiques cumulées"), dcc.Markdown(f""" - - Nombre de marchés publics et accords-cadres : {format_number(statistics["nb_marches"])} - - Nombre d'acheteurs publics : {format_number(statistics["nb_acheteurs_uniques"])} - - Nombre de titulaires uniques : {format_number(statistics["nb_titulaires_uniques"])} + - Nombre de marchés publics et accord-cadres : {format_number(statistics["nb_marches"])} + - Nombre d'acheteurs publics (SIRET) : {format_number(statistics["nb_acheteurs_uniques"])} + - Nombre de titulaires (SIRET) : {format_number(statistics["nb_titulaires_uniques"])} """), + html.H4("Statistiques par année"), get_yearly_statistics(statistics, today_str), get_duplicate_matrix(), dcc.Graph( From 338311fc621c8afcaab4ffb1878904645f97fc26 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 02:57:35 +0100 Subject: [PATCH 06/23] Style et texte --- src/figures.py | 8 ++++++-- src/pages/statistiques.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/figures.py b/src/figures.py index 06d2481..abcd60a 100644 --- a/src/figures.py +++ b/src/figures.py @@ -308,7 +308,11 @@ def get_duplicate_matrix() -> html.Div: """ result_df = pl.read_parquet( "https://www.data.gouv.fr/api/1/datasets/r/a545bf6c-8b24-46ed-b49f-a32bf02eaffa" + ).sort("sourceDataset") + result_df = result_df.select( + ["sourceDataset", "unique"] + sorted(result_df.columns[2:]) ) + description = dcc.Markdown(""" Ce graphique illustre les doublons de marchés publics entre sources. Il s'appuie sur les identifiants `uid` qui sont pour chaque marché la concaténation du SIRET de l'acheteur et de l'identifiant interne du marché. @@ -317,7 +321,7 @@ def get_duplicate_matrix() -> html.Div: On part des codes de sources de données en ordonnée. Ces jeux de données sont documentés dans [À propos](/a-propos#sources). - La première colonne représente le pourcentage de marchés fournis par cette source qui sont uniquement disponibles dans cette source. Plus le bleu est foncé, plus important est le pourcentage. Donc plus le bleu est clair dans la colonne "unique", plus la source en ordonnée a des marchés en commun (ou en doublons selon le point de vue) avec d'autres sources, et donc plus on trouvera sur la ligne d'autres cases plus ou moins foncées qui indiqueront avec quelles autres sources elle partage des marchés. + La première colonne "unique" représente le pourcentage de marchés fournis par cette source qui sont uniquement disponibles dans cette source. Plus le bleu est foncé, plus important est le pourcentage. Donc plus le bleu est clair dans la première colonne, plus la source en ordonnée a des marchés en commun avec d'autres sources, et donc plus on trouvera sur la même ligne d'autres cases plus ou moins foncées qui indiqueront avec quelles autres sources elle partage des marchés. Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""") @@ -352,7 +356,7 @@ def get_duplicate_matrix() -> html.Div: hoverongaps=False, showscale=True, hovertemplate=( - "%{z:.0%} des marchés de %{y} sont également présents dans %{x}" + "%{z:.0%} des marchés de %{y} sont également présents dans %{x}" ), ) ) diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index 12d19db..fb63f3b 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -43,7 +43,7 @@ layout = [ ou publient d'une manière qui rend la récupération des données compliquée. Les données présentées sur ce site ne représentent donc pas tous les marchés attribués en France, seulement une partie significative. - L'ajout de nouvelles plateformes [est en cours](https://github.com/ColinMaudry/decp-processing/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22source%20de%20donn%C3%A9es%22), + L'ajout de nouvelles plateformes [est en cours](https://github.com/ColinMaudry/decp-processing/issues?q=is%3Aissue%20label%3A%22source%20de%20donn%C3%A9es%22), toutes les [contributions](/a-propos#contribuer) sont les bienvenues pour atteindre l'exhaustivité. """), dcc.Graph(figure=get_map_count_marches(df)), @@ -59,6 +59,8 @@ layout = [ - Nombre de marchés publics et accord-cadres : {format_number(statistics["nb_marches"])} - Nombre d'acheteurs publics (SIRET) : {format_number(statistics["nb_acheteurs_uniques"])} - Nombre de titulaires (SIRET) : {format_number(statistics["nb_titulaires_uniques"])} + + Je ne publie pas encore de statistiques sur les montants de marchés car je n'ai pas encore trouvé la bonne formule pour traiter les trop nombreux montants fantaisistes qui polluent les calculs. """), html.H4("Statistiques par année"), get_yearly_statistics(statistics, today_str), From ec28e897881153049e84f1b11c3473b12ca0dd72 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 03:03:24 +0100 Subject: [PATCH 07/23] Style et texte --- src/assets/style.css | 8 +------- src/figures.py | 20 ++++++++++++-------- src/pages/statistiques.py | 9 ++++++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index 24dd152..619b357 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -1,9 +1,3 @@ -/* Titres */ - -h2 { - margin-top: 40px; -} - /* Change la marge bout d'export */ .table-menu { font-size: 16px; @@ -216,7 +210,7 @@ a.nav { } h3 { - margin: 36px 0 24px 0; + margin: 36px 0 20px 0; } summary > h3 { diff --git a/src/figures.py b/src/figures.py index abcd60a..5d6a723 100644 --- a/src/figures.py +++ b/src/figures.py @@ -314,14 +314,13 @@ def get_duplicate_matrix() -> html.Div: ) description = dcc.Markdown(""" - Ce graphique illustre les doublons de marchés publics entre sources. Il s'appuie sur les identifiants `uid` qui sont - pour chaque marché la concaténation du SIRET de l'acheteur et de l'identifiant interne du marché. + Ce graphique illustre les doublons de marchés publics entre sources, c'est-à-dire la proportion de marchés publiés par plus d'une source. Il s'appuie sur les identifiants `uid` qui sont pour chaque marché la concaténation du SIRET de l'acheteur et de l'identifiant interne du marché. **Comment lire ce graphique ?** On part des codes de sources de données en ordonnée. Ces jeux de données sont documentés dans [À propos](/a-propos#sources). - La première colonne "unique" représente le pourcentage de marchés fournis par cette source qui sont uniquement disponibles dans cette source. Plus le bleu est foncé, plus important est le pourcentage. Donc plus le bleu est clair dans la première colonne, plus la source en ordonnée a des marchés en commun avec d'autres sources, et donc plus on trouvera sur la même ligne d'autres cases plus ou moins foncées qui indiqueront avec quelles autres sources elle partage des marchés. + La première colonne (**unique**) représente le pourcentage de marchés fournis par cette source qui sont uniquement disponibles dans cette source. Plus le rouge est foncé, plus important est le pourcentage. Donc, à l'inverse, plus le rouge est clair dans la première colonne, plus la source en ordonnée a des marchés en commun avec d'autres sources, et donc plus on trouvera sur la même ligne d'autres cases plus ou moins foncées qui indiqueront avec quelles autres sources cette source partage des marchés. Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""") @@ -343,11 +342,16 @@ def get_duplicate_matrix() -> html.Div: z=z_data, x=x_labels, y=y_labels, + # colorscale=[ + # [0, "white"], # 0% → white + # [0.10, "lightblue"], # 1% → light blue (soft start) + # [0.50, "steelblue"], # 50% → medium blue + # [1, "darkblue"], # 100% → dark blue + # ], colorscale=[ - [0, "white"], # 0% → white - [0.10, "lightblue"], # 1% → light blue (soft start) - [0.50, "steelblue"], # 50% → medium blue - [1, "darkblue"], # 100% → dark blue + [0.0, "white"], # 0% → white + [0.10, "lightsalmon"], # 10% → light warm tone + [1.0, "darkred"], # 100% → deep red ], zmin=0, zmax=1, @@ -377,7 +381,7 @@ def get_duplicate_matrix() -> html.Div: return html.Div( children=[ - html.H2("Doublons de marchés entre les sources"), + html.H3("Doublons de marchés entre les sources"), description, dcc.Graph(figure=fig), ] diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index fb63f3b..6907fc2 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -37,7 +37,7 @@ layout = [ children=[ html.Div( children=[ - dcc.Markdown(""" + dcc.Markdown(f""" La publication de données essentielles de marchés publics (DECP) est souvent effectuée par les plateformes de marchés publics (profils d'acheteurs). Cependant, certaines plateformes ne publient pas, ou publient d'une manière qui rend la récupération des données compliquée. Les données présentées sur ce site @@ -45,10 +45,12 @@ layout = [ L'ajout de nouvelles plateformes [est en cours](https://github.com/ColinMaudry/decp-processing/issues?q=is%3Aissue%20label%3A%22source%20de%20donn%C3%A9es%22), toutes les [contributions](/a-propos#contribuer) sont les bienvenues pour atteindre l'exhaustivité. + + Les statistiques publiées sur cette page ont été produites automatiquement à partir des données les plus récentes ({today_str}). """), dcc.Graph(figure=get_map_count_marches(df)), - html.H2( - f"Statistiques générales sur les marchés (au {today_str})", + html.H3( + "Statistiques générales sur les marchés", id="marches", ), html.P( @@ -65,6 +67,7 @@ layout = [ html.H4("Statistiques par année"), get_yearly_statistics(statistics, today_str), get_duplicate_matrix(), + html.H3("Nombre de marchés par source dans le temps"), dcc.Graph( figure=get_barchart_sources(df, "dateNotification") ), From 5bcd17e8b0079c48114ff212eb73f3d4f8c858a4 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 16 Jan 2026 13:57:19 +0100 Subject: [PATCH 08/23] =?UTF-8?q?Skip=20les=20colonnes=20absentes=20du=20s?= =?UTF-8?q?ch=C3=A9ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index f1e31f4..aef7acb 100644 --- a/src/utils.py +++ b/src/utils.py @@ -345,7 +345,9 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup if column_object: column_name = column_object.get("title") else: - column_name = column_id + # Si le champ n'est pas dans le schéma, on le skip + print(f"Champ innatendu : {column_id}") + continue column = { "name": column_name, From c3e801a15a356fece96ddf4573a60fdc71b3024f Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 19 Jan 2026 11:33:57 +0100 Subject: [PATCH 09/23] Utilisation des dbc pour /marche #63 --- src/app.py | 3 +++ src/assets/style.css | 28 ---------------------- src/pages/marche.py | 56 +++++++++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 52 deletions(-) diff --git a/src/app.py b/src/app.py index 699c389..b039569 100644 --- a/src/app.py +++ b/src/app.py @@ -14,6 +14,9 @@ app = Dash( title="decp.info", use_pages=True, compress=True, + meta_tags=[ + {"name": "viewport", "content": "width=device-width, initial-scale=1"}, + ], ) # COSMO (belle font, blue), # UNITED (rouge, ubuntu font), diff --git a/src/assets/style.css b/src/assets/style.css index 619b357..e82b1bb 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -263,31 +263,3 @@ summary > h3 { grid-column: 1/3; grid-row: 3; } - -/* Vue marché */ - -.marche_infos p { - margin-top: 6px; - margin-bottom: 6px; -} - -.marche_title { - grid-column: 1/3; - grid-row: 1; -} - -.marche_map { - grid-column: 3; - grid-row: 2; - width: 400px; -} - -.marche_infos_1 { - grid-column: 1; - grid-row: 2; -} - -.marche_infos_2 { - grid-column: 2; - grid-row: 2; -} diff --git a/src/pages/marche.py b/src/pages/marche.py index 807bb7a..ec3ef71 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -1,5 +1,6 @@ from datetime import datetime +import dash_bootstrap_components as dbc import polars as pl from dash import Input, Output, callback, dcc, html, register_page from polars import selectors as cs @@ -20,37 +21,44 @@ layout = [ dcc.Store(id="marche_data"), dcc.Store(id="titulaires_data"), dcc.Location(id="url", refresh="callback-nav"), - html.Div( - className="container marche_infos", + dbc.Container( + className="marche_infos", children=[ - html.P("Vous consultez un résumé des données de ce marché public"), - html.Ul( + dbc.Row( + dbc.Col( + [ + html.P( + "Vous consultez un résumé des données de ce marché public" + ), + html.Ul( + [ + html.Li( + "après son attribution aux titulaires qui l'ont remporté à la suite d'un appel d'offres (ou sans appel d'offres via une attribution directe)" + ), + html.Li( + "après avoir appliqué les éventuelles modifications de montant, durée ou titulaires renseignées par l'acheteur" + ), + ] + ), + html.P( + "Le montant total payé aux titulaires, la durée du marché et la liste des titulaires peuvent cependant encore évoluer jusqu'à la fin de l'exécution du marché." + ), + ] + ) + ), + dbc.Row( [ - html.Li( - "après son attribution aux titulaires qui l'ont remporté à la suite d'un appel d'offres (ou sans appel d'offres via une attribution directe)" - ), - html.Li( - "après avoir appliqué les éventuelles modifications de montant, durée ou titulaires renseignées par l'acheteur" - ), - ] - ), - html.P( - "Le montant total payé aux titulaires, la durée du marché et la liste des titulaires peuvent cependant encore évoluer jusqu'à la fin de l'exécution du marché." - ), - html.Div( - className="wrapper", - children=[ - html.Div( - className="marche_map", - id="marche_map", + dbc.Col(id="marche_infos_1", width=12, md=4), + dbc.Col(id="marche_infos_2", width=12, md=4), + dbc.Col( + width=12, + md=4, children=[ html.H4("Titulaires"), html.Ul(id="marche_infos_titulaires"), ], ), - html.Div(className="marche_infos_1", id="marche_infos_1"), - html.Div(className="marche_infos_2", id="marche_infos_2"), - ], + ] ), ], ), From fc99723b2ec32e6c468d69b00608ac4b3fd1439c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 19 Jan 2026 11:43:47 +0100 Subject: [PATCH 10/23] =?UTF-8?q?Gestion=20des=20colonnes=20ajout=C3=A9es?= =?UTF-8?q?=20mais=20absentes=20du=20sch=C3=A9ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/callbacks.py | 2 +- src/utils.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index d5f1522..62df9ed 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -21,7 +21,7 @@ def get_top_org_table(data, org_type: str): dff_nb = dff_nb.fill_null("") dff_nb = format_values(dff_nb) columns, tooltip = setup_table_columns( - dff_nb, hideable=False, exclude=[f"{org_type}_id"] + dff_nb, hideable=False, exclude=[f"{org_type}_id"], new_columns=["Attributions"] ) data = dff_nb.to_dicts() data = add_links_in_dict(data, f"{org_type}") diff --git a/src/utils.py b/src/utils.py index aef7acb..edbddf5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -334,7 +334,9 @@ def sort_table_data(lff: pl.LazyFrame, sort_by: list) -> pl.LazyFrame: return lff -def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tuple: +def setup_table_columns( + dff, hideable: bool = True, exclude: list = None, new_columns: list = None +) -> tuple: # Liste finale de colonnes columns = [] tooltip = {} @@ -345,8 +347,10 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup if column_object: column_name = column_object.get("title") else: - # Si le champ n'est pas dans le schéma, on le skip - print(f"Champ innatendu : {column_id}") + if column_id not in new_columns: + # Si le champ n'est pas dans le schéma et pas annoncé, on le skip + print("Champ innatendu : ") + print(dff[column_id].head()) continue column = { From 6005b2fcb5f6d0a0add72d987e3bf1b9ef420272 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 19 Jan 2026 11:44:41 +0100 Subject: [PATCH 11/23] =?UTF-8?q?Mise=20en=20valeur=20des=20moyens=20de=20?= =?UTF-8?q?consommer=20les=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/a-propos.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index fc054f6..ab81a16 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -32,11 +32,16 @@ En effet, le potentiel des données d'attribution de marchés et des données qu les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter - de nombreuses visualisations de données (cartes, graphiques, tableaux) sur des thématiques variées (vivacité de la concurrence, secteurs d'activité, insertion par l'activité économique (IAE), distance acheteur-titulaire...) -- la sauvegarde de filtres pour les retrouver plus tard et les partager - des alertes par email si des marchés correspondant à certains critères -- le développement d'une API pour alimenter d'autres logiciels - ...et toutes les fonctionnalités auxquelles vous pourrez penser +#### Consommer les données brutes + +Vous pouvez consommer les données qui alimentent decp.info + +- en les téléchargeant [sur data.gouv.fr](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire) (Parquet, CSV), pensez à lire la description du jeu de données +- en interrogeant l'[API REST ouverte](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire#user-content-api-rest) + """ ), html.H4("Contact", id="contact"), From 3776b18ff5662073ac73b67cb4f34f258d06a7ab Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 20 Jan 2026 11:24:26 +0100 Subject: [PATCH 12/23] =?UTF-8?q?Meilleure=20gestion=20des=20colonnes=20ab?= =?UTF-8?q?sentes=20du=20sch=C3=A9ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index edbddf5..1cb2171 100644 --- a/src/utils.py +++ b/src/utils.py @@ -337,6 +337,8 @@ def sort_table_data(lff: pl.LazyFrame, sort_by: list) -> pl.LazyFrame: def setup_table_columns( dff, hideable: bool = True, exclude: list = None, new_columns: list = None ) -> tuple: + new_columns = new_columns or [] + # Liste finale de colonnes columns = [] tooltip = {} @@ -351,7 +353,8 @@ def setup_table_columns( # Si le champ n'est pas dans le schéma et pas annoncé, on le skip print("Champ innatendu : ") print(dff[column_id].head()) - continue + continue + column_name = column_id column = { "name": column_name, From 421eaa0772f92e869c4e4ae913803e51c17d3b12 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 20 Jan 2026 17:31:42 +0100 Subject: [PATCH 13/23] Premier jet navbar #63 --- src/app.py | 97 ++++++++++++++++++++++++++++++-------------- src/assets/style.css | 2 + 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/app.py b/src/app.py index b039569..40a94fc 100644 --- a/src/app.py +++ b/src/app.py @@ -3,7 +3,7 @@ import os import dash_bootstrap_components as dbc import tomllib -from dash import Dash, dcc, html, page_container, page_registry +from dash import Dash, Input, Output, State, dcc, html, page_container, page_registry from dotenv import load_dotenv from flask import send_from_directory @@ -76,43 +76,80 @@ app.index_string = """ """ -app.layout = html.Div( - [ - html.Div( - [ - html.Div( +navbar = dbc.Navbar( + dbc.Container( + [ + dbc.NavbarBrand( + [ + html.H1("decp.info", className="d-inline-block align-top me-2"), + html.Span(version, className="fs-6 text-muted align-bottom"), + ], + href="/", + ), + dbc.NavbarToggler(id="navbar-toggler"), + dbc.Collapse( + dbc.Nav( [ - html.A(children=html.H1("decp.info"), href="/"), - html.P( - children=html.A( - version, - href="https://github.com/ColinMaudry/decp.info/blob/main/CHANGELOG.md", - target="_blank", - ), - className="version", - ), - ], - className="logo", - ), - html.Div( - id="announcements", - children=dcc.Markdown(os.getenv("ANNOUNCEMENTS")), - ), - html.Div( - [ - dcc.Link( - page["name"], href=page["relative_path"], className="nav" + dbc.NavItem( + dbc.NavLink( + page["name"], + href=page["relative_path"], + active="exact", + ) ) for page in page_registry.values() if page["name"] not in ["Acheteur", "Titulaire", "Marché"] - ] + ], + className="ms-auto", + navbar=True, ), - ], - className="navbar", + id="navbar-collapse", + navbar=True, + ), + ] + ), + color="light", + dark=False, + className="mb-4", +) + +app.layout = html.Div( + [ + dcc.Location(id="url-tracker"), + navbar, + html.Div( + id="announcements", + children=dbc.Container(dcc.Markdown(os.getenv("ANNOUNCEMENTS"))), + ), + dbc.Container( + page_container, + id="page-content-container", + className="mb-4", ), - page_container, ] ) + +@app.callback( + Output("navbar-collapse", "is_open"), + [Input("navbar-toggler", "n_clicks")], + [State("navbar-collapse", "is_open")], +) +def toggle_navbar_collapse(n, is_open): + if n: + return not is_open + return is_open + + +@app.callback( + Output("page-content-container", "fluid"), + Input("url-tracker", "pathname"), +) +def toggle_container_fluid(pathname): + if pathname == "/tableau": + return True + return False + + if __name__ == "__main__": app.run(debug=True) diff --git a/src/assets/style.css b/src/assets/style.css index e82b1bb..b47f0de 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -195,6 +195,7 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c /* Menu de navigation */ +/* .navbar { width: 100%; height: 100px; @@ -208,6 +209,7 @@ a.nav { margin-right: 40px; font-size: 120%; } +*/ h3 { margin: 36px 0 20px 0; From 10443794254dd78b8792e540436a6d92553cfe10 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 14:22:52 +0100 Subject: [PATCH 14/23] =?UTF-8?q?Meilleur=20affichage=20sur=20petit=20?= =?UTF-8?q?=C3=A9cran=20#63?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 37 +++++++++++++++++++++++++------------ src/assets/style.css | 36 +++++++++--------------------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/app.py b/src/app.py index 40a94fc..e92f457 100644 --- a/src/app.py +++ b/src/app.py @@ -78,13 +78,29 @@ app.index_string = """ navbar = dbc.Navbar( dbc.Container( - [ - dbc.NavbarBrand( - [ - html.H1("decp.info", className="d-inline-block align-top me-2"), - html.Span(version, className="fs-6 text-muted align-bottom"), + fluid=True, + children=[ + dbc.NavItem( + children=[ + dcc.Link(html.H1("decp.info"), href="/", className="logo"), + html.P( + [ + html.A( + version, + href="https://github.com/ColinMaudry/decp.info/blob/main/CHANGELOG.md", + ) + ], + className="version", + ), ], - href="/", + style={"min-width": "230px"}, + ), + dbc.NavItem( + [dcc.Markdown(os.getenv("ANNOUNCEMENTS"), id="announcements")], + style={ + "max-width": "1200px", + "display": "inline-block", + }, ), dbc.NavbarToggler(id="navbar-toggler"), dbc.Collapse( @@ -92,7 +108,7 @@ navbar = dbc.Navbar( [ dbc.NavItem( dbc.NavLink( - page["name"], + page["name"].replace(" ", " "), href=page["relative_path"], active="exact", ) @@ -106,21 +122,18 @@ navbar = dbc.Navbar( id="navbar-collapse", navbar=True, ), - ] + ], ), color="light", dark=False, className="mb-4", + expand="md", ) app.layout = html.Div( [ dcc.Location(id="url-tracker"), navbar, - html.Div( - id="announcements", - children=dbc.Container(dcc.Markdown(os.getenv("ANNOUNCEMENTS"))), - ), dbc.Container( page_container, id="page-content-container", diff --git a/src/assets/style.css b/src/assets/style.css index b47f0de..55025b7 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -28,8 +28,10 @@ details > div { /* Logo et version */ -div.logo { - width: 230px; +a.logo { + color: black; + text-decoration: none; + float: left; } p.version { @@ -40,15 +42,11 @@ p.version { p.version > a { text-decoration: none; + margin-top: 10px; } -div.logo h1 { - float: left; -} - -div.logo > a { - text-decoration: none; - color: black; +.navbar-brand { + margin-right: 2px; } /* Réduire la taille du texte de la colonne Objet */ @@ -152,9 +150,9 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c /* Annonces */ #announcements { - margin-top: 25px; - max-width: 30%; + margin: 25px 40px 0 60px; font-size: 90%; + max-width: 900px; } /* Page de recherche */ @@ -195,22 +193,6 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c /* Menu de navigation */ -/* -.navbar { - width: 100%; - height: 100px; - padding: 0 10px; - border: 0; - border-bottom: solid 1px black; -} - -a.nav { - float: left; - margin-right: 40px; - font-size: 120%; -} -*/ - h3 { margin: 36px 0 20px 0; } From 945f7974c8932c16167b0ce69d64584fb24db258 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 15:13:59 +0100 Subject: [PATCH 15/23] =?UTF-8?q?Configuration=20du=20point=20de=20rupture?= =?UTF-8?q?=20de=20la=20navbar=20=C3=A0=20992px=20#63?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 8 +++++--- src/assets/style.css | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index e92f457..4e258f7 100644 --- a/src/app.py +++ b/src/app.py @@ -95,12 +95,14 @@ navbar = dbc.Navbar( ], style={"min-width": "230px"}, ), - dbc.NavItem( - [dcc.Markdown(os.getenv("ANNOUNCEMENTS"), id="announcements")], + dbc.Nav( + children=[dcc.Markdown(os.getenv("ANNOUNCEMENTS"), id="announcements")], style={ "max-width": "1200px", "display": "inline-block", }, + navbar=True, + id="announcements-nav", ), dbc.NavbarToggler(id="navbar-toggler"), dbc.Collapse( @@ -127,7 +129,7 @@ navbar = dbc.Navbar( color="light", dark=False, className="mb-4", - expand="md", + expand="lg", ) app.layout = html.Div( diff --git a/src/assets/style.css b/src/assets/style.css index 55025b7..a534097 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -247,3 +247,9 @@ summary > h3 { grid-column: 1/3; grid-row: 3; } + +@media (max-width: 992px) { + #announcements-nav { + display: none !important; + } +} From 2fc501efbb2430adbc9ccbe91eef02bbb38c405e Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 16:32:20 +0100 Subject: [PATCH 16/23] Container de l'app toujours fluid, fix noms props #63 --- pyproject.toml | 2 +- src/app.py | 20 +++----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1be3d03..db67ade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ { name = "Colin Maudry", email = "colin@colmo.tech" } ] dependencies = [ - "dash==3.3.0", + "dash==3.4.0", "dash[compress]", "polars", "gunicorn", diff --git a/src/app.py b/src/app.py index 4e258f7..1d9cbec 100644 --- a/src/app.py +++ b/src/app.py @@ -93,12 +93,12 @@ navbar = dbc.Navbar( className="version", ), ], - style={"min-width": "230px"}, + style={"minWidth": "230px"}, ), dbc.Nav( children=[dcc.Markdown(os.getenv("ANNOUNCEMENTS"), id="announcements")], style={ - "max-width": "1200px", + "maxWidth": "1200px", "display": "inline-block", }, navbar=True, @@ -134,10 +134,10 @@ navbar = dbc.Navbar( app.layout = html.Div( [ - dcc.Location(id="url-tracker"), navbar, dbc.Container( page_container, + fluid=True, id="page-content-container", className="mb-4", ), @@ -154,17 +154,3 @@ def toggle_navbar_collapse(n, is_open): if n: return not is_open return is_open - - -@app.callback( - Output("page-content-container", "fluid"), - Input("url-tracker", "pathname"), -) -def toggle_container_fluid(pathname): - if pathname == "/tableau": - return True - return False - - -if __name__ == "__main__": - app.run(debug=True) From d499906bbe7c49fcd2610215ac1f73b8f87357e6 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 17:40:53 +0100 Subject: [PATCH 17/23] =?UTF-8?q?G=C3=A9n=C3=A9ration=20d'une=20sitemap=20?= =?UTF-8?q?#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 1d9cbec..61bae1a 100644 --- a/src/app.py +++ b/src/app.py @@ -5,7 +5,7 @@ import dash_bootstrap_components as dbc import tomllib from dash import Dash, Input, Output, State, dcc, html, page_container, page_registry from dotenv import load_dotenv -from flask import send_from_directory +from flask import Response, send_from_directory load_dotenv() @@ -30,6 +30,25 @@ def robots(): return send_from_directory("./assets", "robots.txt", mimetype="text/plain") +@app.server.route("/sitemap.xml") +def sitemap(): + base_url = "https://decp.info" + pages = [ + "/", + "/statistiques", + "/tableau", + "/a-propos", + ] + xml = '\n' + xml += '\n' + for page in pages: + xml += " \n" + xml += f" {base_url}{page}\n" + xml += " \n" + xml += "" + return Response(xml, mimetype="text/xml") + + logger = logging.getLogger("decp.info") logging.basicConfig( format="%(asctime)s %(levelname)-8s %(message)s", From dd52bd51583c9b6a503d9f2311ae0f00025d71be Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 17:47:03 +0100 Subject: [PATCH 18/23] =?UTF-8?q?Am=C3=A9lioration=20des=20titres=20et=20d?= =?UTF-8?q?escriptions=20de=20pages=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/a-propos.py | 6 +++--- src/pages/acheteur.py | 9 +++++++-- src/pages/marche.py | 9 +++++++-- src/pages/recherche.py | 4 ++-- src/pages/statistiques.py | 4 ++-- src/pages/tableau.py | 4 ++-- src/pages/titulaire.py | 9 +++++++-- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index ab81a16..25a47c2 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -10,9 +10,9 @@ name = "À propos" register_page( __name__, path="/a-propos", - title=meta_content["title"], - name=name, - description=meta_content["description"], + title="À propos | decp.info", + name="À propos", + description="En savoir plus sur decp.info, l'outil d'exploration des données essentielles de la commande publique.", image_url=meta_content["image_url"], order=5, ) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 6d51bb8..43c2588 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -18,12 +18,17 @@ from src.utils import ( sort_table_data, ) + +def get_title(acheteur_id: str = None) -> str: + return f"Acheteur {acheteur_id} | decp.info" + + register_page( __name__, path_template="/acheteurs/", - title=meta_content["title"], + title=get_title, name="Acheteur", - description=meta_content["description"], + description="Consultez les marchés publics attribués par cet acheteur.", image_url=meta_content["image_url"], order=5, ) diff --git a/src/pages/marche.py b/src/pages/marche.py index ec3ef71..4fbb8d4 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -7,12 +7,17 @@ from polars import selectors as cs from src.utils import data_schema, df, format_values, meta_content + +def get_title(uid: str = None) -> str: + return f"Marché {uid} | decp.info" + + register_page( __name__, path_template="/marches/", - title=meta_content["title"], + title=get_title, name="Marché", - description=meta_content["description"], + description="Consultez les détails de ce marché public : montant, acheteur, titulaires, modifications, etc.", image_url=meta_content["image_url"], order=7, ) diff --git a/src/pages/recherche.py b/src/pages/recherche.py index bf78231..3deb79f 100644 --- a/src/pages/recherche.py +++ b/src/pages/recherche.py @@ -14,9 +14,9 @@ name = "Recherche" register_page( __name__, path="/", - title=meta_content["title"], + title="Recherche de marchés publics | decp.info", name=name, - description=meta_content["description"], + description="Recherchez des des acheteurs et des titulaires parmi les données essentielles de la commande publique.", image_url=meta_content["image_url"], order=0, ) diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index 6907fc2..dd6c502 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -15,9 +15,9 @@ name = "Statistiques" register_page( __name__, path="/statistiques", - title=meta_content["title"], + title="Statistiques | decp.info", name=name, - description=meta_content["description"], + description="Visualisez l'état de la publication des données essentielles des marchés publics en France.", image_url=meta_content["image_url"], order=3, ) diff --git a/src/pages/tableau.py b/src/pages/tableau.py index cc79b70..58d3942 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -26,9 +26,9 @@ name = "Tableau" register_page( __name__, path="/tableau", - title=meta_content["title"], + title="Tableau des marchés publics | decp.info", name=name, - description=meta_content["description"], + description="Consultez, filtrez et exportez les données essentielles de la commande publique sous forme de tableau.", image_url=meta_content["image_url"], order=1, ) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index d75eb78..393907b 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -18,12 +18,17 @@ from src.utils import ( sort_table_data, ) + +def get_title(titulaire_id: str = None) -> str: + return f"Titulaire {titulaire_id} | decp.info" + + register_page( __name__, path_template="/titulaires/", - title=meta_content["title"], + title=get_title, name="Titulaire", - description=meta_content["description"], + description="Consultez les marchés publics remportés par ce titulaire.", image_url=meta_content["image_url"], order=5, ) From 267e2e15f8148217d5d38a87bb8f74a31a23707a Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 21 Jan 2026 18:04:33 +0100 Subject: [PATCH 19/23] =?UTF-8?q?Possibilit=C3=A9=20dans=20les=20champs=20?= =?UTF-8?q?non-num=C3=A9riques=20de=20filtrer=20le=20texte=20selon=20son?= =?UTF-8?q?=20d=C3=A9but=20ou=20sa=20fin=20(`text*`=20et=20`*text`)=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ src/utils.py | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b19cc4..58525e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +#### 2.4.0 + +- Site à peu près utilisable sur petit écran (smartphone) ([#63](https://github.com/ColinMaudry/decp.info/issues/63)) +- Amélioration du référencement Web (sitemap, titres, descriptions) ([#50](https://github.com/ColinMaudry/decp.info/issues/50)) +- Possibilité dans les champs non-numériques de filtrer le texte selon son début ou sa fin (`text*` et `*text`) + ##### 2.3.1 (16 janvier 2026) - Les champs absents du [schéma](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire?resource_id=9a4144c0-ee44-4dec-bee5-bbef38191d9a) sont ignorés pour éviter les erreurs diff --git a/src/utils.py b/src/utils.py index 1cb2171..a5eff2d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -308,9 +308,18 @@ def filter_table_data(lff: pl.LazyFrame, filter_query: str) -> pl.LazyFrame: elif operator == "contains": if col_type in ["String", "Date"]: filter_value = filter_value.strip('"') - lff = lff.filter( - pl.col(col_name).str.contains("(?i)" + filter_value) - ) + if filter_value.endswith("*"): + lff = lff.filter( + pl.col(col_name).str.starts_with(filter_value[:-1]) + ) + elif filter_value.startswith("*"): + lff = lff.filter( + pl.col(col_name).str.ends_with(filter_value[1:]) + ) + else: + lff = lff.filter( + pl.col(col_name).str.contains("(?i)" + filter_value) + ) elif col_type.startswith("Int") or col_type.startswith("Float"): lff = lff.filter(pl.col(col_name) == filter_value) else: From 92f30a963f3ae70690c27d8cd52a4a750fd6c9e3 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 22 Jan 2026 16:31:25 +0100 Subject: [PATCH 20/23] =?UTF-8?q?Ajout=20d'une=20table=20des=20mati=C3=A8r?= =?UTF-8?q?es=20dans=20A=20propos=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/style.css | 81 ++++++++++++++++++++ src/pages/a-propos.py | 169 ++++++++++++++++++++++++++++-------------- 2 files changed, 196 insertions(+), 54 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index a534097..a8d203d 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -253,3 +253,84 @@ summary > h3 { display: none !important; } } + +/* CSS for the /a-propos page Layout and Table of Contents */ + +.a-propos-container { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + position: relative; + max-width: 1200px; + margin: 0 auto; +} + +.a-propos-content { + flex: 1 1 70%; + max-width: 75%; + padding-right: 40px; +} + +.a-propos-toc { + flex: 0 0 25%; + max-width: 25%; + /* Keeps it from growing too large */ + position: sticky; + top: 40px; + /* Sticks 40px from the top of the viewport */ + border-left: 2px solid #333; + /* Dark vertical line like hedgedoc */ + padding-left: 15px; + margin-top: 40px; + background-color: #fff; + /* Aligns visually with the first header */ +} + +/* Hide TOC on smaller screens */ +@media (max-width: 992px) { + .a-propos-content { + max-width: 100%; + padding-right: 0; + flex: 1 1 100%; + } + + .a-propos-toc { + display: none; + } +} + +/* Styling for TOC links */ +.toc-link { + display: block; + color: #666; + text-decoration: none; + font-size: 0.9em; + padding: 2px 0; + transition: color 0.2s, font-weight 0.2s; + line-height: 1.4; +} + +.toc-link:hover { + color: #000; + text-decoration: none; +} + +.toc-active { + color: #000; + font-weight: bold; +} + +/* Indentation for H5 levels (Level 2 in our simplified TOC) */ +/* Using specific classes if I generate them, or just generic hierarchy if nested */ +.toc-level-2 { + margin-left: 15px; + font-size: 0.85em; +} + +/* Header title for TOC (optional) */ +.toc-header { + font-weight: bold; + margin-bottom: 10px; + display: block; + color: #333; +} diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index 25a47c2..8ff9dab 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -17,13 +17,19 @@ register_page( order=5, ) -layout = [ - html.Div( - className="container", - children=[ - html.H2(name), - dcc.Markdown( - """Outil d'exploration libre et gratuit des données de marchés publics, développé par Colin Maudry. +layout = html.Div( + className="container", + children=[ + html.H2(name), + html.Div( + className="a-propos-container", + children=[ + # Main Content Column + html.Div( + className="a-propos-content", + children=[ + dcc.Markdown( + """Outil d'exploration libre et gratuit des données de marchés publics, développé par Colin Maudry. Ce projet vise à démocratiser l'accès aux données des marchés publics et à un outil performant et gratuit. Si vous le trouvez utile j'aimerais beaucoup échanger avec vous pour comprendre vos cas d'usages et vos besoins. Cet outil ne peut rester performant que si je comprends les problèmes qu'il peut aider à résoudre. Ce projet ne peut rester gratuit que grâce au financement du développement de nouvelles fonctionnalités. @@ -34,33 +40,37 @@ les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter - de nombreuses visualisations de données (cartes, graphiques, tableaux) sur des thématiques variées (vivacité de la concurrence, secteurs d'activité, insertion par l'activité économique (IAE), distance acheteur-titulaire...) - des alertes par email si des marchés correspondant à certains critères - ...et toutes les fonctionnalités auxquelles vous pourrez penser - -#### Consommer les données brutes - +""" + ), + html.H4("Consommer les données brutes", id="donnees-brutes"), + dcc.Markdown( + """ Vous pouvez consommer les données qui alimentent decp.info - en les téléchargeant [sur data.gouv.fr](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire) (Parquet, CSV), pensez à lire la description du jeu de données - en interrogeant l'[API REST ouverte](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire#user-content-api-rest) - - """ - ), - html.H4("Contact", id="contact"), - dcc.Markdown(""" - +""" + ), + html.H4("Contact", id="contact"), + dcc.Markdown( + """ - Email : [colin@colmo.tech](mailto:colin@colmo.tech) - Bluesky : [@col1m.bsky.social](https://bsky.app/profile/col1m.bsky.social) - Mastodon : [col1m@mamot.fr](https://mamot.fr/@col1m) - LinkedIn : [colinmaudry](https://www.linkedin.com/in/colinmaudry/) - - """), - html.H4("Pour contribuer", id="contribuer"), - dcc.Markdown(""" +""" + ), + html.H4("Pour contribuer", id="contribuer"), + dcc.Markdown( + """ - via l'achat d'une prestation de service (devis, prestation, facture), vous pouvez financer le développement de [fonctionnalités prévues](https://github.com/ColinMaudry/decp.info/issues), ou d'autres ! - ma société accepte aussi les dons (pas de réduction d'impôt possible) - écrivez-moi et on discute ! - -#### Pour explorer le projet - +""" + ), + html.H4("Pour explorer le projet", id="explorer"), + dcc.Markdown( + """ - ✉️ [inscription à la liste de diffusion](https://6254d9a3.sibforms.com/serve/MUIFAEonUVkoSVrdgey18CTgLyI16xw4yeu-M-YOUzhWE_AgfQfbgkyT7GvA_RYLro9MfuRqkzQxSvu7-uzbMSv2a2ZQPsliM7wtiiqIL8kR2zOvl6m11fb5qjcOxMAYsLiY_YBi3P7NY95CTJ8vRY4CpsDclF2iLooOElKkTgIgi5nePe7zAIrgiYM5v2EuALlGJZMEG9vBP-Cu) (annonces des mises à jour et évènements, maximum une fois par mois) - 💾 [données consolidées en Open Data](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire/) - 🗞️ [mon blog](https://colin.maudry.com) @@ -68,9 +78,14 @@ Vous pouvez consommer les données qui alimentent decp.info - 🚰 code source - [de decp.info](https://github.com/ColinMaudry/decp.info) - [du traitement des données](https://github.com/ColinMaudry/decp-processing) - """), - html.H4("Qualité et exhaustivité des données", id="qualite-exhausitivite"), - dcc.Markdown("""Les données visibles sur ce site proviennent exclusivement de la publication de données ouvertes par les acheteurs publics ou en leur nom, régie par [l'arrêté du 22 décembre 2022](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000046850496). Leur qualité est donc principalement liée à la qualité de leur saisie par les agents publics, parfois peu aidé·es par la qualité des outils à leur disposition. Je pense que l'analyse de marchés individuels et le comptage de marchés sur des critères autres que financiers sont plutôt fiables. En revanche, certains montants de marché estimés à des valeurs farfelues ([1 euro](https://decp.info/marches/432766947000192025S01301), [1 milliard](https://decp.info/marches/2459004280001320210000000271)) faussent les calculs par aggrégation (sommes, moyennes, médianes) et donc la production de statistiques financières fiables. Acheteurs, acheteuses : s'il vous plaît, essayez d'estimer les montants des marchés publics attribués de manière plus précise. +""" + ), + html.H4( + "Qualité et exhaustivité des données", + id="qualite-exhausitivite", + ), + dcc.Markdown( + """Les données visibles sur ce site proviennent exclusivement de la publication de données ouvertes par les acheteurs publics ou en leur nom, régie par [l'arrêté du 22 décembre 2022](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000046850496). Leur qualité est donc principalement liée à la qualité de leur saisie par les agents publics, parfois peu aidé·es par la qualité des outils à leur disposition. Je pense que l'analyse de marchés individuels et le comptage de marchés sur des critères autres que financiers sont plutôt fiables. En revanche, certains montants de marché estimés à des valeurs farfelues ([1 euro](https://decp.info/marches/432766947000192025S01301), [1 milliard](https://decp.info/marches/2459004280001320210000000271)) faussent les calculs par aggrégation (sommes, moyennes, médianes) et donc la production de statistiques financières fiables. Acheteurs, acheteuses : s'il vous plaît, essayez d'estimer les montants des marchés publics attribués de manière plus précise. Quant à l'exhaustivité, je consolide toutes les sources de données exploitables que j'ai pu identifier (voir [statistiques](/statistiques)). Certains profils d'acheteurs ne publient pas leurs données malgré l'obligation réglementaire : @@ -79,21 +94,24 @@ Quant à l'exhaustivité, je consolide toutes les sources de données exploitabl **marches-publics.info** (AWS) publie ses données de manière assez sporadique depuis début 2023. Compte tenu de son poids dans le secteur, c'est assez dommageable pour la transparence des marchés publics. -Au milieu de ces mauvaises nouvelles, je tiens à souligner la belle continuité de la publication par la DGFiP des données des marchés publics remontées via le [protocole PES](https://www.collectivites-locales.gouv.fr/finances-locales/le-protocole-dechange-standard-pes). Merci à leurs équipes."""), - html.H4("Sources de données ", id="sources"), - get_sources_tables(os.getenv("SOURCE_STATS_CSV_PATH")), - html.H4("Mentions légales", id="mentions-legales"), - dcc.Markdown(""" -##### Publication - +Au milieu de ces mauvaises nouvelles, je tiens à souligner la belle continuité de la publication par la DGFiP des données des marchés publics remontées via le [protocole PES](https://www.collectivites-locales.gouv.fr/finances-locales/le-protocole-dechange-standard-pes). Merci à leurs équipes.""" + ), + html.H4("Sources de données ", id="sources"), + get_sources_tables(os.getenv("SOURCE_STATS_CSV_PATH")), + html.H4("Mentions légales", id="mentions-legales"), + html.H5("Publication", id="publication"), + dcc.Markdown( + """ Site Web développé et édité par [SAS Colmo](https://annuaire-entreprises.data.gouv.fr/entreprise/colmo-989393350), 989 393 350 RCS Rennes au capital de 3 000 euros. Siège social : 1 carrefour Jouaust, 35000 Rennes Hébergement : serveur situé en France et administré par Scaleway, 8 rue de la Ville l’Evêque, 75008 Paris - -##### Suivi d'audience - +""" + ), + html.H5("Suivi d'audience", id="audience"), + dcc.Markdown( + """ Ce site dépose un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le consultez ([Wikipédia](https://fr.wikipedia.org/wiki/Cookie_(informatique))). Cela me permet de mesurer le nombre de visites, de distinguer les nouveaux visiteurs des utilisateurs réguliers et ainsi de communiquer sur l'impact de decp.info. **Ce site n’affiche pas de bannière de consentement aux cookies, pourquoi ?** @@ -102,21 +120,64 @@ C’est vrai, vous n’avez pas eu à cliquer sur un bloc qui recouvre la moiti Rien d’exceptionnel, je respecte simplement la loi, qui dit que certains outils de suivi d’audience, correctement configurés pour respecter la vie privée, sont exemptés d’autorisation préalable. -J’utilise pour cela [Matomo](https://matomo.org/), un outil [libre](https://matomo.org/free-software/), paramétré pour être en conformité avec [la recommandation « Cookies »](https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience) de la CNIL. Cela signifie que votre adresse IP, par exemple, est anonymisée avant d’être enregistrée. Il m’est donc impossible d’associer vos visites sur ce site à votre personne."""), - # Matomo propose cependant ce formulaire si vous souhaitez totalement désactiver le suivi de vos sessions sur ce site :"""), - # html.Div( - # id="matomo-opt-out", - # style={ - # "border": "1pt solid lightgrey", - # "padding": "12px", - # "margin": "auto 0 auto 12px", - # "width": "80%", - # }, - # children=["Vous utilisez un bloqueur de suivi de trafic."], - # ), - # html.Script( - # src="https://analytics.maudry.com/index.php?module=CoreAdminHome&action=optOutJS&divId=matomo-opt-out&language=auto&showIntro=1" - # ), - ], - ) -] +J’utilise pour cela [Matomo](https://matomo.org/), un outil [libre](https://matomo.org/free-software/), paramétré pour être en conformité avec [la recommandation « Cookies »](https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience) de la CNIL. Cela signifie que votre adresse IP, par exemple, est anonymisée avant d’être enregistrée. Il m’est donc impossible d’associer vos visites sur ce site à votre personne.""" + ), + ], + ), + # Table of Contents Column + html.Div( + className="a-propos-toc", + children=[ + html.Div( + [ + html.A( + "Consommer les données brutes", + href="#donnees-brutes", + className="toc-link", + ), + html.A( + "Contact", href="#contact", className="toc-link" + ), + html.A( + "Pour contribuer", + href="#contribuer", + className="toc-link", + ), + html.A( + "Pour explorer le projet", + href="#explorer", + className="toc-link", + ), + html.A( + "Qualité et exhaustivité des données", + href="#qualite-exhausitivite", + className="toc-link", + ), + html.A( + "Sources de données", + href="#sources", + className="toc-link", + ), + html.A( + "Mentions légales", + href="#mentions-legales", + className="toc-link", + ), + html.A( + "Publication", + href="#publication", + className="toc-link toc-level-2", + ), + html.A( + "Suivi d'audience", + href="#audience", + className="toc-link toc-level-2", + ), + ] + ), + ], + ), + ], + ), + ], +) From 82bd82944aa9e034cb05adfc1d1d7842d963ca92 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 22 Jan 2026 16:36:55 +0100 Subject: [PATCH 21/23] Changelog #36 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58525e3..f3235bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Site à peu près utilisable sur petit écran (smartphone) ([#63](https://github.com/ColinMaudry/decp.info/issues/63)) - Amélioration du référencement Web (sitemap, titres, descriptions) ([#50](https://github.com/ColinMaudry/decp.info/issues/50)) - Possibilité dans les champs non-numériques de filtrer le texte selon son début ou sa fin (`text*` et `*text`) +- Ajout d'une table des matières dans la page [À propos](https://decp.infi/a-propos) ([#36](https://github.com/ColinMaudry/decp.info/issues/36)) ##### 2.3.1 (16 janvier 2026) From 39faeca4d3242216fd8f6b0261bc63daffcff820 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 22 Jan 2026 16:46:19 +0100 Subject: [PATCH 22/23] =?UTF-8?q?M=C3=A0j=20mode=20d'emploi=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/tableau.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 58d3942..5967e29 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -70,6 +70,7 @@ layout = [ - Champs numériques : vous pouvez soit taper un nombre pour trouver les valeurs égales, soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres. - Champs date : vous pouvez également utiliser **>** ou **<**. Exemples : `< 2024-01-31` pour "avant le 31 janvier 2024", `2024` pour "en 2024", `> 2022` pour "à partir de 2022". Lorsque vous ouvrez une URL de vue, le format équivalent `i<` ou `i>` est utilisé. + - Pour les champs textuels et dates : pour chercher du texte qui **commence par** votre texte, entez `texte*`, pour chercher du texte qui **finit par** votre texte, entez `*texte`. C'est par exemple utile pour filtrer des acheteurs ou titulaires par numéro SIREN (`123456789*`). Vous pouvez filtrer plusieurs colonnes à la fois. Vos filtres sont remis à zéro quand vous rafraîchissez la page. From 42347366564374612949c06c9302a127b37692f9 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 22 Jan 2026 17:42:39 +0100 Subject: [PATCH 23/23] =?UTF-8?q?D=C3=A9blockage=20de=20tous=20les=20bots?= =?UTF-8?q?=20d'agents=20LLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- pyproject.toml | 2 +- src/assets/robots.txt | 33 --------------------------------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9634c91..12eb33b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # decp.info -> v2.3.1 +> v2.4.0 > Outil d'exploration et de téléchargement des données essentielles de la commande publique. => [decp.info](https://decp.info) diff --git a/pyproject.toml b/pyproject.toml index db67ade..c0b847a 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.3.1" +version = "2.4.0" requires-python = ">= 3.10" authors = [ { name = "Colin Maudry", email = "colin@colmo.tech" } diff --git a/src/assets/robots.txt b/src/assets/robots.txt index 9381e66..0ce9c15 100644 --- a/src/assets/robots.txt +++ b/src/assets/robots.txt @@ -6,36 +6,3 @@ Allow: / # --------------------------- # END YOAST BLOCK - -User-agent: GPTBot -Disallow: / -User-agent: ChatGPT-User -Disallow: / -User-agent: Google-Extended -Disallow: / -User-agent: PerplexityBot -Disallow: / -User-agent: Amazonbot -Disallow: / -User-agent: ClaudeBot -Disallow: / -User-Agent: FacebookBot -Disallow: / -User-Agent: Applebot -Disallow: / -User-agent: anthropic-ai -Disallow: / -User-agent: Bytespider -Disallow: / -User-agent: Claude-Web -Disallow: / -User-agent: Diffbot -Disallow: / -User-agent: ImagesiftBot -Disallow: / -User-agent: Omgilibot -Disallow: / -User-agent: Omgili -Disallow: / -User-agent: YouBot -Disallow: /