rairo commited on
Commit
db1ff31
·
verified ·
1 Parent(s): 1e5732b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +127 -21
main.py CHANGED
@@ -7,8 +7,8 @@ import tempfile
7
  import requests
8
  import json
9
  import pandas as pd
10
- import numpy as np # Added for np.nan
11
- from datetime import datetime, timedelta # Import timedelta for daily refresh logic
12
  from flask import Flask, request, jsonify, send_file
13
  from flask_cors import CORS, cross_origin
14
  from firebase_admin import credentials, db, storage, auth
@@ -16,6 +16,7 @@ import firebase_admin
16
  import logging
17
  import traceback
18
  from bs4 import BeautifulSoup, Comment
 
19
  # Import BRScraper
20
  try:
21
  from BRScraper import nba
@@ -60,6 +61,7 @@ bucket = storage.bucket() if FIREBASE_INITIALIZED else None
60
 
61
  # Helper functions
62
  def verify_token(token):
 
63
  try:
64
  decoded_token = auth.verify_id_token(token)
65
  return decoded_token['uid']
@@ -68,6 +70,7 @@ def verify_token(token):
68
  return None
69
 
70
  def verify_admin(auth_header):
 
71
  if not auth_header or not auth_header.startswith('Bearer '):
72
  raise ValueError('Invalid token format')
73
  token = auth_header.split(' ')[1]
@@ -80,10 +83,12 @@ def verify_admin(auth_header):
80
  raise PermissionError('Admin access required')
81
  return uid
82
 
83
-
84
-
85
  # Decorator for credit deduction
86
  def credit_required(cost=1):
 
 
 
 
87
  def decorator(f):
88
  def wrapper(*args, **kwargs):
89
  auth_header = request.headers.get('Authorization', '')
@@ -107,12 +112,13 @@ def credit_required(cost=1):
107
  return jsonify({'error': f'Insufficient credits. You need {cost} credits, but have {current_credits}.'}), 403
108
 
109
  try:
110
- # Deduct credits
111
- user_ref.update({'credits': current_credits - cost})
112
- logging.info(f"Deducted {cost} credits from user {uid}. New balance: {current_credits - cost}")
 
113
  return f(*args, **kwargs)
114
  except Exception as e:
115
- logging.error(f"Failed to deduct credits for user {uid}: {e}")
116
  return jsonify({'error': 'Failed to process credits. Please try again.'}), 500
117
  wrapper.__name__ = f.__name__ # Preserve original function name for Flask routing
118
  return wrapper
@@ -123,6 +129,7 @@ def credit_required(cost=1):
123
 
124
  @app.route('/api/auth/signup', methods=['POST'])
125
  def signup():
 
126
  try:
127
  data = request.get_json()
128
  email = data.get('email')
@@ -134,7 +141,7 @@ def signup():
134
  user_ref = db.reference(f'users/{user.uid}')
135
  user_data = {
136
  'email': email,
137
- 'credits': 10, # Changed initial credits to 10
138
  'is_admin': False,
139
  'created_at': datetime.utcnow().isoformat()
140
  }
@@ -152,6 +159,7 @@ def signup():
152
 
153
  @app.route('/api/user/profile', methods=['GET'])
154
  def get_user_profile():
 
155
  try:
156
  auth_header = request.headers.get('Authorization', '')
157
  if not auth_header.startswith('Bearer '):
@@ -178,6 +186,7 @@ def get_user_profile():
178
 
179
  @app.route('/api/auth/google-signin', methods=['POST'])
180
  def google_signin():
 
181
  try:
182
  auth_header = request.headers.get('Authorization', '')
183
  if not auth_header.startswith('Bearer '):
@@ -194,7 +203,7 @@ def google_signin():
194
  if not user_data:
195
  user_data = {
196
  'email': email,
197
- 'credits': 10, # Changed initial credits to 10
198
  'is_admin': False,
199
  'created_at': datetime.utcnow().isoformat(),
200
  }
@@ -215,6 +224,7 @@ def google_signin():
215
  @app.route('/api/user/request-credits', methods=['POST'])
216
  @credit_required(cost=0) # This endpoint doesn't cost credits, but requires auth
217
  def request_credits():
 
218
  try:
219
  auth_header = request.headers.get('Authorization', '')
220
  token = auth_header.split(' ')[1]
