- 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>
139 lines
3.9 KiB
Python
139 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
app_v1.py — Turf SaaS Flask application with versioned API /v1/
|
|
|
|
This module creates the Flask app, registers:
|
|
- Auth JWT (from Sprint 2-3)
|
|
- API v1 blueprints
|
|
- Swagger/OpenAPI documentation at /api/v1/docs
|
|
|
|
Usage:
|
|
python app_v1.py
|
|
# or via gunicorn:
|
|
gunicorn -w 2 -b 0.0.0.0:8792 app_v1:app
|
|
|
|
Sprint 3-4: HRT-29 — Refacto API /v1/
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
from flask import Flask, jsonify
|
|
from flask_cors import CORS
|
|
from flask_jwt_extended import JWTManager
|
|
from flasgger import Swagger
|
|
|
|
from auth_db import init_auth_tables
|
|
from auth import auth_bp
|
|
from api_v1 import register_api_v1
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger("turf_saas.app_v1")
|
|
|
|
|
|
def create_app() -> Flask:
|
|
"""Application factory."""
|
|
app = Flask(__name__)
|
|
|
|
# ── CORS ──
|
|
CORS(app, origins=["*"], methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
|
|
|
# ── JWT config ──
|
|
app.config["JWT_SECRET_KEY"] = os.environ.get(
|
|
"JWT_SECRET_KEY", "change-me-in-production-use-strong-random-secret"
|
|
)
|
|
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=15)
|
|
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
|
|
JWTManager(app)
|
|
|
|
# ── Swagger / OpenAPI ──
|
|
swagger_config = {
|
|
"headers": [],
|
|
"specs": [
|
|
{
|
|
"endpoint": "apispec_v1",
|
|
"route": "/api/v1/apispec.json",
|
|
"rule_filter": lambda rule: str(rule).startswith("/api/v1"),
|
|
"model_filter": lambda tag: True,
|
|
}
|
|
],
|
|
"static_url_path": "/flasgger_static",
|
|
"swagger_ui": True,
|
|
"specs_route": "/api/v1/docs",
|
|
}
|
|
|
|
swagger_template = {
|
|
"swagger": "2.0",
|
|
"info": {
|
|
"title": "Turf SaaS API",
|
|
"description": (
|
|
"API v1 — Prédictions turf IA, value bets, backtest & métriques.\n\n"
|
|
"**Plans:** `free` | `premium` | `pro`\n\n"
|
|
"**Auth:** Bearer JWT — obtenir un token via `POST /api/v1/auth/login`"
|
|
),
|
|
"version": "1.0.0",
|
|
"contact": {"name": "H3R7 Tech"},
|
|
},
|
|
"basePath": "/",
|
|
"schemes": ["http", "https"],
|
|
"securityDefinitions": {
|
|
"Bearer": {
|
|
"type": "apiKey",
|
|
"name": "Authorization",
|
|
"in": "header",
|
|
"description": "Entrer: **Bearer <token>**",
|
|
}
|
|
},
|
|
"consumes": ["application/json"],
|
|
"produces": ["application/json"],
|
|
}
|
|
|
|
Swagger(app, config=swagger_config, template=swagger_template)
|
|
|
|
# ── Auth DB init ──
|
|
with app.app_context():
|
|
try:
|
|
init_auth_tables()
|
|
except Exception as e:
|
|
logger.warning("init_auth_tables warning: %s", e)
|
|
|
|
# ── Register auth blueprint ──
|
|
app.register_blueprint(auth_bp)
|
|
|
|
# ── Register API v1 blueprints ──
|
|
register_api_v1(app)
|
|
|
|
# ── Global error handlers ──
|
|
@app.errorhandler(404)
|
|
def not_found_handler(e):
|
|
return jsonify(
|
|
{"status": "error", "message": "Route introuvable", "code": 404}
|
|
), 404
|
|
|
|
@app.errorhandler(405)
|
|
def method_not_allowed_handler(e):
|
|
return jsonify(
|
|
{"status": "error", "message": "Méthode non autorisée", "code": 405}
|
|
), 405
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error_handler(e):
|
|
logger.exception("Unhandled 500 error")
|
|
return jsonify(
|
|
{"status": "error", "message": "Erreur serveur interne", "code": 500}
|
|
), 500
|
|
|
|
logger.info("Turf SaaS API v1 ready — docs at /api/v1/docs")
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
if __name__ == "__main__":
|
|
port = int(os.environ.get("PORT", 8792))
|
|
app.run(host="0.0.0.0", port=port, debug=False)
|