rairo commited on
Commit
c5ab2eb
·
verified ·
1 Parent(s): e460802

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +91 -62
main.py CHANGED
@@ -715,7 +715,7 @@ def get_available_seasons_util(num_seasons=6):
715
  current_year = datetime.now().year
716
  current_month = datetime.now().month
717
  latest_season_end_year = current_year
718
- if current_month >= 7:
719
  latest_season_end_year += 1
720
  seasons_list = []
721
  for i in range(num_seasons):
@@ -750,16 +750,17 @@ def get_player_index_brscraper():
750
  return df
751
 
752
  def _scrape_player_index_brscraper():
753
- seasons_to_try_for_index = get_available_seasons_util(num_seasons=2)
754
 
755
  for season_str in seasons_to_try_for_index:
756
  end_year = int(season_str.split('–')[1])
757
  try:
758
  logging.info(f"Attempting to get player index for year: {end_year} from BRScraper...")
759
- df = nba.get_stats(end_year, info='per_game', rename=False)
760
 
761
  if not df.empty and 'Player' in df.columns:
762
  player_names = df['Player'].dropna().unique().tolist()
 
763
  player_names = [normalize_string(name) for name in player_names]
764
  logging.info(f"Successfully retrieved {len(player_names)} players for index from {season_str}.")
765
  return pd.DataFrame({'name': player_names})
@@ -771,13 +772,13 @@ def _scrape_player_index_brscraper():
771
  logging.error("Failed to fetch player index from recent seasons. Falling back to curated common players list.")
772
  common_players = [
773
  'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo',
774
- 'Nikola Jokic',
775
- 'Joel Embiid', 'Jayson Tatum', 'Luka Doncic',
776
  'Damian Lillard', 'Jimmy Butler', 'Kawhi Leonard', 'Paul George',
777
  'Anthony Davis', 'Rudy Gobert', 'Donovan Mitchell', 'Trae Young',
778
  'Devin Booker', 'Karl-Anthony Towns', 'Zion Williamson', 'Ja Morant',
779
- 'Shai Gilgeous-Alexander', 'Tyrese Maxey', 'Anthony Edwards', 'Victor Wembanyama',
780
- 'Jalen Brunson', 'Paolo Banchero', 'Franz Wagner', 'Cade Cunningham'
781
  ]
782
  return pd.DataFrame({'name': common_players})
783
 
@@ -786,33 +787,36 @@ def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str],
786
  logging.error("BRScraper is not available. Cannot fetch player career stats.")
787
  return pd.DataFrame()
788
 
789
- normalized_player_name = normalize_string(player_name)
790
  all_rows = []
791
 
792
  for season_str in seasons_to_fetch:
793
  end_year = int(season_str.split('–')[1])
794
 
 
795
  cache_key = f"{normalized_player_name}_{end_year}_{'playoffs' if playoffs else 'regular'}"
796
  db_ref = db.reference(f'scraped_data/player_season_stats/{cache_key}')
797
 
798
  if FIREBASE_INITIALIZED:
799
  cached_data = db_ref.get()
800
- if cached_data and not is_data_stale(cached_data.get('last_updated'), max_age_hours=24*7):
801
  logging.info(f"Loading stats for {player_name} in {season_str} (playoffs: {playoffs}) from Firebase cache.")
802
  all_rows.append(pd.DataFrame.from_records(cached_data['data']))
803
  continue # Skip scraping for this season if found in cache
804
  else:
805
  logging.info(f"Stats for {player_name} in {season_str} cache stale or not found. Scraping...")
806
 
807
- for attempt in range(3):
 
808
  try:
809
  logging.info(f"DEBUG: Attempt {attempt+1} for nba.get_stats for player '{player_name}' in season {season_str} (year: {end_year}, playoffs: {playoffs})...")
810
 
 
811
  df_season = nba.get_stats(end_year, info='per_game', playoffs=playoffs, rename=False)
812
 
813
  if df_season.empty:
814
  logging.warning(f"DEBUG: nba.get_stats returned empty DataFrame for {player_name} in {season_str} on attempt {attempt+1}. Retrying...")
815
- time.sleep(1)
816
  continue
817
 
818
  if 'Player' not in df_season.columns:
@@ -820,13 +824,14 @@ def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str],
820
  time.sleep(1)
821
  continue
822
 
 
823
  df_season['Player_Normalized'] = df_season['Player'].apply(normalize_string)
824
  row = df_season[df_season['Player_Normalized'] == normalized_player_name]
825
 
826
  if not row.empty:
827
- row = row.copy()
828
- row['Season'] = season_str
829
- row = row.drop(columns=['Player_Normalized'], errors='ignore')
830
 
831
  if FIREBASE_INITIALIZED:
832
  df_cleaned_for_firebase = clean_df_for_firebase(row.copy())
@@ -838,20 +843,23 @@ def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str],
838
 
839
  all_rows.append(row)
840
  logging.info(f"DEBUG: Found stats for {player_name} in {season_str} on attempt {attempt+1}. Appending row.")
841
- break
842
  else:
 
843
  logging.info(f"DEBUG: Player {player_name} not found in {season_str} stats (after getting season data) on attempt {attempt+1}. Retrying...")
844
  time.sleep(1)
 
 
845
  continue
846
 
847
  except Exception as e:
