From b87eb4328cf7b8b43d6be7794499488c8f025b39 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 22 Jun 2026 14:09:34 +0200 Subject: [PATCH] =?UTF-8?q?docs(api):=20documente=20les=20op=C3=A9rateurs?= =?UTF-8?q?=20(filtres=20+=20agr=C3=A9gation)=20dans=20Swagger=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace la description du paramètre __ par une documentation complète listant tous les opérateurs de filtre (exact, differs, contains, notcontains, in, notin, less, greater, strictly_less, strictly_greater, isnull, isnotnull, sort) et les agrégations (groupby, count, sum, avg, min, max). Met à jour le docstring de data() pour documenter le mode agrégation et les paramètres réservés. Ajoute test_openapi_doc.py qui valide la présence des mots-clés clés dans l'OpenAPI généré. Co-Authored-By: Claude Sonnet 4.6 --- src/api/routes.py | 42 +++++++++++++++++++++++++++-------- tests/api/test_openapi_doc.py | 15 +++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 tests/api/test_openapi_doc.py diff --git a/src/api/routes.py b/src/api/routes.py index dbc5fdc..e4080ce 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -124,25 +124,49 @@ def schema(): "in": "query", "schema": {"type": "string"}, "description": ( - "Filtre dynamique. Remplacer `` par un nom de colonne (voir `/schema`) " - "et `` par : `exact`, `contains`, `notcontains`, `less`, `greater`, " - "`strictly_less`, `strictly_greater`, `in`, `notin`, `isnull`, `isnotnull`, `sort`. " - "Exemple : `acheteur_id__contains=VILLE`, `montant__greater=10000`." + "Filtre ou agrégation dynamique : `__` " + "(voir les colonnes via `/schema`).\n\n" + "**Filtres** (`__=`) :\n" + "- `exact` : égal à la valeur\n" + "- `differs` : différent de la valeur (null-safe, `IS DISTINCT FROM`)\n" + "- `contains` / `notcontains` : contient / ne contient pas (LIKE)\n" + "- `in` / `notin` : dans / hors d'une liste séparée par des virgules\n" + "- `less` / `greater` : ≤ / ≥\n" + "- `strictly_less` / `strictly_greater` : < / >\n" + "- `isnull` / `isnotnull` : valeur nulle / non nulle (sans valeur)\n" + "- `sort` : tri, valeur `asc` ou `desc`\n\n" + "**Agrégation** (drapeaux sans valeur, ex. `acheteur_departement_code__groupby&montant__sum`) :\n" + "- `groupby` : regroupe sur la colonne\n" + "- `count`, `sum`, `avg`, `min`, `max` : agrège la colonne ; " + "la colonne de sortie est nommée `colonne__count`, `colonne__sum`, " + "`colonne__avg`, `colonne__min`, `colonne__max`\n\n" + "En mode agrégation, la réponse contient des lignes groupées, " + "`columns` est interdit et `meta` ne contient pas `total`.\n\n" + "Exemples : `acheteur_id__contains=VILLE`, `montant__greater=10000`, " + "`acheteur_departement_code__groupby&montant__sum`." ), }, ], ) @require_token def data(): - """Récupère des marchés publics filtrés. + """Récupère des marchés publics filtrés, triés ou agrégés. - Filtres en query string sous la forme `__=`. + Filtres en query string : `__=`. + Opérateurs de filtre : exact, differs, contains, notcontains, in, notin, + less, greater, strictly_less, strictly_greater, isnull, isnotnull, sort. - Opérateurs : exact, contains, notcontains, less, greater, - strictly_less, strictly_greater, in, notin, isnull, isnotnull, sort. + Agrégation (drapeaux sans valeur) : `__groupby`, + `__count|sum|avg|min|max`. Les colonnes agrégées sont nommées + `__`. `columns` est interdit avec une agrégation et + `meta` ne contient alors pas `total`. Paramètres réservés : page (défaut 1), page_size (défaut 50, max 1000), - columns (csv), count_results (true|false ; mettre false pour économiser le COUNT(*)). + columns (csv), count_results (true|false ; mettre false pour économiser + le COUNT(*)). + + Exemple d'agrégation : + `?acheteur_departement_code__groupby&uid__count&montant__sum` """ import polars as pl import polars.selectors as cs diff --git a/tests/api/test_openapi_doc.py b/tests/api/test_openapi_doc.py new file mode 100644 index 0000000..87b683d --- /dev/null +++ b/tests/api/test_openapi_doc.py @@ -0,0 +1,15 @@ +def test_openapi_documents_new_keywords(api_client): + client, _ = api_client + resp = client.get("/api/v1/openapi.json") + assert resp.status_code == 200 + raw = resp.get_data(as_text=True) + for keyword in [ + "count_results", + "differs", + "groupby", + "__sum", + "__avg", + "__min", + "__max", + ]: + assert keyword in raw, f"{keyword} absent de la doc OpenAPI"