426 lines
12 KiB
Python
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
|