Initial commit

This commit is contained in:
2026-02-01 09:31:38 +01:00
commit e02db93960
4396 changed files with 1511612 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Routes API de l'application."""

View File

@@ -0,0 +1,95 @@
"""
API dependencies.
This module provides common dependencies for API endpoints.
"""
from typing import Optional
from fastapi import Depends, HTTPException, status, Header
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.apiKeyService import ApiKeyService
async def get_api_key(
x_api_key: Optional[str] = Header(None, description="API Key for authentication"),
db: Session = Depends(get_db)
) -> int:
"""
Dependency to validate API key from header.
Args:
x_api_key: API key from X-API-Key header
db: Database session
Returns:
User ID associated with valid API key
Raises:
401: If API key is missing or invalid
Example:
from fastapi import Depends, APIRouter
@router.get("/protected")
async def protected_endpoint(user_id: int = Depends(get_api_key)):
return {"user_id": user_id}
"""
if not x_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"code": "MISSING_API_KEY",
"message": "X-API-Key header is required for this endpoint"
}
)
service = ApiKeyService(db)
api_key_record = service.validate_api_key(x_api_key)
if not api_key_record:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"code": "INVALID_API_KEY",
"message": "Invalid API key"
}
)
return api_key_record.user_id
async def get_api_key_optional(
x_api_key: Optional[str] = Header(None, description="Optional API Key for authentication"),
db: Session = Depends(get_db)
) -> Optional[int]:
"""
Optional dependency to validate API key from header.
Args:
x_api_key: API key from X-API-Key header (optional)
db: Database session
Returns:
User ID if API key is valid, None otherwise
Example:
from fastapi import Depends, APIRouter
@router.get("/semi-public")
async def semi_public_endpoint(user_id: Optional[int] = Depends(get_api_key_optional)):
if user_id:
return {"message": "Authenticated", "user_id": user_id}
return {"message": "Unauthenticated"}
"""
if not x_api_key:
return None
service = ApiKeyService(db)
api_key_record = service.validate_api_key(x_api_key)
if not api_key_record:
return None
return api_key_record.user_id

View File

@@ -0,0 +1,5 @@
"""
Public API package.
This module provides public API endpoints with OpenAPI documentation.
"""

View File

@@ -0,0 +1,5 @@
"""
Public API v1 package.
This module provides v1 public API endpoints.
"""

View File