848
  logging.warning(f"DEBUG: Exception on attempt {attempt+1} when fetching {season_str} {'playoff' if playoffs else 'regular season'} stats for {player_name}: {e}")
849
- time.sleep(1)
850
- if attempt == 2:
851
  logging.error(f"DEBUG: All 3 attempts failed for {player_name} in {season_str}. Giving up on this season.")
852
- continue
853
 
854
- time.sleep(0.5) # Delay between seasons
855
 
856
  if not all_rows:
857
  logging.warning(f"DEBUG: No stats found for {player_name} in the requested seasons: {seasons_to_fetch}. Returning empty DataFrame.")
@@ -859,6 +867,7 @@ def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str],
859
 
860
  df = pd.concat(all_rows, ignore_index=True)
861
 
 
862
  mapping = {
863
  'G':'GP','GS':'GS','MP':'MIN', 'FG%':'FG_PCT','3P%':'FG3_PCT','FT%':'FT_PCT',
864
  'TRB':'REB','AST':'AST','STL':'STL','BLK':'BLK','TOV':'TO',
@@ -869,13 +878,14 @@ def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str],
869
  }
870
  df = df.rename(columns={o:n for o,n in mapping.items() if o in df.columns})
871
 
872
- non_num = {'Season','Player','Tm','Lg','Pos'}
 
873
  for col in df.columns:
874
  if col not in non_num:
875
  df[col] = pd.to_numeric(df[col], errors='coerce')
876
 
877
- df['Player'] = player_name
878
- df = df.replace({np.nan: None})
879
  return df
880
 
881
  def get_dashboard_info_brscraper():
@@ -896,7 +906,7 @@ def get_dashboard_info_brscraper():
896
  else:
897
  logging.info("Scraping dashboard info (cache stale or not found).")
898
  data = _scrape_dashboard_info_brscraper()
