#!/usr/bin/env python3 """ Courses routes for API v1. GET /api/v1/courses/today — liste des courses du jour (public, paginated) GET /api/v1/courses/{id}/predictions — prédictions ML pour une course (free tier, 1/day limit) """ import os from datetime import datetime, timedelta from flask import Blueprint, jsonify, request, g from api_v1.utils import ( get_db, table_exists, error_response, bad_request, not_found, internal_error, get_pagination_params, paginate_query, ) from auth import jwt_required_middleware, free_daily_limit_check courses_bp = Blueprint("v1_courses", __name__, url_prefix="/api/v1/courses") # ────────────────────────────────────────────────────────────── # GET /api/v1/courses/today # ────────────────────────────────────────────────────────────── @courses_bp.route("/today", methods=["GET"]) @jwt_required_middleware def courses_today(): """ Courses du jour --- tags: - Courses summary: Liste toutes les courses du jour avec info course security: - Bearer: [] parameters: - name: filter in: query type: string enum: [all, quinte, trot, plat] default: all description: Filtre par type de course - name: limit in: query type: integer default: 20 - name: offset in: query type: integer default: 0 responses: 200: description: Liste des courses du jour 401: description: Token manquant ou invalide """ race_filter = request.args.get("filter", "all").lower() limit, offset = get_pagination_params(default_limit=50, max_limit=200) today = datetime.now().strftime("%Y-%m-%d") # Build SQL condition if race_filter == "quinte": cond = "AND (c.libelle LIKE '%Quinté%' OR c.libelle LIKE '%Quinte%')" elif race_filter == "trot": cond = "AND c.discipline LIKE '%Trot%'" elif race_filter == "plat": cond = "AND c.discipline LIKE '%Plat%'" else: cond = "" conn = get_db() try: # Graceful handling if pmu_courses table doesn't exist yet if not table_exists(conn, "pmu_courses"): return jsonify( { "status": "ok", "date": today, "filter": race_filter, "courses": [], "pagination": { "total": 0, "limit": limit, "offset": offset, "has_more": False, }, } ), 200 # Count total count_row = conn.execute( f"""SELECT COUNT(*) as cnt FROM pmu_courses c WHERE c.date_programme = ? {cond}""", (today,), ).fetchone() total = count_row["cnt"] if count_row else 0 rows = conn.execute( f"""SELECT c.date_programme, c.num_reunion, c.num_course, c.libelle, c.discipline, c.distance, c.hippodrome, c.px_type, COUNT(p.id_cheval) as nb_partants FROM pmu_courses c LEFT JOIN pmu_partants p ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE c.date_programme = ? {cond} GROUP BY c.date_programme, c.num_reunion, c.num_course ORDER BY c.num_reunion ASC, c.num_course ASC LIMIT ? OFFSET ?""", (today, limit, offset), ).fetchall() courses = [] for r in rows: course_id = f"{r['num_reunion']}-{r['num_course']}" courses.append( { "id": course_id, "date": r["date_programme"], "num_reunion": r["num_reunion"], "num_course": r["num_course"], "libelle": r["libelle"], "discipline": r["discipline"], "distance": r["distance"], "hippodrome": r["hippodrome"], "type_pari": r["px_type"], "nb_partants": r["nb_partants"], } ) pagination = paginate_query(courses, total, limit, offset) return jsonify( { "status": "ok", "date": today, "filter": race_filter, "courses": courses, **pagination, } ), 200 except Exception as e: return internal_error(str(e)) finally: conn.close() # ────────────────────────────────────────────────────────────── # GET /api/v1/courses//predictions # course_id format: "{num_reunion}-{num_course}" e.g. "1-3" # ────────────────────────────────────────────────────────────── @courses_bp.route("//predictions", methods=["GET"]) @jwt_required_middleware @free_daily_limit_check def course_predictions(course_id): """ Prédictions pour une course --- tags: - Courses summary: Prédictions ML pour une course identifiée par {num_reunion}-{num_course} security: - Bearer: [] parameters: - name: course_id in: path type: string required: true description: Identifiant de la course (format num_reunion-num_course, ex "1-3") - name: date in: query type: string format: date description: Date de la course (YYYY-MM-DD), défaut = aujourd'hui responses: 200: description: Prédictions ML pour la course 400: description: Paramètres invalides 404: description: Course introuvable 429: description: Limite quotidienne free tier atteinte """ # Parse course_id parts = course_id.split("-") if len(parts) != 2: return bad_request( "course_id doit être au format {num_reunion}-{num_course}, ex: 1-3" ) try: num_reunion = int(parts[0]) num_course = int(parts[1]) except ValueError: return bad_request("num_reunion et num_course doivent être des entiers") date_param = request.args.get("date", datetime.now().strftime("%Y-%m-%d")) conn = get_db() try: # Fetch course info course_row = conn.execute( """SELECT libelle, discipline, distance, hippodrome, px_type FROM pmu_courses WHERE date_programme = ? AND num_reunion = ? AND num_course = ?""", (date_param, num_reunion, num_course), ).fetchone() if not course_row: return not_found( f"Course R{num_reunion}C{num_course} introuvable pour le {date_param}" ) # Fetch ML predictions from cache preds = [] if table_exists(conn, "ml_predictions_cache"): preds = conn.execute( """SELECT horse_name, horse_number, odds, prob_top1, prob_top3, ml_score, recommendation, is_value_bet, risque_label, risque_score FROM ml_predictions_cache WHERE date = ? AND num_reunion = ? AND num_course = ? ORDER BY ml_score DESC""", (date_param, num_reunion, num_course), ).fetchall() # Fetch partants partants = conn.execute( """SELECT nom, num_pmu, cote_direct, cote_reference, tendance_cote, favoris, tx_victoire, tx_place, forme_recente, driver, entraineur, musique FROM pmu_partants WHERE date_programme = ? AND num_reunion = ? AND num_course = ? ORDER BY num_pmu ASC""", (date_param, num_reunion, num_course), ).fetchall() return jsonify( { "status": "ok", "date": date_param, "course": { "id": course_id, "libelle": course_row["libelle"], "discipline": course_row["discipline"], "distance": course_row["distance"], "hippodrome": course_row["hippodrome"], "type_pari": course_row["px_type"], }, "predictions": [dict(p) for p in preds], "partants": [dict(p) for p in partants], } ), 200 except Exception as e: return internal_error(str(e)) finally: conn.close()