@@ -237,11 +247,11 @@ def request_credits():
237
  logging.error(f"Request credits error: {e}")
238
  return jsonify({'error': str(e)}), 500
239
 
240
- # Add this new endpoint to your Flask app
241
  @app.route('/api/user/submit_feedback', methods=['POST'])
242
  @credit_required(cost=0) # Requires authentication but no credit cost
243
  @cross_origin()
244
  def submit_feedback():
 
245
  try:
246
  auth_header = request.headers.get('Authorization', '')
247
  token = auth_header.split(' ')[1]
@@ -271,10 +281,11 @@ def submit_feedback():
271
  logging.error(f"Submit feedback error: {e}")
272
  return jsonify({'error': str(e)}), 500
273
 
274
- # ---------- Admin Endpoints for Credit Requests ----------
275
 
276
  @app.route('/api/admin/profile', methods=['GET'])
277
  def get_admin_profile():
 
278
  try:
279
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
280
  admin_data = db.reference(f'users/{admin_uid}').get()
@@ -314,6 +325,7 @@ def get_admin_profile():
314
 
315
  @app.route('/api/admin/credit_requests', methods=['GET'])
316
  def list_credit_requests():
 
317
  try:
318
  verify_admin(request.headers.get('Authorization', ''))
319
  requests_ref = db.reference('credit_requests')
@@ -326,6 +338,7 @@ def list_credit_requests():
326
 
327
  @app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
328
  def process_credit_request(request_id):
 
329
  try:
330
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
331
  req_ref = db.reference(f'credit_requests/{request_id}')
@@ -364,6 +377,7 @@ def process_credit_request(request_id):
364
 
365
  @app.route('/api/admin/users', methods=['GET'])
366
  def admin_list_users():
 
367
  try:
368
  verify_admin(request.headers.get('Authorization', ''))
369
  users_ref = db.reference('users')
@@ -386,6 +400,7 @@ def admin_list_users():
386
 
387
  @app.route('/api/admin/users/search', methods=['GET'])
388
  def admin_search_users():
 
389
  try:
390
  verify_admin(request.headers.get('Authorization', ''))
391
  email_query = request.args.get('email', '').lower().strip()
@@ -414,6 +429,7 @@ def admin_search_users():
414
 
415
  @app.route('/api/admin/users/<string:uid>/suspend', methods=['PUT'])
416
  def admin_suspend_user(uid):
 
417
  try:
418
  verify_admin(request.headers.get('Authorization', ''))
419
  data = request.get_json()
@@ -436,10 +452,9 @@ def admin_suspend_user(uid):
436
  logging.error(f"Admin suspend user error: {e}")
437
  return jsonify({'error': str(e)}), 500
438
 
439
-
440
-
441
  @app.route('/api/admin/notifications', methods=['POST'])
442
  def send_notifications():
 
443
  try:
444
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
445
  data = request.get_json()
@@ -487,6 +502,7 @@ def send_notifications():
487
 
488
  @app.route('/api/admin/feedback', methods=['GET'])
489
  def admin_view_feedback():
 
490
  try:
491
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
492
  feedback_type = request.args.get('type')
@@ -518,6 +534,7 @@ def admin_view_feedback():
518
 
519
  @app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
520
  def admin_update_credits(uid):
 
521
  try:
522
  verify_admin(request.headers.get('Authorization', ''))
523
  data = request.get_json()
@@ -543,7 +560,6 @@ def admin_update_credits(uid):
543
  # ——————————————————————————————————————————————
544
 
545
  # Custom BeautifulSoup Data Fetching Utilities (for teams)
546
- # Re-added for custom BS scraping
547
  def fetch_html(url):
548
  """Fetch raw HTML for a URL (with error handling)."""
549
  try:
@@ -591,7 +607,6 @@ def parse_table(html, table_id=None):
591
  return pd.DataFrame()
592
 
593
  try:
594
- # FIX 1: Wrap tbl_html in io.StringIO
595
  return pd.read_html(io.StringIO(tbl_html))[0]
596
  except ValueError:
597
  logging.warning("No tables found in the provided HTML string for parsing.")
