Spaces:
Build error
Build error
| # Import necessary libraries | |
| import gradio as gr | |
| import requests | |
| import json | |
| import pandas as pd | |
| from collections import OrderedDict # To maintain stat order | |
| import datetime # To potentially get current season | |
| import os # To check if logo file exists | |
| # --- Constants --- | |
| NHL_API_STANDINGS_URL = "https://api-web.nhle.com/v1/standings/now" | |
| NHL_API_ROSTER_URL = "https://api-web.nhle.com/v1/roster/{team_code}/current" | |
| NHL_API_TEAM_STATS_SUMMARY_URL = "https://api.nhle.com/stats/rest/en/team/summary" | |
| LOGO_FILE_PATH = "Logo.png" # Assuming logo is in the same directory as app.py | |
| # Dictionary mapping NHL team names to their 3-letter codes (used for dropdown) | |
| NHL_TEAMS = { | |
| "Anaheim Ducks": "ANA", "Boston Bruins": "BOS", "Buffalo Sabres": "BUF", | |
| "Calgary Flames": "CGY", "Carolina Hurricanes": "CAR", "Chicago Blackhawks": "CHI", | |
| "Colorado Avalanche": "COL", "Columbus Blue Jackets": "CBJ", "Dallas Stars": "DAL", | |
| "Detroit Red Wings": "DET", "Edmonton Oilers": "EDM", "Florida Panthers": "FLA", | |
| "Los Angeles Kings": "LAK", "Minnesota Wild": "MIN", "Montréal Canadiens": "MTL", | |
| "Nashville Predators": "NSH", "New Jersey Devils": "NJD", "New York Islanders": "NYI", | |
| "New York Rangers": "NYR", "Ottawa Senators": "OTT", "Philadelphia Flyers": "PHI", | |
| "Pittsburgh Penguins": "PIT", "San Jose Sharks": "SJS", "Seattle Kraken": "SEA", | |
| "St. Louis Blues": "STL", "Tampa Bay Lightning": "TBL", "Toronto Maple Leafs": "TOR", | |
| "Vancouver Canucks": "VAN", "Vegas Golden Knights": "VGK", "Washington Capitals": "WSH", | |
| "Winnipeg Jets": "WPG", "Utah Hockey Club": "UTA" | |
| } | |
| # Global variables | |
| cached_standings_data = None | |
| current_season_id = None | |
| team_code_map = {} # Map: 'BUF' -> 'Buffalo Sabres' | |
| # --- Helper Function to Get Current Season ID --- | |
| def get_season_id(standings_data): | |
| """Attempts to extract season ID from standings data, otherwise generates based on current date.""" | |
| global current_season_id | |
| if current_season_id: return current_season_id | |
| if isinstance(standings_data, dict): | |
| season = standings_data.get('season') | |
| if season and isinstance(season, (int, str)) and len(str(season)) == 8: # Accept int or string | |
| current_season_id = str(season) | |
| print(f"Extracted season ID from standings: {current_season_id}") | |
| return current_season_id | |
| now = datetime.datetime.now() | |
| current_year = now.year | |
| if now.month >= 10: season_start_year, season_end_year = current_year, current_year + 1 | |
| else: season_start_year, season_end_year = current_year - 1, current_year | |
| current_season_id = f"{season_start_year}{season_end_year}" | |
| print(f"Generated current season ID based on date: {current_season_id}") | |
| return current_season_id | |
| # --- Helper Function to Build Team Code Map --- | |
| def build_team_code_map(standings_data): | |
| """Builds a map from team code (e.g., 'BUF') to full name (e.g., 'Buffalo Sabres') using standings data.""" | |
| global team_code_map | |
| if team_code_map: return True | |
| if isinstance(standings_data, dict) and "error" not in standings_data and 'standings' in standings_data: | |
| try: | |
| temp_map = {} | |
| for team_record in standings_data['standings']: | |
| abbrev = team_record.get('teamAbbrev', {}).get('default') | |
| full_name = team_record.get('teamName', {}).get('default') | |
| if abbrev and full_name: | |
| temp_map[abbrev] = full_name | |
| team_code_map = temp_map | |
| print(f"Successfully built team code map with {len(team_code_map)} entries.") | |
| return True | |
| except Exception as e: | |
| print(f"Error building team code map: {e}") | |
| return False | |
| else: | |
| print("Error: Cannot build team code map, invalid standings data.") | |
| return False | |
| # --- Standings Functions --- | |
| def fetch_nhl_standings(): | |
| """Fetches current NHL standings from the API, caches it, and builds the team map.""" | |
| global cached_standings_data | |
| if cached_standings_data and "error" not in cached_standings_data: | |
| build_team_code_map(cached_standings_data) | |
| return cached_standings_data | |
| print("Fetching fresh standings data...") | |
| try: | |
| response = requests.get(NHL_API_STANDINGS_URL) | |
| response.raise_for_status() | |
| data = response.json() | |
| if 'standings' not in data: | |
| cached_standings_data = {"error": "Unexpected data format from NHL API."} | |
| else: | |
| cached_standings_data = data | |
| get_season_id(cached_standings_data) | |
| build_team_code_map(cached_standings_data) | |
| return cached_standings_data | |
| except requests.exceptions.RequestException as e: | |
| cached_standings_data = {"error": f"Failed to fetch standings: {e}"} | |
| return cached_standings_data | |
| except json.JSONDecodeError: | |
| cached_standings_data = {"error": "Failed to parse standings data from NHL API."} | |
| return cached_standings_data | |
| def process_standings_data(data): | |
| """Processes the raw standings data into Pandas DataFrames grouped by division/conference.""" | |
| if isinstance(data, dict) and "error" in data: return data | |
| processed_standings = {} | |
| try: | |
| standings_list = data.get('standings', []) | |
| if not standings_list: return {"error": "No standings data available."} | |
| for team_record in standings_list: | |
| if not isinstance(team_record.get('teamName'), dict): continue | |
| conf = team_record.get('conferenceName', 'N/A') | |
| div = team_record.get('divisionName', 'N/A') | |
| team_name = team_record.get('teamName', {}).get('default', 'N/A') | |
| stats = { | |
| 'Team': team_name, 'GP': team_record.get('gamesPlayed', 0), 'W': team_record.get('wins', 0), | |
| 'L': team_record.get('losses', 0), 'OT': team_record.get('otLosses', 0), 'Pts': team_record.get('points', 0), | |
| 'Diff': team_record.get('goalDifferential', 0), 'GF': team_record.get('goalFor', 0), | |
| 'GA': team_record.get('goalAgainst', 0), | |
| 'Strk': team_record.get('streakCode', '') + str(team_record.get('streakCount', '')), | |
| } | |
| if conf not in processed_standings: processed_standings[conf] = {} | |
| if div not in processed_standings[conf]: processed_standings[conf][div] = [] | |
| processed_standings[conf][div].append(stats) | |
| for conf, divisions in processed_standings.items(): | |
| for div, teams_list in divisions.items(): | |
| cols = ['Team', 'GP', 'W', 'L', 'OT', 'Pts', 'Diff', 'GF', 'GA', 'Strk'] | |
| df = pd.DataFrame(teams_list) if teams_list else pd.DataFrame(columns=cols) | |
| processed_standings[conf][div] = df.reindex(columns=cols, fill_value='N/A') | |
| return processed_standings | |
| except Exception as e: | |
| print(f"Error processing standings data: {e}") | |
| return {"error": f"Failed to process standings data: {e}"} | |
| # --- Roster Functions --- | |
| def fetch_team_roster(team_code): | |
| """Fetches the current roster for a specific team code.""" | |
| if not team_code: return {"error": "No team selected."} | |
| try: | |
| url = NHL_API_ROSTER_URL.format(team_code=team_code) | |
| response = requests.get(url) | |
| response.raise_for_status() | |
| data = response.json() | |
| if not all(k in data for k in ['forwards', 'defensemen', 'goalies']): | |
| print(f"Warning: Roster data for {team_code} might be incomplete.") | |
| return data | |
| except requests.exceptions.RequestException as e: | |
| print(f"Error fetching NHL roster for {team_code}: {e}") | |
| status_code = e.response.status_code if e.response is not None else None | |
| if status_code == 404: return {"error": f"Roster not found for {team_code}."} | |
| return {"error": f"Failed to fetch roster for {team_code}: {e}"} | |
| except json.JSONDecodeError: | |
| print(f"Error decoding JSON for {team_code} roster") | |
| return {"error": f"Failed to parse roster data for {team_code}."} | |
| def process_roster_data(data): | |
| """Processes raw roster data into Pandas DataFrames for Forwards, Defensemen, and Goalies.""" | |
| cols = ['#', 'Name', 'Pos'] | |
| empty_df = pd.DataFrame(columns=cols) | |
| if isinstance(data, dict) and "error" in data: return empty_df, empty_df, empty_df, data["error"] | |
| rosters = {'Forwards': [], 'Defensemen': [], 'Goalies': []} | |
| try: | |
| for key, pos_code in [('forwards', 'F'), ('defensemen', 'D'), ('goalies', 'G')]: | |
| for player in data.get(key, []): | |
| if not isinstance(player, dict): continue | |
| f_name_data = player.get('firstName', {}) | |
| l_name_data = player.get('lastName', {}) | |
| f_name = f_name_data.get('default', '') if isinstance(f_name_data, dict) else '' | |
| l_name = l_name_data.get('default', '') if isinstance(l_name_data, dict) else '' | |
| info = {'#': player.get('sweaterNumber', 'N/A'), 'Name': f"{f_name} {l_name}".strip(), 'Pos': player.get('positionCode', pos_code)} | |
| actual_pos = player.get('positionCode', pos_code) | |
| if actual_pos == 'G': rosters['Goalies'].append(info) | |
| elif actual_pos == 'D': rosters['Defensemen'].append(info) | |
| else: rosters['Forwards'].append(info) | |
| df_f = pd.DataFrame(rosters['Forwards'], columns=cols) if rosters['Forwards'] else empty_df.copy() | |
| df_d = pd.DataFrame(rosters['Defensemen'], columns=cols) if rosters['Defensemen'] else empty_df.copy() | |
| df_g = pd.DataFrame(rosters['Goalies'], columns=cols) if rosters['Goalies'] else empty_df.copy() | |
| return df_f, df_d, df_g, "" | |
| except Exception as e: | |
| print(f"Error processing roster data: {e}") | |
| return empty_df.copy(), empty_df.copy(), empty_df.copy(), f"Failed to process roster data: {e}" | |
| # --- Team Stats Functions --- | |
| def fetch_team_stats_summary(season_id): | |
| """Fetches the summary stats for ALL teams for a given season using the Stats API.""" | |
| if not season_id: return {"error": "Season ID is required."} | |
| try: | |
| params = {'cayenneExp': f'seasonId={season_id} and gameTypeId=2'} | |
| url = NHL_API_TEAM_STATS_SUMMARY_URL | |
| print(f"Fetching team stats summary from: {url} with params: {params}") | |
| response = requests.get(url, params=params) | |
| response.raise_for_status() | |
| data = response.json() | |
| if "data" not in data or not isinstance(data["data"], list): | |
| print(f"Warning: Stats summary data structure unexpected or missing 'data' list.") | |
| return {"error": "Received invalid stats summary data format."} | |
| return data | |
| except requests.exceptions.RequestException as e: | |
| print(f"Error fetching NHL team stats summary: {e}") | |
| status_code = e.response.status_code if e.response is not None else None | |
| if status_code == 404: return {"error": "Stats summary endpoint not found (404)."} | |
| return {"error": f"Failed to fetch stats summary: {e}"} | |
| except json.JSONDecodeError: | |
| print(f"Error decoding JSON response for team stats summary") | |
| return {"error": "Failed to parse stats summary data."} | |
| def process_team_stats(summary_data, team_code): | |
| """Processes the full stats summary data to extract stats for a specific team code.""" | |
| global team_code_map | |
| cols = ['Statistic', 'Value'] | |
| empty_df = pd.DataFrame(columns=cols) | |
| if isinstance(summary_data, dict) and "error" in summary_data: return empty_df, summary_data["error"] | |
| if not team_code_map: return empty_df, "Team code map is not available. Cannot match team." | |
| target_full_name = team_code_map.get(team_code) | |
| if not target_full_name: return empty_df, f"Could not map team code {team_code} to a full name." | |
| print(f"Searching for team full name: '{target_full_name}' in stats summary...") | |
| if "data" not in summary_data or not isinstance(summary_data["data"], list) or not summary_data["data"]: | |
| return empty_df, "Invalid or empty summary data received." | |
| team_stats_data = None | |
| for team_data in summary_data["data"]: | |
| current_full_name = team_data.get('teamFullName') | |
| if current_full_name == target_full_name: | |
| team_stats_data = team_data | |
| print(f"Found stats data for team: {target_full_name} ({team_code})") | |
| break | |
| if team_stats_data is None: | |
| return empty_df, f"Stats not found for {target_full_name} in the summary response." | |
| desired_stats = OrderedDict([ | |
| ("Games Played", "gamesPlayed"), ("Wins", "wins"), ("Losses", "losses"), ("OT Losses", "otLosses"), | |
| ("Points", "points"), ("Points %", "pointPct"), ("Goals For", "goalsFor"), ("Goals Against", "goalsAgainst"), | |
| ("Goals For/Game", "goalsForPerGame"), ("Goals Against/Game", "goalsAgainstPerGame"), | |
| ("Power Play %", "powerPlayPct"), ("Penalty Kill %", "penaltyKillPct"), | |
| ("Shots For/Game", "shotsForPerGame"), ("Shots Against/Game", "shotsAgainstPerGame"), | |
| ("Faceoff Win %", "faceoffWinPct") | |
| ]) | |
| extracted_stats = [] | |
| try: | |
| for display_name, api_key in desired_stats.items(): | |
| value = team_stats_data.get(api_key) | |
| if value is not None: | |
| if "%" in display_name and isinstance(value, (float, int)): value_str = f"{value:.1f}%" | |
| else: value_str = str(value) | |
| extracted_stats.append({"Statistic": display_name, "Value": value_str}) | |
| else: | |
| extracted_stats.append({"Statistic": display_name, "Value": "N/A"}) | |
| print(f"Note: API key '{api_key}' for stat '{display_name}' not found in team summary data for {team_code}.") | |
| if not extracted_stats or all(s['Value'] == 'N/A' for s in extracted_stats): | |
| return empty_df, f"Could not extract valid stats for {team_code}." | |
| stats_df = pd.DataFrame(extracted_stats, columns=cols) | |
| return stats_df, "" | |
| except Exception as e: | |
| print(f"Error processing team stats data for {team_code}: {e}") | |
| return empty_df, f"Failed to process stats for {team_code}: {e}" | |
| # --- Gradio UI Update Functions --- | |
| def update_roster_display(team_name): | |
| """Callback function to fetch, process, and update roster display.""" | |
| team_code = NHL_TEAMS.get(team_name) | |
| if not team_code: | |
| empty_df = pd.DataFrame(columns=['#', 'Name', 'Pos']) | |
| return empty_df, empty_df, empty_df, "Please select a valid team." | |
| raw_roster_data = fetch_team_roster(team_code) | |
| df_f, df_d, df_g, error_message = process_roster_data(raw_roster_data) | |
| return df_f, df_d, df_g, error_message | |
| def update_stats_display(team_name): | |
| """Callback function to fetch, process, and update team stats display using summary endpoint.""" | |
| team_code = NHL_TEAMS.get(team_name) | |
| if not team_code: | |
| empty_df = pd.DataFrame(columns=['Statistic', 'Value']) | |
| return empty_df, "Please select a valid team." | |
| standings_data = fetch_nhl_standings() | |
| if "error" in standings_data or not build_team_code_map(standings_data): | |
| return empty_df, f"Error preparing prerequisite data (standings/map)." | |
| season_id = get_season_id(standings_data) | |
| if not season_id: return empty_df, "Could not determine current season ID." | |
| raw_stats_summary = fetch_team_stats_summary(season_id) | |
| stats_df, error_message = process_team_stats(raw_stats_summary, team_code) | |
| return stats_df, error_message | |
| # --- Build Gradio Interface --- | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| # --- Display Logo --- | |
| if os.path.exists(LOGO_FILE_PATH): | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=50): # Add min_width for small screens | |
| pass | |
| with gr.Column(scale=2): | |
| gr.Image( | |
| value=LOGO_FILE_PATH, | |
| label="EQ Betz Logo", | |
| show_label=False, | |
| # Set width instead of height | |
| width=200, # Adjust this value to make logo smaller/larger | |
| height=None, # Let height adjust based on aspect ratio | |
| interactive=False, | |
| container=False, | |
| elem_classes="logo-image" | |
| ) | |
| with gr.Column(scale=1, min_width=50): # Add min_width for small screens | |
| pass | |
| else: | |
| print(f"Warning: Logo file not found at {LOGO_FILE_PATH}") | |
| # --- Main Titles --- | |
| gr.Markdown("# EQ Betz's NHL Tool") | |
| gr.Markdown("Your source for NHL Standings, Rosters, and Stats.") | |
| # Fetch standings initially to build map and get season ID | |
| initial_standings = fetch_nhl_standings() | |
| # --- Tabs --- | |
| with gr.Tabs(): | |
| # --- Welcome Tab --- | |
| with gr.TabItem("Welcome"): | |
| gr.Markdown("## Welcome!") | |
| gr.Markdown("Use the tabs above to navigate between NHL Standings, Team Rosters, and Team Stats.") | |
| # --- Standings Tab --- | |
| with gr.TabItem("Standings"): | |
| gr.Markdown("## Current NHL Standings") | |
| standings_data = process_standings_data(initial_standings) | |
| if isinstance(standings_data, dict) and "error" in standings_data: | |
| gr.Markdown(f"**Error loading standings:** {standings_data['error']}") | |
| else: | |
| if isinstance(standings_data, dict): | |
| for conference in sorted(standings_data.keys()): | |
| divisions = standings_data[conference] | |
| with gr.Accordion(conference, open=True): | |
| sorted_divisions = sorted(divisions.items()) | |
| if len(sorted_divisions) > 1: | |
| with gr.Tabs(): | |
| for division, df in sorted_divisions: | |
| with gr.TabItem(division): | |
| gr.Dataframe(df, interactive=False, wrap=True) | |
| elif len(sorted_divisions) == 1: | |
| division, df = sorted_divisions[0] | |
| gr.Markdown(f"### {division} Division") | |
| gr.Dataframe(df, interactive=False, wrap=True) | |
| else: | |
| gr.Markdown("**Error displaying standings data.**") | |
| # --- Rosters Tab --- | |
| with gr.TabItem("Rosters"): | |
| gr.Markdown("## View Team Rosters") | |
| with gr.Row(): | |
| roster_team_dropdown = gr.Dropdown(choices=sorted(list(NHL_TEAMS.keys())), label="Select Team", info="Choose an NHL team to view their current roster.") | |
| roster_error_output = gr.Markdown("") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Forwards") | |
| forwards_df_output = gr.Dataframe(interactive=False, wrap=True, label="Forwards Roster") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Defensemen") | |
| defense_df_output = gr.Dataframe(interactive=False, wrap=True, label="Defensemen Roster") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Goalies") | |
| goalies_df_output = gr.Dataframe(interactive=False, wrap=True, label="Goalies Roster") | |
| roster_team_dropdown.change(fn=update_roster_display, inputs=roster_team_dropdown, outputs=[forwards_df_output, defense_df_output, goalies_df_output, roster_error_output]) | |
| # --- Stats Tab --- | |
| with gr.TabItem("Stats"): | |
| gr.Markdown("## View Team Stats (Summary)") | |
| with gr.Row(): | |
| stats_team_dropdown = gr.Dropdown(choices=sorted(list(NHL_TEAMS.keys())), label="Select Team", info="Choose an NHL team to view their current season summary stats.") | |
| stats_error_output = gr.Markdown("") | |
| stats_df_output = gr.Dataframe(headers=['Statistic', 'Value'], interactive=False, wrap=True, label="Team Statistics Summary") | |
| stats_team_dropdown.change(fn=update_stats_display, inputs=stats_team_dropdown, outputs=[stats_df_output, stats_error_output]) | |
| # --- Launch App --- | |
| if __name__ == "__main__": | |
| demo.launch(share=False, debug=True) | |