chartbastan/backend/app/api/v1/predictions.py
2026-02-01 09:31:38 +01:00

426 lines
12 KiB
Python

"""
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