@@ -700,7 +715,7 @@ def _scrape_team_stats_bs(year):
700
  column_mapping = {
701
  'G': 'GP', 'MP': 'MIN', 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
702
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
703
- 'PF': 'PF', 'PTS': 'PTS', 'Rk': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W/L%': 'WIN_LOSS_PCT',
704
  'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
705
  '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
706
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
@@ -717,7 +732,6 @@ def _scrape_team_stats_bs(year):
717
  if col not in non_numeric_cols:
718
  df[col] = pd.to_numeric(df[col], errors="coerce")
719
 
720
- # FIX 2: Replace NaN values with None for JSON compliance
721
  df = df.replace({np.nan: None})
722
 
723
  return df
@@ -826,6 +840,7 @@ PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
826
  PERP_URL = "https://api.perplexity.ai/chat/completions"
827
 
828
  def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500, temp=0.2):
 
829
  if not PERP_KEY:
830
  logging.error("PERPLEXITY_API_KEY env var not set.")
831
  return "Perplexity API key is not configured."
@@ -859,6 +874,7 @@ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500,
859
  @app.route('/api/nba/players', methods=['GET'])
860
  @cross_origin()
861
  def get_players():
 
862
  try:
863
  players_df = get_player_index_brscraper()
864
  if players_df.empty:
@@ -871,6 +887,7 @@ def get_players():
871
  @app.route('/api/nba/seasons', methods=['GET'])
872
  @cross_origin()
873
  def get_seasons():
 
874
  try:
875
  seasons_list = get_available_seasons_util()
876
  return jsonify({'seasons': seasons_list})
@@ -880,8 +897,8 @@ def get_seasons():
880
 
881
  @app.route('/api/nba/player_stats', methods=['POST'])
882
  @cross_origin()
883
- # No credit_required decorator here, as this is data fetching, not AI interaction
884
  def get_player_stats():
 
885
  try:
886
  data = request.get_json()
887
  selected_players = data.get('players')
@@ -949,8 +966,8 @@ def get_player_stats():
949
 
950
  @app.route('/api/nba/team_stats', methods=['POST'])
951
  @cross_origin()
952
- # No credit_required decorator here, as this is data fetching, not AI interaction
953
  def get_team_stats():
 
954
  try:
955
  data = request.get_json()
956
  selected_teams = data.get('teams')
@@ -1000,6 +1017,9 @@ def get_team_stats():
1000
  @credit_required(cost=1) # Costs 1 credit
1001
  @cross_origin()
1002
  def perplexity_explain():
 
 
 
1003
  try:
1004
  data = request.get_json()
1005
  prompt = data.get('prompt')
@@ -1011,15 +1031,95 @@ def perplexity_explain():
1011
  if "Error from AI" in explanation:
1012
  return jsonify({'error': explanation}), 500
1013
 
1014
- return jsonify({'explanation': explanation})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
  except Exception as e:
1016
  logging.error(f"Error in /api/nba/perplexity_explain: {e}")
1017
  return jsonify({'error': str(e)}), 500
1018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  @app.route('/api/nba/perplexity_chat', methods=['POST'])
1020
  @credit_required(cost=1) # Costs 1 credit
1021
  @cross_origin()
1022
  def perplexity_chat():
 
1023
  try:
1024
  data = request.get_json()
1025
  prompt = data.get('prompt')
@@ -1065,6 +1165,7 @@ def perplexity_chat():
1065
  @credit_required(cost=1) # Costs 1 credit
1066
  @cross_origin()
1067
  def awards_predictor():
 
1068
  try:
1069
  data = request.get_json()
1070
  award_type = data.get('award_type')
@@ -1087,6 +1188,7 @@ def awards_predictor():
1087
  @credit_required(cost=1) # Costs 1 credit
1088
  @cross_origin()
1089
  def young_player_projection():
 
1090
  try:
1091
  data = request.get_json()
1092
  player_name = data.get('player_name')
@@ -1120,6 +1222,7 @@ def young_player_projection():
1120
  @credit_required(cost=1) # Costs 1 credit
1121
  @cross_origin()
1122
  def similar_players():
 
1123
  try:
1124
  data = request.get_json()
1125
  target_player = data.get('target_player')
@@ -1142,6 +1245,7 @@ def similar_players():
1142
  @credit_required(cost=1) # Costs 1 credit
1143
  @cross_origin()
1144
  def manual_player_compare():
 
1145
  try:
1146
  data = request.get_json()
1147
  player1 = data.get('player1')
