- Blueprint Flask api_v1 avec prefix /api/v1/
- GET /api/v1/health — healthcheck public
- GET /api/v1/courses/today — courses du jour (paginé, filtré)
- GET /api/v1/courses/{id}/predictions — prédictions ML pour une course
- GET /api/v1/predictions/top3 — top 3 global (free tier)
- GET /api/v1/predictions/all — toutes prédictions (premium+)
- GET /api/v1/valuebets — value bets du jour (premium+)
- GET /api/v1/backtest — résultats backtest historiques (pro)
- GET /api/v1/export/csv — export CSV prédictions/paris (pro)
- GET /api/v1/metrics — métriques perf ML (premium+)
- Swagger/OpenAPI via flasgger à /api/v1/docs
- Erreurs uniformes {status, message, code}
- Pagination limit/offset sur toutes les listes
- 42 tests d'intégration passants
Co-Authored-By: Paperclip <noreply@paperclip.ing>
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Shared utilities for API v1 — error helpers, pagination, DB access.
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
from flask import jsonify, request
|
|
|
|
DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db")
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Database
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def get_db():
|
|
"""Return a SQLite connection with Row factory."""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def table_exists(conn, table_name: str) -> bool:
|
|
row = conn.execute(
|
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
|
|
).fetchone()
|
|
return row is not None
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Uniform error responses
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def error_response(message: str, code: int, status: str = "error"):
|
|
"""Return a JSON error envelope consistent with the API contract.
|
|
|
|
Shape: {"status": "error", "message": "...", "code": 400}
|
|
"""
|
|
return jsonify({"status": status, "message": message, "code": code}), code
|
|
|
|
|
|
def not_found(message: str = "Resource not found"):
|
|
return error_response(message, 404)
|
|
|
|
|
|
def bad_request(message: str = "Bad request"):
|
|
return error_response(message, 400)
|
|
|
|
|
|
def forbidden(message: str = "Forbidden", required_plans=None, current_plan=None):
|
|
payload = {"status": "error", "message": message, "code": 403}
|
|
if required_plans:
|
|
payload["required_plans"] = required_plans
|
|
if current_plan:
|
|
payload["current_plan"] = current_plan
|
|
payload["upgrade_url"] = "/api/v1/subscription/upgrade"
|
|
return jsonify(payload), 403
|
|
|
|
|
|
def internal_error(message: str = "Internal server error"):
|
|
return error_response(message, 500)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Pagination helpers
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def get_pagination_params(default_limit: int = 20, max_limit: int = 100):
|
|
"""Extract and validate limit/offset from query-string."""
|
|
try:
|
|
limit = int(request.args.get("limit", default_limit))
|
|
except (ValueError, TypeError):
|
|
limit = default_limit
|
|
|
|
try:
|
|
offset = int(request.args.get("offset", 0))
|
|
except (ValueError, TypeError):
|
|
offset = 0
|
|
|
|
limit = max(1, min(limit, max_limit))
|
|
offset = max(0, offset)
|
|
return limit, offset
|
|
|
|
|
|
def paginate_query(rows, total: int, limit: int, offset: int):
|
|
"""Wrap a list of rows in a pagination envelope."""
|
|
return {
|
|
"pagination": {
|
|
"total": total,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": (offset + limit) < total,
|
|
}
|
|
}
|