899
- if data:
900
  db_ref.set({
901
  'last_updated': datetime.utcnow().isoformat(),
902
  'data': data
@@ -907,27 +917,29 @@ def get_dashboard_info_brscraper():
907
  def _scrape_dashboard_info_brscraper():
908
  dashboard_data = {}
909
  try:
 
910
  mvp_2025_df = nba.get_award_votings('mvp', 2025)
911
  if not mvp_2025_df.empty:
912
- if 'Share' in mvp_2025_df.columns:
913
  mvp_2025_df = mvp_2025_df.rename(columns={'Share': 'Votes'})
914
- if 'Votes' in mvp_2025_df.columns:
915
  mvp_2025_df['Votes'] = pd.to_numeric(mvp_2025_df['Votes'], errors='coerce') * 100
916
 
917
  mvp_2025_df = clean_df_for_firebase(mvp_2025_df)
918
  dashboard_data['mvp_2025_votings'] = mvp_2025_df.replace({np.nan: None}).to_dict(orient='records')
919
  else:
920
- dashboard_data['mvp_2025_votings'] = []
921
  logging.warning("Could not retrieve 2025 MVP votings.")
922
 
 
923
  east_probs_df = nba.get_playoffs_probs('east')
924
  if not east_probs_df.empty:
925
  if 'Eastern Conference' in east_probs_df.columns:
926
  east_probs_df = east_probs_df.rename(columns={'Eastern Conference': 'Team'})
927
- elif 'Tm' in east_probs_df.columns:
928
  east_probs_df = east_probs_df.rename(columns={'Tm': 'Team'})
929
 
930
- if 'Team' in east_probs_df.columns:
931
  east_probs_df['Team'] = east_probs_df['Team'].astype(str).apply(clean_team_name)
932
 
933
  east_probs_df = clean_df_for_firebase(east_probs_df)
@@ -984,23 +996,23 @@ def ask_perp(prompt, system=NBA_ANALYST_SYSTEM_PROMPT, max_tokens=1000, temp=0.2
984
  }
985
 
986
  payload = {
987
- "model": "sonar-pro",
988
  "messages": [
989
  {"role": "system", "content": system},
990
- {"role": "user", "content": f"BASKETBALL ONLY: {prompt}"}
991
  ],
992
  "max_tokens": max_tokens,
993
  "temperature": temp,
994
- "web_search_options": {
995
- "search_context_size": "high",
996
- "search_domain_filter": ["nba.com", "espn.com", "basketball-reference.com"]
997
  },
998
- "emit_sources": True
999
  }
1000
 
1001
  try:
1002
- response = requests.post(PERP_URL, json=payload, headers=headers, timeout=45)
1003
- response.raise_for_status()
1004
  return response.json().get("choices", [])[0].get("message", {}).get("content", "")
1005
  except requests.exceptions.RequestException as e:
1006
  error_message = f"Error communicating with Perplexity API: {e}"
@@ -1008,11 +1020,11 @@ def ask_perp(prompt, system=NBA_ANALYST_SYSTEM_PROMPT, max_tokens=1000, temp=0.2
1008
  try:
1009
  error_detail = e.response.json().get("error", {}).get("message", e.response.text)
1010
  error_message = f"Perplexity API error: {e.response.status_code} - {e.response.reason}"
1011
- except ValueError:
1012
  error_message = f"Perplexity API error: {e.response.status_code} - {e.response.reason}"
1013
  logging.error(f"Perplexity API request failed: {error_message}")
1014
  return f"Error from AI: {error_message}"
1015
- except Exception as e:
1016
  logging.error(f"An unexpected error occurred with Perplexity API: {e}")
1017
  return f"An unexpected error occurred with AI: {str(e)}"
1018
 
@@ -1103,11 +1115,15 @@ def get_player_stats():
1103
 
1104
  comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
1105
 
 
1106
  basic_display_df = comparison_df_raw.copy()
1107
  basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
 
1108
  basic_display_df = basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2)
1109
 
 
1110
  advanced_df = comparison_df_raw.copy()
 
1111
  advanced_df['FGA'] = pd.to_numeric(advanced_df.get('FGA', 0), errors='coerce').fillna(0)
1112
  advanced_df['FTA'] = pd.to_numeric(advanced_df.get('FTA', 0), errors='coerce').fillna(0)
1113
  advanced_df['PTS'] = pd.to_numeric(advanced_df.get('PTS', 0), errors='coerce').fillna(0)
@@ -1115,7 +1131,7 @@ def get_player_stats():
1115
  lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0,
1116
  axis=1
1117
  )
1118
- advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
1119
  advanced_display_df = advanced_df[[c for c in advanced_cols if c in advanced_df.columns]].round(3)
1120
 
1121
  return jsonify({
@@ -1141,6 +1157,7 @@ def get_player_playoff_stats():
1141
  all_player_season_data = []
1142
  players_with_no_data = []
1143
 
 
1144
  if len(selected_players) == 1 and len(selected_seasons) == 1:
1145
  player_name = selected_players[0]
1146
  season_str = selected_seasons[0]
@@ -1153,6 +1170,7 @@ def get_player_playoff_stats():
1153
  players_with_no_data.append(player_name)
1154
  logging.info(f"No playoff data found for {player_name} in {season_str}.")
1155
 
 
1156
  elif len(selected_players) == 2 and len(selected_seasons) == 2:
1157
  player1_name = selected_players[0]
1158
  player1_season = selected_seasons[0]
@@ -1219,8 +1237,8 @@ def get_team_stats():
1219
  logging.info("DEBUG: Request successfully entered get_team_stats function!")
1220
  try:
1221
  data = request.get_json()
1222
- selected_teams_abbrs = data.get('teams')
1223
- selected_season_str = data.get('season')
1224
 
1225
  if not selected_teams_abbrs or not selected_season_str:
1226
  return jsonify({'error': 'Teams and season are required'}), 400
@@ -1231,6 +1249,7 @@ def get_team_stats():
1231
  if tm_df.empty:
1232
  return jsonify({'error': f'No team data available for {selected_season_str}'}), 404
1233
 
 
1234
  full_team_names_map = {
1235
  "ATL": "Atlanta Hawks", "BOS": "Boston Celtics", "BRK": "Brooklyn Nets",
1236
  "CHO": "Charlotte Hornets", "CHI": "Chicago Bulls", "CLE": "Cleveland Cavaliers",
@@ -1249,12 +1268,14 @@ def get_team_stats():
1249
  teams_with_no_data = []
1250
 
1251
  for team_full_name_lookup in selected_teams_full_names:
1252
- df_row = tm_df[tm_df.Team == team_full_name_lookup].copy()
 
1253
  if not df_row.empty:
1254
  df_dict = df_row.iloc[0].to_dict()
1255
- df_dict['Season'] = selected_season_str
1256
  stats.append(df_dict)
1257
  else:
 
1258
  original_abbr = next((abbr for abbr, name in full_team_names_map.items() if name == team_full_name_lookup), team_full_name_lookup)
1259
  teams_with_no_data.append(original_abbr)
1260
 
@@ -1265,10 +1286,11 @@ def get_team_stats():
1265
  }), 404
1266
 
1267
  comp = pd.DataFrame(stats)
1268
- for col in ['WINS', 'LOSSES', 'WIN_LOSS_PCT', 'RANK']:
 
1269
  if col in comp.columns:
1270
  comp[col] = pd.to_numeric(comp[col], errors='coerce')
1271
- comp = comp.replace({np.nan: None})
1272
 
1273
  return jsonify({
1274
  'team_stats': comp.to_dict(orient='records'),
@@ -1279,7 +1301,7 @@ def get_team_stats():
1279
  return jsonify({'error': str(e)}), 500
1280
 
1281
  @app.route('/api/nba/dashboard_info', methods=['GET'])
1282
- @credit_required(cost=0)
1283
  @cross_origin()
1284
  def dashboard_info():
1285
  if not FIREBASE_INITIALIZED:
@@ -1288,7 +1310,7 @@ def dashboard_info():
1288
 
1289
  try:
1290
  dashboard_data = get_dashboard_info_brscraper()
1291
- if not dashboard_data:
1292
  return jsonify({'error': 'Could not retrieve dashboard information.'}), 500
1293
  return jsonify(dashboard_data)
1294
  except Exception as e:
@@ -1311,16 +1333,18 @@ def perplexity_explain():
1311
  return jsonify({'error': 'Prompt is required'}), 400
1312
 
1313
  explanation = ask_perp(prompt)
1314
- if "Error from AI" in explanation:
1315
  return jsonify({'error': explanation}), 500
1316
 
 
1317
  auth_header = request.headers.get('Authorization', '')
1318
  token = auth_header.split(' ')[1]
1319
  uid = verify_token(token)
1320
 
 
 
1321
  if FIREBASE_INITIALIZED:
1322
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
1323
- analysis_id = str(uuid.uuid4())
1324
  analysis_data = {
1325
  'prompt': prompt,
1326
  'explanation': explanation,
@@ -1337,7 +1361,7 @@ def perplexity_explain():
1337
  return jsonify({'error': str(e)}), 500
1338
 
1339
  @app.route('/api/user/analyses', methods=['GET'])
1340
- @credit_required(cost=0)
1341
  @cross_origin()
1342
  def get_user_analyses():
1343
  if not FIREBASE_INITIALIZED:
@@ -1349,7 +1373,7 @@ def get_user_analyses():
1349
  token = auth_header.split(' ')[1]
1350
  uid = verify_token(token)
1351
 
1352
- if not FIREBASE_INITIALIZED:
1353
  return jsonify({'error': 'Firebase not initialized. Cannot retrieve analyses.'}), 500
1354
 
1355
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
@@ -1364,6 +1388,7 @@ def get_user_analyses():
1364
  'created_at': data.get('created_at')
1365
  })
1366
 
 
1367
  analyses_list.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1368
 
1369
  return jsonify({'analyses': analyses_list})
@@ -1372,7 +1397,7 @@ def get_user_analyses():
1372
  return jsonify({'error': str(e)}), 500
1373
 
1374
  @app.route('/api/user/analyses/<string:analysis_id>', methods=['DELETE'])
1375
- @credit_required(cost=0)
1376
  @cross_origin()
1377
  def delete_user_analysis(analysis_id):
1378
  if not FIREBASE_INITIALIZED:
@@ -1388,7 +1413,7 @@ def delete_user_analysis(analysis_id):
1388
  return jsonify({'error': 'Firebase not initialized. Cannot delete analysis.'}), 500
1389
 
1390
  analysis_ref = db.reference(f'user_analyses/{uid}/{analysis_id}')
1391
- analysis_data = analysis_ref.get()
1392
 
1393
  if not analysis_data:
1394
  return jsonify({'error': 'Analysis not found or does not belong to this user'}), 404
@@ -1418,14 +1443,15 @@ def perplexity_chat():
1418
 
1419
  auth_header = request.headers.get('Authorization', '')
1420
  token = auth_header.split(' ')[1]
1421
- uid = verify_token(token)
1422
 
1423
  response_content = ask_perp(prompt)
1424
  if "Error from AI" in response_content:
1425
  return jsonify({'error': response_content}), 500
1426
 
 
1427
  if FIREBASE_INITIALIZED:
1428
- user_chat_ref = db.reference(f'users/{uid}/chat_history')
1429
  user_chat_ref.push({
1430
  'role': 'user',
1431
  'content': prompt,
@@ -1455,8 +1481,8 @@ def awards_predictor():
1455
 
1456
  try:
1457
  data = request.get_json()
1458
- award_type = data.get('award_type')
1459
- criteria = data.get('criteria')
1460
 
1461
  if not award_type or not criteria:
1462
  return jsonify({'error': 'Award type and criteria are required'}), 400
@@ -1519,7 +1545,7 @@ def similar_players():
1519
  try:
1520
  data = request.get_json()
1521
  target_player = data.get('target_player')
1522
- criteria = data.get('criteria')
1523
 
1524
  if not target_player or not criteria:
1525
  return jsonify({'error': 'Target player and criteria are required'}), 400
@@ -1529,6 +1555,7 @@ def similar_players():
1529
  if "Error from AI" in similar_players_analysis:
1530
  return jsonify({'error': similar_players_analysis}), 500
1531
 
 
1532
  auth_header = request.headers.get('Authorization', '')
1533
  token = auth_header.split(' ')[1]
1534
  uid = verify_token(token)
@@ -1538,10 +1565,10 @@ def similar_players():
1538
  if FIREBASE_INITIALIZED:
1539
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
1540
  analysis_data = {
1541
- 'type': 'similar_players',
1542
  'target_player': target_player,
1543
  'criteria': criteria,
1544
- 'prompt': prompt,
1545
  'explanation': similar_players_analysis,
1546
  'created_at': datetime.utcnow().isoformat()
1547
  }
@@ -1567,16 +1594,18 @@ def manual_player_compare():
1567
  try:
1568
  data = request.get_json()
1569
  player1_name = data.get('player1_name')
1570
- player1_season = data.get('player1_season')
1571
  player2_name = data.get('player2_name')
1572
- player2_season = data.get('player2_season')
1573
 
1574
  if not player1_name or not player2_name:
1575
  return jsonify({'error': 'Both player names are required'}), 400
1576
 
 
1577
  player1_str = f"{player1_name} ({player1_season} season)" if player1_season else player1_name
1578
  player2_str = f"{player2_name} ({player2_season} season)" if player2_season else player2_name
1579
 
 
1580
  comparison_context = "Statistical comparison"
1581
  if player1_season and player2_season:
1582
  comparison_context += f" (specifically {player1_season} vs {player2_season} seasons)"
 
715
  current_year = datetime.now().year
716
  current_month = datetime.now().month
717
  latest_season_end_year = current_year
718
+ if current_month >= 7: # Assuming season flips around July
719
  latest_season_end_year += 1
720
  seasons_list = []
721
  for i in range(num_seasons):
 
750
  return df
751
 
752
  def _scrape_player_index_brscraper():
753
+ seasons_to_try_for_index = get_available_seasons_util(num_seasons=2) # Try last 2 completed/current seasons
754
 
755
  for season_str in seasons_to_try_for_index:
756
  end_year = int(season_str.split('–')[1])
757
  try:
758
  logging.info(f"Attempting to get player index for year: {end_year} from BRScraper...")
759
+ df = nba.get_stats(end_year, info='per_game', rename=False) # Get per_game stats for the season
760
 
761
  if not df.empty and 'Player' in df.columns:
762
  player_names = df['Player'].dropna().unique().tolist()
763
+ # Normalize names immediately after fetching
764
  player_names = [normalize_string(name) for name in player_names]
765
  logging.info(f"Successfully retrieved {len(player_names)} players for index from {season_str}.")
766
  return pd.DataFrame({'name': player_names})
 
772
  logging.error("Failed to fetch player index from recent seasons. Falling back to curated common players list.")
773
  common_players = [
774
  'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo',
775
+ 'Nikola Jokic', # Added
776
+ 'Joel Embiid', 'Jayson Tatum', 'Luka Doncic', # Added
777
  'Damian Lillard', 'Jimmy Butler', 'Kawhi Leonard', 'Paul George',
778
  'Anthony Davis', 'Rudy Gobert', 'Donovan Mitchell', 'Trae Young',
779
  'Devin Booker', 'Karl-Anthony Towns', 'Zion Williamson', 'Ja Morant',
780
+ 'Shai Gilgeous-Alexander', 'Tyrese Maxey', 'Anthony Edwards', 'Victor Wembanyama', # Added
781
+ 'Jalen Brunson', 'Paolo Banchero', 'Franz Wagner', 'Cade Cunningham' # Added
782
  ]
783
  return pd.DataFrame({'name': common_players})
784
 
 
787
  logging.error("BRScraper is not available. Cannot fetch player career stats.")
788
  return pd.DataFrame()
789
 
790
+ normalized_player_name = normalize_string(player_name) # Normalize input player name once
791
  all_rows = []
792
 
793
  for season_str in seasons_to_fetch:
794
  end_year = int(season_str.split('–')[1])
795
 
796
+ # Define cache key based on normalized name, year, and playoff status
797
  cache_key = f"{normalized_player_name}_{end_year}_{'playoffs' if playoffs else 'regular'}"
798
  db_ref = db.reference(f'scraped_data/player_season_stats/{cache_key}')
799
 
800
  if FIREBASE_INITIALIZED:
801
  cached_data = db_ref.get()
802
+ if cached_data and not is_data_stale(cached_data.get('last_updated'), max_age_hours=24*7): # Cache for 7 days
803
  logging.info(f"Loading stats for {player_name} in {season_str} (playoffs: {playoffs}) from Firebase cache.")
804
  all_rows.append(pd.DataFrame.from_records(cached_data['data']))
805
  continue # Skip scraping for this season if found in cache
806
  else:
807
  logging.info(f"Stats for {player_name} in {season_str} cache stale or not found. Scraping...")
808
 
809
+ # Retry mechanism for scraping
810
+ for attempt in range(3): # Try up to 3 times
811
  try:
812
  logging.info(f"DEBUG: Attempt {attempt+1} for nba.get_stats for player '{player_name}' in season {season_str} (year: {end_year}, playoffs: {playoffs})...")
813
 
814
+ # Fetch all player stats for the given season and type (regular/playoffs)
815
  df_season = nba.get_stats(end_year, info='per_game', playoffs=playoffs, rename=False)
816
 
817
  if df_season.empty:
818
  logging.warning(f"DEBUG: nba.get_stats returned empty DataFrame for {player_name} in {season_str} on attempt {attempt+1}. Retrying...")
819
+ time.sleep(1) # Wait before retrying
820
  continue
821
 
822
  if 'Player' not in df_season.columns:
 
824
  time.sleep(1)
825
  continue
826
 
827
+ # Normalize player names from the scraped data for matching
828
  df_season['Player_Normalized'] = df_season['Player'].apply(normalize_string)
829
  row = df_season[df_season['Player_Normalized'] == normalized_player_name]
830
 
831
  if not row.empty:
832
+ row = row.copy() # Avoid SettingWithCopyWarning
833
+ row['Season'] = season_str # Add the season string
834
+ row = row.drop(columns=['Player_Normalized'], errors='ignore') # Drop helper column
835
 
836
  if FIREBASE_INITIALIZED:
837
  df_cleaned_for_firebase = clean_df_for_firebase(row.copy())
 
843
 
844
  all_rows.append(row)
845
  logging.info(f"DEBUG: Found stats for {player_name} in {season_str} on attempt {attempt+1}. Appending row.")
846
+ break # Success, exit retry loop for this season
847
  else:
848
+ # This case means the season data was fetched, but the specific player wasn't in it.
849
  logging.info(f"DEBUG: Player {player_name} not found in {season_str} stats (after getting season data) on attempt {attempt+1}. Retrying...")
850
  time.sleep(1)
851
+ # If player not found after fetching season data, retrying might not help unless BRScraper has intermittent issues.
852
+ # Consider breaking if player not found in a valid scrape. For now, let it retry.
853
  continue
854
 
855
  except Exception as e:
856
  logging.warning(f"DEBUG: Exception on attempt {attempt+1} when fetching {season_str} {'playoff' if playoffs else 'regular season'} stats for {player_name}: {e}")
857
+ time.sleep(1) # Wait before retrying
858
+ if attempt == 2: # Last attempt failed
859
  logging.error(f"DEBUG: All 3 attempts failed for {player_name} in {season_str}. Giving up on this season.")
860
+ continue # Go to next attempt or next season if all attempts failed
861
 
862
+ time.sleep(0.5) # Small delay between fetching different seasons to be polite to the server
863
 
864
  if not all_rows:
865
  logging.warning(f"DEBUG: No stats found for {player_name} in the requested seasons: {seasons_to_fetch}. Returning empty DataFrame.")
 
867
 
868
  df = pd.concat(all_rows, ignore_index=True)
869
 
870
+ # Standardize column names
871
  mapping = {
872
  'G':'GP','GS':'GS','MP':'MIN', 'FG%':'FG_PCT','3P%':'FG3_PCT','FT%':'FT_PCT',
873
  'TRB':'REB','AST':'AST','STL':'STL','BLK':'BLK','TOV':'TO',
 
878
  }
879
  df = df.rename(columns={o:n for o,n in mapping.items() if o in df.columns})
880
 
881
+ # Convert stats to numeric, coercing errors
882
+ non_num = {'Season','Player','Tm','Lg','Pos'} # Columns that should remain non-numeric
883
  for col in df.columns:
884
  if col not in non_num:
885
  df[col] = pd.to_numeric(df[col], errors='coerce')
886
 
887
+ df['Player'] = player_name # Ensure original (non-normalized) player name is in the final DataFrame
888
+ df = df.replace({np.nan: None}) # Replace NaN with None for JSON compatibility
889
  return df
890
 
891
  def get_dashboard_info_brscraper():
 
906
  else:
907
  logging.info("Scraping dashboard info (cache stale or not found).")
908
  data = _scrape_dashboard_info_brscraper()
909
+ if data: # Only cache if data was successfully scraped
910
  db_ref.set({
911
  'last_updated': datetime.utcnow().isoformat(),
912
  'data': data
 
917
  def _scrape_dashboard_info_brscraper():
918
  dashboard_data = {}
919
  try:
920
+ # Attempt to get MVP votings for 2025 (likely to be empty or error if too early)
921
  mvp_2025_df = nba.get_award_votings('mvp', 2025)
922
  if not mvp_2025_df.empty:
923
+ if 'Share' in mvp_2025_df.columns: # Standardize 'Share' to 'Votes'
924
  mvp_2025_df = mvp_2025_df.rename(columns={'Share': 'Votes'})
925
+ if 'Votes' in mvp_2025_df.columns: # Convert votes to percentage if it's a decimal
926
  mvp_2025_df['Votes'] = pd.to_numeric(mvp_2025_df['Votes'], errors='coerce') * 100
927
 
928
  mvp_2025_df = clean_df_for_firebase(mvp_2025_df)
929
  dashboard_data['mvp_2025_votings'] = mvp_2025_df.replace({np.nan: None}).to_dict(orient='records')
930
  else:
931
+ dashboard_data['mvp_2025_votings'] = [] # Ensure key exists even if no data
932
  logging.warning("Could not retrieve 2025 MVP votings.")
933
 
934
+ # Playoff probabilities
935
  east_probs_df = nba.get_playoffs_probs('east')
936
  if not east_probs_df.empty:
937
  if 'Eastern Conference' in east_probs_df.columns:
938
  east_probs_df = east_probs_df.rename(columns={'Eastern Conference': 'Team'})
939
+ elif 'Tm' in east_probs_df.columns: # Fallback if column name is 'Tm'
940
  east_probs_df = east_probs_df.rename(columns={'Tm': 'Team'})
941
 
942
+ if 'Team' in east_probs_df.columns: # Clean team names
943
  east_probs_df['Team'] = east_probs_df['Team'].astype(str).apply(clean_team_name)
944
 
945
  east_probs_df = clean_df_for_firebase(east_probs_df)
 
996
  }
997
 
998
  payload = {
999
+ "model": "sonar-pro", # Ensure this model is appropriate and available
1000
  "messages": [
1001
  {"role": "system", "content": system},
1002
+ {"role": "user", "content": f"BASKETBALL ONLY: {prompt}"} # Reinforce context
1003
  ],
1004
  "max_tokens": max_tokens,
1005
  "temperature": temp,
1006
+ "web_search_options": { # Added web search options for more current data
1007
+ "search_context_size": "high", # "low", "medium", "high"
1008
+ "search_domain_filter": ["nba.com", "espn.com", "basketball-reference.com"] # Focus search
1009
  },
1010
+ "emit_sources": True # Request sources if available
1011
  }
1012
 
1013
  try:
1014
+ response = requests.post(PERP_URL, json=payload, headers=headers, timeout=45) # Increased timeout
1015
+ response.raise_for_status() # Will raise HTTPError for bad responses (4XX, 5XX)
1016
  return response.json().get("choices", [])[0].get("message", {}).get("content", "")
1017
  except requests.exceptions.RequestException as e:
1018
  error_message = f"Error communicating with Perplexity API: {e}"
 
1020
  try:
1021
  error_detail = e.response.json().get("error", {}).get("message", e.response.text)
1022
  error_message = f"Perplexity API error: {e.response.status_code} - {e.response.reason}"
1023
+ except ValueError: # If response is not JSON
1024
  error_message = f"Perplexity API error: {e.response.status_code} - {e.response.reason}"
1025
  logging.error(f"Perplexity API request failed: {error_message}")
1026
  return f"Error from AI: {error_message}"
1027
+ except Exception as e: # Catch any other unexpected errors
1028
  logging.error(f"An unexpected error occurred with Perplexity API: {e}")
1029
  return f"An unexpected error occurred with AI: {str(e)}"
1030
 
 
1115
 
1116
  comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
1117
 
1118
+ # Basic stats for display
1119
  basic_display_df = comparison_df_raw.copy()
1120
  basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
1121
+ # Ensure only existing columns are selected and then round
1122
  basic_display_df = basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2)
1123
 
1124
+ # Advanced stats calculation (e.g., TS%)
1125
  advanced_df = comparison_df_raw.copy()
1126
+ # Ensure necessary columns for TS% are numeric and handle potential missing columns
1127
  advanced_df['FGA'] = pd.to_numeric(advanced_df.get('FGA', 0), errors='coerce').fillna(0)
1128
  advanced_df['FTA'] = pd.to_numeric(advanced_df.get('FTA', 0), errors='coerce').fillna(0)
1129
  advanced_df['PTS'] = pd.to_numeric(advanced_df.get('PTS', 0), errors='coerce').fillna(0)
 
1131
  lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0,
1132
  axis=1
1133
  )
1134
+ advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT'] # Example advanced stats
1135
  advanced_display_df = advanced_df[[c for c in advanced_cols if c in advanced_df.columns]].round(3)
1136
 
1137
  return jsonify({
 
1157
  all_player_season_data = []
1158
  players_with_no_data = []
1159
 
1160
+ # Handle individual player stats (1 player, 1 season)
1161
  if len(selected_players) == 1 and len(selected_seasons) == 1:
1162
  player_name = selected_players[0]
1163
  season_str = selected_seasons[0]
 
1170
  players_with_no_data.append(player_name)
1171
  logging.info(f"No playoff data found for {player_name} in {season_str}.")
1172
 
1173
+ # Handle comparison (2 players, 2 seasons)
1174
  elif len(selected_players) == 2 and len(selected_seasons) == 2:
1175
  player1_name = selected_players[0]
1176
  player1_season = selected_seasons[0]
 
1237
  logging.info("DEBUG: Request successfully entered get_team_stats function!")
1238
  try:
1239
  data = request.get_json()
1240
+ selected_teams_abbrs = data.get('teams') # Expecting list of abbreviations e.g., ['LAL', 'BOS']
1241
+ selected_season_str = data.get('season') # Expecting "YYYY-YY" format
1242
 
1243
  if not selected_teams_abbrs or not selected_season_str:
1244
  return jsonify({'error': 'Teams and season are required'}), 400
 
1249
  if tm_df.empty:
1250
  return jsonify({'error': f'No team data available for {selected_season_str}'}), 404
1251
 
1252
+ # Map abbreviations to full names for lookup in BRScraper data
1253
  full_team_names_map = {
1254
  "ATL": "Atlanta Hawks", "BOS": "Boston Celtics", "BRK": "Brooklyn Nets",
1255
  "CHO": "Charlotte Hornets", "CHI": "Chicago Bulls", "CLE": "Cleveland Cavaliers",
 
1268
  teams_with_no_data = []
1269
 
1270
  for team_full_name_lookup in selected_teams_full_names:
1271
+ # Match on the 'Team' column which should have full names after cleaning
1272
+ df_row = tm_df[tm_df.Team == team_full_name_lookup].copy() # Use .copy()
1273
  if not df_row.empty:
1274
  df_dict = df_row.iloc[0].to_dict()
1275
+ df_dict['Season'] = selected_season_str # Add season back
1276
  stats.append(df_dict)
1277
  else:
1278
+ # Find original abbreviation if lookup failed
1279
  original_abbr = next((abbr for abbr, name in full_team_names_map.items() if name == team_full_name_lookup), team_full_name_lookup)
1280
  teams_with_no_data.append(original_abbr)
1281
 
 
1286
  }), 404
1287
 
1288
  comp = pd.DataFrame(stats)
1289
+ # Ensure key stats are numeric
1290
+ for col in ['WINS', 'LOSSES', 'WIN_LOSS_PCT', 'RANK']: # Add other numeric stats if needed
1291
  if col in comp.columns:
1292
  comp[col] = pd.to_numeric(comp[col], errors='coerce')
1293
+ comp = comp.replace({np.nan: None}) # For JSON compatibility
1294
 
1295
  return jsonify({
1296
  'team_stats': comp.to_dict(orient='records'),
 
1301
  return jsonify({'error': str(e)}), 500
1302
 
1303
  @app.route('/api/nba/dashboard_info', methods=['GET'])
1304
+ @credit_required(cost=0) # No cost for dashboard info
1305
  @cross_origin()
1306
  def dashboard_info():
1307
  if not FIREBASE_INITIALIZED:
 
1310
 
1311
  try:
1312
  dashboard_data = get_dashboard_info_brscraper()
1313
+ if not dashboard_data: # Check if the dictionary itself is empty
1314
  return jsonify({'error': 'Could not retrieve dashboard information.'}), 500
1315
  return jsonify(dashboard_data)
1316
  except Exception as e:
 
1333
  return jsonify({'error': 'Prompt is required'}), 400
1334
 
1335
  explanation = ask_perp(prompt)
1336
+ if "Error from AI" in explanation: # Check for specific error message from ask_perp
1337
  return jsonify({'error': explanation}), 500
1338
 
1339
+ # Store analysis if Firebase is up
1340
  auth_header = request.headers.get('Authorization', '')
1341
  token = auth_header.split(' ')[1]
1342
  uid = verify_token(token)
1343
 
1344
+ analysis_id = str(uuid.uuid4()) # Generate ID regardless of Firebase status for return
1345
+
1346
  if FIREBASE_INITIALIZED:
1347
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
 
1348
  analysis_data = {
1349
  'prompt': prompt,
1350
  'explanation': explanation,
 
1361
  return jsonify({'error': str(e)}), 500
1362
 
1363
  @app.route('/api/user/analyses', methods=['GET'])
1364
+ @credit_required(cost=0) # No cost to view own analyses
1365
  @cross_origin()
1366
  def get_user_analyses():
1367
  if not FIREBASE_INITIALIZED:
 
1373
  token = auth_header.split(' ')[1]
1374
  uid = verify_token(token)
1375
 
1376
+ if not FIREBASE_INITIALIZED: # Double check, though credit_required should handle
1377
  return jsonify({'error': 'Firebase not initialized. Cannot retrieve analyses.'}), 500
1378
 
1379
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
 
1388
  'created_at': data.get('created_at')
1389
  })
1390
 
1391
+ # Sort by creation date, newest first
1392
  analyses_list.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1393
 
1394
  return jsonify({'analyses': analyses_list})
 
1397
  return jsonify({'error': str(e)}), 500
1398
 
1399
  @app.route('/api/user/analyses/<string:analysis_id>', methods=['DELETE'])
1400
+ @credit_required(cost=0) # No cost to delete own analysis
1401
  @cross_origin()
1402
  def delete_user_analysis(analysis_id):
1403
  if not FIREBASE_INITIALIZED:
 
1413
  return jsonify({'error': 'Firebase not initialized. Cannot delete analysis.'}), 500
1414
 
1415
  analysis_ref = db.reference(f'user_analyses/{uid}/{analysis_id}')
1416
+ analysis_data = analysis_ref.get() # Check if it exists before deleting
1417
 
1418
  if not analysis_data:
1419
  return jsonify({'error': 'Analysis not found or does not belong to this user'}), 404
 
1443
 
1444
  auth_header = request.headers.get('Authorization', '')
1445
  token = auth_header.split(' ')[1]
1446
+ uid = verify_token(token) # Get UID for chat history
1447
 
1448
  response_content = ask_perp(prompt)
1449
  if "Error from AI" in response_content:
1450
  return jsonify({'error': response_content}), 500
1451
 
1452
+ # Store chat history if Firebase is up
1453
  if FIREBASE_INITIALIZED:
1454
+ user_chat_ref = db.reference(f'users/{uid}/chat_history') # Store under user's profile
1455
  user_chat_ref.push({
1456
  'role': 'user',
1457
  'content': prompt,
 
1481
 
1482
  try:
1483
  data = request.get_json()
1484
+ award_type = data.get('award_type') # e.g., "MVP", "Rookie of the Year"
1485
+ criteria = data.get('criteria') # e.g., "early season performance", "team success"
1486
 
1487
  if not award_type or not criteria:
1488
  return jsonify({'error': 'Award type and criteria are required'}), 400
 
1545
  try:
1546
  data = request.get_json()
1547
  target_player = data.get('target_player')
1548
+ criteria = data.get('criteria') # Expecting a list of strings
1549
 
1550
  if not target_player or not criteria:
1551
  return jsonify({'error': 'Target player and criteria are required'}), 400
 
1555
  if "Error from AI" in similar_players_analysis:
1556
  return jsonify({'error': similar_players_analysis}), 500
1557
 
1558
+ # Store analysis
1559
  auth_header = request.headers.get('Authorization', '')
1560
  token = auth_header.split(' ')[1]
1561
  uid = verify_token(token)
 
1565
  if FIREBASE_INITIALIZED:
1566
  user_analyses_ref = db.reference(f'user_analyses/{uid}')
1567
  analysis_data = {
1568
+ 'type': 'similar_players', # Add a type for easier filtering later if needed
1569
  'target_player': target_player,
1570
  'criteria': criteria,
1571
+ 'prompt': prompt, # Store the exact prompt for record
1572
  'explanation': similar_players_analysis,
1573
  'created_at': datetime.utcnow().isoformat()
1574
  }
 
1594
  try:
1595
  data = request.get_json()
1596
  player1_name = data.get('player1_name')
1597
+ player1_season = data.get('player1_season') # Optional
1598
  player2_name = data.get('player2_name')
1599
+ player2_season = data.get('player2_season') # Optional
1600
 
1601
  if not player1_name or not player2_name:
1602
  return jsonify({'error': 'Both player names are required'}), 400
1603
 
1604
+ # Construct player strings for the prompt
1605
  player1_str = f"{player1_name} ({player1_season} season)" if player1_season else player1_name
1606
  player2_str = f"{player2_name} ({player2_season} season)" if player2_season else player2_name
1607
 
1608
+ # Define comparison context based on provided seasons
1609
  comparison_context = "Statistical comparison"
1610
  if player1_season and player2_season:
1611
  comparison_context += f" (specifically {player1_season} vs {player2_season} seasons)"