@@ -1168,6 +1272,7 @@ def manual_player_compare():
1168
  @credit_required(cost=1) # Costs 1 credit
1169
  @cross_origin()
1170
  def roster_suggestions():
 
1171
  try:
1172
  data = request.get_json()
1173
  salary_cap = data.get('salary_cap')
@@ -1208,6 +1313,7 @@ def roster_suggestions():
1208
  @credit_required(cost=1) # Costs 1 credit
1209
  @cross_origin()
1210
  def trade_analysis():
 
1211
  try:
1212
  data = request.get_json()
1213
  team1_trades = data.get('team1_trades')
 
7
  import requests
8
  import json
9
  import pandas as pd
10
+ import numpy as np
11
+ from datetime import datetime, timedelta
12
  from flask import Flask, request, jsonify, send_file
13
  from flask_cors import CORS, cross_origin
14
  from firebase_admin import credentials, db, storage, auth
 
16
  import logging
17
  import traceback
18
  from bs4 import BeautifulSoup, Comment
19
+
20
  # Import BRScraper
21
  try:
22
  from BRScraper import nba
 
61
 
62
  # Helper functions
63
  def verify_token(token):
64
+ """Verifies Firebase ID token and returns UID."""
65
  try:
66
  decoded_token = auth.verify_id_token(token)
67
  return decoded_token['uid']
 
70
  return None
71
 
72
  def verify_admin(auth_header):
73
+ """Verifies admin privileges based on Firebase Realtime DB 'is_admin' flag."""
74
  if not auth_header or not auth_header.startswith('Bearer '):
75
  raise ValueError('Invalid token format')
76
  token = auth_header.split(' ')[1]
 
83
  raise PermissionError('Admin access required')
84
  return uid
85
 
 
 
86
  # Decorator for credit deduction
87
  def credit_required(cost=1):
88
+ """
89
+ Decorator to check user authentication, suspension status, and deduct credits.
90
+ If cost is 0, it only checks authentication and suspension.
91
+ """
92
  def decorator(f):
93
  def wrapper(*args, **kwargs):
94
  auth_header = request.headers.get('Authorization', '')
 
112
  return jsonify({'error': f'Insufficient credits. You need {cost} credits, but have {current_credits}.'}), 403
113
 
114
  try:
115
+ # Deduct credits only if cost > 0
116
+ if cost > 0:
117
+ user_ref.update({'credits': current_credits - cost})
118
+ logging.info(f"Deducted {cost} credits from user {uid}. New balance: {current_credits - cost}")
119
  return f(*args, **kwargs)
120
  except Exception as e:
121
+ logging.error(f"Failed to process credits for user {uid}: {e}")
122
  return jsonify({'error': 'Failed to process credits. Please try again.'}), 500
123
  wrapper.__name__ = f.__name__ # Preserve original function name for Flask routing
124
  return wrapper
 
129
 
130
  @app.route('/api/auth/signup', methods=['POST'])
131
  def signup():
132
+ """Handles user signup and creates a new user entry in Firebase Auth and DB."""
133
  try:
134
  data = request.get_json()
135
  email = data.get('email')
 
141
  user_ref = db.reference(f'users/{user.uid}')
142
  user_data = {
143
  'email': email,
144
+ 'credits': 10, # Initial credits for new users
145
  'is_admin': False,
146
  'created_at': datetime.utcnow().isoformat()
147
  }
 
159
 
160
  @app.route('/api/user/profile', methods=['GET'])
161
  def get_user_profile():
162
+ """Retrieves the authenticated user's profile information."""
163
  try:
164
  auth_header = request.headers.get('Authorization', '')
165
  if not auth_header.startswith('Bearer '):
 
186
 
187
  @app.route('/api/auth/google-signin', methods=['POST'])
188
  def google_signin():
189
+ """Handles Google Sign-In, creating user DB entry if it doesn't exist."""
190
  try:
191
  auth_header = request.headers.get('Authorization', '')
192
  if not auth_header.startswith('Bearer '):
 
203
  if not user_data:
204
  user_data = {
205
  'email': email,
206
+ 'credits': 10, # Initial credits for new users
207
  'is_admin': False,
208
  'created_at': datetime.utcnow().isoformat(),
209
  }
 
224
  @app.route('/api/user/request-credits', methods=['POST'])
