angry-meow's picture
test
d3c5ba3
import requests
from dotenv import load_dotenv
import os
import pandas as pd # Import pandas
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"
# Define CSV constants
CSV_ACHIEVEMENT_FILE_PATH = 'util/raid_achievements.csv'
CSV_ID_COLUMN_NAME = 'ID'
CSV_NAME_COLUMN_NAME = 'Name'
# Global store for pre-loaded CSV data and any loading error
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 = [] # Ensure it's an empty list on column error
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 = [] # Ensure it's an empty list on file not found
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 = [] # Ensure it's an empty list on other errors
# Load CSV data when the module is imported
_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() # Raises an HTTPError for bad responses (4XX or 5XX)
token_data = response.json()
print("Access token fetched successfully!")
# print(f"Token: {token_data.get('access_token')[:20]}...") # Print part of the token for brevity
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.'''
# Sanitize realm and guild names for URL (simple lowercase and space to hyphen)
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() # Raise an exception for bad status codes
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 # Access globals
# Check if CSV loading failed during startup
if CSV_LOAD_ERROR_MESSAGE:
# If CSV failed to load, we can't provide the comprehensive list.
# Return an error reflecting this. The `data: []` part is for consistency.
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: # Should be initialized by _load_raid_achievements_from_csv
# This case implies the CSV was empty or another issue post-initial load attempt.
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)
# Even if API fails for char, we should still return the CSV list marked as incomplete
# Or, indicate this specific character API fetch failed. For now, let's try to return CSV marked as incomplete.
# This behavior might need adjustment based on desired user experience.
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 # Mark as false due to API error for this char
})
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)
# Similar handling for request errors
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)
# Similar handling for unexpected errors
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: # Iterate over pre-loaded 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 # Successful return
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 = {}
# Example: achievement_details = {ach_id: {'name': 'Ach Name', 'completed_count': 0, 'completed_by': [], 'not_completed_by': []}}
valid_character_names_for_count = []
for char_name, char_ach_list in ach_data_by_character.items():
if not isinstance(char_ach_list, list): # Skip if a character's data isn't a 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) # Count this character as processed for denominator
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: # Skip malformed entries
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 []
# Ensure all characters are in not_completed_by if they weren't in completed_by for an achievement
# This handles cases where an achievement might not have been in a specific character's list for some reason,
# though ideally each char_ach_list is comprehensive based on the CSV.
# For robustness, we iterate through all known valid characters for each achievement.
for ach_id in achievement_details:
current_completed_by = set(achievement_details[ach_id]['completed_by'])
# Rebuild not_completed_by based on all valid characters minus those who completed it
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']), # Sort for consistent output
'not_completed_by': sorted(details['not_completed_by']) # Sort for consistent output
})
result_summary.sort(key=lambda x: x['id']) # Sort by achievement 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 # Access globals
# Check if CSV loading failed during startup
if CSV_LOAD_ERROR_MESSAGE:
# If CSV failed to load, we can't provide the comprehensive list.
# Return an error reflecting this. The `data: []` part is for consistency.
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: # Should be initialized by _load_raid_achievements_from_csv
# This case implies the CSV was empty or another issue post-initial load attempt.
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)
# Even if API fails for char, we should still return the CSV list marked as incomplete
# Or, indicate this specific character API fetch failed. For now, let's try to return CSV marked as incomplete.
# This behavior might need adjustment based on desired user experience.
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 # Mark as false due to API error for this char
})
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)
# Similar handling for request errors
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)
# Similar handling for unexpected errors
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: # Iterate over pre-loaded 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 # Successful return