Skip to content

Commit 29f1af7

Browse files
committed
Merge branch 'release/2.9.0'
2 parents 25e5b2c + 8d55664 commit 29f1af7

15 files changed

Lines changed: 223 additions & 72 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 2.9.0 2026-01-29
2+
3+
- Ajout du champ `type` qui indique le type de marché : fournitures, services ou travaux (dérivé du code CPV)
4+
- `distance` renommé `titulaire_distance` par cohérence, cette donnée étant liée au titulaire, et un marché peut avoir plusieurs titulaires
5+
16
### 2.8.0 2026-01-23
27

38
- Ajout du champ `titulaire_categorie` (PME, ETI, GE)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DECP processing
22

3-
> version 2.8.0 ([notes de version](https://github.com/ColinMaudry/decp-processing/blob/main/CHANGELOG.md))
3+
> version 2.9.0 ([notes de version](https://github.com/ColinMaudry/decp-processing/blob/main/CHANGELOG.md))
44
55
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
66
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.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[project]
22
name = "decp-processing"
33
description = "Traitement des données des marchés publics français."
4-
version = "2.8.0"
4+
version = "2.9.0"
55
requires-python = ">= 3.10"
66
authors = [
7-
{ name = "Colin Maudry", email = "colin+decp@maudry.com" }
7+
{ name = "Colin Maudry", email = "colin@colmo.tech" }
88
]
99
dependencies = [
1010
"python-dotenv",

reference/schema_base.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@
7070
"description": "Montant forfaitaire ou montant maximum estimé hors-taxes, en euros. Ce montant est le montant attribué. Le montant final payé aux titulaires peut évoluer lors de la signature du contrat et de l'exécution du marché.",
7171
"short_title": "Montant"
7272
},
73+
{
74+
"name": "type",
75+
"type": "string",
76+
"title": "Type",
77+
"description": "Type de marché public : fournitures, services ou travaux (dérivé du code CPV).",
78+
"short_title": "Type"
79+
},
7380
{
7481
"type": "string",
7582
"name": "codeCPV",
@@ -241,8 +248,8 @@
241248
"short_title": "Id. accord-cadre"
242249
},
243250
{
244-
"type": "string",
245-
"name": "distance",
251+
"type": "integer",
252+
"name": "titulaire_distance",
246253
"title": "Distance acheteur-titulaire",
247254
"description": "Distance en kilomètres entre l'adresse de l'acheteur et celle du titulaire.",
248255
"short_title": "Distance"

reference/source_datasets.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,5 +323,12 @@
323323
"name": "Données essentielles de la commande publique (DECP) de Klekoon",
324324
"code": "scrap_klekoon",
325325
"owner_org_name": "Colin Maudry"
326+
},
327+
{
328+
"id": "5cd57bf68b4c4179299eb0e9",
329+
"name": "Données essentielles de la commande publique - fichiers consolidés (MINEF)",
330+
"code": "minef",
331+
"owner_org_name": "MINEF",
332+
"skip": true
326333
}
327334
]

src/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@ def make_sirene_data_dir(sirene_data_parent_dir) -> Path:
218218
tracked_datasets_complete = json.load(f)
219219
TRACKED_DATASETS = []
220220
for dataset in tracked_datasets_complete:
221-
if len(SOLO_DATASETS) == 0 or dataset["id"] in SOLO_DATASETS:
221+
if (len(SOLO_DATASETS) == 0 and not (dataset.get("skip"))) or dataset[
222+
"id"
223+
] in SOLO_DATASETS:
224+
logger.debug(f"{dataset['name']} added.")
222225
TRACKED_DATASETS.append(dataset)
223226

224227

src/flows/decp_processing.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@
2424
)
2525
from src.flows.sirene_preprocess import sirene_preprocess
2626
from src.tasks.dataset_utils import list_resources
27-
from src.tasks.enrich import enrich_from_sirene
27+
from src.tasks.enrich import add_type_marche, enrich_from_sirene
2828
from src.tasks.get import get_clean
2929
from src.tasks.output import generate_final_schema, sink_to_files
3030
from src.tasks.publish import publish_to_datagouv
3131
from src.tasks.transform import (
32-
add_duree_restante,
3332
calculate_naf_cpv_matching,
3433
concat_parquet_files,
3534
sort_columns,
@@ -42,6 +41,7 @@
4241
print_all_config,
4342
remove_unused_cache,
4443
)
44+
from tasks.enrich import add_duree_restante
4545

4646

4747
@flow(log_prints=True)
@@ -119,6 +119,9 @@ def decp_processing(enable_cache_removal: bool = True):
119119
logger.info("Ajout de la colonne 'dureeRestanteMois'...")
120120
lf = add_duree_restante(lf)
121121

122+
logger.info("Ajout du type de marché...")
123+
lf = add_type_marche(lf)
124+
122125
logger.info("Génération des probabilités NAF/CPV...")
123126
calculate_naf_cpv_matching(lf)
124127
lf = lf.drop(cs.starts_with("activite"))

src/tasks/enrich.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import datetime
2+
13
import polars as pl
24
import polars.selectors as cs
35

@@ -206,7 +208,7 @@ def calculate_distance(lf: pl.LazyFrame) -> pl.LazyFrame:
206208
)
207209
.round(mode="half_away_from_zero")
208210
.cast(pl.Int16)
209-
.alias("distance")
211+
.alias("titulaire_distance")
210212
)
211213
return lf
212214