@@ -0,0 +1,162 @@
"""
Public API endpoints for matches.
This module provides public endpoints for retrieving matches without authentication.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Query, HTTPException, status, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.match import Match
from app.schemas.public import (
PublicMatchResponse,
SuccessResponse,
SuccessMeta
)
router = APIRouter(prefix="/api/public/v1", tags=["public-matches"])
@router.get("/matches", response_model=SuccessResponse)
def get_public_matches(
limit: int = Query(20, ge=1, le=100, description="Maximum number of matches to return (max 100)"),
offset: int = Query(0, ge=0, description="Number of matches to skip"),
league: Optional[str] = Query(None, description="Filter by league name (case-insensitive)"),
status_filter: Optional[str] = Query(None, alias="status", description="Filter by match status"),
db: Session = Depends(get_db)
):
"""
Get public matches with pagination and filters.
This endpoint provides publicly accessible matches without authentication.
Data is limited to non-sensitive information only.
Args:
limit: Maximum number of matches to return (1-100, default: 20)
offset: Number of matches to skip (default: 0)
league: Optional filter by league name (case-insensitive)
status: Optional filter by match status (e.g., "scheduled", "completed", "ongoing")
db: Database session (injected)
Returns:
Paginated list of public matches
Example Requests:
GET /api/public/v1/matches
GET /api/public/v1/matches?limit=10&offset=0
GET /api/public/v1/matches?league=Ligue%201
GET /api/public/v1/matches?status=scheduled
Example Response:
{
"data": [
{
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled"
}
],
"meta": {
"total": 45,
"limit": 20,
"offset": 0,
"timestamp": "2026-01-17T14:30:00Z",
"version": "v1"
}
}
"""
# Build query
query = db.query(Match)
# Apply filters
if league:
query = query.filter(Match.league.ilike(f"%{league}%"))
if status_filter:
query = query.filter(Match.status == status_filter)
# Get total count
total = query.count()
# Apply pagination
matches = query.order_by(Match.date).offset(offset).limit(limit).all()
# Build response
match_responses = []
for match in matches:
match_responses.append({
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status
})
return {
"data": match_responses,
"meta": {
"total": total,
"limit": limit,
"offset": offset,
"timestamp": datetime.utcnow().isoformat() + "Z",
"version": "v1"
}
}
@router.get("/matches/{match_id}", response_model=PublicMatchResponse)
def get_public_match(
match_id: int,
db: Session = Depends(get_db)
):
"""
Get a specific public match by ID.
This endpoint provides a publicly accessible match without authentication.
Args:
match_id: ID of match
db: Database session (injected)
Returns:
Public match details
Raises:
404: If match doesn't exist
Example Request:
GET /api/public/v1/matches/1
Example Response:
{
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled"
}
"""
match = db.query(Match).filter(Match.id == match_id).first()
if not match:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Match with id {match_id} not found"
)
return {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status
}

View File

@@ -0,0 +1,189 @@
"""
Public API endpoints for predictions.
This module provides public endpoints for retrieving predictions without authentication.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Query, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.prediction_service import PredictionService
from app.schemas.public import (
PublicPredictionResponse,
SuccessResponse,
SuccessMeta
)
router = APIRouter(prefix="/api/public/v1", tags=["public-predictions"])
@router.get("/predictions", response_model=SuccessResponse)
def get_public_predictions(
limit: int = Query(20, ge=1, le=100, description="Maximum number of predictions to return (max 100)"),
offset: int = Query(0, ge=0, description="Number of predictions to skip"),
league: Optional[str] = Query(None, description="Filter by league name (case-insensitive)"),
db: Session = Depends(get_db)
):
"""
Get public predictions with pagination and filters.
This endpoint provides publicly accessible predictions without authentication.
Data is limited to non-sensitive information only.
Args:
limit: Maximum number of predictions to return (1-100, default: 20)
offset: Number of predictions to skip (default: 0)
league: Optional filter by league name (case-insensitive partial match)
db: Database session (injected)
Returns:
Paginated list of public predictions with match details
Example Requests:
GET /api/public/v1/predictions
GET /api/public/v1/predictions?limit=10&offset=0
GET /api/public/v1/predictions?league=Ligue%201
Example Response:
{
"data": [
{
"id": 1,
"match_id": 1,
"match": {
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled"
},
"energy_score": "high",
"confidence": "65.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
],
"meta": {
"total": 45,
"limit": 20,
"offset": 0,
"timestamp": "2026-01-17T14:30:00Z",
"version": "v1"
}
}
"""
service = PredictionService(db)
predictions, total = service.get_predictions_with_pagination(
limit=limit,
offset=offset,
team_id=None, # No team filter for public API
league=league,
date_min=None, # No date filter for public API
date_max=None
)
# Build response with match details
prediction_responses = []
for prediction in predictions:
match = prediction.match
prediction_responses.append({
"id": prediction.id,
"match_id": prediction.match_id,
"match": {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status
},
"energy_score": prediction.energy_score,
"confidence": prediction.confidence,
"predicted_winner": prediction.predicted_winner,
"created_at": prediction.created_at.isoformat() if prediction.created_at else None
})
return {
"data": prediction_responses,
"meta": {
"total": total,
"limit": limit,
"offset": offset,
"timestamp": datetime.utcnow().isoformat() + "Z",
"version": "v1"
}
}
@router.get("/predictions/{prediction_id}", response_model=PublicPredictionResponse)
def get_public_prediction(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Get a specific public prediction by ID.
This endpoint provides a publicly accessible prediction without authentication.
Args:
prediction_id: ID of the prediction
db: Database session (injected)
Returns:
Public prediction with match details
Raises:
404: If prediction doesn't exist
Example Request:
GET /api/public/v1/predictions/1
Example Response:
{
"id": 1,
"match_id": 1,
"match": {
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled"
},
"energy_score": "high",
"confidence": "65.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
"""
service = PredictionService(db)
prediction = service.get_prediction_by_id(prediction_id)
if not prediction:
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prediction with id {prediction_id} not found"
)
match = prediction.match
return {
"id": prediction.id,
"match_id": prediction.match_id,
"match": {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status
},
"energy_score": prediction.energy_score,
"confidence": prediction.confidence,
"predicted_winner": prediction.predicted_winner,
"created_at": prediction.created_at.isoformat() if prediction.created_at else None
}

View File

@@ -0,0 +1,30 @@
"""Routes API v1 de l'application."""
from fastapi import APIRouter
from .predictions import router as predictions_router
from .users import router as users_router
from .backtesting import router as backtesting_router
from .user_predictions import router as user_predictions_router
from .badges import router as badges_router
from .leaderboard import router as leaderboard_router
# #region agent log
def write_debug_log(hypothesisId: str, location: str, message: str, data: dict = None):
"""Écrit un log NDJSON pour le debug."""
import json
from datetime import datetime
log_entry = {
"sessionId": "debug-session",
"runId": "run1",
"hypothesisId": hypothesisId,
"location": location,
"message": message,
"data": data or {},
"timestamp": datetime.now().timestamp() * 1000
}
with open(r"d:\\dev_new_pc\\chartbastan\\.cursor\\debug.log", "a") as f:
f.write(json.dumps(log_entry) + "\n")
write_debug_log("C", "__init__.py:20", "API v1 routers initialized without main v1 router", {"routers": ["users", "auth", "predictions", "backtesting", "leaderboard", "badges", "user_predictions"]})
# #endregion
__all__ = ["predictions_router", "users_router", "backtesting_router", "user_predictions_router", "badges_router", "leaderboard_router", "auth_router"]

261
backend/app/api/v1/auth.py Normal file
View File

@@ -0,0 +1,261 @@
"""
Authentication API Endpoints.
This module provides endpoints for user authentication including
login, registration, and logout.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from app.database import get_db
from app.models.user import User
from app.models.badge import Badge, UserBadge
from app.schemas.auth import (
LoginRequest,
RegisterRequest,
AuthResponse,
ErrorResponse
)
# #region agent log
def write_debug_log(hypothesisId: str, location: str, message: str, data: dict = None):
"""Écrit un log NDJSON pour le debug."""
import json
log_entry = {
"sessionId": "debug-session",
"runId": "run1",
"hypothesisId": hypothesisId,
"location": location,
"message": message,
"data": data or {},
"timestamp": datetime.now().timestamp() * 1000
}
with open(r"d:\\dev_new_pc\\chartbastan\\.cursor\\debug.log", "a") as f:
f.write(json.dumps(log_entry) + "\n")
# #endregion
# Configuration du hashage de mot de passe
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
router = APIRouter(tags=["authentication"])
# #region agent log
write_debug_log("C", "auth.py:29", "Auth router initialized", {"prefix": "/api/v1/auth", "tags": ["authentication"]})
# #endregion
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Vérifie un mot de passe en clair contre le hash"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Génère un hash sécurisé pour un mot de passe"""
return pwd_context.hash(password)
def generate_referral_code() -> str:
"""Génère un code de parrainage unique de 8 caractères"""
import secrets
import string
alphabet = string.ascii_uppercase + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(8))
@router.post("/login", response_model=AuthResponse, responses={401: {"model": ErrorResponse}})
def login_user(
request: LoginRequest,
db: Session = Depends(get_db)
):
"""
Connecter un utilisateur avec email et mot de passe.
Args:
request: Email et mot de passe de l'utilisateur
db: Session de base de données
Returns:
AuthResponse avec token et données utilisateur
Raises:
401: Si email ou mot de passe incorrect
500: Si erreur serveur
"""
# #region agent log
write_debug_log("D", "auth.py:48", "Login endpoint called", {"email": request.email})
# #endregion
user = db.query(User).filter(User.email == request.email).first()
# #region agent log
write_debug_log("D", "auth.py:67", "User lookup", {"email": request.email, "user_found": user is not None})
# #endregion
# Vérifier si l'utilisateur existe
if not user:
# #region agent log
write_debug_log("D", "auth.py:70", "User not found", {"email": request.email})
# #endregion
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou mot de passe incorrect"
)
# Vérifier le mot de passe
if not user.password_hash:
# #region agent log
write_debug_log("D", "auth.py:81", "No password hash", {"email": request.email, "user_id": user.id})
# #endregion
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou mot de passe incorrect"
)
password_valid = verify_password(request.password, user.password_hash)
# #region agent log
write_debug_log("D", "auth.py:91", "Password verification", {"email": request.email, "user_id": user.id, "password_valid": password_valid})
# #endregion
if not password_valid:
# #region agent log
write_debug_log("D", "auth.py:96", "Invalid password", {"email": request.email, "user_id": user.id})
# #endregion
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou mot de passe incorrect"
)
# Retourner les données utilisateur (sans le hash de mot de passe)
# #region agent log
write_debug_log("D", "auth.py:107", "Login successful", {"email": request.email, "user_id": user.id})
# #endregion
return {
"data": {
"id": user.id,
"email": user.email,
"name": user.name,
"is_premium": user.is_premium,
"referral_code": user.referral_code,
"created_at": user.created_at,
},
"meta": {
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
}
@router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
def register_user(
request: RegisterRequest,
db: Session = Depends(get_db)
):
"""
Inscrire un nouvel utilisateur.
Args:
request: Email, mot de passe, nom optionnel et code de parrainage
db: Session de base de données
Returns:
AuthResponse avec données utilisateur
Raises:
400: Si validation échoue
409: Si email déjà utilisé
500: Si erreur serveur
"""
# Vérifier si l'email existe déjà
existing_user = db.query(User).filter(User.email == request.email).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Cet email est déjà utilisé"
)
# Valider le code de parrainage si fourni
referrer: Optional[User] = None
if request.referral_code:
referrer = db.query(User).filter(
User.referral_code == request.referral_code
).first()
if not referrer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Code de parrainage invalide"
)
# Générer un code de parrainage unique
referral_code = generate_referral_code()
while db.query(User).filter(User.referral_code == referral_code).first():
referral_code = generate_referral_code()
# Hasher le mot de passe
password_hash = get_password_hash(request.password)
# Créer l'utilisateur
new_user = User(
email=request.email,
password_hash=password_hash,
name=request.name,
is_premium=False,
referral_code=referral_code,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
db.add(new_user)
db.commit()
db.refresh(new_user)
# Attribuer le badge de bienvenue (première connexion)
welcome_badge = db.query(Badge).filter(
Badge.badge_id == "first_login"
).first()
if welcome_badge:
user_badge = UserBadge(
user_id=new_user.id,
badge_id=welcome_badge.id,
unlocked_at=datetime.utcnow()
)
db.add(user_badge)
db.commit()
return {
"data": {
"id": new_user.id,
"email": new_user.email,
"name": new_user.name,
"is_premium": new_user.is_premium,
"referral_code": new_user.referral_code,
"created_at": new_user.created_at,
},
"meta": {
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
}
@router.post("/logout")
def logout_user():
"""
Déconnecter l'utilisateur.
Note: Le backend utilise des cookies côté client.
Cette endpoint est disponible pour compatibilité future avec JWT.
"""
return {
"data": {
"message": "Déconnexion réussie"
},
"meta": {
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
}

View File

@@ -0,0 +1,177 @@
"""
Backtesting API Endpoints.
This module provides API endpoints for running backtesting
on historical match data and exporting results.
"""
import logging
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.backtesting_service import BacktestingService
from app.schemas.backtesting import (
BacktestingRequest,
BacktestingResponse,
BacktestingErrorResponse
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/backtesting", tags=["backtesting"])
@router.post("/run", response_model=BacktestingResponse, responses={400: {"model": BacktestingErrorResponse}})
def run_backtesting(
request: BacktestingRequest,
db: Session = Depends(get_db)
):
"""
Run backtesting on historical matches.
Analyzes historical match predictions against actual results to calculate
accuracy metrics and validate prediction system performance.
- **leagues**: Optional list of leagues to filter by (e.g., ['Ligue 1', 'Premier League'])
- **start_date**: Optional start date for filtering matches (ISO 8601 format)
- **end_date**: Optional end date for filtering matches (ISO 8601 format)
- **export_format**: Optional export format ('json', 'csv', 'html')
Returns:
Comprehensive backtesting report including:
- Total matches analyzed
- Correct/incorrect predictions
- Overall accuracy percentage
- Validation status (VALIDATED, REVISION_REQUIRED, BELOW_TARGET)
- Detailed results per match
- Metrics breakdown by league
Example:
```json
{
"leagues": ["Ligue 1", "Premier League"],
"start_date": "2025-01-01T00:00:00Z",
"end_date": "2025-12-31T23:59:59Z",
"export_format": "html"
}
```
Raises:
400: If no matches found with specified filters
500: If internal server error occurs
"""
try:
logger.info("Backtesting request received")
# Parse dates if provided
start_date = None
end_date = None
if request.start_date:
try:
start_date = datetime.fromisoformat(request.start_date.replace('Z', '+00:00'))
except ValueError as e:
raise HTTPException(
status_code=400,
detail=f"Invalid start_date format. Use ISO 8601 format: {str(e)}"
)
if request.end_date:
try:
end_date = datetime.fromisoformat(request.end_date.replace('Z', '+00:00'))
except ValueError as e:
raise HTTPException(
status_code=400,
detail=f"Invalid end_date format. Use ISO 8601 format: {str(e)}"
)
# Initialize backtesting service
backtesting_service = BacktestingService(db)
# Run backtesting
result = backtesting_service.run_backtesting(
leagues=request.leagues,
start_date=start_date,
end_date=end_date
)
logger.info(
f"Backtesting completed: {result['total_matches']} matches, "
f"{result['accuracy']:.2f}% accuracy"
)
# Export results if format specified
if request.export_format:
try:
exported = backtesting_service.export_results(result, request.export_format)
result['export'] = {
'format': request.export_format,
'data': exported
}
except ValueError as e:
logger.warning(f"Export failed: {str(e)}")
# Continue without export, just warn
# Wrap response with metadata
response = {
'data': result,
'meta': {
'timestamp': result.get('timestamp'),
'version': 'v1'
}
}
return response
except ValueError as e:
logger.error(f"Backtesting value error: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Backtesting error: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Internal server error during backtesting: {str(e)}"
)
@router.get("/status")
def get_backtesting_status():
"""
Get backtesting service status.
Returns information about the backtesting system configuration
and validation thresholds.
Returns:
Status information including:
- Available leagues
- Validation thresholds
- Service health status
"""
from app.ml.backtesting import (
ACCURACY_VALIDATED_THRESHOLD,
ACCURACY_ALERT_THRESHOLD
)
return {
'data': {
'status': 'operational',
'validation_thresholds': {
'validated_threshold': ACCURACY_VALIDATED_THRESHOLD,
'alert_threshold': ACCURACY_ALERT_THRESHOLD
},
'supported_export_formats': ['json', 'csv', 'html'],
'available_filters': {
'leagues': True,
'date_range': True
}
},
'meta': {
'timestamp': datetime.utcnow().isoformat(),
'version': 'v1'
}
}

View File

@@ -0,0 +1,166 @@
"""
API endpoints pour les badges
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
from app.database import get_db
from app.models.badge import Badge, UserBadge
from app.models.user import User
from app.schemas.badge import (
BadgeResponse,
BadgeListResponse,
BadgeCheckResponse,
UserBadgeResponse,
UserBadgeListResponse,
BadgeUnlockRequest,
)
from app.services.badge_service import BadgeService
router = APIRouter()
@router.get("/", response_model=BadgeListResponse)
def get_all_badges(
db: Session = Depends(get_db),
user_id: Optional[int] = None,
):
"""
Récupère tous les badges disponibles
- Indique les badges débloqués par l'utilisateur si user_id est fourni
- Retourne les critères de débloquage
"""
# Récupérer tous les badges
badges = db.query(Badge).all()
# Si user_id est fourni, récupérer les badges débloqués
unlocked_badge_ids = set()
if user_id:
unlocked_badges = db.query(UserBadge).filter(UserBadge.user_id == user_id).all()
unlocked_badge_ids = set(ub.badge_id for ub in unlocked_badges)
# Formater la réponse
badge_responses = []
for badge in badges:
badge_responses.append(
BadgeResponse(
id=badge.id,
badgeId=badge.badge_id,
name=badge.name,
description=badge.description,
icon=badge.icon,
category=badge.category,
criteriaType=badge.criteria_type,
criteriaValue=badge.criteria_value,
criteriaDescription=badge.criteria_description,
rarity=badge.rarity,
points=badge.points,
createdAt=badge.created_at.isoformat(),
unlocked=badge.badge_id in unlocked_badge_ids if user_id else None,
)
)
return BadgeListResponse(
data=badge_responses,
meta={
"count": len(badge_responses),
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
)
@router.post("/check", response_model=BadgeCheckResponse)
def check_badges(
request: BadgeUnlockRequest,
db: Session = Depends(get_db),
):
"""
Vérifie et débloque les nouveaux badges pour un utilisateur
- Vérifie les critères de badges de l'utilisateur
- Débloque les nouveaux badges atteints
- Retourne les badges débloqués
- Envoie les notifications
"""
# Vérifier que l'utilisateur existe
user = db.query(User).filter(User.id == request.userId).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Utilisateur non trouvé"
)
# Utiliser le service pour vérifier et débloquer les badges
badge_service = BadgeService(db)
result = badge_service.check_and_unlock_badges(request.userId)
return BadgeCheckResponse(
data={
"new_badges": result["new_badges"],
"total_badges": result["total_badges"],
"message": result["message"],
},
meta={
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
)
@router.get("/users/{user_id}", response_model=UserBadgeListResponse)
def get_user_badges(
user_id: int,
db: Session = Depends(get_db),
):
"""
Récupère tous les badges débloqués par un utilisateur
"""
# Vérifier que l'utilisateur existe
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Utilisateur non trouvé"
)
# Récupérer les badges débloqués
user_badges = db.query(UserBadge).filter(UserBadge.user_id == user_id).all()
# Formater la réponse
badge_responses = []
for user_badge in user_badges:
badge = db.query(Badge).filter(Badge.id == user_badge.badge_id).first()
if badge:
badge_responses.append(
UserBadgeResponse(
id=user_badge.id,
userId=user_badge.user_id,
badgeId=user_badge.badge_id,
unlockedAt=user_badge.unlocked_at.isoformat(),
badge={
"id": badge.id,
"badgeId": badge.badge_id,
"name": badge.name,
"description": badge.description,
"icon": badge.icon,
"category": badge.category,
"criteriaType": badge.criteria_type,
"criteriaValue": badge.criteria_value,
"criteriaDescription": badge.criteria_description,
"rarity": badge.rarity,
"points": badge.points,
"createdAt": badge.created_at.isoformat(),
}
)
)
return UserBadgeListResponse(
data={"badges": badge_responses},
meta={
"count": len(badge_responses),
"timestamp": datetime.utcnow().isoformat(),
"version": "v1"
}
)

View File

@@ -0,0 +1,142 @@
"""
Leaderboard API Routes.
This module provides REST endpoints for retrieving user rankings
and leaderboards based on prediction accuracy.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.leaderboard_service import LeaderboardService
from app.schemas.leaderboard import LeaderboardResponse, LeaderboardEntry, PersonalRankData
router = APIRouter(prefix="/api/v1/leaderboard", tags=["leaderboard"])
@router.get("", response_model=LeaderboardResponse)
def get_leaderboard(
limit: int = Query(100, ge=1, le=100, description="Maximum number of users to return (max 100)"),
user_id: Optional[int] = Query(None, description="Current user ID to include personal rank data"),
db: Session = Depends(get_db)
):
"""
Get leaderboard with top 100 users sorted by accuracy.
This endpoint retrieves the top users based on their prediction accuracy
and includes personal rank data if user_id is provided.
Ranking criteria:
- Primary: Accuracy percentage (higher is better)
- Secondary: Number of predictions viewed (more is better, used as tie-breaker)
Args:
limit: Maximum number of users to return (1-100, default: 100)
user_id: Optional current user ID to include personal rank data
db: Database session (injected)
Returns:
Leaderboard with top users and optional personal rank data
Raises:
404: If user_id provided but user doesn't exist
Example Request:
GET /api/v1/leaderboard
GET /api/v1/leaderboard?user_id=1
Example Response:
{
"data": [
{
"user_id": 1,
"username": "JohnDoe",
"accuracy": 95.5,
"predictions_count": 100
},
{
"user_id": 2,
"username": "JaneSmith",
"accuracy": 90.0,
"predictions_count": 85
}
],
"personal_data": {
"rank": 42,
"accuracy": 75.5,
"predictions_count": 25
},
"meta": {
"total": 2,
"limit": 100,
"timestamp": "2026-01-18T10:30:00Z",
"version": "v1"
}
}
"""
service = LeaderboardService(db)
leaderboard = service.get_leaderboard(limit=limit)
# Get personal rank if user_id provided
personal_data = None
if user_id:
personal_rank = service.get_personal_rank(user_id)
if personal_rank:
personal_data = PersonalRankData(**personal_rank)
# Format leaderboard entries
entries = [LeaderboardEntry(**entry) for entry in leaderboard]
return {
"data": entries,
"personal_data": personal_data,
"meta": {
"total": len(entries),
"limit": limit,
"timestamp": datetime.utcnow().isoformat() + "Z",
"version": "v1"
}
}
@router.get("/personal/{user_id}", response_model=PersonalRankData)
def get_personal_rank(
user_id: int,
db: Session = Depends(get_db)
):
"""
Get personal rank data for a specific user.
Args:
user_id: ID of the user
db: Database session (injected)
Returns:
Personal rank data with rank, accuracy, and predictions count
Raises:
404: If user doesn't exist or has no completed predictions
Example Request:
GET /api/v1/leaderboard/personal/1
Example Response:
{
"rank": 42,
"accuracy": 75.5,
"predictions_count": 25
}
"""
service = LeaderboardService(db)
personal_rank = service.get_personal_rank(user_id)
if not personal_rank:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found or has no completed predictions"
)
return PersonalRankData(**personal_rank)

View File

@@ -0,0 +1,425 @@
"""
Prediction API Routes.
This module provides REST endpoints for creating and retrieving match predictions.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.prediction_service import PredictionService
from app.schemas.prediction import PredictionListResponse, PredictionResponse
router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"])
@router.get("", response_model=PredictionListResponse)
def get_predictions(
limit: int = Query(20, ge=1, le=100, description="Maximum number of predictions to return (max 100)"),
offset: int = Query(0, ge=0, description="Number of predictions to skip"),
team_id: Optional[int] = Query(None, description="Filter by team ID"),
league: Optional[str] = Query(None, description="Filter by league name (case-insensitive)"),
date_min: Optional[datetime] = Query(None, description="Filter for matches after this date (ISO 8601)"),
date_max: Optional[datetime] = Query(None, description="Filter for matches before this date (ISO 8601)"),
db: Session = Depends(get_db)
):
"""
Get all predictions with pagination and filters.
This endpoint retrieves predictions joined with match data, applies optional filters,
and returns paginated results sorted by match date (upcoming matches first).
Args:
limit: Maximum number of predictions to return (1-100, default: 20)
offset: Number of predictions to skip (default: 0)
team_id: Optional filter by team ID (matches where team is home or away)
league: Optional filter by league name (case-insensitive partial match)
date_min: Optional filter for matches after this date
date_max: Optional filter for matches before this date
db: Database session (injected)
Returns:
Paginated list of predictions with match details and metadata
Example Requests:
GET /api/v1/predictions
GET /api/v1/predictions?limit=10&offset=0
GET /api/v1/predictions?league=Ligue%201
GET /api/v1/predictions?team_id=1&limit=5
GET /api/v1/predictions?date_min=2026-01-15T00:00:00Z&date_max=2026-01-20T23:59:59Z
Example Response:
{
"data": [
{
"id": 1,
"match_id": 1,
"match": {
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled"
},
"energy_score": "high",
"confidence": "65.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
],
"meta": {
"total": 45,
"limit": 20,
"offset": 0,
"timestamp": "2026-01-17T14:30:00Z",
"version": "v1"
}
}
"""
service = PredictionService(db)
predictions, total = service.get_predictions_with_pagination(
limit=limit,
offset=offset,
team_id=team_id,
league=league,
date_min=date_min,
date_max=date_max
)
# Build response with match details
prediction_responses = []
for prediction in predictions:
match = prediction.match
prediction_responses.append({
"id": prediction.id,
"match_id": prediction.match_id,
"match": {
"id": match.id,
"home_team": match.home_team,
"away_team": match.away_team,
"date": match.date.isoformat() if match.date else None,
"league": match.league,
"status": match.status
},
"energy_score": prediction.energy_score,
"confidence": prediction.confidence,
"predicted_winner": prediction.predicted_winner,
"created_at": prediction.created_at.isoformat() if prediction.created_at else None
})
return {
"data": prediction_responses,
"meta": {
"total": total,
"limit": limit,
"offset": offset,
"timestamp": datetime.utcnow().isoformat() + "Z",
"version": "v1"
}
}
@router.get("/match/{match_id}")
def get_prediction_by_match_id(
match_id: int,
db: Session = Depends(get_db)
):
"""
Get prediction details for a specific match.
This endpoint retrieves the latest prediction for a match and includes
full match details, energy score information, and historical predictions.
Args:
match_id: ID of the match
db: Database session (injected)
Returns:
Prediction with full details including match info and history
Raises:
404: If match or prediction not found
Example Request:
GET /api/v1/predictions/match/1
Example Response:
{
"id": 3,
"match_id": 1,
"match": {
"id": 1,
"home_team": "PSG",
"away_team": "Olympique de Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "scheduled",
"actual_winner": null
},
"energy_score": "high",
"confidence": "70.5%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T14:00:00Z",
"history": [
{
"id": 3,
"energy_score": "high",
"confidence": "70.5%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T14:00:00Z"
},
{
"id": 2,
"energy_score": "medium",
"confidence": "60.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
]
}
"""
service = PredictionService(db)
prediction_details = service.get_prediction_with_details(match_id)
if not prediction_details:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No predictions found for match {match_id}"
)
return prediction_details
@router.post("/matches/{match_id}/predict", response_model=PredictionResponse, status_code=status.HTTP_201_CREATED)
def create_prediction(
match_id: int,
home_energy: float,
away_energy: float,
energy_score_label: str | None = None,
db: Session = Depends(get_db)
):
"""
Create a prediction for a specific match.
This endpoint calculates a prediction based on energy scores for both teams
and stores it in the database.
Args:
match_id: ID of the match to predict
home_energy: Energy score of the home team (0.0+)
away_energy: Energy score of the away team (0.0+)
energy_score_label: Optional label for energy score (e.g., "high", "medium", "low")
db: Database session (injected)
Returns:
Created prediction object
Raises:
404: If match doesn't exist
400: If energy scores are invalid
422: If validation fails
Example Request:
POST /api/v1/predictions/matches/1/predict?home_energy=65.0&away_energy=45.0
Example Response:
{
"id": 1,
"match_id": 1,
"energy_score": "high",
"confidence": "40.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
"""
try:
service = PredictionService(db)
prediction = service.create_prediction_for_match(
match_id=match_id,
home_energy=home_energy,
away_energy=away_energy,
energy_score_label=energy_score_label
)
return prediction.to_dict()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create prediction: {str(e)}"
)
@router.get("/{prediction_id}", response_model=PredictionResponse)
def get_prediction(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Get a prediction by its ID.
Args:
prediction_id: ID of the prediction to retrieve
db: Database session (injected)
Returns:
Prediction object
Raises:
404: If prediction doesn't exist
Example Request:
GET /api/v1/predictions/1
Example Response:
{
"id": 1,
"match_id": 1,
"energy_score": "high",
"confidence": "40.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
"""
service = PredictionService(db)
prediction = service.get_prediction_by_id(prediction_id)
if not prediction:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prediction with id {prediction_id} not found"
)
return prediction.to_dict()
@router.get("/matches/{match_id}", response_model=PredictionListResponse)
def get_predictions_for_match(
match_id: int,
db: Session = Depends(get_db)
):
"""
Get all predictions for a specific match.
Args:
match_id: ID of the match
db: Database session (injected)
Returns:
List of predictions for the match
Example Request:
GET /api/v1/predictions/matches/1
Example Response:
{
"data": [
{
"id": 1,
"match_id": 1,
"energy_score": "high",
"confidence": "40.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
}
],
"count": 1,
"meta": {}
}
"""
service = PredictionService(db)
predictions = service.get_predictions_for_match(match_id)
return {
"data": [pred.to_dict() for pred in predictions],
"count": len(predictions),
"meta": {
"match_id": match_id
}
}
@router.get("/matches/{match_id}/latest", response_model=PredictionResponse)
def get_latest_prediction_for_match(
match_id: int,
db: Session = Depends(get_db)
):
"""
Get the most recent prediction for a specific match.
Args:
match_id: ID of the match
db: Database session (injected)
Returns:
Latest prediction object
Raises:
404: If no predictions exist for the match
Example Request:
GET /api/v1/predictions/matches/1/latest
Example Response:
{
"id": 3,
"match_id": 1,
"energy_score": "high",
"confidence": "45.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T14:00:00Z"
}
"""
service = PredictionService(db)
prediction = service.get_latest_prediction_for_match(match_id)
if not prediction:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No predictions found for match {match_id}"
)
return prediction.to_dict()
@router.delete("/{prediction_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_prediction(
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Delete a prediction by its ID.
Args:
prediction_id: ID of the prediction to delete
db: Database session (injected)
Returns:
No content (204)
Raises:
404: If prediction doesn't exist
Example Request:
DELETE /api/v1/predictions/1
Example Response:
(HTTP 204 No Content)
"""
service = PredictionService(db)
deleted = service.delete_prediction(prediction_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prediction with id {prediction_id} not found"
)
return None

View File

@@ -0,0 +1,241 @@
"""
User Prediction API Routes.
This module provides REST endpoints for tracking user predictions
and retrieving user statistics.
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services.user_prediction_service import UserPredictionService
from app.schemas.user_prediction import (
UserPredictionResponse,
UserPredictionListResponse,
UserStatsResponse
)
router = APIRouter(prefix="/api/v1/user-predictions", tags=["user-predictions"])
@router.post("", status_code=status.HTTP_201_CREATED)
def record_prediction_view(
user_id: int,
prediction_id: int,
db: Session = Depends(get_db)
):
"""
Record that a user viewed a prediction.
This endpoint tracks when a user views a prediction for ROI and accuracy calculations.
Duplicate views are ignored (unique constraint on user_id + prediction_id).
Args:
user_id: ID of the user
prediction_id: ID of the prediction viewed
db: Database session (injected)
Returns:
Created user prediction record
Raises:
404: If user or prediction doesn't exist
422: If validation fails
Example Request:
POST /api/v1/user-predictions?user_id=1&prediction_id=5
Example Response:
{
"id": 1,
"user_id": 1,
"prediction_id": 5,
"viewed_at": "2026-01-18T10:00:00Z",
"was_correct": null
}
"""
try:
service = UserPredictionService(db)
user_prediction = service.record_prediction_view(user_id, prediction_id)
return user_prediction.to_dict()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to record prediction view: {str(e)}"
)
@router.get("/history/{user_id}", response_model=UserPredictionListResponse)
def get_prediction_history(
user_id: int,
limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return (max 100)"),
offset: int = Query(0, ge=0, description="Number of records to skip"),
db: Session = Depends(get_db)
):
"""
Get a user's prediction viewing history.
This endpoint retrieves all predictions a user has viewed, sorted by most recent.
Includes full prediction and match details.
Args:
user_id: ID of the user
limit: Maximum number of records to return (1-100, default: 50)
offset: Number of records to skip (default: 0)
db: Database session (injected)
Returns:
Paginated list of user predictions with full details
Example Request:
GET /api/v1/user-predictions/history/1?limit=10&offset=0
Example Response:
{
"data": [
{
"id": 5,
"user_id": 1,
"prediction_id": 10,
"viewed_at": "2026-01-18T10:00:00Z",
"was_correct": true,
"prediction": {
"id": 10,
"match_id": 3,
"energy_score": "high",
"confidence": "75.0%",
"predicted_winner": "PSG",
"created_at": "2026-01-17T12:00:00Z"
},
"match": {
"id": 3,
"home_team": "PSG",
"away_team": "Marseille",
"date": "2026-01-18T20:00:00Z",
"league": "Ligue 1",
"status": "completed",
"actual_winner": "PSG"
}
}
],
"meta": {
"total": 25,
"limit": 10,
"offset": 0,
"timestamp": "2026-01-18T15:30:00Z"
}
}
"""
from datetime import datetime
service = UserPredictionService(db)
predictions, total = service.get_user_prediction_history(
user_id=user_id,
limit=limit,
offset=offset
)
return {
"data": predictions,
"meta": {
"total": total,
"limit": limit,
"offset": offset,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
}
@router.get("/stats/{user_id}", response_model=UserStatsResponse)
def get_user_statistics(
user_id: int,
db: Session = Depends(get_db)
):
"""
Get user statistics including accuracy and ROI.
This endpoint calculates:
- Total predictions viewed
- Number of correct/incorrect predictions
- Accuracy rate percentage
- ROI (Return on Investment) in EUR
Args:
user_id: ID of the user
db: Database session (injected)
Returns:
User statistics
Raises:
404: If user doesn't exist
Example Request:
GET /api/v1/user-predictions/stats/1
Example Response:
{
"total_predictions_viewed": 25,
"correct_predictions": 18,
"incorrect_predictions": 5,
"accuracy_rate": 78.3,
"roi": 1550.0
}
"""
service = UserPredictionService(db)
stats = service.get_user_stats(user_id)
return UserStatsResponse(**stats)
@router.put("/result/{prediction_id}")
def update_prediction_result(
prediction_id: int,
actual_winner: Optional[str] = Query(None, description="Actual winner: home, away, draw, or null"),
db: Session = Depends(get_db)
):
"""
Update prediction result when match completes.
This endpoint is called when a match finishes to update all user
predictions that referenced this prediction.
Args:
prediction_id: ID of the prediction
actual_winner: Actual winner of the match (home/away/draw or null)
db: Database session (injected)
Returns:
Success message
Example Request:
PUT /api/v1/user-predictions/result/10?actual_winner=home
Example Response:
{
"message": "Prediction results updated successfully",
"prediction_id": 10,
"actual_winner": "home"
}
"""
try:
service = UserPredictionService(db)
service.update_prediction_result(prediction_id, actual_winner)
return {
"message": "Prediction results updated successfully",
"prediction_id": prediction_id,
"actual_winner": actual_winner
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update prediction result: {str(e)}"
)

153
backend/app/api/v1/users.py Normal file
View File

@@ -0,0 +1,153 @@
"""Routes API pour les utilisateurs."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from passlib.context import CryptContext
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserResponse, UserLoginRequest
# Configuration du hashage de mot de passe
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
router = APIRouter()
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Vérifie un mot de passe en clair contre le hash"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Génère un hash sécurisé pour un mot de passe"""
return pwd_context.hash(password)
@router.post("/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
"""
Créer un nouvel utilisateur (inscription).
Args:
user: Données utilisateur à créer.
db: Session de base de données.
Returns:
UserResponse: L'utilisateur créé.
Raises:
400: Si validation échoue
409: Si email déjà utilisé
"""
# Vérifier si l'email existe déjà
existing_user = db.query(User).filter(User.email == user.email).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Cet email est déjà utilisé"
)
# Hasher le mot de passe
password_hash = get_password_hash(user.password)
# Générer un code de parrainage unique
import secrets
import string
referral_code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
while db.query(User).filter(User.referral_code == referral_code).first():
referral_code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
# Créer l'utilisateur avec le mot de passe hashé
new_user = User(
email=user.email,
name=user.name,
password_hash=password_hash,
is_premium=False,
referral_code=referral_code
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return UserResponse(
id=new_user.id,
email=new_user.email,
name=new_user.name,
created_at=new_user.created_at,
updated_at=new_user.updated_at
)
@router.post("/login", response_model=UserResponse)
def login_user(user: UserLoginRequest, db: Session = Depends(get_db)):
"""
Connecter un utilisateur.
Args:
user: Email et mot de passe de l'utilisateur.
db: Session de base de données.
Returns:
UserResponse: L'utilisateur connecté.
Raises:
401: Si email ou mot de passe incorrect
"""
# Vérifier si l'utilisateur existe
db_user = db.query(User).filter(User.email == user.email).first()
if not db_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou mot de passe incorrect"
)
# Vérifier le mot de passe
if not verify_password(user.password, db_user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email ou mot de passe incorrect"
)
return UserResponse(
id=db_user.id,
email=db_user.email,
name=db_user.name,
created_at=db_user.created_at,
updated_at=db_user.updated_at
)
@router.get("/", response_model=List[UserResponse])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""
Récupérer la liste des utilisateurs.
Args:
skip: Nombre d'éléments à sauter.
limit: Nombre maximum d'éléments à retourner.
db: Session de base de données.
Returns:
List[UserResponse]: Liste des utilisateurs.
"""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
"""
Récupérer un utilisateur par son ID.
Args:
user_id: ID de l'utilisateur.
db: Session de base de données.
Returns:
UserResponse: L'utilisateur trouvé.
"""
db_user = db.query(User).filter(User.id == user_id).first()
if db_user is None:
raise HTTPException(status_code=404, detail="Utilisateur non trouvé")
return db_user