From 6d2a95d775d63c6b7ebf07a1903bc226a7f1b027 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 13 May 2026 14:16:27 +0200 Subject: [PATCH] =?UTF-8?q?API:=20retirer=20benchmark=20non=20demand=C3=A9?= =?UTF-8?q?=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/benchmark_api.py | 208 ----------------------------------------- 1 file changed, 208 deletions(-) delete mode 100644 tests/benchmark_api.py diff --git a/tests/benchmark_api.py b/tests/benchmark_api.py deleted file mode 100644 index 9431342..0000000 --- a/tests/benchmark_api.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python -"""Benchmark de l'endpoint /data de l'API privée decp.info. - -Usage : - python tests/benchmark_api.py --url http://localhost:8050/api/v1/data --token decpinfo_xxx - python tests/benchmark_api.py --url https://decp.info/api/v1/data --token decpinfo_xxx --runs 20 - -Le script mesure le temps de réponse de chaque scénario et affiche un tableau -de statistiques (min / médiane / p95 / max). -""" - -import argparse -import statistics -import sys -import time - -import httpx - -SCENARIOS: list[dict] = [ - { - "name": "sans filtre (page 1, 50 résultats)", - "params": {"page": 1, "page_size": 50}, - }, - { - "name": "filtre __exact sur département", - "params": {"acheteur_departement_code__exact": "44", "page_size": 50}, - }, - { - "name": "filtre __contains sur objet", - "params": {"objet__contains": "informatique", "page_size": 50}, - }, - { - "name": "filtre __greater sur date", - "params": {"dateNotification__greater": "2024-01-01", "page_size": 50}, - }, - { - "name": "filtre __strictly_greater sur montant", - "params": {"montant__strictly_greater": "100000", "page_size": 50}, - }, - { - "name": "filtre __in (CPV multiples)", - "params": {"cpv_8__in": "72000000,72200000", "page_size": 50}, - }, - { - "name": "filtre __isnull", - "params": {"montant__isnull": "", "page_size": 50}, - }, - { - "name": "tri desc + colonnes sélectionnées", - "params": { - "dateNotification__sort": "desc", - "columns": "uid,objet,montant,dateNotification", - "page_size": 50, - }, - }, - { - "name": "filtres combinés", - "params": { - "acheteur_departement_code__exact": "75", - "dateNotification__greater": "2023-01-01", - "montant__strictly_greater": "50000", - "dateNotification__sort": "desc", - "page_size": 50, - }, - }, - { - "name": "count=false (économise COUNT(*))", - "params": {"page_size": 50, "count": "false"}, - }, - { - "name": "page 2", - "params": {"page": 2, "page_size": 50}, - }, -] - -COL_NAME = 44 -COL_STATUS = 8 -COL_STAT = 10 - - -def percentile(data: list[float], p: float) -> float: - if not data: - return float("nan") - sorted_data = sorted(data) - k = (len(sorted_data) - 1) * p / 100 - lo, hi = int(k), min(int(k) + 1, len(sorted_data) - 1) - return sorted_data[lo] + (sorted_data[hi] - sorted_data[lo]) * (k - lo) - - -def run_benchmark(base_url: str, token: str | None, runs: int) -> None: - auth_header = {"Authorization": f"Bearer {token}"} if token else {} - results: list[dict] = [] - - print(f"\nBenchmark {base_url}") - print(f"Scénarios : {len(SCENARIOS)} | Répétitions : {runs}\n") - - for scenario in SCENARIOS: - timings: list[float] = [] - last_status = 0 - - for _ in range(runs): - headers = auth_header - url = base_url - params = scenario["params"] - - try: - t0 = time.perf_counter() - resp = httpx.get(url, params=params, headers=headers, timeout=30) - elapsed = (time.perf_counter() - t0) * 1000 - last_status = resp.status_code - timings.append(elapsed) - except httpx.RequestError as exc: - print(f" ERREUR réseau : {exc}") - last_status = 0 - break - - if timings: - results.append( - { - "name": scenario["name"], - "status": last_status, - "min": min(timings), - "median": percentile(timings, 50), - "p95": percentile(timings, 95), - "max": max(timings), - "mean": statistics.mean(timings), - "runs": len(timings), - } - ) - status_str = f"[{last_status}]" - print( - f" {scenario['name'][:COL_NAME]:<{COL_NAME}}" - f" {status_str:<{COL_STATUS}}" - f" médiane {results[-1]['median']:>7.1f} ms" - f" p95 {results[-1]['p95']:>7.1f} ms" - ) - else: - print(f" {scenario['name'][:COL_NAME]:<{COL_NAME}} ÉCHEC") - - _print_summary(results) - - -def _print_summary(results: list[dict]) -> None: - if not results: - return - - sep = "-" * (COL_NAME + COL_STATUS + 4 * (COL_STAT + 3) + 6) - header = ( - f"\n{'Scénario':<{COL_NAME}} {'Status':<{COL_STATUS}}" - f" {'Min (ms)':>{COL_STAT}}" - f" {'Médiane (ms)':>{COL_STAT}}" - f" {'P95 (ms)':>{COL_STAT}}" - f" {'Max (ms)':>{COL_STAT}}" - ) - print(f"\n{'=' * len(sep)}") - print("RÉSUMÉ") - print(f"{'=' * len(sep)}") - print(header) - print(sep) - - for r in results: - status_str = f"[{r['status']}]" - print( - f"{r['name'][:COL_NAME]:<{COL_NAME}}" - f" {status_str:<{COL_STATUS}}" - f" {r['min']:>{COL_STAT}.1f}" - f" {r['median']:>{COL_STAT}.1f}" - f" {r['p95']:>{COL_STAT}.1f}" - f" {r['max']:>{COL_STAT}.1f}" - ) - - print(sep) - medians = [r["median"] for r in results] - print( - f"\nMédiane globale : {statistics.mean(medians):.1f} ms" - f" | Pire p95 : {max(r['p95'] for r in results):.1f} ms" - ) - - -def main() -> None: - parser = argparse.ArgumentParser(description="Benchmark de l'API privée decp.info") - parser.add_argument( - "--url", - default="http://localhost:8050/api/v1/data", - help="URL complète de l'endpoint /data (défaut : http://localhost:8050/api/v1/data)", - ) - parser.add_argument( - "--token", - default=None, - help="Token Bearer API (format : decpinfo_xxxxx) — omis si l'API n'exige pas d'auth", - ) - parser.add_argument( - "--runs", - type=int, - default=5, - help="Nombre de répétitions par scénario (défaut : 5)", - ) - args = parser.parse_args() - - if args.runs < 1: - print("--runs doit être ≥ 1", file=sys.stderr) - sys.exit(1) - - run_benchmark(args.url, args.token, args.runs) - - -if __name__ == "__main__": - main()