"""
The Odds API Client
This module handles interactions with The Odds API to retrieve
college football game odds and scores.
"""
import requests
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime
logger = logging.getLogger(__name__)
ODDS_API_BASE_URL = "https://api.the-odds-api.com/v4"
def get_ncaaf_odds(
api_key: str,
regions: str = "us",
markets: str = "h2h,spreads,totals",
odds_format: str = "american",
date_format: str = "iso"
) -> Optional[List[Dict[str, Any]]]:
"""
Fetch NCAA football odds from The Odds API.
Args:
api_key: The Odds API key
regions: Comma-separated list of regions (default: "us")
markets: Comma-separated list of markets (default: "h2h,spreads,totals")
odds_format: Format for odds (default: "american")
date_format: Format for dates (default: "iso")
Returns:
List of game odds data or None if request fails
"""
if not api_key:
logger.error("ODDS_API_KEY is not set")
return None
url = f"{ODDS_API_BASE_URL}/sports/americanfootball_ncaaf/odds"
params = {
"apiKey": api_key,
"regions": regions,
"markets": markets,
"oddsFormat": odds_format,
"dateFormat": date_format
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
# Check API usage limits
remaining_requests = response.headers.get("x-requests-remaining")
used_requests = response.headers.get("x-requests-used")
if remaining_requests:
logger.info(f"Odds API usage: {used_requests} used, {remaining_requests} remaining")
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching odds from The Odds API: {e}")
return None
def find_game_by_teams(
games: List[Dict[str, Any]],
team1: str,
team2: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""
Find a game by team names.
Uses improved matching to handle team name variations.
Args:
games: List of game data from The Odds API
team1: First team name (home or away)
team2: Optional second team name for exact match
Returns:
Matching game data or None if not found
"""
if not games:
return None
from src.team_normalizer import normalize_team_name
team1_normalized = normalize_team_name(team1)
team1_lower = team1.lower()
for game in games:
home_team = game.get("home_team", "")
away_team = game.get("away_team", "")
home_normalized = normalize_team_name(home_team)
away_normalized = normalize_team_name(away_team)
home_lower = home_team.lower()
away_lower = away_team.lower()
# Check multiple matching strategies
team1_matches_home = (
team1_normalized == home_normalized or
team1_normalized in home_normalized or
home_normalized in team1_normalized or
team1_lower in home_lower or
home_lower in team1_lower
)
team1_matches_away = (
team1_normalized == away_normalized or
team1_normalized in away_normalized or
away_normalized in team1_normalized or
team1_lower in away_lower or
away_lower in team1_lower
)
if team1_matches_home or team1_matches_away:
# If team2 is provided, check for match
if team2:
team2_normalized = normalize_team_name(team2)
team2_lower = team2.lower()
team2_matches_home = (
team2_normalized == home_normalized or
team2_normalized in home_normalized or
home_normalized in team2_normalized or
team2_lower in home_lower or
home_lower in team2_lower
)
team2_matches_away = (
team2_normalized == away_normalized or
team2_normalized in away_normalized or
away_normalized in team2_normalized or
team2_lower in away_lower or
away_lower in team2_lower
)
if team2_matches_home or team2_matches_away:
return game
else:
return game
return None
def find_next_game_for_team(
games: List[Dict[str, Any]],
team_name: str
) -> Optional[Dict[str, Any]]:
"""
Find the next scheduled game for a team.
Uses improved matching to handle team name variations.
Args:
games: List of game data from The Odds API
team_name: Team name to search for
Returns:
Next upcoming game for the team or None if not found
"""
if not games:
return None
from src.team_normalizer import normalize_team_name
team_normalized = normalize_team_name(team_name)
team_lower = team_name.lower()
now = datetime.now()
next_game = None
next_game_time = None
for game in games:
home_team = game.get("home_team", "")
away_team = game.get("away_team", "")
home_normalized = normalize_team_name(home_team)
away_normalized = normalize_team_name(away_team)
home_lower = home_team.lower()
away_lower = away_team.lower()
commence_time_str = game.get("commence_time", "")
# Check if team matches using multiple strategies
team_matches = (
team_normalized == home_normalized or
team_normalized == away_normalized or
team_normalized in home_normalized or
team_normalized in away_normalized or
home_normalized in team_normalized or
away_normalized in team_normalized or
team_lower in home_lower or
team_lower in away_lower or
home_lower in team_lower or
away_lower in team_lower
)
if not team_matches:
continue
# Parse commence time
try:
if commence_time_str:
commence_time = datetime.fromisoformat(commence_time_str.replace('Z', '+00:00'))
# Convert to naive datetime for comparison (or handle timezone properly)
if commence_time.tzinfo:
from datetime import timezone
now_aware = now.replace(tzinfo=timezone.utc)
if commence_time > now_aware:
if next_game_time is None or commence_time < next_game_time:
next_game = game
next_game_time = commence_time
except (ValueError, AttributeError):
# If date parsing fails, still consider it if it's in the list
# (might be a future game)
if next_game is None:
next_game = game
return next_game
def format_next_game_odds(
team_name: str,
game: Dict[str, Any]
) -> Dict[str, Any]:
"""
Format next game odds response.
Args:
team_name: Name of the team
game: Game data from The Odds API
Returns:
Formatted response with next game and odds
"""
home_team = game.get("home_team", "")
away_team = game.get("away_team", "")
commence_time = game.get("commence_time", "")
# Determine opponent
is_home = team_name.lower() in home_team.lower()
opponent = away_team if is_home else home_team
# Extract odds from bookmakers
bookmakers = game.get("bookmakers", [])
odds_data = {
"spread": None,
"moneyline": {},
"over_under": None
}
if bookmakers:
# Use the first bookmaker's odds
bookmaker = bookmakers[0]
markets = bookmaker.get("markets", [])
for market in markets:
market_key = market.get("key", "")
outcomes = market.get("outcomes", [])
if market_key == "spreads":
# Extract spread odds
for outcome in outcomes:
team = outcome.get("name", "")
point = outcome.get("point", 0)
price = outcome.get("price", 0)
if team.lower() == team_name.lower():
# Format spread (e.g., "Team A -6.5" or "Team B +6.5")
spread_sign = "-" if point > 0 else "+"
odds_data["spread"] = {
"team": f"{team_name} {spread_sign}{abs(point)}",
"point": point,
"odds": price
}
if len(outcomes) > 1:
other_outcome = outcomes[1] if outcomes[0].get("name") == team else outcomes[0]
odds_data["spread"]["opponent_odds"] = other_outcome.get("price", 0)
elif market_key == "h2h":
# Extract moneyline odds
for outcome in outcomes:
team = outcome.get("name", "")
price = outcome.get("price", 0)
if team.lower() == team_name.lower():
odds_data["moneyline"]["team"] = price
elif team.lower() == opponent.lower():
odds_data["moneyline"]["opponent"] = price
elif market_key == "totals":
# Extract over/under
for outcome in outcomes:
if outcome.get("name", "").lower() == "over":
odds_data["over_under"] = {
"total": outcome.get("point", 0),
"over_odds": outcome.get("price", 0),
"under_odds": outcomes[1].get("price", 0) if len(outcomes) > 1 else None
}
return {
"team": team_name,
"next_game": {
"opponent": opponent,
"date": commence_time,
"location": "Home" if is_home else "Away"
},
"odds": odds_data
}
def format_game_odds_response(game: Dict[str, Any]) -> Dict[str, Any]:
"""
Format game odds data into a structured response.
Args:
game: Raw game data from The Odds API
Returns:
Formatted game data with odds and scores
"""
home_team = game.get("home_team", "")
away_team = game.get("away_team", "")
commence_time = game.get("commence_time", "")
sport_key = game.get("sport_key", "")
# Extract scores if available (The Odds API may include scores for completed games)
scores = game.get("scores", [])
home_score = None
away_score = None
status = "scheduled"
# Check if game time has passed to determine status
if commence_time:
try:
from datetime import timezone
game_time = datetime.fromisoformat(commence_time.replace('Z', '+00:00'))
now = datetime.now(timezone.utc)
if scores:
for score in scores:
if score.get("name") == home_team:
home_score = score.get("score")
elif score.get("name") == away_team:
away_score = score.get("score")
if home_score is not None and away_score is not None:
status = "completed"
elif game_time <= now:
# Game time has passed but no scores - likely in progress or just started
status = "in_progress"
except (ValueError, AttributeError):
# If date parsing fails, fall back to default
pass
# Extract odds from bookmakers
bookmakers = game.get("bookmakers", [])
odds_data = {
"spread": None,
"moneyline": {},
"over_under": None
}
if bookmakers:
# Use the first bookmaker's odds (or could aggregate)
bookmaker = bookmakers[0]
markets = bookmaker.get("markets", [])
for market in markets:
market_key = market.get("key", "")
outcomes = market.get("outcomes", [])
if market_key == "spreads":
# Extract spread odds
for outcome in outcomes:
team = outcome.get("name", "")
point = outcome.get("point", 0)
price = outcome.get("price", 0)
if team == home_team:
odds_data["spread"] = {
"home_team": point,
"away_team": -point,
"home_odds": price,
"away_odds": outcomes[1].get("price", 0) if len(outcomes) > 1 else None
}
elif market_key == "h2h":
# Extract moneyline odds
for outcome in outcomes:
team = outcome.get("name", "")
price = outcome.get("price", 0)
if team == home_team:
odds_data["moneyline"]["home_team"] = price
elif team == away_team:
odds_data["moneyline"]["away_team"] = price
elif market_key == "totals":
# Extract over/under
for outcome in outcomes:
if outcome.get("name", "").lower() == "over":
odds_data["over_under"] = {
"total": outcome.get("point", 0),
"over_odds": outcome.get("price", 0),
"under_odds": outcomes[1].get("price", 0) if len(outcomes) > 1 else None
}
return {
"home_team": home_team,
"away_team": away_team,
"start_time": commence_time,
"status": status,
"score": {
"home": home_score,
"away": away_score
} if home_score is not None and away_score is not None else None,
"odds": odds_data
}