@@ -215,7 +217,7 @@ def haversine(
215217
lat1: pl.Expr, lon1: pl.Expr, lat2: pl.Expr, lon2: pl.Expr, R: float = 6371.0
216218
) -> pl.Expr:
217219
"""
218-
Calcule la distance haversine entre deux points (lat1, lon1) et (lat2, lon2) en km.
220+
Calcule la distance haversine entre deux points (lat1, lon1) et (lat2, lon2) en km.
219221
Utilise des opérations vectorisées Polars.
220222
Généré par la LLM Euria, développée et hébergée en Suisse par Infomaniak.
221223
"""
@@ -235,3 +237,44 @@ def haversine(
235237

236238
# Distance
237239
return R * c
240+
241+
242+
def add_type_marche(lf: pl.LazyFrame) -> pl.LazyFrame:
243+
cpv_division = pl.col("codeCPV").str.slice(0, 2).cast(pl.Int8, strict=False)
244+
245+
lf = lf.with_columns(
246+
pl.when(cpv_division == 45)
247+
.then(pl.lit("Travaux"))
248+
.when(cpv_division.is_in(range(1, 45)) | (cpv_division == 48))
249+
.then(pl.lit("Fournitures"))
250+
.when(cpv_division.is_in(range(50, 99)))
251+
.then(pl.lit("Services"))
252+
.otherwise(pl.lit("Non catégorisé"))
253+
.fill_null(pl.lit("Code CPV invalide"))
254+
.alias("type")
255+
)
256+
return lf
257+
258+
259+
def add_duree_restante(lff: pl.LazyFrame):
260+
today = datetime.now().date()
261+
duree_mois_days_int = pl.col("dureeMois") * 30.5
262+
end_date = pl.col("dateNotification") + pl.duration(days=duree_mois_days_int)
263+
duree_restante_mois = ((end_date - today).dt.total_days() / 30).round(1)
264+
265+
# Pas de valeurs négatives.
266+
lff = lff.with_columns(
267+
pl.when(duree_restante_mois < 0)
268+
.then(pl.lit(0))
269+
.otherwise(duree_restante_mois)
270+
.alias("dureeRestanteMois")
271+
)
272+
273+
# Si dureeRestanteMois > dureeMois, dureeRestanteMois = dureeMois
274+
lff = lff.with_columns(
275+
pl.when(pl.col("dureeRestanteMois") > pl.col("dureeMois"))
276+
.then(pl.col("dureeMois").cast(pl.Float32))
277+
.otherwise(pl.col("dureeRestanteMois"))
278+
.alias("dureeRestanteMois")
279+
)
280+
return lff

src/tasks/transform.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from datetime import datetime
21
from pathlib import Path
32

43
import polars as pl
@@ -194,7 +193,7 @@ def prepare_unites_legales(lf: pl.LazyFrame) -> pl.LazyFrame:
194193
"nomUsageUniteLegale", # parfois rempli, a la priorité sur nomUniteLegale
195194
"statutDiffusionUniteLegale", # P = non-diffusible
196195
"categorieEntreprise", # PME, ETI, GE
197-
# "categorieJuridiqueUniteLegale" # 1000, etc.
196+
# "categorieJuridiqueUniteLegale", # 1000, etc.
198197
]
199198
)
200199
.filter(
@@ -402,30 +401,6 @@ def calculate_naf_cpv_matching(lf_naf_cpv: pl.LazyFrame):
402401
save_to_files(df_results, DIST_DIR / "probabilites_naf_cpv", "csv")
403402

404403

405-
def add_duree_restante(lff: pl.LazyFrame):
406-
today = datetime.now().date()
407-
duree_mois_days_int = pl.col("dureeMois") * 30.5
408-
end_date = pl.col("dateNotification") + pl.duration(days=duree_mois_days_int)
409-
duree_restante_mois = ((end_date - today).dt.total_days() / 30).round(1)
410-
411-
# Pas de valeurs négatives.
412-
lff = lff.with_columns(
413-
pl.when(duree_restante_mois < 0)
414-
.then(pl.lit(0))
415-
.otherwise(duree_restante_mois)
416-
.alias("dureeRestanteMois")
417-
)
418-
419-
# Si dureeRestanteMois > dureeMois, dureeRestanteMois = dureeMois
420-
lff = lff.with_columns(
421-
pl.when(pl.col("dureeRestanteMois") > pl.col("dureeMois"))
422-
.then(pl.col("dureeMois").cast(pl.Float32))
423-
.otherwise(pl.col("dureeRestanteMois"))
424-
.alias("dureeRestanteMois")
425-
)
426-
return lff
427-
428-
429404
#
430405
# ⬇️⬇️⬇️ Fonctions à refactorer avec Polars et le format DECP 2022 ⬇️⬇️⬇️
431406
#

tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
import subprocess
3+
import time
4+
5+
import pytest
6+
from prefect.testing.utilities import prefect_test_harness
7+
8+
9+
@pytest.fixture(autouse=True, scope="session")
10+
def prefect_test_fixture(tmp_path_factory):
11+
os.environ["PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS"] = "90"
12+
13+
with prefect_test_harness():
14+
yield
15+
16+
# Force cleanup: try to stop any lingering Prefect server
17+
# (This is a workaround — Prefect doesn't expose a clean stop API in test harness)
18+
# Généré par Euria, la LLM d'Infomaniak.
19+
20+
# Try to kill any remaining prefect server process
21+
try:
22+
# This is a bit brute-force, but works if no other prefect server is running
23+
subprocess.run(
24+
["pkill", "-f", "prefect server start"],
25+
stdout=subprocess.DEVNULL,
26+
stderr=subprocess.DEVNULL,
27+
)
28+
time.sleep(0.5) # Give time to terminate
29+
except Exception:
30+
pass

0 commit comments

Comments
 (0)