diff --git a/.template.env b/.template.env index 5cbcdfa..c47e456 100644 --- a/.template.env +++ b/.template.env @@ -1,3 +1,4 @@ DATA_FILE_PARQUET_PATH=https://www.data.gouv.fr/fr/datasets/r/11cea8e8-df3e-4ed1-932b-781e2635e432 PORT=8050 DEVELOPMENT=True +SOURCE_STATS_CSV_PATH="https://www.data.gouv.fr/api/1/datasets/r/8ded94de-3b80-4840-a5bb-7faad1c9c234" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..285710b --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +decp.info is a web application that enable analysis and download of +French public procurement data. +Copyright (C) 2025 Colin Maudry + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md index ba10c99..801a712 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # decp.info +> v2.0 + Outil d'exploration et de téléchargement des données essentielles de la commande publique. => [decp.info](https://decp.info) @@ -10,15 +12,27 @@ Outil d'exploration et de téléchargement des données essentielles de la comma python -m venv .venv source .venv/bin/activate pip install . + +# Pour la production gunicorn app:server + +# Pour avoir le debuggage et le hot reload +python run.py ``` -## Dépôts de code connexes + +## Liens connexes - [decp-processing](https://github.com/ColinMaudry/decp-processing) (traitement et publication des données) -- [decp-table-schema](https://github.com/ColinMaudry/decp-table-schema) (schéma de données tabulaire) +- [colin.maudry.com](https://colin.maudry.com) (blog) ## Notes de version +### 2.0.0 (23 septembre 2025) + +- détails des sources de données +- section "À propos" plus développée +- correction de bugs dans les filtres de la data table + ### 2.0.0-alpha - Data table fonctionnelle diff --git a/pyproject.toml b/pyproject.toml index 8f49bae..021fa6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = [ ] dependencies = [ "dash==3.2.0", + "dash[compress]", "polars", "gunicorn", "dash-bootstrap-components", diff --git a/src/app.py b/src/app.py index c30949f..e45d9ef 100644 --- a/src/app.py +++ b/src/app.py @@ -2,13 +2,26 @@ import logging import dash_bootstrap_components as dbc from dash import Dash, dcc, html, page_container, page_registry +from flask import send_from_directory -app = Dash(external_stylesheets=[dbc.themes.SIMPLEX], title="decp.info", use_pages=True) +app = Dash( + external_stylesheets=[dbc.themes.SIMPLEX], + title="decp.info", + use_pages=True, + compress=True, +) # COSMO (belle font, blue), # UNITED (rouge, ubuntu font), # LUMEN (gros séparateur, blue clair), # SIMPLEX (rouge, séparateur) + +# robots.txt +@app.server.route("/robots.txt") +def robots(): + return send_from_directory("./assets", "robots.txt", mimetype="text/plain") + + logger = logging.getLogger("decp.info") logging.basicConfig( format="%(asctime)s %(levelname)-8s %(message)s", diff --git a/src/assets/robots.txt b/src/assets/robots.txt new file mode 100644 index 0000000..9381e66 --- /dev/null +++ b/src/assets/robots.txt @@ -0,0 +1,41 @@ +# START YOAST BLOCK +# Copié depuis https://next.ink/robots.txt +# --------------------------- +User-agent: * +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: / diff --git a/src/assets/style.css b/src/assets/style.css index a77995f..2532b92 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -10,6 +10,14 @@ float: left; } +#source_table p { + line-height: 1.5; +} + +#source_table { + margin-bottom: 25px; +} + /* Réduire la taille du texte de la colonne Objet */ td[data-dash-column="objet"] { diff --git a/src/figures.py b/src/figures.py index 9d93c0c..8ca0c5d 100644 --- a/src/figures.py +++ b/src/figures.py @@ -2,6 +2,7 @@ import json import plotly.express as px import polars as pl +from dash import dash_table, html def get_map_count_marches(lf: pl.LazyFrame): @@ -112,3 +113,47 @@ def get_barchart_sources(lf: pl.LazyFrame, type_date: str): ) return fig + + +def get_sources_tables(source_path) -> html.Div: + df = pl.read_csv(source_path) + df = df.with_columns( + ( + pl.lit('') + + pl.col("nom") + + pl.lit("") + ).alias("nom") + ) + df = df.drop("url") + df = df.sort(by=["nb_marchés"], descending=True) + + datatable = dash_table.DataTable( + id="source_table", + columns=[ + { + "name": i, + "id": i, + "presentation": "markdown", + "type": "text", + "format": {"nully": "N/A"}, + } + for i in df.schema.names() + ], + style_cell_conditional=[ + { + "if": {"column_id": ["nom", "organisation"]}, + "minWidth": "350px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "14px", + "whiteSpace": "normal", + }, + ], + sort_action="native", + markdown_options={"html": True}, + ) + datatable.data = df.to_dicts() + + return html.Div(children=datatable) diff --git a/src/pages/about.py b/src/pages/about.py index 1aeb1cb..ff564f9 100644 --- a/src/pages/about.py +++ b/src/pages/about.py @@ -1,7 +1,14 @@ +import os + from dash import dcc, html, register_page +from dotenv import load_dotenv + +from src.figures import get_sources_tables title = "À propos" +load_dotenv() + register_page( __name__, path="/a-propos", title=f"decp.info - {title}", name=title, order=5 ) @@ -12,7 +19,7 @@ layout = [ children=[ html.H2(title), dcc.Markdown( - """Outil d'exploration des [Données Essentielles de la Commande Publique](), développé par Colin Maudry, sous licence GPL v3 (libre et gratuit). + """Outil d'exploration libre et gratuit des [Données Essentielles de la Commande Publique](), 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. @@ -21,28 +28,73 @@ 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-fournisseur...) -- la sauvegarde de filtre pour les retrouver plus tard et les partager +- 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 :) +- ...et toutes les fonctionnalités auxquelles vous pourrez penser + """ + ), + html.H4("Pour contribuer", id="contribuer"), + dcc.Markdown(""" +- via l'achat d'un 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) +- envoyez un mail et on discute ! -#### Pour aller plus loin +#### Pour explorer le projet - ✉️ [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), qui parle beaucoup de transparence des marchés publics - 📔 [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) - -#### Contact - -- venez discuter de la transparence de la commande publique [sur le forum teamopendata.org](https://teamopendata.org/c/commande-publique/101) + """), + html.H4("Contact", id="contact"), + dcc.Markdown(""" - Email : [colin+decp@maudry.com](mailto:colin+decp@maudry.com) - 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/) -""" - ), +- venez discuter de la transparence de la commande publique [sur le forum teamopendata.org](https://teamopendata.org/c/commande-publique/101) +"""), + 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 + +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 + +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."""), + # 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" + # ), ], ) ] diff --git a/src/pages/home.py b/src/pages/home.py index cdf2c7f..d7c3e44 100644 --- a/src/pages/home.py +++ b/src/pages/home.py @@ -83,6 +83,14 @@ datatable = dash_table.DataTable( "lineHeight": "14px", "whiteSpace": "normal", }, + { + "if": {"column_id": "acheteur_nom"}, + "minWidth": "250px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "14px", + "whiteSpace": "normal", + }, ], data_timestamp=0, markdown_options={"html": True}, @@ -92,7 +100,9 @@ layout = [ html.Div( html.Details( children=[ - html.Summary(html.H3("Mode d'emploi")), + html.Summary( + html.H3("Mode d'emploi", style={"text-decoration": "underline"}), + ), dcc.Markdown( """ diff --git a/src/pages/statistiques.py b/src/pages/statistiques.py index d692ad6..877457d 100644 --- a/src/pages/statistiques.py +++ b/src/pages/statistiques.py @@ -15,43 +15,33 @@ layout = [ className="container", children=[ html.H2(title), - dcc.Markdown(""" - À savoir, les données suivantes existent mais sont en cours d'intégration dans ce projet : - - - les données publiées dans le [format DECP 2019 (période 2018-2022)](https://www.data.gouv.fr/fr/datasets/donnees-essentielles-de-la-commande-publique-fichiers-consolides/#/resources/16962018-5c31-4296-9454-5998585496d2) - - les données [collectées par l'AIFE](https://github.com/ColinMaudry/decp-processing/issues/68) (API DUME, notamment achatpublic.info) - - les données [des plateformes Atexo](https://github.com/ColinMaudry/decp-processing/issues/57) - """), dcc.Loading( overlay_style={"visibility": "visible", "filter": "blur(2px)"}, - id="loading-1", + id="loading-statistques", type="default", children=[ html.Div( children=[ - dcc.Loading( - overlay_style={ - "visibility": "visible", - "filter": "blur(2px)", - }, - id="loading-stats", - type="default", - children=[ - dcc.Graph(figure=get_map_count_marches(lf)), - dcc.Graph( - figure=get_barchart_sources( - lf, "dateNotification" - ) - ), - dcc.Graph( - figure=get_barchart_sources( - lf, "datePublicationDonnees" - ) - ), - ], - ) - ] - ), + dcc.Markdown(""" + 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 + 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), + toutes les [contributions](/a-propos#contribuer) sont les bienvenues pour atteindre l'exhaustivité. + """), + dcc.Graph(figure=get_map_count_marches(lf)), + dcc.Graph( + figure=get_barchart_sources(lf, "dateNotification") + ), + dcc.Graph( + figure=get_barchart_sources( + lf, "datePublicationDonnees" + ) + ), + ], + ) ], ), ],