feat: Sprint 3-4 — Refacto API /v1/ (HRT-29)

- 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>
This commit is contained in:
DevOps Engineer
2026-04-25 18:00:54 +02:00
parent c8f1bfd478
commit b8ef1ed35d
14 changed files with 2691 additions and 0 deletions

111
api_v1/routes/valuebets.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Value bets route for API v1.
GET /api/v1/valuebets — Value bets du jour (premium+)
"""
from datetime import datetime
from flask import Blueprint, jsonify, request
from api_v1.utils import (
get_db,
table_exists,
internal_error,
get_pagination_params,
paginate_query,
)
from auth import jwt_required_middleware, plan_required
valuebets_bp = Blueprint("v1_valuebets", __name__, url_prefix="/api/v1")
@valuebets_bp.route("/valuebets", methods=["GET"])
@jwt_required_middleware
@plan_required("premium", "pro")
def valuebets():
"""
Value bets du jour
---
tags:
- Value Bets
summary: Value bets du jour — chevaux à cote surévaluée par le marché (premium+)
security:
- Bearer: []
parameters:
- name: date
in: query
type: string
format: date
description: Date YYYY-MM-DD (défaut aujourd'hui)
- name: min_odds
in: query
type: number
default: 2.0
description: Cote minimale pour filtrer les value bets
- name: limit
in: query
type: integer
default: 20
- name: offset
in: query
type: integer
default: 0
responses:
200:
description: Value bets du jour
401:
description: Token invalide
403:
description: Plan insuffisant (premium ou pro requis)
"""
date_param = request.args.get("date", datetime.now().strftime("%Y-%m-%d"))
limit, offset = get_pagination_params(default_limit=20, max_limit=100)
try:
min_odds = float(request.args.get("min_odds", 2.0))
except (ValueError, TypeError):
min_odds = 2.0
conn = get_db()
try:
rows = []
total = 0
if table_exists(conn, "ml_predictions_cache"):
count_row = conn.execute(
"""SELECT COUNT(*) as cnt
FROM ml_predictions_cache
WHERE date = ? AND is_value_bet = 1 AND odds >= ?""",
(date_param, min_odds),
).fetchone()
total = count_row["cnt"] if count_row else 0
rows = conn.execute(
"""SELECT race_label, hippodrome, discipline, distance, heure,
horse_name, horse_number, odds, prob_top1, prob_top3,
ml_score, recommendation, risque_label, risque_score
FROM ml_predictions_cache
WHERE date = ? AND is_value_bet = 1 AND odds >= ?
ORDER BY ml_score DESC
LIMIT ? OFFSET ?""",
(date_param, min_odds, limit, offset),
).fetchall()
valuebets_list = [dict(r) for r in rows]
pagination = paginate_query(valuebets_list, total, limit, offset)
return jsonify(
{
"status": "ok",
"date": date_param,
"min_odds": min_odds,
"valuebets": valuebets_list,
**pagination,
}
), 200
except Exception as e:
return internal_error(str(e))
finally:
conn.close()