import os import random from datetime import date, datetime, timedelta import requests from dotenv import load_dotenv PREMIER_LEAGUE_CLUBS_DATA = [ {"name": "Arsenal FC", "stadium": "Emirates Stadium"}, {"name": "Aston Villa FC", "stadium": "Villa Park"}, {"name": "AFC Bournemouth", "stadium": "Vitality Stadium"}, {"name": "Brentford FC", "stadium": "Gtech Community Stadium"}, {"name": "Brighton & Hove Albion FC", "stadium": "American Express Stadium"}, {"name": "Chelsea FC", "stadium": "Stamford Bridge"}, {"name": "Crystal Palace FC", "stadium": "Selhurst Park"}, {"name": "Everton FC", "stadium": "Goodison Park"}, {"name": "Fulham FC", "stadium": "Craven Cottage"}, {"name": "Ipswich Town FC", "stadium": "Portman Road"}, {"name": "Leicester City FC", "stadium": "King Power Stadium"}, {"name": "Liverpool FC", "stadium": "Anfield"}, {"name": "Manchester City FC", "stadium": "Etihad Stadium"}, {"name": "Manchester United FC", "stadium": "Old Trafford"}, {"name": "Newcastle United FC", "stadium": "St James' Park"}, {"name": "Nottingham Forest FC", "stadium": "City Ground"}, {"name": "Southampton FC", "stadium": "St Mary's Stadium"}, {"name": "Tottenham Hotspur FC", "stadium": "Tottenham Hotspur Stadium"}, {"name": "West Ham United FC", "stadium": "London Stadium"}, {"name": "Wolverhampton Wanderers FC", "stadium": "Molineux Stadium"}, ] def get_future_matches( team_name: str, all_clubs_data: list, num_matches: int = 12, date_from: date = None, date_to: date = None, ) -> list: """Generate a set of future Premier League matches for ``team_name``. This is a purely mocked schedule. It returns up to ``num_matches`` fixtures, respecting the ``date_from`` and ``date_to`` constraints. Matches are typically on Saturdays or Sundays. """ matches = [] team_details = next((c for c in all_clubs_data if c["name"] == team_name), None) if not team_details: return [] opponents_pool = [c for c in all_clubs_data if c["name"] != team_name] if not opponents_pool: return [] # Determine the maximum number of matches we can generate based on opponents # and the requested num_matches num_actual_matches_to_generate = min(num_matches, len(opponents_pool)) if num_actual_matches_to_generate == 0: return [] # Shuffle opponents once and pick them sequentially random.shuffle(opponents_pool) # Shuffle in place # Determine the initial Saturday for match week consideration today_date = date.today() # Default to next Saturday current_match_week_saturday = today_date + timedelta( days=(5 - today_date.weekday() + 7) % 7 ) # If today is Saturday and it's late evening, or if today is Sunday, # advance to the following Saturday. now_time = datetime.now().time() if ( today_date.weekday() == 5 and now_time > datetime.strptime("20:00", "%H:%M").time() ) or (today_date.weekday() == 6): current_match_week_saturday += timedelta(days=7) # If date_from is specified, ensure our starting Saturday is not before it. if date_from: if current_match_week_saturday < date_from: current_match_week_saturday = date_from # Align current_match_week_saturday to be a Saturday on or after the potentially adjusted date current_match_week_saturday += timedelta( days=(5 - current_match_week_saturday.weekday() + 7) % 7 ) opponent_idx = 0 while len(matches) < num_actual_matches_to_generate and opponent_idx < len( opponents_pool ): # If the current week's Saturday is already past date_to, stop. if date_to and current_match_week_saturday > date_to: break opponent_details = opponents_pool[opponent_idx] is_saturday_game = random.choice([True, True, False]) actual_match_date = None kick_off_time = "" if is_saturday_game: actual_match_date = current_match_week_saturday kick_off_time = random.choice(["12:30", "15:00", "17:30"]) else: # Sunday game actual_match_date = current_match_week_saturday + timedelta(days=1) kick_off_time = random.choice(["14:00", "16:30"]) # Check if this specific match date is within the date_to constraint if date_to and actual_match_date > date_to: # If this game is too late, try the next week if possible. # (This mainly affects Sunday games if Saturday was the last valid day) current_match_week_saturday += timedelta(days=7) continue # Skip adding this match, try next week. match_datetime_gmt = ( f"{actual_match_date.strftime('%Y-%m-%d')} {kick_off_time} GMT" ) is_home_match = random.choice([True, False]) if is_home_match: team1_name = team_details["name"] team2_name = opponent_details["name"] stadium_name = team_details["stadium"] else: team1_name = opponent_details["name"] team2_name = team_details["name"] stadium_name = opponent_details["stadium"] matches.append( { "team1": team1_name, "team2": team2_name, "stadium": stadium_name, "datetime_gmt": match_datetime_gmt, } ) opponent_idx += 1 current_match_week_saturday += timedelta( days=7 ) # Advance to next week's Saturday return matches BASE_URL = "https://api.football-data.org/v4" def search_fixtures(args: dict) -> dict: load_dotenv(override=True) api_key = os.getenv("FOOTBALL_DATA_API_KEY") team_name = args.get("team") date_from_str = args.get("date_from") date_to_str = args.get("date_to") if not team_name: return {"error": "Team name is required."} parsed_date_from = None if date_from_str: try: parsed_date_from = datetime.strptime(date_from_str, "%Y-%m-%d").date() except ValueError: return { "error": f"Invalid date_from: '{date_from_str}'. Expected format YYYY-MM-DD." } parsed_date_to = None if date_to_str: try: parsed_date_to = datetime.strptime(date_to_str, "%Y-%m-%d").date() except ValueError: return { "error": f"Invalid date_to: '{date_to_str}'. Expected format YYYY-MM-DD." } if parsed_date_from and parsed_date_to and parsed_date_from > parsed_date_to: return {"error": "date_from cannot be after date_to."} # If no API key, fall back to mocked data if not api_key: # Use the parsed date objects (which can be None) fixtures = get_future_matches( team_name, PREMIER_LEAGUE_CLUBS_DATA, date_from=parsed_date_from, date_to=parsed_date_to, # num_matches can be passed explicitly if needed, otherwise defaults to 12 ) if not fixtures: # Check if the team name itself was invalid, as get_future_matches returns [] for that too team_details_check = next( (c for c in PREMIER_LEAGUE_CLUBS_DATA if c["name"] == team_name), None ) if not team_details_check: return {"error": f"Team '{team_name}' not found in mocked data."} # If team is valid, an empty fixtures list means no matches fit the criteria (e.g., date range) return {"fixtures": fixtures} # API Key is present, proceed with API logic # The API requires both date_from and date_to if not parsed_date_from or not parsed_date_to: return { "error": "Both date_from and date_to (YYYY-MM-DD) are required for API search." } headers = {"X-Auth-Token": api_key} # For API calls, team name matching might be case-insensitive or require specific handling # The existing logic uses team_name.lower() for the API search path later. # Fetch team ID teams_response = requests.get(f"{BASE_URL}/competitions/PL/teams", headers=headers) if teams_response.status_code != 200: return { "error": f"Failed to fetch teams data from API (status {teams_response.status_code})." } teams_data = teams_response.json() team_id = None # Using lower() for comparison, assuming API team names might have varied casing # or the input team_name might not be exact. # The `ToolDefinition` lists exact names, so direct match might also be an option. for team_api_data in teams_data.get("teams", []): if team_name.lower() in team_api_data.get("name", "").lower(): team_id = team_api_data["id"] break if not team_id: return {"error": f"Team '{team_name}' not found via API."} date_from_formatted = parsed_date_from.strftime("%Y-%m-%d") date_to_formatted = parsed_date_to.strftime("%Y-%m-%d") fixtures_url = f"{BASE_URL}/teams/{team_id}/matches?dateFrom={date_from_formatted}&dateTo={date_to_formatted}" # print(fixtures_url) # Keep for debugging if necessary fixtures_response = requests.get(fixtures_url, headers=headers) if fixtures_response.status_code != 200: return { "error": f"Failed to fetch fixtures data from API (status {fixtures_response.status_code})." } fixtures_data = fixtures_response.json() matching_fixtures = [] for match in fixtures_data.get("matches", []): # Ensure match datetime parsing is robust try: match_datetime_utc = datetime.strptime( match["utcDate"], "%Y-%m-%dT%H:%M:%SZ" ) except (ValueError, TypeError): # Skip malformed match entries or log an error continue if match.get("competition", {}).get("code") == "PL": matching_fixtures.append( { "date": match_datetime_utc.strftime("%Y-%m-%d"), "homeTeam": match.get("homeTeam", {}).get("name", "N/A"), "awayTeam": match.get("awayTeam", {}).get("name", "N/A"), } ) return {"fixtures": matching_fixtures} def search_fixtures_example(args: dict) -> dict: """ Example version of search_fixtures that returns hardcoded data without making API calls. The function respects the team name provided and generates fixture dates within the specified range. Args: args: Dictionary containing 'team', 'date_from', and 'date_to' Returns: Dictionary with 'fixtures' key containing a list of fixture objects """ team_name = args.get("team", "Default Team FC") date_from_str = args.get("date_from") date_to_str = args.get("date_to") # Validate dates try: # Ensure date strings are not None before parsing if date_from_str is None or date_to_str is None: raise ValueError("Date strings cannot be None") date_from_obj = datetime.strptime(date_from_str, "%Y-%m-%d") date_to_obj = datetime.strptime(date_to_str, "%Y-%m-%d") except ValueError: return { "error": "Invalid date provided. Expected format YYYY-MM-DD for both date_from and date_to." } # Calculate 3 reasonable fixture dates within the given range date_range = (date_to_obj - date_from_obj).days if date_range < 0: # date_from is after date_to return {"fixtures": []} # No fixtures possible fixture_dates_timestamps = [] if date_range < 21: # If range is less than 3 weeks, use evenly spaced fixtures if possible if date_range >= 2: # Need at least some gap for 3 fixtures fixture_dates_timestamps = [ date_from_obj + timedelta(days=max(0, date_range // 4)), # Closer to start date_from_obj + timedelta(days=max(1, date_range // 2)), # Middle date_to_obj - timedelta(days=max(0, date_range // 4)), # Closer to end ] elif date_range == 1: # Only two days fixture_dates_timestamps = [date_from_obj, date_to_obj] elif date_range == 0: # Only one day fixture_dates_timestamps = [date_from_obj] else: # date_range is negative, handled above, or 0 (single day) fixture_dates_timestamps = [date_from_obj] if date_range == 0 else [] else: # Otherwise space them out by weeks, ensuring they are within the bounds d1 = date_from_obj + timedelta(days=7) d2 = date_from_obj + timedelta(days=14) d3 = date_to_obj - timedelta(days=7) # Potential third game from the end fixture_dates_timestamps.append(d1) if d2 <= date_to_obj and d2 > d1: # ensure d2 is valid and distinct fixture_dates_timestamps.append(d2) if ( d3 >= date_from_obj and d3 > d2 and d3 <= date_to_obj ): # ensure d3 is valid and distinct fixture_dates_timestamps.append(d3) elif ( d3 < date_from_obj and len(fixture_dates_timestamps) < 3 ): # if d3 is too early, try using date_to_obj itself if distinct if date_to_obj not in fixture_dates_timestamps: fixture_dates_timestamps.append(date_to_obj) # Ensure unique dates and sort, then take up to 3. fixture_dates_timestamps = sorted( list( set( f_date for f_date in fixture_dates_timestamps if date_from_obj <= f_date <= date_to_obj ) ) ) fixture_dates_final = fixture_dates_timestamps[:3] all_opponents = [ "Manchester United FC", "Leicester City FC", "Manchester City FC", "Liverpool FC", "Chelsea FC", "Arsenal FC", "Tottenham Hotspur FC", "West Ham United FC", "Everton FC", "Generic Opponent A", "Generic Opponent B", "Generic Opponent C", # Fallbacks ] available_opponents = [ team for team in all_opponents if team.lower() != team_name.lower() ] # Ensure we have enough opponents for the number of fixtures we'll generate if len(available_opponents) < len(fixture_dates_final): needed = len(fixture_dates_final) - len(available_opponents) for i in range(needed): available_opponents.append(f"Placeholder Opponent {i+1}") opponents = available_opponents[: len(fixture_dates_final)] fixtures = [] for i, fixture_date_obj in enumerate(fixture_dates_final): if i >= len(opponents): # Should not happen with the logic above break date_str = fixture_date_obj.strftime("%Y-%m-%d") if i % 2 == 0: # Home game fixtures.append( {"date": date_str, "homeTeam": team_name, "awayTeam": opponents[i]} ) else: # Away game fixtures.append( {"date": date_str, "homeTeam": opponents[i], "awayTeam": team_name} ) return {"fixtures": fixtures}