diff --git a/src/api/routes.py b/src/api/routes.py index 077d394..dbc5fdc 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -3,8 +3,8 @@ from flask_smorest import Blueprint, abort from src.api import tracking from src.api.auth import require_token -from src.api.filters import FilterError, build_where -from src.db import count_marches, query_marches +from src.api.filters import FilterError, build_where, parse_aggregators +from src.db import aggregate_marches, count_marches, query_marches from src.db import schema as duckdb_schema from src.utils.data import DATA_SCHEMA @@ -151,13 +151,34 @@ def data(): columns = _parse_columns() count_results = request.args.get("count_results", "true").lower() != "false" + args = list(request.args.items(multi=True)) try: - where_sql, params, order_sql = build_where( - list(request.args.items(multi=True)), duckdb_schema - ) + agg = parse_aggregators(args, duckdb_schema) + where_sql, params, order_sql = build_where(args, duckdb_schema) except FilterError as e: abort(400, message=str(e), errors={"field": e.field}) + if agg is not None: + if columns: + abort( + 400, + message="`columns` ne peut pas être combiné avec une agrégation", + ) + df = aggregate_marches( + select_sql=agg.select_sql, + where_sql=where_sql, + params=params, + group_by=agg.group_by_sql, + limit=page_size, + offset=(page - 1) * page_size, + ) + df_ready = df.with_columns(cs.temporal().cast(pl.String)) + return { + "data": df_ready.to_dicts(), + "meta": {"page": page, "page_size": page_size}, + "links": _build_links(page, page_size, None), + } + df = query_marches( where_sql=where_sql, params=params, diff --git a/tests/api/test_endpoints_data.py b/tests/api/test_endpoints_data.py index 010f20a..d024de5 100644 --- a/tests/api/test_endpoints_data.py +++ b/tests/api/test_endpoints_data.py @@ -112,3 +112,44 @@ def test_data_differs_excludes_value(api_client, valid_token_header): assert resp.status_code == 200 body = resp.get_json() assert all(row["uid"] != uid for row in body["data"]) + + +def test_data_aggregation_groupby_count(api_client, valid_token_header): + client, _ = api_client + resp = client.get( + "/api/v1/data?acheteur_departement_code__groupby&uid__count", + headers=valid_token_header, + ) + assert resp.status_code == 200 + body = resp.get_json() + assert body["data"], "agrégation vide ?" + for row in body["data"]: + assert set(row.keys()) == {"acheteur_departement_code", "uid__count"} + assert "total" not in body["meta"] + + +def test_data_aggregation_global_count(api_client, valid_token_header): + client, _ = api_client + resp = client.get("/api/v1/data?uid__count", headers=valid_token_header) + assert resp.status_code == 200 + body = resp.get_json() + assert len(body["data"]) == 1 + assert "uid__count" in body["data"][0] + + +def test_data_aggregation_with_filter(api_client, valid_token_header): + client, _ = api_client + resp = client.get( + "/api/v1/data?acheteur_departement_code__groupby&uid__count&montant__greater=0", + headers=valid_token_header, + ) + assert resp.status_code == 200 + + +def test_data_aggregation_with_columns_returns_400(api_client, valid_token_header): + client, _ = api_client + resp = client.get( + "/api/v1/data?uid__count&columns=uid", + headers=valid_token_header, + ) + assert resp.status_code == 400