OttoCorrect's picture
Update app.py
48dae47 verified
# 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)