225
  @credit_required(cost=0) # This endpoint doesn't cost credits, but requires auth
226
  def request_credits():
227
+ """Allows a user to request more credits from an admin."""
228
  try:
229
  auth_header = request.headers.get('Authorization', '')
230
  token = auth_header.split(' ')[1]
 
247
  logging.error(f"Request credits error: {e}")
248
  return jsonify({'error': str(e)}), 500
249
 
 
250
  @app.route('/api/user/submit_feedback', methods=['POST'])
251
  @credit_required(cost=0) # Requires authentication but no credit cost
252
  @cross_origin()
253
  def submit_feedback():
254
+ """Allows a user to submit feedback to administrators."""
255
  try:
256
  auth_header = request.headers.get('Authorization', '')
257
  token = auth_header.split(' ')[1]
 
281
  logging.error(f"Submit feedback error: {e}")
282
  return jsonify({'error': str(e)}), 500
283
 
284
+ # ---------- Admin Endpoints ----------
285
 
286
  @app.route('/api/admin/profile', methods=['GET'])
287
  def get_admin_profile():
288
+ """Retrieves admin profile and aggregated user/credit statistics."""
289
  try:
290
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
291
  admin_data = db.reference(f'users/{admin_uid}').get()
 
325
 
326
  @app.route('/api/admin/credit_requests', methods=['GET'])
327
  def list_credit_requests():
328
+ """Lists all credit requests submitted by users."""
329
  try:
330
  verify_admin(request.headers.get('Authorization', ''))
331
  requests_ref = db.reference('credit_requests')
 
338
 
339
  @app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
340
  def process_credit_request(request_id):
341
+ """Processes a specific credit request (approves or declines)."""
342
  try:
343
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
344
  req_ref = db.reference(f'credit_requests/{request_id}')
 
377
 
378
  @app.route('/api/admin/users', methods=['GET'])
379
  def admin_list_users():
380
+ """Lists all users in the system."""
381
  try:
382
  verify_admin(request.headers.get('Authorization', ''))
383
  users_ref = db.reference('users')
 
400
 
401
  @app.route('/api/admin/users/search', methods=['GET'])
402
  def admin_search_users():
403
+ """Searches for users by email."""
404
  try:
405
  verify_admin(request.headers.get('Authorization', ''))
406
  email_query = request.args.get('email', '').lower().strip()
 
429
 
430
  @app.route('/api/admin/users/<string:uid>/suspend', methods=['PUT'])
431
  def admin_suspend_user(uid):
432
+ """Suspends or unsuspends a user account."""
433
  try:
434
  verify_admin(request.headers.get('Authorization', ''))
435
  data = request.get_json()
 
452
  logging.error(f"Admin suspend user error: {e}")
453
  return jsonify({'error': str(e)}), 500
454
 
 
 
455
  @app.route('/api/admin/notifications', methods=['POST'])
456
  def send_notifications():
457
+ """Sends notifications to users (all, single, or list)."""
458
  try:
459
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
460
  data = request.get_json()
 
502
 
503
  @app.route('/api/admin/feedback', methods=['GET'])
504
  def admin_view_feedback():
505
+ """Retrieves user feedback, with optional filtering by type and status."""
506
  try:
507
  admin_uid = verify_admin(request.headers.get('Authorization', ''))
508
  feedback_type = request.args.get('type')
 
534
 
535
  @app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
536
  def admin_update_credits(uid):
537
+ """Directly adds or subtracts credits from a user's account."""
538
  try:
539
  verify_admin(request.headers.get('Authorization', ''))
540
  data = request.get_json()
 
560
  # ——————————————————————————————————————————————
561
 
562
  # Custom BeautifulSoup Data Fetching Utilities (for teams)
 
563
  def fetch_html(url):
564
  """Fetch raw HTML for a URL (with error handling)."""
565
  try:
 
607
  return pd.DataFrame()
608
 
609
  try:
 
610
  return pd.read_html(io.StringIO(tbl_html))[0]
611
  except ValueError:
612
  logging.warning("No tables found in the provided HTML string for parsing.")
 
