feat(mcp): tools stats_acheteur / stats_titulaire (agrégations)
This commit is contained in:
+92
-1
@@ -2,7 +2,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from src.api.filters import FilterError, build_where
|
from src.api.filters import FilterError, build_where
|
||||||
from src.db import count_marches, query_marches
|
from src.db import aggregate_marches, count_marches, query_marches
|
||||||
from src.db import schema as duckdb_schema
|
from src.db import schema as duckdb_schema
|
||||||
from src.mcp.serialization import to_json_records
|
from src.mcp.serialization import to_json_records
|
||||||
from src.utils.data import DF_ACHETEURS, DF_TITULAIRES
|
from src.utils.data import DF_ACHETEURS, DF_TITULAIRES
|
||||||
@@ -129,3 +129,94 @@ def search_organisations(
|
|||||||
}
|
}
|
||||||
for r in df.to_dicts()
|
for r in df.to_dicts()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _org_identite(org_type: str, org_id: str) -> dict:
|
||||||
|
df = query_marches(
|
||||||
|
where_sql=f'"{org_type}_id" = ?',
|
||||||
|
params=[org_id],
|
||||||
|
columns=[
|
||||||
|
f"{org_type}_id",
|
||||||
|
f"{org_type}_nom",
|
||||||
|
f"{org_type}_departement_nom",
|
||||||
|
f"{org_type}_commune_nom",
|
||||||
|
],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
if df.height == 0:
|
||||||
|
return {"id": org_id, "nom": None, "departement": None, "commune": None}
|
||||||
|
r = df.row(0, named=True)
|
||||||
|
return {
|
||||||
|
"id": org_id,
|
||||||
|
"nom": r[f"{org_type}_nom"],
|
||||||
|
"departement": r[f"{org_type}_departement_nom"],
|
||||||
|
"commune": r[f"{org_type}_commune_nom"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_org_stats(org_type: str, org_id: str) -> dict:
|
||||||
|
"""Statistiques agrégées d'un acheteur ou titulaire."""
|
||||||
|
if org_type not in ORG_FRAMES:
|
||||||
|
raise ValueError(
|
||||||
|
f"type invalide: {org_type!r} (attendu 'acheteur' ou 'titulaire')"
|
||||||
|
)
|
||||||
|
other = "titulaire" if org_type == "acheteur" else "acheteur"
|
||||||
|
where_sql = f'"{org_type}_id" = ?'
|
||||||
|
params = [org_id]
|
||||||
|
identite = _org_identite(org_type, org_id)
|
||||||
|
|
||||||
|
totals = aggregate_marches(
|
||||||
|
select_sql='COUNT("uid") AS nb, COALESCE(SUM("montant"), 0) AS montant_total',
|
||||||
|
where_sql=where_sql,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
nb = int(totals["nb"][0])
|
||||||
|
if nb == 0:
|
||||||
|
return {
|
||||||
|
"identite": identite,
|
||||||
|
"nb_marches": 0,
|
||||||
|
"montant_total": 0,
|
||||||
|
"repartition_annuelle": [],
|
||||||
|
f"top_{other}s": [],
|
||||||
|
"top_cpv": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
annuelle = aggregate_marches(
|
||||||
|
select_sql=(
|
||||||
|
"CAST(date_part('year', \"dateNotification\") AS INTEGER) AS annee, "
|
||||||
|
'COUNT("uid") AS nb_marches, '
|
||||||
|
'COALESCE(SUM("montant"), 0) AS montant_total'
|
||||||
|
),
|
||||||
|
where_sql=where_sql,
|
||||||
|
params=params,
|
||||||
|
group_by="date_part('year', \"dateNotification\")",
|
||||||
|
order_by="annee",
|
||||||
|
)
|
||||||
|
top_other = aggregate_marches(
|
||||||
|
select_sql=(
|
||||||
|
f'"{other}_id" AS id, any_value("{other}_nom") AS nom, '
|
||||||
|
'COUNT("uid") AS nb_marches, '
|
||||||
|
'COALESCE(SUM("montant"), 0) AS montant_total'
|
||||||
|
),
|
||||||
|
where_sql=where_sql,
|
||||||
|
params=params,
|
||||||
|
group_by=f'"{other}_id"',
|
||||||
|
order_by="nb_marches DESC",
|
||||||
|
limit=TOP_N,
|
||||||
|
)
|
||||||
|
top_cpv = aggregate_marches(
|
||||||
|
select_sql='"codeCPV" AS cpv, COUNT("uid") AS nb_marches',
|
||||||
|
where_sql=where_sql,
|
||||||
|
params=params,
|
||||||
|
group_by='"codeCPV"',
|
||||||
|
order_by="nb_marches DESC",
|
||||||
|
limit=TOP_N,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"identite": identite,
|
||||||
|
"nb_marches": nb,
|
||||||
|
"montant_total": float(totals["montant_total"][0]),
|
||||||
|
"repartition_annuelle": to_json_records(annuelle),
|
||||||
|
f"top_{other}s": to_json_records(top_other),
|
||||||
|
"top_cpv": to_json_records(top_cpv),
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import src.utils.search as search_mod
|
import src.utils.search as search_mod
|
||||||
from src.mcp.queries import build_where_args, search_marches, search_organisations
|
from src.mcp.queries import (
|
||||||
|
build_where_args,
|
||||||
|
compute_org_stats,
|
||||||
|
search_marches,
|
||||||
|
search_organisations,
|
||||||
|
)
|
||||||
from src.utils.data import DF_ACHETEURS
|
from src.utils.data import DF_ACHETEURS
|
||||||
from src.utils.search import search_org
|
from src.utils.search import search_org
|
||||||
|
|
||||||
@@ -80,3 +85,31 @@ def test_search_marches_no_match_is_empty():
|
|||||||
def test_search_marches_bad_filter_returns_error():
|
def test_search_marches_bad_filter_returns_error():
|
||||||
result = search_marches(filtres_avances={"colonne_bidon__exact": "x"})
|
result = search_marches(filtres_avances={"colonne_bidon__exact": "x"})
|
||||||
assert "error" in result
|
assert "error" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_org_stats_acheteur_known():
|
||||||
|
stats = compute_org_stats("acheteur", "123")
|
||||||
|
assert stats["nb_marches"] >= 1
|
||||||
|
assert stats["montant_total"] == 10
|
||||||
|
assert stats["identite"]["id"] == "123"
|
||||||
|
assert stats["identite"]["nom"] == "ACHETEUR 1"
|
||||||
|
assert "top_titulaires" in stats
|
||||||
|
assert "top_cpv" in stats
|
||||||
|
# répartition annuelle dérivée de dateNotification (2025)
|
||||||
|
annees = [row["annee"] for row in stats["repartition_annuelle"]]
|
||||||
|
assert 2025 in annees
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_org_stats_titulaire_known():
|
||||||
|
stats = compute_org_stats("titulaire", "345")
|
||||||
|
assert stats["nb_marches"] >= 1
|
||||||
|
assert "top_acheteurs" in stats
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_org_stats_unknown_is_empty():
|
||||||
|
stats = compute_org_stats("acheteur", "inconnu-xyz")
|
||||||
|
assert stats["nb_marches"] == 0
|
||||||
|
assert stats["montant_total"] == 0
|
||||||
|
assert stats["repartition_annuelle"] == []
|
||||||
|
assert stats["top_titulaires"] == []
|
||||||
|
assert stats["top_cpv"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user