|
|
import requests |
|
|
from dotenv import load_dotenv |
|
|
import os |
|
|
import pandas as pd |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
BLIZZ_CLIENT_ID = os.getenv("BLIZZ_CLIENT_ID") |
|
|
BLIZZ_CLIENT_SECRET = os.getenv("BLIZZ_CLIENT_SECRET") |
|
|
|
|
|
TOKEN_URL = "https://oauth.battle.net/token" |
|
|
NAMESPACE = "profile-classic-us" |
|
|
LOCALE = "en_US" |
|
|
|
|
|
|
|
|
CSV_ACHIEVEMENT_FILE_PATH = 'util/raid_achievements.csv' |
|
|
CSV_ID_COLUMN_NAME = 'ID' |
|
|
CSV_NAME_COLUMN_NAME = 'Name' |
|
|
|
|
|
|
|
|
RAID_ACHIEVEMENTS_DATA = None |
|
|
CSV_LOAD_ERROR_MESSAGE = None |
|
|
|
|
|
def _load_raid_achievements_from_csv(): |
|
|
"""Loads achievement data from the CSV file into a global variable once.""" |
|
|
global RAID_ACHIEVEMENTS_DATA, CSV_LOAD_ERROR_MESSAGE |
|
|
try: |
|
|
df = pd.read_csv(CSV_ACHIEVEMENT_FILE_PATH) |
|
|
if CSV_ID_COLUMN_NAME not in df.columns or CSV_NAME_COLUMN_NAME not in df.columns: |
|
|
CSV_LOAD_ERROR_MESSAGE = f"Error: Required columns ('{CSV_ID_COLUMN_NAME}', '{CSV_NAME_COLUMN_NAME}') not found in {CSV_ACHIEVEMENT_FILE_PATH}." |
|
|
print(CSV_LOAD_ERROR_MESSAGE) |
|
|
RAID_ACHIEVEMENTS_DATA = [] |
|
|
return |
|
|
|
|
|
temp_list = [] |
|
|
for index, row in df.iterrows(): |
|
|
temp_list.append({ |
|
|
'id': row[CSV_ID_COLUMN_NAME], |
|
|
'name': row[CSV_NAME_COLUMN_NAME] |
|
|
}) |
|
|
RAID_ACHIEVEMENTS_DATA = temp_list |
|
|
print(f"Successfully loaded {len(RAID_ACHIEVEMENTS_DATA)} achievements from {CSV_ACHIEVEMENT_FILE_PATH} at startup.") |
|
|
except FileNotFoundError: |
|
|
CSV_LOAD_ERROR_MESSAGE = f"Error: CSV file {CSV_ACHIEVEMENT_FILE_PATH} not found at startup." |
|
|
print(CSV_LOAD_ERROR_MESSAGE) |
|
|
RAID_ACHIEVEMENTS_DATA = [] |
|
|
except Exception as e: |
|
|
CSV_LOAD_ERROR_MESSAGE = f"An unexpected error occurred while loading {CSV_ACHIEVEMENT_FILE_PATH} at startup: {e}" |
|
|
print(CSV_LOAD_ERROR_MESSAGE) |
|
|
RAID_ACHIEVEMENTS_DATA = [] |
|
|
|
|
|
|
|
|
_load_raid_achievements_from_csv() |
|
|
|
|
|
def get_token(): |
|
|
'''Fetches an access token from the Battle.net API.''' |
|
|
if not BLIZZ_CLIENT_ID or not BLIZZ_CLIENT_SECRET: |
|
|
print("Error: CLIENT_ID or CLIENT_SECRET not found in .env file.") |
|
|
print("Please create a .env file in the project root with your credentials:") |
|
|
print("CLIENT_ID=\"YOUR_CLIENT_ID\"") |
|
|
print("CLIENT_SECRET=\"YOUR_CLIENT_SECRET\"") |
|
|
return None |
|
|
|
|
|
data = { |
|
|
"grant_type": "client_credentials" |
|
|
} |
|
|
auth = (BLIZZ_CLIENT_ID, BLIZZ_CLIENT_SECRET) |
|
|
|
|
|
try: |
|
|
response = requests.post(TOKEN_URL, data=data, auth=auth, timeout=10) |
|
|
response.raise_for_status() |
|
|
token_data = response.json() |
|
|
print("Access token fetched successfully!") |
|
|
|
|
|
return token_data.get("access_token") |
|
|
except requests.exceptions.HTTPError as http_err: |
|
|
print(f"HTTP error occurred while fetching token: {http_err}") |
|
|
print(f"Response content: {response.content}") |
|
|
except requests.exceptions.RequestException as req_err: |
|
|
print(f"Request error occurred while fetching token: {req_err}") |
|
|
except Exception as e: |
|
|
print(f"An unexpected error occurred: {e}") |
|
|
return None |
|
|
|
|
|
def get_raiders(access_token, realm, guild): |
|
|
'''Fetches a guild's roster from the Battle.net API and returns level 85 characters.''' |
|
|
|
|
|
realm_slug = realm.lower().replace(" ", "-") |
|
|
guild_slug = guild.lower().replace(" ", "-") |
|
|
|
|
|
url = f"https://us.api.blizzard.com/data/wow/guild/{realm_slug}/{guild_slug}/roster?namespace={NAMESPACE}&locale={LOCALE}" |
|
|
headers = { |
|
|
"Authorization": f"Bearer {access_token}" |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.get(url, headers=headers, timeout=15) |
|
|
response.raise_for_status() |
|
|
|
|
|
roster_data = response.json() |
|
|
character_names = [] |
|
|
|
|
|
if roster_data and 'members' in roster_data and isinstance(roster_data['members'], list): |
|
|
for member_entry in roster_data['members']: |
|
|
if isinstance(member_entry, dict) and 'character' in member_entry: |
|
|
character = member_entry['character'] |
|
|
if isinstance(character, dict) and \ |
|
|
'name' in character and \ |
|
|
'level' in character and \ |
|
|
character['level'] == 85: |
|
|
character_names.append(character['name']) |
|
|
|
|
|
if not character_names: |
|
|
return "No level 85 characters found in the guild." |
|
|
return character_names |
|
|
else: |
|
|
return "Could not find 'members' list in roster data, or the roster is empty. The guild might not exist or is not accessible." |
|
|
|
|
|
except requests.exceptions.HTTPError as http_err: |
|
|
if http_err.response.status_code == 404: |
|
|
return f"Error: Guild '{guild}' on realm '{realm}' not found (404)." |
|
|
return f"HTTP error occurred while fetching roster: {http_err} - {http_err.response.text}" |
|
|
except requests.exceptions.RequestException as req_err: |
|
|
return f"Request error occurred while fetching roster: {req_err}" |
|
|
except Exception as e: |
|
|
return f"An unexpected error occurred: {e}" |
|
|
|
|
|
def get_char_achievements(access_token, realm, character): |
|
|
''' |
|
|
Fetches a character's achievements from the Battle.net API, |
|
|
compares them against a predefined, pre-loaded CSV list of achievements, |
|
|
and returns a comprehensive list indicating completion status for CSV achievements. |
|
|
''' |
|
|
global RAID_ACHIEVEMENTS_DATA, CSV_LOAD_ERROR_MESSAGE |
|
|
|
|
|
|
|
|
if CSV_LOAD_ERROR_MESSAGE: |
|
|
|
|
|
|
|
|
return {"error": True, "message": f"Prerequisite CSV data failed to load: {CSV_LOAD_ERROR_MESSAGE}", "data": []} |
|
|
|
|
|
if RAID_ACHIEVEMENTS_DATA is None or not RAID_ACHIEVEMENTS_DATA: |
|
|
|
|
|
return {"error": True, "message": "Raid achievements list from CSV is unavailable or empty.", "data": []} |
|
|
|
|
|
realm_slug = realm.lower().replace(" ", "-") |
|
|
character_name_slug = character.lower() |
|
|
api_url = f"https://us.api.blizzard.com/profile/wow/character/{realm_slug}/{character_name_slug}/achievements?namespace={NAMESPACE}&locale={LOCALE}" |
|
|
headers = {"Authorization": f"Bearer {access_token}"} |
|
|
|
|
|
char_raw_achievements = None |
|
|
try: |
|
|
response = requests.get(api_url, headers=headers, timeout=15) |
|
|
response.raise_for_status() |
|
|
char_raw_achievements = response.json() |
|
|
except requests.exceptions.HTTPError as http_err: |
|
|
msg = f"HTTP error fetching achievements for {character} on {realm}: {http_err}" |
|
|
if http_err.response is not None: msg += f" - {http_err.response.text}" |
|
|
print(msg) |
|
|
|
|
|
|
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "status_code": http_err.response.status_code if http_err.response else 500, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
except requests.exceptions.RequestException as req_err: |
|
|
msg = f"Request error fetching achievements for {character} on {realm}: {req_err}" |
|
|
print(msg) |
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
except Exception as e: |
|
|
msg = f"Unexpected error fetching raw achievements for {character} on {realm}: {e}" |
|
|
print(msg) |
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
|
|
|
completed_char_achievement_ids = set() |
|
|
if char_raw_achievements and 'achievements' in char_raw_achievements and isinstance(char_raw_achievements['achievements'], list): |
|
|
for ach_details in char_raw_achievements['achievements']: |
|
|
if isinstance(ach_details, dict) and ach_details.get('id') is not None: |
|
|
completed_char_achievement_ids.add(ach_details.get('id')) |
|
|
else: |
|
|
print(f"Warning: Could not parse 'achievements' list from API for {character}. Assuming no achievements completed.") |
|
|
|
|
|
comprehensive_status_list = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
is_completed = ach_from_csv['id'] in completed_char_achievement_ids |
|
|
comprehensive_status_list.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': is_completed |
|
|
}) |
|
|
|
|
|
return comprehensive_status_list |
|
|
|
|
|
def calculate_achievement_completion_percentages(ach_data_by_character): |
|
|
""" |
|
|
Analyzes a dictionary of character achievement statuses to determine completion percentages |
|
|
and lists of characters who have/haven't completed each achievement. |
|
|
|
|
|
Args: |
|
|
ach_data_by_character: A dictionary where keys are character names and values are |
|
|
lists of achievement statuses (output from get_char_achievements or |
|
|
the 'achievements_list' from an error object). |
|
|
Each status dict: {'id': ach_id, 'name': ach_name, 'completed': True/False}. |
|
|
|
|
|
Returns: |
|
|
A list of dictionaries, where each dictionary contains: |
|
|
{'id': achievement_id, 'name': achievement_name, 'percentage_completed': float, |
|
|
'completed_by': [char_name1, ...], 'not_completed_by': [char_name2, ...]} |
|
|
""" |
|
|
if not ach_data_by_character or not isinstance(ach_data_by_character, dict): |
|
|
print("Warning: Received invalid or empty input for percentage calculation.") |
|
|
return [] |
|
|
|
|
|
achievement_details = {} |
|
|
|
|
|
|
|
|
valid_character_names_for_count = [] |
|
|
|
|
|
for char_name, char_ach_list in ach_data_by_character.items(): |
|
|
if not isinstance(char_ach_list, list): |
|
|
print(f"Warning: Skipping non-list achievement data for character '{char_name}'. Type: {type(char_ach_list)}") |
|
|
continue |
|
|
|
|
|
valid_character_names_for_count.append(char_name) |
|
|
|
|
|
for ach_status in char_ach_list: |
|
|
ach_id = ach_status.get('id') |
|
|
ach_name = ach_status.get('name') |
|
|
|
|
|
if ach_id is None or ach_name is None: |
|
|
continue |
|
|
|
|
|
if ach_id not in achievement_details: |
|
|
achievement_details[ach_id] = { |
|
|
'name': ach_name, |
|
|
'completed_count': 0, |
|
|
'completed_by': [], |
|
|
'not_completed_by': [] |
|
|
} |
|
|
|
|
|
if ach_status.get('completed', False): |
|
|
achievement_details[ach_id]['completed_count'] += 1 |
|
|
achievement_details[ach_id]['completed_by'].append(char_name) |
|
|
else: |
|
|
achievement_details[ach_id]['not_completed_by'].append(char_name) |
|
|
|
|
|
num_valid_characters = len(valid_character_names_for_count) |
|
|
if num_valid_characters == 0: |
|
|
print("Warning: No valid character achievement lists to process for percentages after filtering.") |
|
|
return [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for ach_id in achievement_details: |
|
|
current_completed_by = set(achievement_details[ach_id]['completed_by']) |
|
|
|
|
|
achievement_details[ach_id]['not_completed_by'] = [ |
|
|
name for name in valid_character_names_for_count if name not in current_completed_by |
|
|
] |
|
|
|
|
|
result_summary = [] |
|
|
for ach_id, details in achievement_details.items(): |
|
|
percentage = (details['completed_count'] / num_valid_characters) * 100.0 if num_valid_characters > 0 else 0.0 |
|
|
result_summary.append({ |
|
|
'id': ach_id, |
|
|
'name': details['name'], |
|
|
'percentage_completed': round(percentage, 2), |
|
|
'completed_by': sorted(details['completed_by']), |
|
|
'not_completed_by': sorted(details['not_completed_by']) |
|
|
}) |
|
|
|
|
|
result_summary.sort(key=lambda x: x['id']) |
|
|
|
|
|
return result_summary |
|
|
|
|
|
def get_char_achievement_stats(access_token, realm, character): |
|
|
''' |
|
|
Fetches a character's achievements from the Battle.net API, |
|
|
compares them against a predefined, pre-loaded CSV list of achievements, |
|
|
and returns a comprehensive list indicating completion status for CSV achievements. |
|
|
''' |
|
|
global RAID_ACHIEVEMENTS_DATA, CSV_LOAD_ERROR_MESSAGE |
|
|
|
|
|
|
|
|
if CSV_LOAD_ERROR_MESSAGE: |
|
|
|
|
|
|
|
|
return {"error": True, "message": f"Prerequisite CSV data failed to load: {CSV_LOAD_ERROR_MESSAGE}", "data": []} |
|
|
|
|
|
if RAID_ACHIEVEMENTS_DATA is None or not RAID_ACHIEVEMENTS_DATA: |
|
|
|
|
|
return {"error": True, "message": "Raid achievements list from CSV is unavailable or empty.", "data": []} |
|
|
|
|
|
realm_slug = realm.lower().replace(" ", "-") |
|
|
character_name_slug = character.lower() |
|
|
api_url = f"https://us.api.blizzard.com/profile/wow/character/{realm_slug}/{character_name_slug}/achievements?namespace={NAMESPACE}&locale={LOCALE}" |
|
|
headers = {"Authorization": f"Bearer {access_token}"} |
|
|
|
|
|
char_raw_achievements = None |
|
|
try: |
|
|
response = requests.get(api_url, headers=headers, timeout=15) |
|
|
response.raise_for_status() |
|
|
char_raw_achievements = response.json() |
|
|
except requests.exceptions.HTTPError as http_err: |
|
|
msg = f"HTTP error fetching achievements for {character} on {realm}: {http_err}" |
|
|
if http_err.response is not None: msg += f" - {http_err.response.text}" |
|
|
print(msg) |
|
|
|
|
|
|
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "status_code": http_err.response.status_code if http_err.response else 500, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
except requests.exceptions.RequestException as req_err: |
|
|
msg = f"Request error fetching achievements for {character} on {realm}: {req_err}" |
|
|
print(msg) |
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
except Exception as e: |
|
|
msg = f"Unexpected error fetching raw achievements for {character} on {realm}: {e}" |
|
|
print(msg) |
|
|
|
|
|
comprehensive_status_list_on_api_error = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
comprehensive_status_list_on_api_error.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': False |
|
|
}) |
|
|
return {"error": True, "message": msg, "data_source": "csv_marked_incomplete", "achievements_list": comprehensive_status_list_on_api_error} |
|
|
|
|
|
completed_char_achievement_ids = set() |
|
|
if char_raw_achievements and 'achievements' in char_raw_achievements and isinstance(char_raw_achievements['achievements'], list): |
|
|
for ach_details in char_raw_achievements['achievements']: |
|
|
if isinstance(ach_details, dict) and ach_details.get('id') is not None: |
|
|
completed_char_achievement_ids.add(ach_details.get('id')) |
|
|
else: |
|
|
print(f"Warning: Could not parse 'achievements' list from API for {character}. Assuming no achievements completed.") |
|
|
|
|
|
comprehensive_status_list = [] |
|
|
for ach_from_csv in RAID_ACHIEVEMENTS_DATA: |
|
|
is_completed = ach_from_csv['id'] in completed_char_achievement_ids |
|
|
comprehensive_status_list.append({ |
|
|
'id': ach_from_csv['id'], |
|
|
'name': ach_from_csv['name'], |
|
|
'completed': is_completed |
|
|
}) |
|
|
|
|
|
return comprehensive_status_list |