715
  column_mapping = {
716
  'G': 'GP', 'MP': 'MIN', 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
717
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
718
+ 'PF': 'PF', 'PTS': 'PTS', 'Rk': 'RANK', 'W': 'WINS', 'L': 'L', 'W/L%': 'WIN_LOSS_PCT', # Corrected 'L' mapping
719
  'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
720
  '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
721
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
 
732
  if col not in non_numeric_cols:
733
  df[col] = pd.to_numeric(df[col], errors="coerce")
734
 
 
735
  df = df.replace({np.nan: None})
736
 
737
  return df
 
840
  PERP_URL = "https://api.perplexity.ai/chat/completions"
841
 
842
  def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500, temp=0.2):
843
+ """Sends a prompt to the Perplexity AI API and returns the response."""
844
  if not PERP_KEY:
845
  logging.error("PERPLEXITY_API_KEY env var not set.")
846
  return "Perplexity API key is not configured."
 
874
  @app.route('/api/nba/players', methods=['GET'])
875
  @cross_origin()
876
  def get_players():
877
+ """Returns a list of available NBA player names."""
878
  try:
879
  players_df = get_player_index_brscraper()
880
  if players_df.empty:
 
887
  @app.route('/api/nba/seasons', methods=['GET'])
888
  @cross_origin()
889
  def get_seasons():
890
+ """Returns a list of available NBA seasons."""
891
  try:
892
  seasons_list = get_available_seasons_util()
893
  return jsonify({'seasons': seasons_list})
 
897
 
898
  @app.route('/api/nba/player_stats', methods=['POST'])
899
  @cross_origin()
 
900
  def get_player_stats():
901
+ """Retrieves per-game statistics for selected NBA players and seasons."""
902
  try:
903
  data = request.get_json()
904
  selected_players = data.get('players')
 
966
 
967
  @app.route('/api/nba/team_stats', methods=['POST'])
968
  @cross_origin()
 
969
  def get_team_stats():
970
+ """Retrieves per-game statistics for selected NBA teams and a specific season."""
971
  try:
972
  data = request.get_json()
973
  selected_teams = data.get('teams')
 
1017
  @credit_required(cost=1) # Costs 1 credit
1018
  @cross_origin()
1019
  def perplexity_explain():
1020
+ """
1021
+ Provides an AI explanation for a given prompt and stores it in the database.
1022
+ """
1023
  try:
1024
  data = request.get_json()
1025
  prompt = data.get('prompt')
 
1031
  if "Error from AI" in explanation:
1032
  return jsonify({'error': explanation}), 500
1033
 
1034
+ # Get UID from the authenticated request (credit_required decorator ensures it's available)
1035
+ auth_header = request.headers.get('Authorization', '')
1036
+ token = auth_header.split(' ')[1]
1037
+ uid = verify_token(token)
1038
+
1039
+ # Store the analysis in Firebase Realtime Database
1040
+ if FIREBASE_INITIALIZED:
1041
+ user_analyses_ref = db.reference(f'user_analyses/{uid}')
1042
+ analysis_id = str(uuid.uuid4()) # Generate a unique ID for this analysis
1043
+ analysis_data = {
1044
+ 'prompt': prompt,
1045
+ 'explanation': explanation,
1046
+ 'created_at': datetime.utcnow().isoformat()
1047
+ }
1048
+ user_analyses_ref.child(analysis_id).set(analysis_data)
1049
+ logging.info(f"Analysis stored for user {uid} with ID: {analysis_id}")
1050
+ else:
1051
+ logging.warning("Firebase not initialized. Analysis will not be saved.")
1052
+
1053
+ return jsonify({'explanation': explanation, 'analysis_id': analysis_id})
1054
  except Exception as e:
1055
  logging.error(f"Error in /api/nba/perplexity_explain: {e}")
1056
  return jsonify({'error': str(e)}), 500
1057
 
