From 187feee5449f340913a664ebf7eb0e8b499b38f6 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 27 Jan 2026 17:48:34 +0100 Subject: [PATCH 01/13] Ajout d'une arborescence de departements #66 --- src/assets/style.css | 3 +- src/pages/a-propos.py | 8 +++++ src/pages/departement.py | 61 +++++++++++++++++++++++++++++++++++++++ src/pages/departements.py | 20 +++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/pages/departement.py create mode 100644 src/pages/departements.py diff --git a/src/assets/style.css b/src/assets/style.css index a8d203d..ff06faa 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -254,7 +254,8 @@ summary > h3 { } } -/* CSS for the /a-propos page Layout and Table of Contents */ +/* CSS for the /a-propos page Layout and Table of Contents + Généré par Gemini 3.0 Pro */ .a-propos-container { display: flex; diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index 8ff9dab..04c6006 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -122,6 +122,14 @@ Rien d’exceptionnel, je respecte simplement la loi, qui dit que certains outil 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.""" ), + html.H4( + "Liste des marchés par département", id="liste_marches" + ), + dcc.Markdown( + """ + - [Marchés par département](/departements) + """ + ), ], ), # Table of Contents Column diff --git a/src/pages/departement.py b/src/pages/departement.py new file mode 100644 index 0000000..7ac55b3 --- /dev/null +++ b/src/pages/departement.py @@ -0,0 +1,61 @@ +import polars as pl +from dash import Input, Output, callback, dcc, html, register_page + +from src.utils import departements, df_acheteurs_departement + +name = "Département" + + +def get_title(code): + return f"Marchés publics de {departements[code]['departement']} | decp.info" + + +def get_description(code): + return f"Marchés publics passés dans le département {departements[code]['departement']} | decp.info" + + +register_page( + __name__, + path_template="/departements/", + title=get_title, + description=get_description, + order=50, + name=name, +) + +layout = html.Div( + [ + dcc.Location(id="departement_url", refresh="callback-nav"), + html.Div(id="departement_marches"), + ] +) + + +@callback( + Output(component_id="departement_marches", component_property="children"), + Input(component_id="departement_url", component_property="pathname"), +) +def departement_marches(url): + departement = url.split("/")[-1] + acheteurs = [] + df = df_acheteurs_departement.filter( + pl.col("acheteur_departement_code") == departement + ) + for row in df.iter_rows(named=True): + p = html.P( + [ + dcc.Link( + row["acheteur_nom"], + href=url + f"/{row['acheteur_id']}", + title=f"Marchés publics de {row['acheteur_nom']}", + ), + " ", + dcc.Link( + "(page dédiée)", + href=f"/acheteurs/{row['acheteur_id']}", + title=f"Page dédiée aux marchés publics de {row['acheteur_nom']}", + ), + ] + ) + acheteurs.append(p) + return acheteurs diff --git a/src/pages/departements.py b/src/pages/departements.py new file mode 100644 index 0000000..9b122ca --- /dev/null +++ b/src/pages/departements.py @@ -0,0 +1,20 @@ +from dash import dcc, html, register_page + +from src.utils import departements + +name = "Départements" + +register_page( + __name__, + path="/departements", + title="Marchés par département | decp.info", + name="Départements", + description="Tous les marchés publics, classés par départements", +) + +layout = html.Div( + [ + html.P(dcc.Link(d["departement"], href=f"/departements/{k}")) + for k, d in departements.items() + ] +) From 1c4fccac6e0ae1481811b83124da55e59ac08cb8 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 11:49:23 +0100 Subject: [PATCH 02/13] Nom de l'acheteur et du titulaire dans les titres #66 --- src/pages/acheteur.py | 8 +++++++- src/pages/titulaire.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 65419ec..f3ae843 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -7,6 +7,7 @@ from src.callbacks import get_top_org_table from src.figures import DataTable, point_on_map from src.utils import ( df, + df_acheteurs, filter_table_data, format_number, get_annuaire_data, @@ -20,7 +21,12 @@ from src.utils import ( def get_title(acheteur_id: str = None) -> str: - return f"Acheteur {acheteur_id} | decp.info" + acheteur_nom = ( + df_acheteurs.filter(pl.col("acheteur_id") == acheteur_id) + .select("acheteur_nom") + .item() + ) + return f"Marchés publics attribués par {acheteur_nom} | decp.info" register_page( diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index a17e505..60fdc2b 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -7,6 +7,7 @@ from src.callbacks import get_top_org_table from src.figures import DataTable, point_on_map from src.utils import ( df, + df_titulaires, filter_table_data, format_number, get_annuaire_data, @@ -20,7 +21,12 @@ from src.utils import ( def get_title(titulaire_id: str = None) -> str: - return f"Titulaire {titulaire_id} | decp.info" + titulaire_nom = ( + df_titulaires.filter(pl.col("titulaire_id") == titulaire_id) + .select("titulaire_nom") + .item() + ) + return f"Marchés publics remportés par {titulaire_nom} | decp.info" register_page( From 9a1e5bee6325a7580146bf05123e1fed1aa6b47e Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 11:57:41 +0100 Subject: [PATCH 03/13] =?UTF-8?q?D=C3=A9partements=20sous=20forme=20de=20l?= =?UTF-8?q?iste,=20ajout=20des=20titulaires=20par=20d=C3=A9partement=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 3 +- src/pages/departement.py | 63 +++++++++++++++++++++++++-------------- src/pages/departements.py | 9 ++++-- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/app.py b/src/app.py index 61bae1a..8889894 100644 --- a/src/app.py +++ b/src/app.py @@ -135,7 +135,8 @@ navbar = dbc.Navbar( ) ) for page in page_registry.values() - if page["name"] not in ["Acheteur", "Titulaire", "Marché"] + if page["name"] + in ["Recherche", "À propos", "Tableau", "Statistiques"] ], className="ms-auto", navbar=True, diff --git a/src/pages/departement.py b/src/pages/departement.py index 7ac55b3..9822e89 100644 --- a/src/pages/departement.py +++ b/src/pages/departement.py @@ -1,7 +1,7 @@ import polars as pl from dash import Input, Output, callback, dcc, html, register_page -from src.utils import departements, df_acheteurs_departement +from src.utils import departements, df_acheteurs_departement, df_titulaires_departement name = "Département" @@ -37,25 +37,42 @@ layout = html.Div( ) def departement_marches(url): departement = url.split("/")[-1] - acheteurs = [] - df = df_acheteurs_departement.filter( - pl.col("acheteur_departement_code") == departement - ) - for row in df.iter_rows(named=True): - p = html.P( - [ - dcc.Link( - row["acheteur_nom"], - href=url + f"/{row['acheteur_id']}", - title=f"Marchés publics de {row['acheteur_nom']}", - ), - " ", - dcc.Link( - "(page dédiée)", - href=f"/acheteurs/{row['acheteur_id']}", - title=f"Page dédiée aux marchés publics de {row['acheteur_nom']}", - ), - ] - ) - acheteurs.append(p) - return acheteurs + + def make_link_list(org_type) -> list: + link_list = [] + if org_type == "acheteur": + df = df_acheteurs_departement + elif org_type == "titulaire": + df = df_titulaires_departement + else: + raise ValueError + + df = df.filter(pl.col(f"{org_type}_departement_code") == departement) + + for row in df.iter_rows(named=True): + li = html.Li( + [ + dcc.Link( + row[f"{org_type}_nom"], + href=url + f"/{row[f'{org_type}_id']}", + title=f"Marchés publics de {row[f'{org_type}_nom']}", + ), + " ", + dcc.Link( + "(page dédiée)", + href=f"/{org_type}s/{row[f'{org_type}_id']}", + title=f"Page dédiée aux marchés publics de {row[f'{org_type}_nom']}", + ), + ] + ) + link_list.append(li) + return link_list + + content = [ + html.H3("Acheteurs publics du département"), + html.Ul(make_link_list("acheteur")), + html.H3("Titulaires du département"), + html.Ul(make_link_list("titulaire")), + ] + + return content diff --git a/src/pages/departements.py b/src/pages/departements.py index 9b122ca..48c74d5 100644 --- a/src/pages/departements.py +++ b/src/pages/departements.py @@ -14,7 +14,12 @@ register_page( layout = html.Div( [ - html.P(dcc.Link(d["departement"], href=f"/departements/{k}")) - for k, d in departements.items() + html.H3("Départements"), + html.Ul( + [ + html.Li(dcc.Link(d["departement"], href=f"/departements/{k}")) + for k, d in departements.items() + ] + ), ] ) From a4bda0483fd4fea98a7972b7ff17f6e2d6f6a8d4 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 11:58:41 +0100 Subject: [PATCH 04/13] =?UTF-8?q?D=C3=A9placement=20de=20la=20g=C3=A9n?= =?UTF-8?q?=C3=A9ration=20des=20df=20en=20bas,=20nouveaux=20dfs=20de=20bas?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils.py | 60 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/utils.py b/src/utils.py index 1c8474e..3de7848 100644 --- a/src/utils.py +++ b/src/utils.py @@ -701,27 +701,6 @@ def invert_columns(columns): return inverted_columns -df: pl.DataFrame = get_decp_data() -schema = df.collect_schema() - -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" -) -meta_content = { - "image_url": f"https://{domain_name}/assets/decp.info.png", - "title": "decp.info - exploration des marchés publics français", - "description": ( - "Explorez et analysez les données des marchés publics français avec cet outil libre et gratuit. " - "Pour une commande publique accessible à toutes et tous." - ), -} -data_schema = get_data_schema() - - def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dict: org_types = {"acheteur": "GovernmentOrganization", "titulaire": "Organization"} address = None @@ -759,3 +738,42 @@ def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dic jsonld["address"] = address return jsonld + + +df: pl.DataFrame = get_decp_data() +schema = df.collect_schema() + +df_acheteurs = get_org_data(df, "acheteur") +df_titulaires = get_org_data(df, "titulaire") +df_acheteurs_departement: pl.DataFrame = ( + df_acheteurs.select(["acheteur_id", "acheteur_nom", "acheteur_departement_code"]) + .unique() + .sort("acheteur_nom") +) +df_titulaires_departement: pl.DataFrame = ( + df_titulaires.select( + ["titulaire_id", "titulaire_nom", "titulaire_departement_code"] + ) + .unique() + .sort("titulaire_nom") +) +df_acheteurs_marches: pl.DataFrame = ( + df.select("uid", "acheteur_id").unique().sort("acheteur_id") +) +df_titulaires_marches: pl.DataFrame = ( + df.select("uid", "titulaire_id").unique().sort("titulaire_id") +) + +departements = get_departements() +domain_name = ( + "test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info" +) +meta_content = { + "image_url": f"https://{domain_name}/assets/decp.info.png", + "title": "decp.info - exploration des marchés publics français", + "description": ( + "Explorez et analysez les données des marchés publics français avec cet outil libre et gratuit. " + "Pour une commande publique accessible à toutes et tous." + ), +} +data_schema = get_data_schema() From a385e999d38e844bcdb82369eb565f66b8d82678 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 11:59:04 +0100 Subject: [PATCH 05/13] Plus jolie table des sources --- src/figures.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/figures.py b/src/figures.py index 5d6a723..aff70c1 100644 --- a/src/figures.py +++ b/src/figures.py @@ -169,14 +169,22 @@ def get_sources_tables(source_path) -> html.Div: + pl.lit("") ).alias("nom") ) - df = df.drop("url") + df = df.drop("url", "unique") df = df.sort(by=["nb_marchés"], descending=True) + columns = { + "nom": "Nom de la source", + "organisation": "Responsable de publication", + "nb_marchés": "Nb de marchés", + "nb_acheteurs": "Nb d'acheteurs", + "code": "Code", + } + datatable = dash_table.DataTable( id="source_table", columns=[ { - "name": i, + "name": columns[i], "id": i, "presentation": "markdown", "type": "text", From b3bd488882b785b3b35413def6b24410547f8ab3 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 12:48:47 +0100 Subject: [PATCH 06/13] =?UTF-8?q?Liste=20des=20march=C3=A9s=20par=20achete?= =?UTF-8?q?ur=20et=20par=20titulaire=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/departement.py | 2 +- src/pages/liste_marches_org.py | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/pages/liste_marches_org.py diff --git a/src/pages/departement.py b/src/pages/departement.py index 9822e89..303ef80 100644 --- a/src/pages/departement.py +++ b/src/pages/departement.py @@ -54,7 +54,7 @@ def departement_marches(url): [ dcc.Link( row[f"{org_type}_nom"], - href=url + f"/{row[f'{org_type}_id']}", + href=url + f"/{org_type}/{row[f'{org_type}_id']}", title=f"Marchés publics de {row[f'{org_type}_nom']}", ), " ", diff --git a/src/pages/liste_marches_org.py b/src/pages/liste_marches_org.py new file mode 100644 index 0000000..6629333 --- /dev/null +++ b/src/pages/liste_marches_org.py @@ -0,0 +1,99 @@ +import polars as pl +from dash import Input, Output, callback, dcc, html, register_page + +from src.utils import ( + df_acheteurs, + df_acheteurs_marches, + df_titulaires, + df_titulaires_marches, +) + +name = "Liste des marchés publics" + + +def make_org_nom_verbe(org_type, org_id) -> tuple: + if org_type == "titulaire": + df = df_titulaires + verbe = "remportés" + elif org_type == "acheteur": + df = df_acheteurs + verbe = "attribués" + else: + raise ValueError + + org_nom = ( + df.filter(pl.col(f"{org_type}_id") == org_id).select(f"{org_type}_nom").item() + ) + + return org_nom, verbe + + +def get_title(code, org_type, org_id): + org_nom, verbe = make_org_nom_verbe(org_type, org_id) + + return f"Marchés publics {verbe} par {org_nom} | decp.info" + + +def get_description(code, org_type, org_id): + org_nom, verbe = make_org_nom_verbe(org_type, org_id) + + return f"Liste complète des marchés publics {verbe} par {org_nom} et publiés par decp.info. Cliquez sur les liens pour consulter les détails de chaque marché." + + +register_page( + __name__, + path_template="/departements///", + title=get_title, + description=get_description, + order=40, + name=name, +) + +layout = html.Div( + [ + dcc.Location(id="liste_marches_url", refresh="callback-nav"), + html.Div(id="liste_marches"), + ] +) + + +@callback( + Output(component_id="liste_marches", component_property="children"), + Input(component_id="liste_marches_url", component_property="pathname"), +) +def liste_marches(url): + org_type = url.split("/")[-2] + org_id = url.split("/")[-1] + + def make_link_list() -> list: + link_list = [] + if org_type == "acheteur": + df = df_acheteurs_marches + elif org_type == "titulaire": + df = df_titulaires_marches + else: + raise ValueError + + df = df.filter(pl.col(f"{org_type}_id") == org_id) + + for row in df.iter_rows(named=True): + li = html.Li( + [ + dcc.Link( + row["objet"], + href=f"/marches/{row['uid']}", + title=f"Marchés public attribué : {row['objet']}", + ) + ] + ) + link_list.append(li) + return link_list + + nom, verbe = make_org_nom_verbe(org_type, org_id) + + content = [ + html.H3(f"Marchés publics {verbe} par {nom}"), + html.Ul(make_link_list()), + ] + + return content From a76a14b030ecadc65141d7859977e945f2335603 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 12:49:04 +0100 Subject: [PATCH 07/13] Petits ajustements --- src/pages/marche.py | 7 ++++--- src/utils.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/marche.py b/src/pages/marche.py index 632cac7..f971ed5 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -114,7 +114,7 @@ def get_marche_data(url) -> tuple[dict, list]: Input("titulaires_data", "data"), ) def update_marche_info(marche, titulaires): - def make_parameter(col): + def make_parameter(col, bold=True): column_object = data_schema.get(col) column_name = column_object.get("title") if column_object else col @@ -158,10 +158,11 @@ def update_marche_info(marche, titulaires): else: value = "" - param_content = html.P([column_name, " : ", html.Strong(value)]) + value = html.Strong(value) if bold else value + param_content = html.P([column_name, " : ", value]) return param_content - marche_objet = make_parameter("objet") + marche_objet = make_parameter("objet", bold=False) marche_infos = [ make_parameter("id"), diff --git a/src/utils.py b/src/utils.py index 3de7848..01b2d97 100644 --- a/src/utils.py +++ b/src/utils.py @@ -145,11 +145,12 @@ def format_number(number) -> str: return number -def unformat_montant(number: str) -> int: +def unformat_montant(number: str) -> float: number = number.replace(" €", "") number = number.replace(" €", "").replace(" ", "") + number = number.replace(",", ".") number = number.strip() - return int(number) + return float(number) def format_values(dff: pl.DataFrame) -> pl.DataFrame: @@ -758,10 +759,10 @@ df_titulaires_departement: pl.DataFrame = ( .sort("titulaire_nom") ) df_acheteurs_marches: pl.DataFrame = ( - df.select("uid", "acheteur_id").unique().sort("acheteur_id") + df.select("uid", "objet", "acheteur_id").unique().sort("acheteur_id") ) df_titulaires_marches: pl.DataFrame = ( - df.select("uid", "titulaire_id").unique().sort("titulaire_id") + df.select("uid", "objet", "titulaire_id").unique().sort("titulaire_id") ) departements = get_departements() From 6e992e0a6ed2658085d9d7b494c54b61d328aed4 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 13:30:47 +0100 Subject: [PATCH 08/13] =?UTF-8?q?Ajout=20des=20donn=C3=A9es=20JSON-LD=20ac?= =?UTF-8?q?heteur=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/acheteur.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index f3ae843..b05ede9 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -1,4 +1,5 @@ import datetime +import json import polars as pl from dash import Input, Output, State, callback, dcc, html, register_page @@ -14,6 +15,7 @@ from src.utils import ( get_button_properties, get_default_hidden_columns, get_departement_region, + make_org_jsonld, meta_content, prepare_table_data, sort_table_data, @@ -54,6 +56,7 @@ datatable = html.Div( layout = [ dcc.Store(id="acheteur_data", storage_type="memory"), dcc.Location(id="acheteur_url", refresh="callback-nav"), + html.Script(type="application/ld+json", id="acheteur_jsonld"), html.Div( children=[ html.Div( @@ -331,3 +334,19 @@ def download_filtered_acheteur_data( return dcc.send_bytes( to_bytes, filename=f"decp_filtrées_{acheteur_nom}_{date}.xlsx" ) + + +@callback( + Output(component_id="acheteur_jsonld", component_property="children"), + Input("acheteur_data", "data"), +) +def get_acheteur_jsonld(acheteur) -> str: + acheteur = acheteur[0] + acheteur_id = acheteur.get("acheteur_id") + + jsonld = make_org_jsonld( + acheteur_id, org_name=acheteur.get("acheteur_nom"), org_type="acheteur" + ) + jsonld["@context"] = "http://schema.org/" + + return json.dumps(jsonld, indent=2) From 07bdcf232d9d90207e323fdf75036a6fd54791f5 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 13:32:08 +0100 Subject: [PATCH 09/13] =?UTF-8?q?Ajout=20des=20donn=C3=A9es=20JSON-LD=20ac?= =?UTF-8?q?heteur=20et=20fix=20make=5Forg=5Fjsonld=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/marche.py | 4 +-- src/pages/tableau.py | 62 ++++++++++++++++++++++++++++++++++++++++++-- src/utils.py | 3 ++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/pages/marche.py b/src/pages/marche.py index f971ed5..f0f4f2f 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -34,9 +34,7 @@ layout = [ dcc.Store(id="marche_data"), dcc.Store(id="titulaires_data"), dcc.Location(id="marche_url", refresh="callback-nav"), - html.Script( - type="application/ld+json", id="marche_jsonld", children=['{"test": "1"}'] - ), + html.Script(type="application/ld+json", id="marche_jsonld"), dbc.Container( className="marche_infos", children=[ diff --git a/src/pages/tableau.py b/src/pages/tableau.py index 35bdfc9..a8cf2bd 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -18,8 +18,9 @@ from src.utils import ( ) from utils import prepare_table_data -update_date = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH")) -update_date = datetime.fromtimestamp(update_date).strftime("%d/%m/%Y") +update_date_timestamp = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH")) +update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y") +update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat() name = "Tableau" @@ -48,6 +49,63 @@ datatable = html.Div( layout = [ dcc.Location(id="tableau_url", refresh=False), + html.Script( + type="application/ld+json", + id="dataset_jsonld", + children=[ + json.dumps( + { + "@context": "https://schema.org/", + "@type": "Dataset", + "name": "Données essentielles des marchés publics français (DECP)", + "description": "Données de marchés publics exhaustives décrivant les marchés publics attribués en France depuis 2018.", + "url": "https://decp.info", + "sameAs": "https://www.data.gouv.fr/datasets/608c055b35eb4e6ee20eb325", + "keywords": [ + "marchés publics", + "commande publique", + "decp", + "public procurement", + ], + "license": "https://www.etalab.gouv.fr/licence-ouverte-open-licence", + "isAccessibleForFree": True, + "creator": { + "@type": "Organization", + "url": "https://colmo.tech", + "name": "Colmo", + "sameAs": "https://annuaire-entreprises.data.gouv.fr/entreprise/colmo-989393350", + "contactPoint": { + "@type": "ContactPoint", + "contactType": "Support et contact commercial", + "email": "colin@colmo.tech", + }, + }, + "includedInDataCatalog": { + "@type": "DataCatalog", + "name": "data.gouv.fr", + }, + "distribution": [ + { + "@type": "DataDownload", + "encodingFormat": "CSV", + "contentUrl": "https://www.data.gouv.fr/api/1/datasets/r/22847056-61df-452d-837d-8b8ceadbfc52", + }, + { + "@type": "DataDownload", + "encodingFormat": "Parquet", + "contentUrl": "https://www.data.gouv.fr/api/1/datasets/r/11cea8e8-df3e-4ed1-932b-781e2635e432", + }, + ], + "temporalCoverage": f"2018-01-01/{update_date_iso[:10]}", + "spatialCoverage": { + "@type": "Place", + "address": {"countryCode": "FR"}, + }, + }, + indent=2, + ) + ], + ), html.Div( html.Details( children=[ diff --git a/src/utils.py b/src/utils.py index 01b2d97..016752e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -727,7 +727,8 @@ def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dic jsonld = { "@type": org_types[org_type], "name": org_name, - "url": f"https://decp.info/titulaires/{org_id}", + "url": f"https://decp.info/{org_type}s/{org_id}", + "sameAs": f"https://annuaire-entreprises.data.gouv.fr/etablissement/{org_id}", "identifier": { "@type": "PropertyValue", "propertyID": type_org_id.lower(), From 1eecc45e29babba7054678289715f16a0b8fb52a Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 13:49:19 +0100 Subject: [PATCH 10/13] =?UTF-8?q?Meta=20keywords=20(m=C3=AAme=20si=20a=20p?= =?UTF-8?q?riori=20=C3=A7a=20aide=20pas=20pour=20le=20SEO)=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.py b/src/app.py index 8889894..260ed45 100644 --- a/src/app.py +++ b/src/app.py @@ -16,6 +16,10 @@ app = Dash( compress=True, meta_tags=[ {"name": "viewport", "content": "width=device-width, initial-scale=1"}, + { + "name": "keywords", + "content": "commande publique, decp, marchés publics, données essentielles", + }, ], ) # COSMO (belle font, blue), From f5d802606156ddfac5e0fcd8b12a27ad6e9f3421 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 13:50:05 +0100 Subject: [PATCH 11/13] =?UTF-8?q?Ajout=20des=20donn=C3=A9es=20JSON-LD=20au?= =?UTF-8?q?x=20pages=20titulaires=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/titulaire.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 60fdc2b..f3ae17d 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -1,4 +1,5 @@ import datetime +import json import polars as pl from dash import Input, Output, State, callback, dcc, html, register_page @@ -14,6 +15,7 @@ from src.utils import ( get_button_properties, get_default_hidden_columns, get_departement_region, + make_org_jsonld, meta_content, prepare_table_data, sort_table_data, @@ -54,6 +56,7 @@ datatable = html.Div( layout = [ dcc.Store(id="titulaire_data", storage_type="memory"), dcc.Location(id="titulaire_url", refresh="callback-nav"), + html.Script(type="application/ld+json", id="titulaire_jsonld"), html.Div( children=[ html.Div( @@ -340,3 +343,19 @@ def download_filtered_titulaire_data( return dcc.send_bytes( to_bytes, filename=f"decp_filtrées_{titulaire_nom}_{date}.xlsx" ) + + +@callback( + Output(component_id="titulaire_jsonld", component_property="children"), + Input("titulaire_data", "data"), +) +def get_titulaire_jsonld(titulaire) -> str: + titulaire = titulaire[0] + titulaire_id = titulaire.get("acheteur_id") + + jsonld = make_org_jsonld( + titulaire_id, org_name=titulaire.get("titulaire_nom"), org_type="titulaire" + ) + jsonld["@context"] = "http://schema.org/" + + return json.dumps(jsonld, indent=2) From e8534c81088fe9b4246d403f48866654367c82c5 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 14:01:15 +0100 Subject: [PATCH 12/13] Finalement suppression des json-ld titulaire et acheteur #66 --- src/pages/acheteur.py | 19 ------------------- src/pages/titulaire.py | 19 ------------------- 2 files changed, 38 deletions(-) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index b05ede9..f3ae843 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -1,5 +1,4 @@ import datetime -import json import polars as pl from dash import Input, Output, State, callback, dcc, html, register_page @@ -15,7 +14,6 @@ from src.utils import ( get_button_properties, get_default_hidden_columns, get_departement_region, - make_org_jsonld, meta_content, prepare_table_data, sort_table_data, @@ -56,7 +54,6 @@ datatable = html.Div( layout = [ dcc.Store(id="acheteur_data", storage_type="memory"), dcc.Location(id="acheteur_url", refresh="callback-nav"), - html.Script(type="application/ld+json", id="acheteur_jsonld"), html.Div( children=[ html.Div( @@ -334,19 +331,3 @@ def download_filtered_acheteur_data( return dcc.send_bytes( to_bytes, filename=f"decp_filtrées_{acheteur_nom}_{date}.xlsx" ) - - -@callback( - Output(component_id="acheteur_jsonld", component_property="children"), - Input("acheteur_data", "data"), -) -def get_acheteur_jsonld(acheteur) -> str: - acheteur = acheteur[0] - acheteur_id = acheteur.get("acheteur_id") - - jsonld = make_org_jsonld( - acheteur_id, org_name=acheteur.get("acheteur_nom"), org_type="acheteur" - ) - jsonld["@context"] = "http://schema.org/" - - return json.dumps(jsonld, indent=2) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index f3ae17d..60fdc2b 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -1,5 +1,4 @@ import datetime -import json import polars as pl from dash import Input, Output, State, callback, dcc, html, register_page @@ -15,7 +14,6 @@ from src.utils import ( get_button_properties, get_default_hidden_columns, get_departement_region, - make_org_jsonld, meta_content, prepare_table_data, sort_table_data, @@ -56,7 +54,6 @@ datatable = html.Div( layout = [ dcc.Store(id="titulaire_data", storage_type="memory"), dcc.Location(id="titulaire_url", refresh="callback-nav"), - html.Script(type="application/ld+json", id="titulaire_jsonld"), html.Div( children=[ html.Div( @@ -343,19 +340,3 @@ def download_filtered_titulaire_data( return dcc.send_bytes( to_bytes, filename=f"decp_filtrées_{titulaire_nom}_{date}.xlsx" ) - - -@callback( - Output(component_id="titulaire_jsonld", component_property="children"), - Input("titulaire_data", "data"), -) -def get_titulaire_jsonld(titulaire) -> str: - titulaire = titulaire[0] - titulaire_id = titulaire.get("acheteur_id") - - jsonld = make_org_jsonld( - titulaire_id, org_name=titulaire.get("titulaire_nom"), org_type="titulaire" - ) - jsonld["@context"] = "http://schema.org/" - - return json.dumps(jsonld, indent=2) From 639b342178a432e8b31076d644639c788d9496d0 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 28 Jan 2026 14:02:25 +0100 Subject: [PATCH 13/13] Changelog #66 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3aa24..efc84aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +#### 2.5.0 () + +- Renforcement du SEO avec une arborescence permettant l'accès aux marchés et des snippets JSON-LD + ##### 2.4.1 (22 janvier 2026) - Meilleure gestion des colonnes absentes du schéma