Update main.py
Browse files
main.py
CHANGED
|
@@ -30,29 +30,33 @@ CORS(app)
|
|
| 30 |
|
| 31 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
FIREBASE_INITIALIZED = False
|
|
|
|
| 37 |
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
credentials_json = json.loads(credentials_json_string)
|
| 42 |
cred = credentials.Certificate(credentials_json)
|
| 43 |
firebase_admin.initialize_app(cred, {
|
| 44 |
-
'databaseURL':
|
| 45 |
-
'storageBucket':
|
| 46 |
})
|
| 47 |
FIREBASE_INITIALIZED = True
|
|
|
|
| 48 |
logging.info("Firebase Admin SDK initialized successfully.")
|
| 49 |
else:
|
| 50 |
-
logging.
|
| 51 |
except Exception as e:
|
| 52 |
logging.error(f"Error initializing Firebase: {e}")
|
| 53 |
traceback.print_exc()
|
|
|
|
| 54 |
|
| 55 |
-
bucket = storage.bucket() if FIREBASE_INITIALIZED else None
|
| 56 |
|
| 57 |
def verify_token(token):
|
| 58 |
try:
|
|
@@ -63,21 +67,35 @@ def verify_token(token):
|
|
| 63 |
return None
|
| 64 |
|
| 65 |
def verify_admin(auth_header):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 67 |
raise ValueError('Invalid token format')
|
| 68 |
token = auth_header.split(' ')[1]
|
| 69 |
uid = verify_token(token)
|
| 70 |
if not uid:
|
| 71 |
raise PermissionError('Invalid user token')
|
|
|
|
| 72 |
user_ref = db.reference(f'users/{uid}')
|
| 73 |
user_data = user_ref.get()
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
raise PermissionError('Admin access required')
|
| 76 |
return uid
|
| 77 |
|
| 78 |
def credit_required(cost=1):
|
| 79 |
def decorator(f):
|
| 80 |
def wrapper(*args, **kwargs):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
auth_header = request.headers.get('Authorization', '')
|
| 82 |
if not auth_header.startswith('Bearer '):
|
| 83 |
return jsonify({'error': 'Authorization header missing or malformed'}), 401
|
|
@@ -88,8 +106,10 @@ def credit_required(cost=1):
|
|
| 88 |
|
| 89 |
user_ref = db.reference(f'users/{uid}')
|
| 90 |
user_data = user_ref.get()
|
|
|
|
| 91 |
if not user_data:
|
| 92 |
-
|
|
|
|
| 93 |
|
| 94 |
if user_data.get('suspended', False):
|
| 95 |
return jsonify({'error': 'Account suspended. Please contact support.'}), 403
|
|
@@ -113,6 +133,10 @@ def credit_required(cost=1):
|
|
| 113 |
|
| 114 |
@app.route('/api/auth/signup', methods=['POST'])
|
| 115 |
def signup():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
try:
|
| 117 |
data = request.get_json()
|
| 118 |
email = data.get('email')
|
|
@@ -120,15 +144,21 @@ def signup():
|
|
| 120 |
if not email or not password:
|
| 121 |
return jsonify({'error': 'Email and password are required'}), 400
|
| 122 |
|
|
|
|
| 123 |
user = auth.create_user(email=email, password=password)
|
|
|
|
|
|
|
| 124 |
user_ref = db.reference(f'users/{user.uid}')
|
| 125 |
user_data = {
|
| 126 |
'email': email,
|
| 127 |
'credits': 10,
|
| 128 |
'is_admin': False,
|
| 129 |
-
'created_at': datetime.utcnow().isoformat()
|
|
|
|
| 130 |
}
|
| 131 |
user_ref.set(user_data)
|
|
|
|
|
|
|
| 132 |
return jsonify({
|
| 133 |
'success': True,
|
| 134 |
'user': {
|
|
@@ -138,10 +168,17 @@ def signup():
|
|
| 138 |
}), 201
|
| 139 |
except Exception as e:
|
| 140 |
logging.error(f"Signup error: {e}")
|
|
|
|
|
|
|
|
|
|
| 141 |
return jsonify({'error': str(e)}), 400
|
| 142 |
|
| 143 |
@app.route('/api/user/profile', methods=['GET'])
|
| 144 |
def get_user_profile():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
try:
|
| 146 |
auth_header = request.headers.get('Authorization', '')
|
| 147 |
if not auth_header.startswith('Bearer '):
|
|
@@ -154,13 +191,15 @@ def get_user_profile():
|
|
| 154 |
|
| 155 |
user_data = db.reference(f'users/{uid}').get()
|
| 156 |
if not user_data:
|
| 157 |
-
|
|
|
|
| 158 |
|
| 159 |
return jsonify({
|
| 160 |
'uid': uid,
|
| 161 |
'email': user_data.get('email'),
|
| 162 |
'credits': user_data.get('credits', 0),
|
| 163 |
-
'is_admin': user_data.get('is_admin', False)
|
|
|
|
| 164 |
})
|
| 165 |
except Exception as e:
|
| 166 |
logging.error(f"Error fetching user profile: {e}")
|
|
@@ -168,6 +207,10 @@ def get_user_profile():
|
|
| 168 |
|
| 169 |
@app.route('/api/auth/google-signin', methods=['POST'])
|
| 170 |
def google_signin():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
try:
|
| 172 |
auth_header = request.headers.get('Authorization', '')
|
| 173 |
if not auth_header.startswith('Bearer '):
|
|
@@ -182,13 +225,18 @@ def google_signin():
|
|
| 182 |
user_data = user_ref.get()
|
| 183 |
|
| 184 |
if not user_data:
|
|
|
|
| 185 |
user_data = {
|
| 186 |
'email': email,
|
| 187 |
'credits': 10,
|
| 188 |
'is_admin': False,
|
| 189 |
'created_at': datetime.utcnow().isoformat(),
|
|
|
|
| 190 |
}
|
| 191 |
user_ref.set(user_data)
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
return jsonify({
|
| 194 |
'success': True,
|
|
@@ -205,6 +253,10 @@ def google_signin():
|
|
| 205 |
@app.route('/api/user/request-credits', methods=['POST'])
|
| 206 |
@credit_required(cost=0)
|
| 207 |
def request_credits():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
try:
|
| 209 |
auth_header = request.headers.get('Authorization', '')
|
| 210 |
token = auth_header.split(' ')[1]
|
|
@@ -231,6 +283,10 @@ def request_credits():
|
|
| 231 |
@credit_required(cost=0)
|
| 232 |
@cross_origin()
|
| 233 |
def submit_feedback():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
try:
|
| 235 |
auth_header = request.headers.get('Authorization', '')
|
| 236 |
token = auth_header.split(' ')[1]
|
|
@@ -262,6 +318,10 @@ def submit_feedback():
|
|
| 262 |
|
| 263 |
@app.route('/api/admin/profile', methods=['GET'])
|
| 264 |
def get_admin_profile():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 267 |
admin_data = db.reference(f'users/{admin_uid}').get()
|
|
@@ -300,6 +360,10 @@ def get_admin_profile():
|
|
| 300 |
|
| 301 |
@app.route('/api/admin/credit_requests', methods=['GET'])
|
| 302 |
def list_credit_requests():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
try:
|
| 304 |
verify_admin(request.headers.get('Authorization', ''))
|
| 305 |
requests_ref = db.reference('credit_requests')
|
|
@@ -312,6 +376,10 @@ def list_credit_requests():
|
|
| 312 |
|
| 313 |
@app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
|
| 314 |
def process_credit_request(request_id):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
try:
|
| 316 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 317 |
req_ref = db.reference(f'credit_requests/{request_id}')
|
|
@@ -350,6 +418,10 @@ def process_credit_request(request_id):
|
|
| 350 |
|
| 351 |
@app.route('/api/admin/users', methods=['GET'])
|
| 352 |
def admin_list_users():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
try:
|
| 354 |
verify_admin(request.headers.get('Authorization', ''))
|
| 355 |
users_ref = db.reference('users')
|
|
@@ -372,6 +444,10 @@ def admin_list_users():
|
|
| 372 |
|
| 373 |
@app.route('/api/admin/users/search', methods=['GET'])
|
| 374 |
def admin_search_users():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
try:
|
| 376 |
verify_admin(request.headers.get('Authorization', ''))
|
| 377 |
email_query = request.args.get('email', '').lower().strip()
|
|
@@ -400,6 +476,10 @@ def admin_search_users():
|
|
| 400 |
|
| 401 |
@app.route('/api/admin/users/<string:uid>/suspend', methods=['PUT'])
|
| 402 |
def admin_suspend_user(uid):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
try:
|
| 404 |
verify_admin(request.headers.get('Authorization', ''))
|
| 405 |
data = request.get_json()
|
|
@@ -424,6 +504,10 @@ def admin_suspend_user(uid):
|
|
| 424 |
|
| 425 |
@app.route('/api/admin/notifications', methods=['POST'])
|
| 426 |
def send_notifications():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
try:
|
| 428 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 429 |
data = request.get_json()
|
|
@@ -471,6 +555,10 @@ def send_notifications():
|
|
| 471 |
|
| 472 |
@app.route('/api/admin/feedback', methods=['GET'])
|
| 473 |
def admin_view_feedback():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
try:
|
| 475 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 476 |
feedback_type = request.args.get('type')
|
|
@@ -502,6 +590,10 @@ def admin_view_feedback():
|
|
| 502 |
|
| 503 |
@app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
|
| 504 |
def admin_update_credits(uid):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
try:
|
| 506 |
verify_admin(request.headers.get('Authorization', ''))
|
| 507 |
data = request.get_json()
|
|
@@ -658,8 +750,7 @@ def get_player_index_brscraper():
|
|
| 658 |
return df
|
| 659 |
|
| 660 |
def _scrape_player_index_brscraper():
|
| 661 |
-
|
| 662 |
-
seasons_to_try_for_index = get_available_seasons_util(num_seasons=2) # Try current and previous season
|
| 663 |
|
| 664 |
for season_str in seasons_to_try_for_index:
|
| 665 |
end_year = int(season_str.split('–')[1])
|
|
@@ -669,7 +760,6 @@ def _scrape_player_index_brscraper():
|
|
| 669 |
|
| 670 |
if not df.empty and 'Player' in df.columns:
|
| 671 |
player_names = df['Player'].dropna().unique().tolist()
|
| 672 |
-
# Normalize player names before returning
|
| 673 |
player_names = [normalize_string(name) for name in player_names]
|
| 674 |
logging.info(f"Successfully retrieved {len(player_names)} players for index from {season_str}.")
|
| 675 |
return pd.DataFrame({'name': player_names})
|
|
@@ -678,12 +768,11 @@ def _scrape_player_index_brscraper():
|
|
| 678 |
except Exception as e:
|
| 679 |
logging.warning(f"Error fetching player index with BRScraper for {season_str}: {e}. Trying next season.")
|
| 680 |
|
| 681 |
-
# Fallback to a curated list if recent seasons fail
|
| 682 |
logging.error("Failed to fetch player index from recent seasons. Falling back to curated common players list.")
|
| 683 |
common_players = [
|
| 684 |
'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo',
|
| 685 |
-
'Nikola Jokic',
|
| 686 |
-
'Joel Embiid', 'Jayson Tatum', 'Luka Doncic',
|
| 687 |
'Damian Lillard', 'Jimmy Butler', 'Kawhi Leonard', 'Paul George',
|
| 688 |
'Anthony Davis', 'Rudy Gobert', 'Donovan Mitchell', 'Trae Young',
|
| 689 |
'Devin Booker', 'Karl-Anthony Towns', 'Zion Williamson', 'Ja Morant',
|
|
@@ -692,22 +781,30 @@ def _scrape_player_index_brscraper():
|
|
| 692 |
]
|
| 693 |
return pd.DataFrame({'name': common_players})
|
| 694 |
|
| 695 |
-
def get_player_career_stats_brscraper(player_name,
|
| 696 |
if not BRSCRAPER_AVAILABLE:
|
| 697 |
logging.error("BRScraper is not available. Cannot fetch player career stats.")
|
| 698 |
return pd.DataFrame()
|
| 699 |
-
all_rows = []
|
| 700 |
|
| 701 |
-
# Normalize the input player name for consistent lookup
|
| 702 |
normalized_player_name = normalize_string(player_name)
|
|
|
|
| 703 |
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
for season_str in seasons_to_try:
|
| 707 |
end_year = int(season_str.split('–')[1])
|
| 708 |
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
try:
|
| 712 |
logging.info(f"DEBUG: Attempt {attempt+1} for nba.get_stats for player '{player_name}' in season {season_str} (year: {end_year}, playoffs: {playoffs})...")
|
| 713 |
|
|
@@ -715,41 +812,49 @@ def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs
|
|
| 715 |
|
| 716 |
if df_season.empty:
|
| 717 |
logging.warning(f"DEBUG: nba.get_stats returned empty DataFrame for {player_name} in {season_str} on attempt {attempt+1}. Retrying...")
|
| 718 |
-
time.sleep(1)
|
| 719 |
-
continue
|
| 720 |
|
| 721 |
if 'Player' not in df_season.columns:
|
| 722 |
logging.warning(f"DEBUG: DataFrame for {player_name} in {season_str} has no 'Player' column on attempt {attempt+1}. Columns: {df_season.columns.tolist()}. Retrying...")
|
| 723 |
time.sleep(1)
|
| 724 |
continue
|
| 725 |
|
| 726 |
-
# Normalize player names in the DataFrame for comparison
|
| 727 |
df_season['Player_Normalized'] = df_season['Player'].apply(normalize_string)
|
| 728 |
-
|
| 729 |
row = df_season[df_season['Player_Normalized'] == normalized_player_name]
|
| 730 |
|
| 731 |
if not row.empty:
|
| 732 |
row = row.copy()
|
| 733 |
row['Season'] = season_str
|
| 734 |
-
# Remove the temporary normalized column before appending
|
| 735 |
row = row.drop(columns=['Player_Normalized'], errors='ignore')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
all_rows.append(row)
|
| 737 |
logging.info(f"DEBUG: Found stats for {player_name} in {season_str} on attempt {attempt+1}. Appending row.")
|
| 738 |
-
break
|
| 739 |
else:
|
| 740 |
logging.info(f"DEBUG: Player {player_name} not found in {season_str} stats (after getting season data) on attempt {attempt+1}. Retrying...")
|
| 741 |
time.sleep(1)
|
| 742 |
-
continue
|
| 743 |
|
| 744 |
except Exception as e:
|
| 745 |
logging.warning(f"DEBUG: Exception on attempt {attempt+1} when fetching {season_str} {'playoff' if playoffs else 'regular season'} stats for {player_name}: {e}")
|
| 746 |
-
time.sleep(1)
|
| 747 |
-
if attempt == 2:
|
| 748 |
logging.error(f"DEBUG: All 3 attempts failed for {player_name} in {season_str}. Giving up on this season.")
|
| 749 |
-
continue
|
| 750 |
|
|
|
|
|
|
|
| 751 |
if not all_rows:
|
| 752 |
-
logging.warning(f"DEBUG: No stats found for {player_name}
|
| 753 |
return pd.DataFrame()
|
| 754 |
|
| 755 |
df = pd.concat(all_rows, ignore_index=True)
|
|
@@ -769,13 +874,13 @@ def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs
|
|
| 769 |
if col not in non_num:
|
| 770 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 771 |
|
| 772 |
-
df['Player'] = player_name
|
| 773 |
df = df.replace({np.nan: None})
|
| 774 |
return df
|
| 775 |
|
| 776 |
def get_dashboard_info_brscraper():
|
| 777 |
-
if not
|
| 778 |
-
logging.error("
|
| 779 |
return {}
|
| 780 |
|
| 781 |
if not FIREBASE_INITIALIZED:
|
|
@@ -947,22 +1052,47 @@ def get_player_stats():
|
|
| 947 |
all_player_season_data = []
|
| 948 |
players_with_no_data = []
|
| 949 |
|
| 950 |
-
|
| 951 |
-
|
|
|
|
|
|
|
|
|
|
| 952 |
|
| 953 |
-
if
|
| 954 |
-
|
|
|
|
|
|
|
| 955 |
players_with_no_data.append(player_name)
|
| 956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
|
|
|
|
|
|
| 963 |
else:
|
| 964 |
-
|
| 965 |
-
|
|
|
|
|
|
|
|
|
|
| 966 |
|
| 967 |
if not all_player_season_data:
|
| 968 |
logging.warning("After processing all players, 'all_player_season_data' is empty. Returning 404.")
|
|
@@ -973,11 +1103,7 @@ def get_player_stats():
|
|
| 973 |
|
| 974 |
comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
|
| 975 |
|
| 976 |
-
|
| 977 |
-
basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
|
| 978 |
-
else:
|
| 979 |
-
basic_display_df = comparison_df_raw.copy()
|
| 980 |
-
|
| 981 |
basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
|
| 982 |
basic_display_df = basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2)
|
| 983 |
|
|
@@ -989,13 +1115,8 @@ def get_player_stats():
|
|
| 989 |
lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0,
|
| 990 |
axis=1
|
| 991 |
)
|
| 992 |
-
if len(selected_seasons) > 1:
|
| 993 |
-
advanced_display_df = advanced_df.groupby('Player').mean(numeric_only=True).reset_index()
|
| 994 |
-
else:
|
| 995 |
-
advanced_display_df = advanced_df.copy()
|
| 996 |
-
|
| 997 |
advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
|
| 998 |
-
advanced_display_df =
|
| 999 |
|
| 1000 |
return jsonify({
|
| 1001 |
'basic_stats': basic_display_df.to_dict(orient='records'),
|
|
@@ -1020,21 +1141,44 @@ def get_player_playoff_stats():
|
|
| 1020 |
all_player_season_data = []
|
| 1021 |
players_with_no_data = []
|
| 1022 |
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
players_with_no_data.append(player_name)
|
| 1028 |
-
continue
|
| 1029 |
-
|
| 1030 |
-
filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
|
| 1031 |
|
| 1032 |
-
if not
|
| 1033 |
-
all_player_season_data.append(
|
| 1034 |
-
logging.info(f"Successfully
|
| 1035 |
else:
|
| 1036 |
-
logging.info(f"No playoff data found for {player_name} in the specific requested seasons: {selected_seasons}. Adding to no_data list.")
|
| 1037 |
players_with_no_data.append(player_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1038 |
|
| 1039 |
if not all_player_season_data:
|
| 1040 |
logging.warning("After processing all players, 'all_player_season_data' is empty for playoffs. Returning 404.")
|
|
@@ -1045,11 +1189,7 @@ def get_player_playoff_stats():
|
|
| 1045 |
|
| 1046 |
comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
|
| 1047 |
|
| 1048 |
-
|
| 1049 |
-
basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
|
| 1050 |
-
else:
|
| 1051 |
-
basic_display_df = comparison_df_raw.copy()
|
| 1052 |
-
|
| 1053 |
basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
|
| 1054 |
basic_display_df = basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2)
|
| 1055 |
|
|
@@ -1061,13 +1201,8 @@ def get_player_playoff_stats():
|
|
| 1061 |
lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0,
|
| 1062 |
axis=1
|
| 1063 |
)
|
| 1064 |
-
if len(selected_seasons) > 1:
|
| 1065 |
-
advanced_display_df = advanced_df.groupby('Player').mean(numeric_only=True).reset_index()
|
| 1066 |
-
else:
|
| 1067 |
-
advanced_display_df = advanced_df.copy()
|
| 1068 |
-
|
| 1069 |
advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
|
| 1070 |
-
advanced_display_df =
|
| 1071 |
|
| 1072 |
return jsonify({
|
| 1073 |
'basic_stats': basic_display_df.to_dict(orient='records'),
|
|
@@ -1147,6 +1282,10 @@ def get_team_stats():
|
|
| 1147 |
@credit_required(cost=0)
|
| 1148 |
@cross_origin()
|
| 1149 |
def dashboard_info():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1150 |
try:
|
| 1151 |
dashboard_data = get_dashboard_info_brscraper()
|
| 1152 |
if not dashboard_data:
|
|
@@ -1160,6 +1299,10 @@ def dashboard_info():
|
|
| 1160 |
@credit_required(cost=1)
|
| 1161 |
@cross_origin()
|
| 1162 |
def perplexity_explain():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
try:
|
| 1164 |
data = request.get_json()
|
| 1165 |
prompt = data.get('prompt')
|
|
@@ -1197,6 +1340,10 @@ def perplexity_explain():
|
|
| 1197 |
@credit_required(cost=0)
|
| 1198 |
@cross_origin()
|
| 1199 |
def get_user_analyses():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
try:
|
| 1201 |
auth_header = request.headers.get('Authorization', '')
|
| 1202 |
token = auth_header.split(' ')[1]
|
|
@@ -1228,6 +1375,10 @@ def get_user_analyses():
|
|
| 1228 |
@credit_required(cost=0)
|
| 1229 |
@cross_origin()
|
| 1230 |
def delete_user_analysis(analysis_id):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
try:
|
| 1232 |
auth_header = request.headers.get('Authorization', '')
|
| 1233 |
token = auth_header.split(' ')[1]
|
|
@@ -1254,6 +1405,10 @@ def delete_user_analysis(analysis_id):
|
|
| 1254 |
@credit_required(cost=1)
|
| 1255 |
@cross_origin()
|
| 1256 |
def perplexity_chat():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1257 |
try:
|
| 1258 |
data = request.get_json()
|
| 1259 |
prompt = data.get('prompt')
|
|
@@ -1294,6 +1449,10 @@ def perplexity_chat():
|
|
| 1294 |
@credit_required(cost=1)
|
| 1295 |
@cross_origin()
|
| 1296 |
def awards_predictor():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1297 |
try:
|
| 1298 |
data = request.get_json()
|
| 1299 |
award_type = data.get('award_type')
|
|
@@ -1316,6 +1475,10 @@ def awards_predictor():
|
|
| 1316 |
@credit_required(cost=1)
|
| 1317 |
@cross_origin()
|
| 1318 |
def young_player_projection():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1319 |
try:
|
| 1320 |
data = request.get_json()
|
| 1321 |
player_name = data.get('player_name')
|
|
@@ -1349,6 +1512,10 @@ def young_player_projection():
|
|
| 1349 |
@credit_required(cost=1)
|
| 1350 |
@cross_origin()
|
| 1351 |
def similar_players():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1352 |
try:
|
| 1353 |
data = request.get_json()
|
| 1354 |
target_player = data.get('target_player')
|
|
@@ -1393,6 +1560,10 @@ def similar_players():
|
|
| 1393 |
@credit_required(cost=1)
|
| 1394 |
@cross_origin()
|
| 1395 |
def manual_player_compare():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
try:
|
| 1397 |
data = request.get_json()
|
| 1398 |
player1_name = data.get('player1_name')
|
|
@@ -1435,4 +1606,4 @@ def manual_player_compare():
|
|
| 1435 |
|
| 1436 |
|
| 1437 |
if __name__ == '__main__':
|
| 1438 |
-
app.run(debug=True, host="0.0.0.0", port=7860)
|
|
|
|
| 30 |
|
| 31 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 32 |
|
| 33 |
+
# --- Firebase Initialization (CRITICAL FIXES HERE) ---
|
| 34 |
+
# Ensure these environment variables are correctly set in your Hugging Face Space
|
| 35 |
+
FIREBASE_CREDENTIALS_JSON_STRING = os.getenv("FIREBASE")
|
| 36 |
+
FIREBASE_DB_URL = os.getenv("Firebase_DB") # Using a more descriptive name
|
| 37 |
+
FIREBASE_STORAGE_BUCKET = os.getenv("Firebase_Storage") # Using a more descriptive name
|
| 38 |
|
| 39 |
FIREBASE_INITIALIZED = False
|
| 40 |
+
bucket = None # Initialize bucket to None
|
| 41 |
|
| 42 |
try:
|
| 43 |
+
if FIREBASE_CREDENTIALS_JSON_STRING and FIREBASE_DB_URL and FIREBASE_STORAGE_BUCKET:
|
| 44 |
+
credentials_json = json.loads(FIREBASE_CREDENTIALS_JSON_STRING)
|
|
|
|
| 45 |
cred = credentials.Certificate(credentials_json)
|
| 46 |
firebase_admin.initialize_app(cred, {
|
| 47 |
+
'databaseURL': FIREBASE_DB_URL,
|
| 48 |
+
'storageBucket': FIREBASE_STORAGE_BUCKET
|
| 49 |
})
|
| 50 |
FIREBASE_INITIALIZED = True
|
| 51 |
+
bucket = storage.bucket() # Initialize bucket only if Firebase is initialized
|
| 52 |
logging.info("Firebase Admin SDK initialized successfully.")
|
| 53 |
else:
|
| 54 |
+
logging.error("Firebase environment variables (FIREBASE, Firebase_DB, Firebase_Storage) not fully set. Firebase Admin SDK not initialized.")
|
| 55 |
except Exception as e:
|
| 56 |
logging.error(f"Error initializing Firebase: {e}")
|
| 57 |
traceback.print_exc()
|
| 58 |
+
# --- END Firebase Initialization Fixes ---
|
| 59 |
|
|
|
|
| 60 |
|
| 61 |
def verify_token(token):
|
| 62 |
try:
|
|
|
|
| 67 |
return None
|
| 68 |
|
| 69 |
def verify_admin(auth_header):
|
| 70 |
+
if not FIREBASE_INITIALIZED:
|
| 71 |
+
logging.error("Firebase not initialized. Admin verification skipped.")
|
| 72 |
+
raise PermissionError('Server configuration error: Firebase not ready.')
|
| 73 |
+
|
| 74 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 75 |
raise ValueError('Invalid token format')
|
| 76 |
token = auth_header.split(' ')[1]
|
| 77 |
uid = verify_token(token)
|
| 78 |
if not uid:
|
| 79 |
raise PermissionError('Invalid user token')
|
| 80 |
+
|
| 81 |
user_ref = db.reference(f'users/{uid}')
|
| 82 |
user_data = user_ref.get()
|
| 83 |
+
|
| 84 |
+
if not user_data:
|
| 85 |
+
logging.warning(f"User {uid} found in Auth but not in Realtime DB. Cannot verify admin status.")
|
| 86 |
+
raise PermissionError('User profile not found in database. Admin access denied.')
|
| 87 |
+
|
| 88 |
+
if not user_data.get('is_admin', False):
|
| 89 |
raise PermissionError('Admin access required')
|
| 90 |
return uid
|
| 91 |
|
| 92 |
def credit_required(cost=1):
|
| 93 |
def decorator(f):
|
| 94 |
def wrapper(*args, **kwargs):
|
| 95 |
+
if not FIREBASE_INITIALIZED:
|
| 96 |
+
logging.error("Firebase not initialized. Credit deduction skipped.")
|
| 97 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 98 |
+
|
| 99 |
auth_header = request.headers.get('Authorization', '')
|
| 100 |
if not auth_header.startswith('Bearer '):
|
| 101 |
return jsonify({'error': 'Authorization header missing or malformed'}), 401
|
|
|
|
| 106 |
|
| 107 |
user_ref = db.reference(f'users/{uid}')
|
| 108 |
user_data = user_ref.get()
|
| 109 |
+
|
| 110 |
if not user_data:
|
| 111 |
+
logging.warning(f"User {uid} found in Auth but not in Realtime DB. Cannot process credits.")
|
| 112 |
+
return jsonify({'error': 'User profile not found in database. Please try logging in again.'}), 404
|
| 113 |
|
| 114 |
if user_data.get('suspended', False):
|
| 115 |
return jsonify({'error': 'Account suspended. Please contact support.'}), 403
|
|
|
|
| 133 |
|
| 134 |
@app.route('/api/auth/signup', methods=['POST'])
|
| 135 |
def signup():
|
| 136 |
+
if not FIREBASE_INITIALIZED:
|
| 137 |
+
logging.error("Firebase not initialized. Signup skipped.")
|
| 138 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 139 |
+
|
| 140 |
try:
|
| 141 |
data = request.get_json()
|
| 142 |
email = data.get('email')
|
|
|
|
| 144 |
if not email or not password:
|
| 145 |
return jsonify({'error': 'Email and password are required'}), 400
|
| 146 |
|
| 147 |
+
# Create user in Firebase Authentication
|
| 148 |
user = auth.create_user(email=email, password=password)
|
| 149 |
+
|
| 150 |
+
# Create corresponding user entry in Firebase Realtime Database
|
| 151 |
user_ref = db.reference(f'users/{user.uid}')
|
| 152 |
user_data = {
|
| 153 |
'email': email,
|
| 154 |
'credits': 10,
|
| 155 |
'is_admin': False,
|
| 156 |
+
'created_at': datetime.utcnow().isoformat(),
|
| 157 |
+
'suspended': False # Ensure new accounts are not suspended by default
|
| 158 |
}
|
| 159 |
user_ref.set(user_data)
|
| 160 |
+
logging.info(f"New user {user.uid} signed up and DB entry created.")
|
| 161 |
+
|
| 162 |
return jsonify({
|
| 163 |
'success': True,
|
| 164 |
'user': {
|
|
|
|
| 168 |
}), 201
|
| 169 |
except Exception as e:
|
| 170 |
logging.error(f"Signup error: {e}")
|
| 171 |
+
# Firebase Auth errors can be specific, e.g., email-already-in-use
|
| 172 |
+
if 'email-already-in-use' in str(e):
|
| 173 |
+
return jsonify({'error': 'Email already in use.'}), 409 # Conflict
|
| 174 |
return jsonify({'error': str(e)}), 400
|
| 175 |
|
| 176 |
@app.route('/api/user/profile', methods=['GET'])
|
| 177 |
def get_user_profile():
|
| 178 |
+
if not FIREBASE_INITIALIZED:
|
| 179 |
+
logging.error("Firebase not initialized. Profile retrieval skipped.")
|
| 180 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 181 |
+
|
| 182 |
try:
|
| 183 |
auth_header = request.headers.get('Authorization', '')
|
| 184 |
if not auth_header.startswith('Bearer '):
|
|
|
|
| 191 |
|
| 192 |
user_data = db.reference(f'users/{uid}').get()
|
| 193 |
if not user_data:
|
| 194 |
+
logging.warning(f"User {uid} found in Auth but not in Realtime DB when fetching profile.")
|
| 195 |
+
return jsonify({'error': 'User profile not found in database. Please try logging in again.'}), 404
|
| 196 |
|
| 197 |
return jsonify({
|
| 198 |
'uid': uid,
|
| 199 |
'email': user_data.get('email'),
|
| 200 |
'credits': user_data.get('credits', 0),
|
| 201 |
+
'is_admin': user_data.get('is_admin', False),
|
| 202 |
+
'suspended': user_data.get('suspended', False) # Include suspended status
|
| 203 |
})
|
| 204 |
except Exception as e:
|
| 205 |
logging.error(f"Error fetching user profile: {e}")
|
|
|
|
| 207 |
|
| 208 |
@app.route('/api/auth/google-signin', methods=['POST'])
|
| 209 |
def google_signin():
|
| 210 |
+
if not FIREBASE_INITIALIZED:
|
| 211 |
+
logging.error("Firebase not initialized. Google Sign-In skipped.")
|
| 212 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 213 |
+
|
| 214 |
try:
|
| 215 |
auth_header = request.headers.get('Authorization', '')
|
| 216 |
if not auth_header.startswith('Bearer '):
|
|
|
|
| 225 |
user_data = user_ref.get()
|
| 226 |
|
| 227 |
if not user_data:
|
| 228 |
+
# If user doesn't exist in DB, create a new entry
|
| 229 |
user_data = {
|
| 230 |
'email': email,
|
| 231 |
'credits': 10,
|
| 232 |
'is_admin': False,
|
| 233 |
'created_at': datetime.utcnow().isoformat(),
|
| 234 |
+
'suspended': False
|
| 235 |
}
|
| 236 |
user_ref.set(user_data)
|
| 237 |
+
logging.info(f"New Google user {uid} signed up and DB entry created.")
|
| 238 |
+
else:
|
| 239 |
+
logging.info(f"Existing Google user {uid} logged in.")
|
| 240 |
|
| 241 |
return jsonify({
|
| 242 |
'success': True,
|
|
|
|
| 253 |
@app.route('/api/user/request-credits', methods=['POST'])
|
| 254 |
@credit_required(cost=0)
|
| 255 |
def request_credits():
|
| 256 |
+
if not FIREBASE_INITIALIZED:
|
| 257 |
+
logging.error("Firebase not initialized. Request credits skipped.")
|
| 258 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 259 |
+
|
| 260 |
try:
|
| 261 |
auth_header = request.headers.get('Authorization', '')
|
| 262 |
token = auth_header.split(' ')[1]
|
|
|
|
| 283 |
@credit_required(cost=0)
|
| 284 |
@cross_origin()
|
| 285 |
def submit_feedback():
|
| 286 |
+
if not FIREBASE_INITIALIZED:
|
| 287 |
+
logging.error("Firebase not initialized. Submit feedback skipped.")
|
| 288 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 289 |
+
|
| 290 |
try:
|
| 291 |
auth_header = request.headers.get('Authorization', '')
|
| 292 |
token = auth_header.split(' ')[1]
|
|
|
|
| 318 |
|
| 319 |
@app.route('/api/admin/profile', methods=['GET'])
|
| 320 |
def get_admin_profile():
|
| 321 |
+
if not FIREBASE_INITIALIZED:
|
| 322 |
+
logging.error("Firebase not initialized. Admin profile skipped.")
|
| 323 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 324 |
+
|
| 325 |
try:
|
| 326 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 327 |
admin_data = db.reference(f'users/{admin_uid}').get()
|
|
|
|
| 360 |
|
| 361 |
@app.route('/api/admin/credit_requests', methods=['GET'])
|
| 362 |
def list_credit_requests():
|
| 363 |
+
if not FIREBASE_INITIALIZED:
|
| 364 |
+
logging.error("Firebase not initialized. List credit requests skipped.")
|
| 365 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 366 |
+
|
| 367 |
try:
|
| 368 |
verify_admin(request.headers.get('Authorization', ''))
|
| 369 |
requests_ref = db.reference('credit_requests')
|
|
|
|
| 376 |
|
| 377 |
@app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
|
| 378 |
def process_credit_request(request_id):
|
| 379 |
+
if not FIREBASE_INITIALIZED:
|
| 380 |
+
logging.error("Firebase not initialized. Process credit request skipped.")
|
| 381 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 382 |
+
|
| 383 |
try:
|
| 384 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 385 |
req_ref = db.reference(f'credit_requests/{request_id}')
|
|
|
|
| 418 |
|
| 419 |
@app.route('/api/admin/users', methods=['GET'])
|
| 420 |
def admin_list_users():
|
| 421 |
+
if not FIREBASE_INITIALIZED:
|
| 422 |
+
logging.error("Firebase not initialized. Admin list users skipped.")
|
| 423 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 424 |
+
|
| 425 |
try:
|
| 426 |
verify_admin(request.headers.get('Authorization', ''))
|
| 427 |
users_ref = db.reference('users')
|
|
|
|
| 444 |
|
| 445 |
@app.route('/api/admin/users/search', methods=['GET'])
|
| 446 |
def admin_search_users():
|
| 447 |
+
if not FIREBASE_INITIALIZED:
|
| 448 |
+
logging.error("Firebase not initialized. Admin search users skipped.")
|
| 449 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 450 |
+
|
| 451 |
try:
|
| 452 |
verify_admin(request.headers.get('Authorization', ''))
|
| 453 |
email_query = request.args.get('email', '').lower().strip()
|
|
|
|
| 476 |
|
| 477 |
@app.route('/api/admin/users/<string:uid>/suspend', methods=['PUT'])
|
| 478 |
def admin_suspend_user(uid):
|
| 479 |
+
if not FIREBASE_INITIALIZED:
|
| 480 |
+
logging.error("Firebase not initialized. Admin suspend user skipped.")
|
| 481 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 482 |
+
|
| 483 |
try:
|
| 484 |
verify_admin(request.headers.get('Authorization', ''))
|
| 485 |
data = request.get_json()
|
|
|
|
| 504 |
|
| 505 |
@app.route('/api/admin/notifications', methods=['POST'])
|
| 506 |
def send_notifications():
|
| 507 |
+
if not FIREBASE_INITIALIZED:
|
| 508 |
+
logging.error("Firebase not initialized. Send notifications skipped.")
|
| 509 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 510 |
+
|
| 511 |
try:
|
| 512 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 513 |
data = request.get_json()
|
|
|
|
| 555 |
|
| 556 |
@app.route('/api/admin/feedback', methods=['GET'])
|
| 557 |
def admin_view_feedback():
|
| 558 |
+
if not FIREBASE_INITIALIZED:
|
| 559 |
+
logging.error("Firebase not initialized. Admin view feedback skipped.")
|
| 560 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 561 |
+
|
| 562 |
try:
|
| 563 |
admin_uid = verify_admin(request.headers.get('Authorization', ''))
|
| 564 |
feedback_type = request.args.get('type')
|
|
|
|
| 590 |
|
| 591 |
@app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
|
| 592 |
def admin_update_credits(uid):
|
| 593 |
+
if not FIREBASE_INITIALIZED:
|
| 594 |
+
logging.error("Firebase not initialized. Admin update credits skipped.")
|
| 595 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 596 |
+
|
| 597 |
try:
|
| 598 |
verify_admin(request.headers.get('Authorization', ''))
|
| 599 |
data = request.get_json()
|
|
|
|
| 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])
|
|
|
|
| 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})
|
|
|
|
| 768 |
except Exception as e:
|
| 769 |
logging.warning(f"Error fetching player index with BRScraper for {season_str}: {e}. Trying next season.")
|
| 770 |
|
|
|
|
| 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',
|
|
|
|
| 781 |
]
|
| 782 |
return pd.DataFrame({'name': common_players})
|
| 783 |
|
| 784 |
+
def get_player_career_stats_brscraper(player_name, seasons_to_fetch: list[str], playoffs=False):
|
| 785 |
if not BRSCRAPER_AVAILABLE:
|
| 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 |
|
|
|
|
| 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:
|
| 819 |
logging.warning(f"DEBUG: DataFrame for {player_name} in {season_str} has no 'Player' column on attempt {attempt+1}. Columns: {df_season.columns.tolist()}. Retrying...")
|
| 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())
|
| 833 |
+
db_ref.set({
|
| 834 |
+
'last_updated': datetime.utcnow().isoformat(),
|
| 835 |
+
'data': df_cleaned_for_firebase.to_dict(orient='records')
|
| 836 |
+
})
|
| 837 |
+
logging.info(f"Stats for {player_name} in {season_str} saved to Firebase cache.")
|
| 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.")
|
| 858 |
return pd.DataFrame()
|
| 859 |
|
| 860 |
df = pd.concat(all_rows, ignore_index=True)
|
|
|
|
| 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():
|
| 882 |
+
if not FIREBASE_INITIALIZED:
|
| 883 |
+
logging.error("Firebase not initialized. Dashboard info skipped.")
|
| 884 |
return {}
|
| 885 |
|
| 886 |
if not FIREBASE_INITIALIZED:
|
|
|
|
| 1052 |
all_player_season_data = []
|
| 1053 |
players_with_no_data = []
|
| 1054 |
|
| 1055 |
+
# Handle individual player stats (1 player, 1 season)
|
| 1056 |
+
if len(selected_players) == 1 and len(selected_seasons) == 1:
|
| 1057 |
+
player_name = selected_players[0]
|
| 1058 |
+
season_str = selected_seasons[0]
|
| 1059 |
+
df_player_data = get_player_career_stats_brscraper(player_name, seasons_to_fetch=[season_str], playoffs=False)
|
| 1060 |
|
| 1061 |
+
if not df_player_data.empty:
|
| 1062 |
+
all_player_season_data.append(df_player_data)
|
| 1063 |
+
logging.info(f"Successfully retrieved data for {player_name} in {season_str}.")
|
| 1064 |
+
else:
|
| 1065 |
players_with_no_data.append(player_name)
|
| 1066 |
+
logging.info(f"No data found for {player_name} in {season_str}.")
|
| 1067 |
+
|
| 1068 |
+
# Handle comparison (2 players, 2 seasons)
|
| 1069 |
+
elif len(selected_players) == 2 and len(selected_seasons) == 2:
|
| 1070 |
+
player1_name = selected_players[0]
|
| 1071 |
+
player1_season = selected_seasons[0]
|
| 1072 |
+
player2_name = selected_players[1]
|
| 1073 |
+
player2_season = selected_seasons[1]
|
| 1074 |
+
|
| 1075 |
+
df_player1_data = get_player_career_stats_brscraper(player1_name, seasons_to_fetch=[player1_season], playoffs=False)
|
| 1076 |
+
if not df_player1_data.empty:
|
| 1077 |
+
all_player_season_data.append(df_player1_data)
|
| 1078 |
+
logging.info(f"Successfully retrieved data for {player1_name} in {player1_season}.")
|
| 1079 |
+
else:
|
| 1080 |
+
players_with_no_data.append(player1_name)
|
| 1081 |
+
logging.info(f"No data found for {player1_name} in {player1_season}.")
|
| 1082 |
|
| 1083 |
+
# Add a delay between fetching data for player 1 and player 2
|
| 1084 |
+
time.sleep(2) # Introduce a 2-second delay
|
| 1085 |
+
|
| 1086 |
+
df_player2_data = get_player_career_stats_brscraper(player2_name, seasons_to_fetch=[player2_season], playoffs=False)
|
| 1087 |
+
if not df_player2_data.empty:
|
| 1088 |
+
all_player_season_data.append(df_player2_data)
|
| 1089 |
+
logging.info(f"Successfully retrieved data for {player2_name} in {player2_season}.")
|
| 1090 |
else:
|
| 1091 |
+
players_with_no_data.append(player2_name)
|
| 1092 |
+
logging.info(f"No data found for {player2_name} in {player2_season}.")
|
| 1093 |
+
else:
|
| 1094 |
+
return jsonify({'error': 'Invalid combination of players and seasons. Expected 1 player/1 season or 2 players/2 seasons.'}), 400
|
| 1095 |
+
|
| 1096 |
|
| 1097 |
if not all_player_season_data:
|
| 1098 |
logging.warning("After processing all players, 'all_player_season_data' is empty. Returning 404.")
|
|
|
|
| 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 |
|
|
|
|
| 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({
|
| 1122 |
'basic_stats': basic_display_df.to_dict(orient='records'),
|
|
|
|
| 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]
|
| 1147 |
+
df_player_data = get_player_career_stats_brscraper(player_name, seasons_to_fetch=[season_str], playoffs=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1148 |
|
| 1149 |
+
if not df_player_data.empty:
|
| 1150 |
+
all_player_season_data.append(df_player_data)
|
| 1151 |
+
logging.info(f"Successfully retrieved playoff data for {player_name} in {season_str}.")
|
| 1152 |
else:
|
|
|
|
| 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]
|
| 1159 |
+
player2_name = selected_players[1]
|
| 1160 |
+
player2_season = selected_seasons[1]
|
| 1161 |
+
|
| 1162 |
+
df_player1_data = get_player_career_stats_brscraper(player1_name, seasons_to_fetch=[player1_season], playoffs=True)
|
| 1163 |
+
if not df_player1_data.empty:
|
| 1164 |
+
all_player_season_data.append(df_player1_data)
|
| 1165 |
+
logging.info(f"Successfully retrieved playoff data for {player1_name} in {player1_season}.")
|
| 1166 |
+
else:
|
| 1167 |
+
players_with_no_data.append(player1_name)
|
| 1168 |
+
logging.info(f"No playoff data found for {player1_name} in {player1_season}.")
|
| 1169 |
+
|
| 1170 |
+
time.sleep(2) # Introduce a 2-second delay
|
| 1171 |
+
|
| 1172 |
+
df_player2_data = get_player_career_stats_brscraper(player2_name, seasons_to_fetch=[player2_season], playoffs=True)
|
| 1173 |
+
if not df_player2_data.empty:
|
| 1174 |
+
all_player_season_data.append(df_player2_data)
|
| 1175 |
+
logging.info(f"Successfully retrieved playoff data for {player2_name} in {player2_season}.")
|
| 1176 |
+
else:
|
| 1177 |
+
players_with_no_data.append(player2_name)
|
| 1178 |
+
logging.info(f"No playoff data found for {player2_name} in {player2_season}.")
|
| 1179 |
+
else:
|
| 1180 |
+
return jsonify({'error': 'Invalid combination of players and seasons. Expected 1 player/1 season or 2 players/2 seasons.'}), 400
|
| 1181 |
+
|
| 1182 |
|
| 1183 |
if not all_player_season_data:
|
| 1184 |
logging.warning("After processing all players, 'all_player_season_data' is empty for playoffs. Returning 404.")
|
|
|
|
| 1189 |
|
| 1190 |
comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
|
| 1191 |
|
| 1192 |
+
basic_display_df = comparison_df_raw.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
|
| 1194 |
basic_display_df = basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2)
|
| 1195 |
|
|
|
|
| 1201 |
lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0,
|
| 1202 |
axis=1
|
| 1203 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
|
| 1205 |
+
advanced_display_df = advanced_df[[c for c in advanced_cols if c in advanced_df.columns]].round(3)
|
| 1206 |
|
| 1207 |
return jsonify({
|
| 1208 |
'basic_stats': basic_display_df.to_dict(orient='records'),
|
|
|
|
| 1282 |
@credit_required(cost=0)
|
| 1283 |
@cross_origin()
|
| 1284 |
def dashboard_info():
|
| 1285 |
+
if not FIREBASE_INITIALIZED:
|
| 1286 |
+
logging.error("Firebase not initialized. Dashboard info skipped.")
|
| 1287 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1288 |
+
|
| 1289 |
try:
|
| 1290 |
dashboard_data = get_dashboard_info_brscraper()
|
| 1291 |
if not dashboard_data:
|
|
|
|
| 1299 |
@credit_required(cost=1)
|
| 1300 |
@cross_origin()
|
| 1301 |
def perplexity_explain():
|
| 1302 |
+
if not FIREBASE_INITIALIZED:
|
| 1303 |
+
logging.error("Firebase not initialized. Perplexity explain skipped.")
|
| 1304 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1305 |
+
|
| 1306 |
try:
|
| 1307 |
data = request.get_json()
|
| 1308 |
prompt = data.get('prompt')
|
|
|
|
| 1340 |
@credit_required(cost=0)
|
| 1341 |
@cross_origin()
|
| 1342 |
def get_user_analyses():
|
| 1343 |
+
if not FIREBASE_INITIALIZED:
|
| 1344 |
+
logging.error("Firebase not initialized. Get user analyses skipped.")
|
| 1345 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1346 |
+
|
| 1347 |
try:
|
| 1348 |
auth_header = request.headers.get('Authorization', '')
|
| 1349 |
token = auth_header.split(' ')[1]
|
|
|
|
| 1375 |
@credit_required(cost=0)
|
| 1376 |
@cross_origin()
|
| 1377 |
def delete_user_analysis(analysis_id):
|
| 1378 |
+
if not FIREBASE_INITIALIZED:
|
| 1379 |
+
logging.error("Firebase not initialized. Delete user analysis skipped.")
|
| 1380 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1381 |
+
|
| 1382 |
try:
|
| 1383 |
auth_header = request.headers.get('Authorization', '')
|
| 1384 |
token = auth_header.split(' ')[1]
|
|
|
|
| 1405 |
@credit_required(cost=1)
|
| 1406 |
@cross_origin()
|
| 1407 |
def perplexity_chat():
|
| 1408 |
+
if not FIREBASE_INITIALIZED:
|
| 1409 |
+
logging.error("Firebase not initialized. Perplexity chat skipped.")
|
| 1410 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1411 |
+
|
| 1412 |
try:
|
| 1413 |
data = request.get_json()
|
| 1414 |
prompt = data.get('prompt')
|
|
|
|
| 1449 |
@credit_required(cost=1)
|
| 1450 |
@cross_origin()
|
| 1451 |
def awards_predictor():
|
| 1452 |
+
if not FIREBASE_INITIALIZED:
|
| 1453 |
+
logging.error("Firebase not initialized. Awards predictor skipped.")
|
| 1454 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1455 |
+
|
| 1456 |
try:
|
| 1457 |
data = request.get_json()
|
| 1458 |
award_type = data.get('award_type')
|
|
|
|
| 1475 |
@credit_required(cost=1)
|
| 1476 |
@cross_origin()
|
| 1477 |
def young_player_projection():
|
| 1478 |
+
if not FIREBASE_INITIALIZED:
|
| 1479 |
+
logging.error("Firebase not initialized. Young player projection skipped.")
|
| 1480 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1481 |
+
|
| 1482 |
try:
|
| 1483 |
data = request.get_json()
|
| 1484 |
player_name = data.get('player_name')
|
|
|
|
| 1512 |
@credit_required(cost=1)
|
| 1513 |
@cross_origin()
|
| 1514 |
def similar_players():
|
| 1515 |
+
if not FIREBASE_INITIALIZED:
|
| 1516 |
+
logging.error("Firebase not initialized. Similar players skipped.")
|
| 1517 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1518 |
+
|
| 1519 |
try:
|
| 1520 |
data = request.get_json()
|
| 1521 |
target_player = data.get('target_player')
|
|
|
|
| 1560 |
@credit_required(cost=1)
|
| 1561 |
@cross_origin()
|
| 1562 |
def manual_player_compare():
|
| 1563 |
+
if not FIREBASE_INITIALIZED:
|
| 1564 |
+
logging.error("Firebase not initialized. Manual player compare skipped.")
|
| 1565 |
+
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 500
|
| 1566 |
+
|
| 1567 |
try:
|
| 1568 |
data = request.get_json()
|
| 1569 |
player1_name = data.get('player1_name')
|
|
|
|
| 1606 |
|
| 1607 |
|
| 1608 |
if __name__ == '__main__':
|
| 1609 |
+
app.run(debug=True, host="0.0.0.0", port=7860)
|