1058
+ @app.route('/api/user/analyses', methods=['GET'])
1059
+ @credit_required(cost=0) # No credit cost, but requires authentication
1060
+ @cross_origin()
1061
+ def get_user_analyses():
1062
+ """Retrieves all stored AI analyses for the authenticated user."""
1063
+ try:
1064
+ auth_header = request.headers.get('Authorization', '')
1065
+ token = auth_header.split(' ')[1]
1066
+ uid = verify_token(token)
1067
+
1068
+ if not FIREBASE_INITIALIZED:
1069
+ return jsonify({'error': 'Firebase not initialized. Cannot retrieve analyses.'}), 500
1070
+
1071
+ user_analyses_ref = db.reference(f'user_analyses/{uid}')
1072
+ analyses_data = user_analyses_ref.get() or {}
1073
+
1074
+ analyses_list = []
1075
+ for analysis_id, data in analyses_data.items():
1076
+ analyses_list.append({
1077
+ 'analysis_id': analysis_id,
1078
+ 'prompt': data.get('prompt'),
1079
+ 'explanation': data.get('explanation'),
1080
+ 'created_at': data.get('created_at')
1081
+ })
1082
+
1083
+ # Sort by creation date, newest first
1084
+ analyses_list.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1085
+
1086
+ return jsonify({'analyses': analyses_list})
1087
+ except Exception as e:
1088
+ logging.error(f"Error in /api/user/analyses: {e}")
1089
+ return jsonify({'error': str(e)}), 500
1090
+
1091
+ @app.route('/api/user/analyses/<string:analysis_id>', methods=['DELETE'])
1092
+ @credit_required(cost=0) # No credit cost, but requires authentication
1093
+ @cross_origin()
1094
+ def delete_user_analysis(analysis_id):
1095
+ """Deletes a specific AI analysis for the authenticated user."""
1096
+ try:
1097
+ auth_header = request.headers.get('Authorization', '')
1098
+ token = auth_header.split(' ')[1]
1099
+ uid = verify_token(token)
1100
+
1101
+ if not FIREBASE_INITIALIZED:
1102
+ return jsonify({'error': 'Firebase not initialized. Cannot delete analysis.'}), 500
1103
+
1104
+ analysis_ref = db.reference(f'user_analyses/{uid}/{analysis_id}')
1105
+ analysis_data = analysis_ref.get()
1106
+
1107
+ if not analysis_data:
1108
+ return jsonify({'error': 'Analysis not found or does not belong to this user'}), 404
1109
+
1110
+ analysis_ref.delete()
1111
+ logging.info(f"Analysis {analysis_id} deleted for user {uid}.")
1112
+ return jsonify({'success': True, 'message': 'Analysis deleted successfully'})
1113
+ except Exception as e:
1114
+ logging.error(f"Error in /api/user/analyses/<id> DELETE: {e}")
1115
+ return jsonify({'error': str(e)}), 500
1116
+
1117
+
1118
  @app.route('/api/nba/perplexity_chat', methods=['POST'])
1119
  @credit_required(cost=1) # Costs 1 credit
1120
  @cross_origin()
1121
  def perplexity_chat():
1122
+ """Handles AI chat interactions and stores chat history."""
1123
  try:
1124
  data = request.get_json()
1125
  prompt = data.get('prompt')
 
1165
  @credit_required(cost=1) # Costs 1 credit
1166
  @cross_origin()
1167
  def awards_predictor():
1168
+ """Predicts NBA award candidates based on user criteria."""
1169
  try:
1170
  data = request.get_json()
1171
  award_type = data.get('award_type')
 
1188
  @credit_required(cost=1) # Costs 1 credit
1189
  @cross_origin()
1190
  def young_player_projection():
1191
+ """Projects the future potential of a young NBA player."""
1192
  try:
1193
  data = request.get_json()
1194
  player_name = data.get('player_name')
 
1222
  @credit_required(cost=1) # Costs 1 credit
1223
  @cross_origin()
1224
  def similar_players():
1225
+ """Finds similar players based on specified criteria."""
1226
  try:
1227
  data = request.get_json()
1228
  target_player = data.get('target_player')
 
1245
  @credit_required(cost=1) # Costs 1 credit
1246
  @cross_origin()
1247
  def manual_player_compare():
1248
+ """Compares two NBA players in detail."""
1249
  try:
1250
  data = request.get_json()
1251
  player1 = data.get('player1')
 
1272
  @credit_required(cost=1) # Costs 1 credit
1273
  @cross_origin()
1274
  def roster_suggestions():
1275
+ """Generates NBA roster suggestions based on user-defined constraints."""
1276
  try:
1277
  data = request.get_json()
1278
  salary_cap = data.get('salary_cap')
 
1313
  @credit_required(cost=1) # Costs 1 credit
1314
  @cross_origin()
1315
  def trade_analysis():
1316
+ """Analyzes a potential NBA trade scenario."""
1317
  try:
1318
  data = request.get_json()
1319
  team1_trades = data.get('team1_trades')