Skip to content

Commit 7e8bc28

Browse files
committed
Merge branch 'release/2.12.0'
2 parents 01fe1a6 + 67d9aea commit 7e8bc28

6 files changed

Lines changed: 124 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
### 2.12.0 2026-06-05
2+
3+
- Ajout de nouvelles sources de données :
4+
- Modula :
5+
- <www.proxilegales.fr>
6+
- <www.marches-demat.com>
7+
- <www.laprovencemarchespublics.com>
8+
- <www.corsematinmarchespublics.com>
9+
- <www.achatspublicscorse.com>
10+
- <www.marchespublics.pro>
11+
- <www.antilles-legales.fr>
12+
- <www.guyane-legales.fr>
13+
- alsacemarchespublics.eu
14+
- Eurométropole de Strasbourg
15+
- Ville de Strasbourg
16+
- Meilleur gestion du tampon d'ingestion XML qui ne voyait pas les petits fichiers
17+
118
### 2.11.0 2026-06-02
219

320
- Utilisation du [géocodage de Géoplateforme (IGN)](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/geocodage/) au lieu des données SIRENE géocodées d'Etalab, qui ne sont plus publiées

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# DECP processing
22

3-
> version 2.10.2([notes de version](https://github.com/ColinMaudry/decp-processing/blob/main/CHANGELOG.md))
4-
53
Projet de traitement et de publication de meilleures données sur les marchés publics attribués en France. Vous pouvez consulter, filtrer et télécharger
64
ces données sur le site [decp.info](https://decp.info). Enfin la section [À propos](https://decp.info/a-propos) décrit les objectifs du projet et regroupe toutes les informations clés.
75

@@ -13,9 +11,9 @@ Ce projet prend sa source dans la complexité de la publication des données par
1311

1412
- code source de l'agrégation [ouvert mais très peu lisible](https://github.com/139bercy/decp-arr2022)
1513
- documentation incomplète et éparpillée
16-
- https://github.com/139bercy/decp-arr2022/blob/main/README.md
17-
- https://www.data.gouv.fr/fr/datasets/5cd57bf68b4c4179299eb0e9
18-
- https://data.economie.gouv.fr/pages/donnees-essentielles-de-la-commande-publique/
14+
- <https://github.com/139bercy/decp-arr2022/blob/main/README.md>
15+
- <https://www.data.gouv.fr/fr/datasets/5cd57bf68b4c4179299eb0e9>
16+
- <https://data.economie.gouv.fr/pages/donnees-essentielles-de-la-commande-publique/>
1917
- schéma de données DECP 2 [complexe à utiliser](https://github.com/ColinMaudry/decp-processing/issues/4)
2018

2119
Vous trouverez des informations sur le contexte, le cadre réglementaire et les données de la commande publique sur [le wiki](https://github.com/ColinMaudry/decp-processing/wiki).
@@ -133,7 +131,7 @@ Attention, la version de prefect du client utilisé pour le déploiement et cell
133131
python src/deployments.py
134132
```
135133

136-
4. Le run se lancera tous les jours selon la configuration cron. Si tu souhaites exécuter le run maintenant :
134+
1. Le run se lancera tous les jours selon la configuration cron. Si tu souhaites exécuter le run maintenant :
137135

138136
```bash
139137
prefect deployment run decp-processing

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "decp-processing"
33
description = "Traitement des données des marchés publics français."
4-
version = "2.10.2"
4+
version = "2.12.0"
55
requires-python = ">= 3.10"
66
authors = [{ name = "Colin Maudry", email = "colin@colmo.tech" }]
77
dependencies = [

reference/source_datasets.json

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@
254254
},
255255
{
256256
"id": "5df410e86f44413a91d34be3",
257-
"name": "AIFE_DE_13001977100018",
257+
"name": "API DECP (AIFE DUME)",
258258
"owner_org_name": "Agence pour l'Informatique Financière de l'Etat",
259259
"code": "aife_dume"
260260
},
@@ -330,5 +330,59 @@
330330
"code": "minef",
331331
"owner_org_name": "MINEF",
332332
"skip": true
333+
},
334+
{
335+
"id": "68c951f5501fc2243b7996b1",
336+
"name": "Données Essentielles - www.proxilegales.fr",
337+
"code": "modula_proxilegales",
338+
"owner_org_name": "Modula"
339+
},
340+
{
341+
"id": "68c93247fda0dc4d2aa52a86",
342+
"name": "Données Essentielles - www.marches-demat.com",
343+
"code": "modula_marches-demat",
344+
"owner_org_name": "Modula"
345+
},
346+
{
347+
"id": "68c9571b7d6a0e64a5f7582b",
348+
"name": "Données Essentielles - www.laprovencemarchespublics.com et www.corsematinmarchespublics.com ",
349+
"code": "modula_laprovence_corsematin",
350+
"owner_org_name": "Modula"
351+
},
352+
{
353+
"id": "68c955112e0cc622a82626b8",
354+
"name": "Données Essentielles - www.achatspublicscorse.com",
355+
"code": "modula_achatspublicscorse",
356+
"owner_org_name": "Modula"
357+
},
358+
{
359+
"id": "68c953c24aa801f3ce4289ea",
360+
"name": "Données Essentielles - www.marchespublics.pro",
361+
"code": "modula_marchespublics.pro",
362+
"owner_org_name": "Modula"
363+
},
364+
{
365+
"id": "68c959a6fecb75ff0b47f079",
366+
"name": "Données Essentielles - www.antilles-legales.fr et www.guyane-legales.fr",
367+
"code": "modula_antilles_guyane_legales",
368+
"owner_org_name": "Modula"
369+
},
370+
{
371+
"id": "68834dc0f3f93ee2ff6050e2",
372+
"name": "Données essentielles de la commande publique - Eurométropole de Strasbourg",
373+
"code": "eurometropole_strasbourg",
374+
"owner_org_name": "Eurométropole de Strasbourg"
375+
},
376+
{
377+
"id": "68834a9b7285ebd8466fb23e",
378+
"name": "Données essentielles de la commande publique - Ville de Strasbourg",
379+
"code": "ville_strasbourg",
380+
"owner_org_name": "Eurométropole de Strasbourg"
381+
},
382+
{
383+
"id": "67b4575f32c309fd76327c87",
384+
"name": "Données Essentielles du profil acheteur Alsace Marchés Publics",
385+
"code": "alsace_mp",
386+
"owner_org_name": "Alsace marchés publics"
333387
}
334388
]

src/tasks/get.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def get_resource(
8686
output_path.parent.mkdir(exist_ok=True, parents=True)
8787
url = r["url"]
8888
file_format = r["format"]
89+
logger.debug(f"Récupération de {r['dataset_code']} - {r['ori_filename']}")
8990
if file_format == "json":
9091
fields, decp_format = json_stream_to_parquet(url, output_path, r)
9192
elif file_format == "xml":
@@ -182,8 +183,12 @@ def json_stream_to_parquet(
182183
http_stream_iter = stream_get(url)
183184

184185
stream_replace_iter = stream_replace_bytestring(
185-
http_stream_iter, rb"NaN([,\n])", rb"null\1"
186-
) # Nan => null
186+
http_stream_iter, b"\xef\xbb\xbf", b""
187+
) # Strip UTF-8 BOM
188+
189+
stream_replace_iter = stream_replace_bytestring(
190+
stream_replace_iter, rb"NaN([,\n])", rb"null\1"
191+
) # NaN => null
187192

188193
# Le dataset AWS scraping a pas mal de bugs de backslash
189194
if "/68caf6b135f19236a4f37a32/" in url or "/aws/" in url:
@@ -246,8 +251,6 @@ def json_stream_to_parquet(
246251
def xml_stream_to_parquet(
247252
url: str, output_path: Path, fix_chars=False
248253
) -> tuple[set, DecpFormat]:
249-
"""Uniquement utilisé pour les données publiées par l'AIFE."""
250-
251254
decp_format_2022 = DecpFormat("DECP 2022", SCHEMA_MARCHE_2022, "marches.marche")
252255

253256
fields = set()
@@ -263,6 +266,10 @@ def xml_stream_to_parquet(
263266
marche = parse_element(elem)
264267
new_fields = write_marche_rows(marche, tmp_file, decp_format_2022)
265268
fields = fields.union(new_fields)
269+
# Vidage du buffer d'écriture sur le disque avant la lecture par son nom :
270+
# sans cela, les petits fichiers (< taille du buffer) restent en mémoire et
271+
# pl.scan_ndjson lit un fichier vide → 0 ligne (le chemin JSON, lui, fait seek(0)).
272+
tmp_file.flush()
266273
lf = pl.scan_ndjson(tmp_file.name, schema=decp_format_2022.schema)
267274
sink_to_files(lf, output_path, file_format="parquet")
268275
return fields, decp_format_2022
@@ -441,9 +448,10 @@ def get_insee_cog_data(url, schema_overrides, columns) -> pl.DataFrame:
441448

442449
def get_clean(
443450
resource, resources_artifact: list, available_parquet_files: set
444-
) -> pl.DataFrame or None:
451+
) -> pl.DataFrame | None:
445452
logger = get_logger(level=LOG_LEVEL)
446453

454+
logger.debug(f"get_clean {resource['ori_filename']}")
447455
with transaction():
448456
checksum = resource["checksum"]
449457
parquet_path = RESOURCE_CACHE_DIR / f"{checksum}"

tests/test_get.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import polars as pl
22

33
from src.config import SIRET_LATLONG_SCHEMA
4-
from src.tasks.get import bootstrap_siret_latlong, get_etablissements
4+
from src.tasks.get import (
5+
bootstrap_siret_latlong,
6+
get_etablissements,
7+
xml_stream_to_parquet,
8+
)
59

610
REQUIRED_GEO_COLUMNS = {
711
"numeroVoieEtablissement",
@@ -19,6 +23,35 @@ def test_get_etablissements_includes_geocoding_columns():
1923
assert not missing, f"Colonnes manquantes : {missing}"
2024

2125

26+
def test_xml_stream_to_parquet_small_file_is_not_empty(tmp_path):
27+
"""Régression : un petit XML (ndjson < taille du buffer d'écriture) doit
28+
quand même produire des lignes. Sans flush() avant scan_ndjson, le buffer
29+
n'était jamais vidé sur le disque et le parquet ressortait vide (0 ligne)."""
30+
xml = (
31+
'<?xml version="1.0" encoding="ISO-8859-15"?>\n'
32+
"<marches>\n"
33+
" <marche>\n"
34+
" <id>20261115446200</id>\n"
35+
" <acheteur><id>21130112200084</id></acheteur>\n"
36+
" <nature>March\xe9</nature>\n"
37+
" <montant>262578.28</montant>\n"
38+
" <dateNotification>2026-02-11</dateNotification>\n"
39+
" </marche>\n"
40+
"</marches>\n"
41+
).encode("ISO-8859-15")
42+
xml_path = tmp_path / "decp_small.xml"
43+
xml_path.write_bytes(xml)
44+
45+
output_path = tmp_path / "out"
46+
xml_stream_to_parquet(str(xml_path), output_path, fix_chars=False)
47+
48+
df = pl.read_parquet(output_path.with_suffix(".parquet"))
49+
assert df.height == 1
50+
assert df["acheteur_id"].to_list() == ["21130112200084"]
51+
# L'ISO-8859-15 doit être décodé correctement
52+
assert df["nature"].to_list() == ["Marché"]
53+
54+
2255
def test_bootstrap_siret_latlong_produces_extended_schema(tmp_path, monkeypatch):
2356
fake_decp = tmp_path / "decp_fake.parquet"
2457
pl.DataFrame(

0 commit comments

Comments
 (0)