From 3eec248dabdcdfb837185fbf1651eed6698aea65 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 09:50:19 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Fix=20tokens=5Fcli=20est=20maintenant=20aut?= =?UTF-8?q?onome=20pour=20r=C3=A9cup=C3=A9rer=20l'env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/tokens_cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/tokens_cli.py b/src/api/tokens_cli.py index 287ac06..34072e6 100644 --- a/src/api/tokens_cli.py +++ b/src/api/tokens_cli.py @@ -2,10 +2,13 @@ import argparse import os import sys +from dotenv import load_dotenv + from src.api import tokens_db def main(argv=None, env=None) -> int: + load_dotenv() env = env if env is not None else os.environ parser = argparse.ArgumentParser(prog="python -m src.api.tokens_cli") sub = parser.add_subparsers(dest="cmd", required=True) From 06fad7ec2de4c1bfd61155f7c3fc6b48832641c6 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 09:54:45 +0200 Subject: [PATCH 2/6] Changelog 2.8.1 --- CHANGELOG.md | 6 +++++- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a3ce1..e71efc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -#### 2.8.0 +##### 2.8.1 (25 juin 2026) + +- Correction du bug dans la création de token d'API + +#### 2.8.0 (23 juin 2026) - Ajout des considérations sociales et environnementales à l'observatoire - Les filtres textuels dans les vues tableaux ne sont plus sensibles à l'accentuation diff --git a/pyproject.toml b/pyproject.toml index bbe0206..3f2967c 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.8.0" +version = "2.8.1" requires-python = ">= 3.10" authors = [{ name = "Colin Maudry", email = "colin@colmo.tech" }] dependencies = [ From b4f7e94800f6ff7fdf7e51a969342ddd53e73ec3 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 01:19:36 +0200 Subject: [PATCH 3/6] Redesign de a-propos --- src/assets/css/style.css | 17 ++ src/figures.py | 2 +- src/pages/_apropos_shell.py | 67 ++++++++ src/pages/a-propos.py | 221 +++---------------------- src/pages/a_propos/__init__.py | 0 src/pages/a_propos/contact.py | 29 ++++ src/pages/a_propos/contribuer.py | 28 ++++ src/pages/a_propos/donnees_brutes.py | 28 ++++ src/pages/a_propos/explorer.py | 32 ++++ src/pages/a_propos/mentions_legales.py | 61 +++++++ src/pages/a_propos/presentation.py | 35 ++++ src/pages/a_propos/qualite.py | 33 ++++ src/pages/a_propos/sources.py | 25 +++ 13 files changed, 378 insertions(+), 200 deletions(-) create mode 100644 src/pages/_apropos_shell.py create mode 100644 src/pages/a_propos/__init__.py create mode 100644 src/pages/a_propos/contact.py create mode 100644 src/pages/a_propos/contribuer.py create mode 100644 src/pages/a_propos/donnees_brutes.py create mode 100644 src/pages/a_propos/explorer.py create mode 100644 src/pages/a_propos/mentions_legales.py create mode 100644 src/pages/a_propos/presentation.py create mode 100644 src/pages/a_propos/qualite.py create mode 100644 src/pages/a_propos/sources.py diff --git a/src/assets/css/style.css b/src/assets/css/style.css index ebe7ecb..2cc9b78 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -138,6 +138,23 @@ p.version > a { font-weight: 600; } +.account-nav .nav-link { + color: inherit; + padding-left: 10px; + border-left: 3px solid transparent; + border-radius: 0; +} + +.account-nav .nav-link:hover { + background-color: rgba(255, 240, 240, 0.9); +} + +.account-nav .nav-link.active { + color: #d9230f; + font-weight: 600; + border-left-color: #d9230f; + background-color: transparent; +} #announcements { margin: 25px 40px 0 60px; font-size: 90%; diff --git a/src/figures.py b/src/figures.py index 565001f..92bc86b 100644 --- a/src/figures.py +++ b/src/figures.py @@ -161,7 +161,7 @@ def get_sources_tables(source_path) -> html.Div: style_cell_conditional=[ { "if": {"column_id": ["nom", "organisation"]}, - "minWidth": "350px", + "minWidth": "220px", "textAlign": "left", "overflow": "hidden", "lineHeight": "14px", diff --git a/src/pages/_apropos_shell.py b/src/pages/_apropos_shell.py new file mode 100644 index 0000000..48fd15d --- /dev/null +++ b/src/pages/_apropos_shell.py @@ -0,0 +1,67 @@ +import dash_bootstrap_components as dbc +from dash import Input, Output, State, callback, html + +SECTIONS = [ + {"key": "presentation", "label": "Présentation", "href": "/a-propos/presentation"}, + { + "key": "donnees-brutes", + "label": "Données brutes", + "href": "/a-propos/donnees-brutes", + }, + {"key": "contact", "label": "Contact", "href": "/a-propos/contact"}, + {"key": "contribuer", "label": "Contribuer", "href": "/a-propos/contribuer"}, + {"key": "explorer", "label": "Explorer le projet", "href": "/a-propos/explorer"}, + {"key": "qualite", "label": "Qualité des données", "href": "/a-propos/qualite"}, + {"key": "sources", "label": "Sources de données", "href": "/a-propos/sources"}, + { + "key": "mentions-legales", + "label": "Mentions légales", + "href": "/a-propos/mentions-legales", + }, +] + + +def _nav(active: str): + links = [ + dbc.NavLink(s["label"], href=s["href"], active=(s["key"] == active)) + for s in SECTIONS + ] + return dbc.Nav(links, vertical=True, class_name="account-nav") + + +def apropos_shell(active: str, contenu): + sidebar = dbc.Col( + html.Div([html.H5("À propos", className="mb-3"), _nav(active)]), + md=3, + className="d-none d-md-block", + ) + mobile = html.Div( + [ + dbc.Button( + "☰ Sections", + id="apropos-offcanvas-open", + color="secondary", + outline=True, + className="mb-3", + ), + dbc.Offcanvas( + _nav(active), + id="apropos-offcanvas", + title="À propos", + is_open=False, + ), + ], + className="d-md-none", + ) + content = dbc.Col([mobile, contenu], md=9) + return dbc.Container(dbc.Row([sidebar, content]), className="py-4") + + +@callback( + Output("apropos-offcanvas", "is_open"), + Input("apropos-offcanvas-open", "n_clicks"), + State("apropos-offcanvas", "is_open"), + prevent_initial_call=True, +) +def _toggle_apropos_offcanvas(_n, is_open): + return not is_open diff --git a/src/pages/a-propos.py b/src/pages/a-propos.py index 2e69d49..0610df9 100644 --- a/src/pages/a-propos.py +++ b/src/pages/a-propos.py @@ -1,12 +1,7 @@ -import os +from dash import Input, Output, callback, dcc, html, register_page -from dash import dcc, html, register_page - -from src.figures import get_sources_tables from src.utils.seo import META_CONTENT -NAME = "À propos" - register_page( __name__, path="/a-propos", @@ -17,201 +12,29 @@ register_page( order=5, ) -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. +_HASH_MAP = { + "#donnees-brutes": "/a-propos/donnees-brutes", + "#api-privee": "/a-propos/api-privee", + "#contact": "/a-propos/contact", + "#contribuer": "/a-propos/contribuer", + "#explorer": "/a-propos/explorer", + "#qualite-exhausitivite": "/a-propos/qualite", + "#sources": "/a-propos/sources", + "#mentions-legales": "/a-propos/mentions-legales", + "#publication": "/a-propos/mentions-legales", + "#audience": "/a-propos/mentions-legales", + "#attributions": "/a-propos/mentions-legales", + "#liste_marches": "/a-propos/mentions-legales", +} -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. -En effet, le potentiel des données d'attribution de marchés et des données qui peuvent les enrichir est très loin d'être exploité par -les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter +def layout(**_): + return html.Div(dcc.Location(id="apropos-redirect-loc")) -- 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 -""" - ), - 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("API privée", id="api-privee"), - dcc.Markdown( - """ -Une API HTTP est disponible pour accéder aux mêmes données par programme. -Documentation interactive : [Swagger UI](/api/v1/swagger). - -L'accès se fait sur token. Pour en obtenir un, contactez -[colin@maudry.com](mailto:colin@maudry.com). -""" - ), - 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( - """ -- 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 ! -""" - ), - 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) -- 📔 [wiki du projet](https://github.com/ColinMaudry/decp-processing/wiki) -- 🚰 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. - -Quant à l'exhaustivité, je consolide toutes les sources de données exploitables que j'ai pu identifier (voir [ci-dessous](/bin.usr-is-merged/)). 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 -""" - ), - 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 ?** - -C’est vrai, vous n’avez pas eu à cliquer sur un bloc qui recouvre la moitié de la page pour dire que vous êtes d’accord avec le dépôt de cookies. - -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. - -J'enregistre également les données suivantes, de manière anonyme, afin de mieux comprendre comment vous utilisez le site et l'améliorer : - -- recherches sur la page d'accueil -- filtres appliqués aux données -""" - ), - html.H5("Attributions", id="attributions"), - dcc.Markdown(""" - Les polices de caractères sont distribuées par [Bunny fonts](https://fonts.bunny.net), une alternative européenne et qualitative à Google Fonts. - - - la police de caractère [Inter](https://fonts.bunny.net/family/inter), principale police de ce site, a été créée par The Inter Project Authors ([source](https://github.com/rsms/inter)) - - la police de caractère [Fira Code](https://fonts.bunny.net/family/fira-code), la police à largeure fixe, a été créée par The Fira Code Project Authors (https://github.com/tonsky/FiraCode) - """), - html.H4( - "Liste des marchés par département", id="liste_marches" - ), - dcc.Markdown( - """ - - [Marchés par département](/departements) - """ - ), - ], - ), - # 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( - "API privée", - href="#api-privee", - 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", - ), - html.A( - "Attributions", - href="#attributions", - className="toc-link toc-level-2", - ), - ] - ), - ], - ), - ], - ), - ], +@callback( + Output("apropos-redirect-loc", "href"), + Input("apropos-redirect-loc", "hash"), ) +def _redirect(hash_val): + return _HASH_MAP.get(hash_val or "", "/a-propos/presentation") diff --git a/src/pages/a_propos/__init__.py b/src/pages/a_propos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/a_propos/contact.py b/src/pages/a_propos/contact.py new file mode 100644 index 0000000..2da12a8 --- /dev/null +++ b/src/pages/a_propos/contact.py @@ -0,0 +1,29 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/contact", + title="Contact | À propos | decp.info", + description="Contactez Colin Maudry, développeur de decp.info.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("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/) +""" + ), + ] + ) + return apropos_shell("contact", contenu) diff --git a/src/pages/a_propos/contribuer.py b/src/pages/a_propos/contribuer.py new file mode 100644 index 0000000..55c564b --- /dev/null +++ b/src/pages/a_propos/contribuer.py @@ -0,0 +1,28 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/contribuer", + title="Contribuer | À propos | decp.info", + description="Comment contribuer au projet decp.info.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Pour 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 ! +""" + ), + ] + ) + return apropos_shell("contribuer", contenu) diff --git a/src/pages/a_propos/donnees_brutes.py b/src/pages/a_propos/donnees_brutes.py new file mode 100644 index 0000000..cfa2e2e --- /dev/null +++ b/src/pages/a_propos/donnees_brutes.py @@ -0,0 +1,28 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/donnees-brutes", + title="Données brutes | À propos | decp.info", + description="Téléchargez ou interrogez les données brutes qui alimentent decp.info.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Consommer les données 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 + +Une API REST tabulaire (JSON) est également disponible par abonnement mensuel pour accéder aux mêmes données et alimenter une application. +Documentation interactive : [Swagger UI](/api/v1/swagger). Si cela vous intéresse, [contactez-moi](/a-propos/contact).""" + ), + ] + ) + return apropos_shell("donnees-brutes", contenu) diff --git a/src/pages/a_propos/explorer.py b/src/pages/a_propos/explorer.py new file mode 100644 index 0000000..923f29b --- /dev/null +++ b/src/pages/a_propos/explorer.py @@ -0,0 +1,32 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/explorer", + title="Explorer le projet | À propos | decp.info", + description="Ressources pour explorer le projet decp.info : données, code source, blog.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Pour explorer le projet"), + 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) +- 📔 [wiki du projet](https://github.com/ColinMaudry/decp-processing/wiki) +- 🚰 code source + - [de decp.info](https://github.com/ColinMaudry/decp.info) + - [du traitement des données](https://github.com/ColinMaudry/decp-processing) +""" + ), + ] + ) + return apropos_shell("explorer", contenu) diff --git a/src/pages/a_propos/mentions_legales.py b/src/pages/a_propos/mentions_legales.py new file mode 100644 index 0000000..151662e --- /dev/null +++ b/src/pages/a_propos/mentions_legales.py @@ -0,0 +1,61 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/mentions-legales", + title="Mentions légales | À propos | decp.info", + description="Mentions légales de decp.info : éditeur, hébergement, suivi d'audience, attributions.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Mentions légales"), + html.H4("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 +""" + ), + html.H4("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 ?** + +C'est vrai, vous n'avez pas eu à cliquer sur un bloc qui recouvre la moitié de la page pour dire que vous êtes d'accord avec le dépôt de cookies. + +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. + +J'enregistre également les données suivantes, de manière anonyme, afin de mieux comprendre comment vous utilisez le site et l'améliorer : + +- recherches sur la page d'accueil +- filtres appliqués aux données +""" + ), + html.H4("Attributions", id="attributions"), + dcc.Markdown( + """ +Les polices de caractères sont distribuées par [Bunny fonts](https://fonts.bunny.net), une alternative européenne et qualitative à Google Fonts. + +- la police de caractère [Inter](https://fonts.bunny.net/family/inter), principale police de ce site, a été créée par The Inter Project Authors ([source](https://github.com/rsms/inter)) +- la police de caractère [Fira Code](https://fonts.bunny.net/family/fira-code), la police à largeure fixe, a été créée par The Fira Code Project Authors (https://github.com/tonsky/FiraCode) +""" + ), + html.H4("Marchés par département", id="liste_marches"), + dcc.Markdown("- [Marchés par département](/departements)"), + ] + ) + return apropos_shell("mentions-legales", contenu) diff --git a/src/pages/a_propos/presentation.py b/src/pages/a_propos/presentation.py new file mode 100644 index 0000000..03914e6 --- /dev/null +++ b/src/pages/a_propos/presentation.py @@ -0,0 +1,35 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/presentation", + title="Présentation | À propos | decp.info", + 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"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Présentation"), + 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. + +En effet, le potentiel des données d'attribution de marchés et des données qui peuvent les enrichir est très loin d'être exploité par +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 +""" + ), + ] + ) + return apropos_shell("presentation", contenu) diff --git a/src/pages/a_propos/qualite.py b/src/pages/a_propos/qualite.py new file mode 100644 index 0000000..2ee6ac7 --- /dev/null +++ b/src/pages/a_propos/qualite.py @@ -0,0 +1,33 @@ +from dash import dcc, html, register_page + +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/qualite", + title="Qualité des données | À propos | decp.info", + description="Informations sur la qualité et l'exhaustivité des données de marchés publics sur decp.info.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Qualité et exhaustivité des données"), + 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 [Sources de données](/a-propos/sources)). 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.""" + ), + ] + ) + return apropos_shell("qualite", contenu) diff --git a/src/pages/a_propos/sources.py b/src/pages/a_propos/sources.py new file mode 100644 index 0000000..bcf58eb --- /dev/null +++ b/src/pages/a_propos/sources.py @@ -0,0 +1,25 @@ +import os + +from dash import html, register_page + +from src.figures import get_sources_tables +from src.pages._apropos_shell import apropos_shell +from src.utils.seo import META_CONTENT + +register_page( + __name__, + path="/a-propos/sources", + title="Sources de données | À propos | decp.info", + description="Sources de données utilisées par decp.info pour consolider les marchés publics français.", + image_url=META_CONTENT["image_url"], +) + + +def layout(**_): + contenu = html.Div( + [ + html.H2("Sources de données"), + get_sources_tables(os.getenv("SOURCE_STATS_CSV_PATH")), + ] + ) + return apropos_shell("sources", contenu) From 8926c5b02cda2f15907f7f8a3e63fc51712d156e Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 26 Jun 2026 12:24:15 +0200 Subject: [PATCH 4/6] Layout fluide (pleine largeur) pour /a-propos --- src/pages/_apropos_shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_apropos_shell.py b/src/pages/_apropos_shell.py index 48fd15d..6db5635 100644 --- a/src/pages/_apropos_shell.py +++ b/src/pages/_apropos_shell.py @@ -54,7 +54,7 @@ def apropos_shell(active: str, contenu): className="d-md-none", ) content = dbc.Col([mobile, contenu], md=9) - return dbc.Container(dbc.Row([sidebar, content]), className="py-4") + return dbc.Container(dbc.Row([sidebar, content]), className="py-4", fluid=True) @callback( From b25150faa305ca9ac6c111db98728914cf523ce9 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 13:04:24 +0200 Subject: [PATCH 5/6] =?UTF-8?q?orjson=20pour=20s=C3=A9rialiser=20les=20sor?= =?UTF-8?q?ties=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + src/api/routes.py | 40 +++++++++++--------- tests/api/benchmark.py | 3 -- uv.lock | 85 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f2967c..da20225 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "duckdb", "flask-caching", "pyarrow>=23.0.1", + "orjson", "flask-cors>=6.0.2", "flask-smorest>=0.46.0", "marshmallow>=3.20.0", diff --git a/src/api/routes.py b/src/api/routes.py index 6baf0a5..32cf472 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,4 +1,5 @@ -from flask import g, request +import orjson +from flask import Response, g, request from flask_smorest import Blueprint, abort from src.api import tracking @@ -172,9 +173,6 @@ def data(): Exemple d'agrégation : `?acheteur_departement_code__groupby&uid__count&montant__sum` """ - import polars as pl - import polars.selectors as cs - page, page_size = _parse_pagination() columns = _parse_columns() count_results = request.args.get("count_results", "true").lower() != "false" @@ -201,16 +199,20 @@ def data(): limit=page_size, offset=(page - 1) * page_size, ) - df_ready = df.with_columns(cs.temporal().cast(pl.String)) # Si la page est partielle, on connaît le total exact ; sinon on ne sait pas. agg_total = ( (page - 1) * page_size + df.height if df.height < page_size else None ) - return { - "data": df_ready.to_dicts(), - "meta": {"page": page, "page_size": page_size}, - "links": _build_links(page, page_size, agg_total), - } + return Response( + orjson.dumps( + { + "data": df.to_dicts(), + "meta": {"page": page, "page_size": page_size}, + "links": _build_links(page, page_size, agg_total), + } + ), + mimetype="application/json", + ) df = query_marches( where_sql=where_sql, @@ -221,16 +223,18 @@ def data(): offset=(page - 1) * page_size, ) - # JSON ne sérialise pas date/datetime nativement → cast en string ISO - df_ready = df.with_columns(cs.temporal().cast(pl.String)) - total = count_marches(where_sql, params) if count_results else None meta = {"page": page, "page_size": page_size} if total is not None: meta["total"] = total - return { - "data": df_ready.to_dicts(), - "meta": meta, - "links": _build_links(page, page_size, total), - } + return Response( + orjson.dumps( + { + "data": df.to_dicts(), + "meta": meta, + "links": _build_links(page, page_size, total), + } + ), + mimetype="application/json", + ) diff --git a/tests/api/benchmark.py b/tests/api/benchmark.py index 8eab3a0..86f6900 100644 --- a/tests/api/benchmark.py +++ b/tests/api/benchmark.py @@ -13,9 +13,6 @@ Usage : Par défaut, --url pointe vers la production decp.info ; data.gouv.fr est interrogé sans authentification. -AVERTISSEMENT : les deux bases n'ont pas le même volume (data.gouv.fr ~3M -lignes, decp.info ~1,5M). C'est une comparaison d'implémentation, pas à -volume égal. """ import argparse diff --git a/uv.lock b/uv.lock index 576455f..2874813 100644 --- a/uv.lock +++ b/uv.lock @@ -799,7 +799,7 @@ wheels = [ [[package]] name = "decp-info" -version = "2.7.9" +version = "2.8.1" source = { virtual = "." } dependencies = [ { name = "dash", extra = ["compress"] }, @@ -815,6 +815,7 @@ dependencies = [ { name = "gunicorn" }, { name = "httpx" }, { name = "marshmallow" }, + { name = "orjson" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "plotly", extra = ["express"] }, @@ -850,6 +851,7 @@ requires-dist = [ { name = "gunicorn" }, { name = "httpx" }, { name = "marshmallow", specifier = ">=3.20.0" }, + { name = "orjson" }, { name = "pandas" }, { name = "plotly", extras = ["express"] }, { name = "polars" }, @@ -1647,6 +1649,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce", size = 228986, upload-time = "2026-05-06T15:09:14.765Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd", size = 132558, upload-time = "2026-05-06T15:09:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4", size = 128213, upload-time = "2026-05-06T15:09:18.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4", size = 135430, upload-time = "2026-05-06T15:09:20.257Z" }, + { url = "https://files.pythonhosted.org/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e", size = 146178, upload-time = "2026-05-06T15:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb", size = 133068, upload-time = "2026-05-06T15:09:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47", size = 134217, upload-time = "2026-05-06T15:09:24.847Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d", size = 141917, upload-time = "2026-05-06T15:09:26.647Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13", size = 415356, upload-time = "2026-05-06T15:09:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92", size = 148112, upload-time = "2026-05-06T15:09:29.783Z" }, + { url = "https://files.pythonhosted.org/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48", size = 137112, upload-time = "2026-05-06T15:09:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94", size = 131706, upload-time = "2026-05-06T15:09:32.707Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244", size = 127282, upload-time = "2026-05-06T15:09:33.955Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + [[package]] name = "outcome" version = "1.3.0.post0" From 57dd4d22eb6e0b9a0416f3633ef6ef1571b5c05c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 26 Jun 2026 15:46:10 +0200 Subject: [PATCH 6/6] Lien a propos direct, ordre section, mini erreur dans marche.py --- src/app.py | 4 +++- src/pages/_apropos_shell.py | 8 ++++---- src/pages/marche.py | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app.py b/src/app.py index 6585e0a..9e150a9 100644 --- a/src/app.py +++ b/src/app.py @@ -177,7 +177,9 @@ navbar = dbc.Navbar( dbc.NavItem( dbc.NavLink( page["name"].replace(" ", " "), - href=page["relative_path"], + href=page["relative_path"] + "/presentation" + if page["name"] == "À propos" + else page["relative_path"], active="exact", ) ) diff --git a/src/pages/_apropos_shell.py b/src/pages/_apropos_shell.py index 6db5635..b2fa6d0 100644 --- a/src/pages/_apropos_shell.py +++ b/src/pages/_apropos_shell.py @@ -3,16 +3,16 @@ from dash import Input, Output, State, callback, html SECTIONS = [ {"key": "presentation", "label": "Présentation", "href": "/a-propos/presentation"}, + {"key": "explorer", "label": "Explorer le projet", "href": "/a-propos/explorer"}, + {"key": "sources", "label": "Sources de données", "href": "/a-propos/sources"}, + {"key": "qualite", "label": "Qualité des données", "href": "/a-propos/qualite"}, { "key": "donnees-brutes", "label": "Données brutes", "href": "/a-propos/donnees-brutes", }, - {"key": "contact", "label": "Contact", "href": "/a-propos/contact"}, {"key": "contribuer", "label": "Contribuer", "href": "/a-propos/contribuer"}, - {"key": "explorer", "label": "Explorer le projet", "href": "/a-propos/explorer"}, - {"key": "qualite", "label": "Qualité des données", "href": "/a-propos/qualite"}, - {"key": "sources", "label": "Sources de données", "href": "/a-propos/sources"}, + {"key": "contact", "label": "Contact", "href": "/a-propos/contact"}, { "key": "mentions-legales", "label": "Mentions légales", diff --git a/src/pages/marche.py b/src/pages/marche.py index 49b42be..928e3db 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -135,6 +135,7 @@ def update_marche_info(marche, titulaires): "considerationsEnvironnementales", ] and col in marche + and marche[col] and "," in marche[col] ): col_values = marche[col].split(", ")