Test isolation fixes: - auth_db.get_db(): read TURF_SAAS_DB dynamically (not frozen at import) - api_v1/utils.get_db(): read TURF_SAAS_DB dynamically (not frozen at import) - api_tokens_db.get_db(): read TURF_SAAS_DB dynamically (not frozen at import) - tests/test_history.py: enforce _tmp_db.name + call init_auth_tables() in fixtures - tests/test_user_tokens.py: enforce _tmp_db.name + call migrate_api_tokens_tables() in app fixture Auth compatibility fixes: - api_v1/routes/history.py: use auth.jwt_required_middleware (flask_jwt_extended) with saas_auth fallback for portal_server context - api_v1/routes/ml_feedback.py: same auth import strategy - api_v1/routes/user.py: same auth import strategy Dependencies: - requirements.txt: add optuna>=4.0.0 (used in ML ensemble tests and training) Co-Authored-By: Paperclip <noreply@paperclip.ing>
200 lines
5.9 KiB
Python
200 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
ml_feedback.py — API routes pour le feedback loop ML (turf_saas).
|
|
|
|
Routes:
|
|
POST /api/v1/ml/feedback/run — Déclenche le feedback loop (admin uniquement)
|
|
GET /api/v1/ml/feedback/stats — Stats performances par stratégie
|
|
|
|
Sécurité admin : token via variable d'environnement ML_ADMIN_TOKEN
|
|
ou plan "pro" en fallback pour les stats.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
from flask import Blueprint, jsonify, request, g
|
|
|
|
# Ajoute le répertoire parent de api_v1 dans le path pour importer ml_feedback_saas
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
|
|
from api_v1.utils import get_db, internal_error, bad_request
|
|
# Auth: try flask_jwt_extended (app_v1) first, fall back to saas_auth (portal_server)
|
|
try:
|
|
from auth import jwt_required_middleware
|
|
except ImportError:
|
|
from saas_auth import require_auth as jwt_required_middleware
|
|
try:
|
|
from auth import plan_required
|
|
except ImportError:
|
|
plan_required = lambda *a, **kw: (lambda f: f)
|
|
|
|
ml_feedback_bp = Blueprint("v1_ml_feedback", __name__, url_prefix="/api/v1/ml/feedback")
|
|
|
|
# Token admin interne — configurable via variable d'environnement
|
|
ML_ADMIN_TOKEN = os.environ.get("ML_ADMIN_TOKEN", "")
|
|
|
|
|
|
def _check_admin(req):
|
|
"""Vérifie le token admin via header X-Admin-Token ou Authorization Bearer (plan pro)."""
|
|
# 1. Token interne (scheduler/cron)
|
|
admin_token = req.headers.get("X-Admin-Token", "").strip()
|
|
if ML_ADMIN_TOKEN and admin_token == ML_ADMIN_TOKEN:
|
|
return True, None
|
|
|
|
# 2. Pas de token admin configuré → autoriser les utilisateurs "pro" authentifiés
|
|
user = getattr(request, "current_user", None) or getattr(g, "current_user", None)
|
|
if user and user.get("plan") == "pro":
|
|
return True, None
|
|
|
|
return False, jsonify({"error": "Accès admin requis", "code": 403}), 403
|
|
|
|
|
|
@ml_feedback_bp.route("/run", methods=["POST"])
|
|
@jwt_required_middleware
|
|
def feedback_run():
|
|
"""
|
|
Déclenche le feedback loop ML pour une date donnée.
|
|
---
|
|
tags:
|
|
- ML Feedback
|
|
summary: Déclenche le feedback loop XGBoost (admin only)
|
|
security:
|
|
- Bearer: []
|
|
- AdminToken: []
|
|
parameters:
|
|
- name: body
|
|
in: body
|
|
schema:
|
|
type: object
|
|
properties:
|
|
date:
|
|
type: string
|
|
description: Date YYYY-MM-DD (défaut aujourd'hui)
|
|
example: "2026-04-25"
|
|
mode:
|
|
type: string
|
|
description: "run (défaut) ou backfill"
|
|
enum: [run, backfill]
|
|
example: run
|
|
responses:
|
|
200:
|
|
description: Feedback loop exécuté avec succès
|
|
400:
|
|
description: Paramètre invalide
|
|
403:
|
|
description: Accès refusé
|
|
500:
|
|
description: Erreur interne
|
|
"""
|
|
# Vérification admin
|
|
user = getattr(request, "current_user", None) or getattr(g, "current_user", None)
|
|
admin_token = request.headers.get("X-Admin-Token", "").strip()
|
|
is_admin = (ML_ADMIN_TOKEN and admin_token == ML_ADMIN_TOKEN) or (
|
|
user and user.get("plan") == "pro"
|
|
)
|
|
if not is_admin:
|
|
return jsonify({"error": "Accès admin requis", "code": 403}), 403
|
|
|
|
body = request.get_json(silent=True) or {}
|
|
date_str = body.get("date") or datetime.now().strftime("%Y-%m-%d")
|
|
mode = body.get("mode", "run")
|
|
|
|
# Validation date
|
|
try:
|
|
datetime.strptime(date_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
return bad_request(f"Date invalide : {date_str}. Format attendu : YYYY-MM-DD")
|
|
|
|
if mode not in ("run", "backfill"):
|
|
return bad_request("mode doit être 'run' ou 'backfill'")
|
|
|
|
try:
|
|
import ml_feedback_saas
|
|
|
|
if mode == "backfill":
|
|
inseres, maj = ml_feedback_saas.backfill(date_str)
|
|
total_inseres = inseres
|
|
else:
|
|
result = ml_feedback_saas.run(date_str)
|
|
total_inseres = sum(result["inseres"].values())
|
|
maj = result["maj"]
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"date": date_str,
|
|
"mode": mode,
|
|
"paris_inseres": total_inseres,
|
|
"paris_mis_a_jour": maj,
|
|
}
|
|
), 200
|
|
|
|
except Exception as e:
|
|
return internal_error(str(e))
|
|
|
|
|
|
@ml_feedback_bp.route("/stats", methods=["GET"])
|
|
@jwt_required_middleware
|
|
@plan_required("premium", "pro")
|
|
def feedback_stats():
|
|
"""
|
|
Stats performances ML par stratégie.
|
|
---
|
|
tags:
|
|
- ML Feedback
|
|
summary: Stats paris ML par stratégie (premium+)
|
|
security:
|
|
- Bearer: []
|
|
parameters:
|
|
- name: date_debut
|
|
in: query
|
|
type: string
|
|
description: Date de début YYYY-MM-DD
|
|
- name: date_fin
|
|
in: query
|
|
type: string
|
|
description: Date de fin YYYY-MM-DD
|
|
responses:
|
|
200:
|
|
description: Stats par stratégie
|
|
401:
|
|
description: Token invalide
|
|
403:
|
|
description: Plan insuffisant (premium ou pro requis)
|
|
"""
|
|
date_debut = request.args.get("date_debut")
|
|
date_fin = request.args.get("date_fin")
|
|
|
|
# Validation optionnelle des dates
|
|
for d_str, label in [(date_debut, "date_debut"), (date_fin, "date_fin")]:
|
|
if d_str:
|
|
try:
|
|
datetime.strptime(d_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
return bad_request(f"{label} invalide : {d_str}. Format : YYYY-MM-DD")
|
|
|
|
conn = get_db()
|
|
try:
|
|
import ml_feedback_saas
|
|
|
|
stats = ml_feedback_saas.get_feedback_stats(conn, date_debut, date_fin)
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"strategies": stats,
|
|
"filters": {
|
|
"date_debut": date_debut,
|
|
"date_fin": date_fin,
|
|
},
|
|
"total_strategies": len(stats),
|
|
}
|
|
), 200
|
|
|
|
except Exception as e:
|
|
return internal_error(str(e))
|
|
finally:
|
|
conn.close()
|