Update main.py
Browse files
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
|
| 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,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 |
-
|
| 112 |
-
|
|
|
|
| 113 |
return f(*args, **kwargs)
|
| 114 |
except Exception as e:
|
| 115 |
-
logging.error(f"Failed to
|
| 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, #
|
| 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, #
|
| 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
|
| 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': '
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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')
|