New: - backlog, cognitive-memory, optimise-claude skills - commands/ and scripts/ directories - usage-data tracking Updated: - Paper Dynasty: consolidated workflows, updated API client and CLI - .gitignore, CLAUDE.md, settings.json Removed: - Deprecated Paper Dynasty workflows (card-refresh, database-sync, discord-app-troubleshooting, gauntlet-cleanup, custom-player-db) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
439 lines
13 KiB
Python
439 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Card Refresh Utility Functions
|
|
|
|
Reusable functions for regenerating and uploading player cards to S3.
|
|
|
|
HOW CACHING WORKS:
|
|
The API caches generated card images by the `d=YYYY-M-D` query parameter.
|
|
Once a card is rendered for a given date, subsequent requests return the cached
|
|
image — even if the underlying database data changed. To force regeneration,
|
|
pass a future date (typically tomorrow) as cache_bust_date.
|
|
|
|
TROUBLESHOOTING STALE CARDS:
|
|
1. Verify the database has correct values (battingcards / pitchingcards tables)
|
|
2. Ensure cache_bust_date is a future date (not today or earlier)
|
|
3. Confirm S3 upload succeeded (file exists in s3://paper-dynasty/cards/...)
|
|
4. Confirm player.image URL was updated with the new date
|
|
5. Clear browser cache / use incognito — CloudFront may also cache
|
|
|
|
S3 UPLOAD ISSUES:
|
|
- Check AWS creds: `aws sts get-caller-identity`
|
|
- Bucket: paper-dynasty (us-east-1)
|
|
- Required IAM permissions: s3:PutObject, s3:GetObject
|
|
"""
|
|
import os
|
|
import sys
|
|
import asyncio
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional, Tuple
|
|
import aiohttp
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
|
|
# Add parent directory for imports
|
|
sys.path.insert(0, '/home/cal/.claude/skills/paper-dynasty')
|
|
from api_client import PaperDynastyAPI
|
|
|
|
|
|
# AWS Configuration
|
|
AWS_BUCKET = "paper-dynasty"
|
|
AWS_REGION = "us-east-1"
|
|
|
|
|
|
def get_cache_bust_date(days_ahead: int = 1) -> str:
|
|
"""
|
|
Get a cache-busting date for card regeneration
|
|
|
|
Args:
|
|
days_ahead: Number of days in the future (default: 1 = tomorrow)
|
|
|
|
Returns:
|
|
Date string in format "YYYY-M-D"
|
|
"""
|
|
future_date = datetime.now() + timedelta(days=days_ahead)
|
|
return f"{future_date.year}-{future_date.month}-{future_date.day}"
|
|
|
|
|
|
async def fetch_card_image(
|
|
session: aiohttp.ClientSession,
|
|
player_id: int,
|
|
card_type: str,
|
|
cache_bust_date: str,
|
|
output_dir: Path
|
|
) -> Tuple[int, str, Optional[str]]:
|
|
"""
|
|
Fetch a single card image from the API
|
|
|
|
Args:
|
|
session: aiohttp session
|
|
player_id: Player ID
|
|
card_type: 'batting' or 'pitching'
|
|
cache_bust_date: Date string for cache-busting
|
|
output_dir: Directory to save the image
|
|
|
|
Returns:
|
|
Tuple of (player_id, status, file_path or error_message)
|
|
"""
|
|
card_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/{card_type}card?d={cache_bust_date}"
|
|
|
|
try:
|
|
async with session.get(card_url, timeout=aiohttp.ClientTimeout(total=30)) as response:
|
|
if response.status != 200:
|
|
return (player_id, 'error', f"HTTP {response.status}")
|
|
|
|
file_path = output_dir / f"player-{player_id}-{card_type}card.png"
|
|
|
|
with open(file_path, 'wb') as f:
|
|
f.write(await response.read())
|
|
|
|
return (player_id, 'success', str(file_path))
|
|
|
|
except asyncio.TimeoutError:
|
|
return (player_id, 'error', 'Request timeout')
|
|
except Exception as e:
|
|
return (player_id, 'error', str(e))
|
|
|
|
|
|
async def fetch_cards_batch(
|
|
player_ids: List[int],
|
|
card_type: str,
|
|
cache_bust_date: str,
|
|
output_dir: Path
|
|
) -> Dict[int, str]:
|
|
"""
|
|
Fetch multiple cards in parallel
|
|
|
|
Args:
|
|
player_ids: List of player IDs
|
|
card_type: 'batting' or 'pitching'
|
|
cache_bust_date: Date string for cache-busting
|
|
output_dir: Directory to save images
|
|
|
|
Returns:
|
|
Dict mapping player_id to local file path
|
|
"""
|
|
results = {}
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
tasks = [
|
|
fetch_card_image(session, pid, card_type, cache_bust_date, output_dir)
|
|
for pid in player_ids
|
|
]
|
|
|
|
completed = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
for player_id, status, result in completed:
|
|
if isinstance(result, Exception):
|
|
print(f" ❌ Player {player_id}: {result}")
|
|
elif status == 'success':
|
|
results[player_id] = result
|
|
print(f" ✓ Player {player_id}: {Path(result).name}")
|
|
else:
|
|
print(f" ❌ Player {player_id}: {result}")
|
|
|
|
return results
|
|
|
|
|
|
def upload_to_s3(
|
|
file_path: str,
|
|
player_id: int,
|
|
cardset_id: int,
|
|
cache_bust_date: str,
|
|
card_type: str = 'batting'
|
|
) -> str:
|
|
"""
|
|
Upload a card image to S3
|
|
|
|
Args:
|
|
file_path: Local file path
|
|
player_id: Player ID
|
|
cardset_id: Cardset ID
|
|
cache_bust_date: Date string for cache-busting
|
|
card_type: 'batting' or 'pitching'
|
|
|
|
Returns:
|
|
S3 URL with cache-busting query parameter
|
|
"""
|
|
s3_client = boto3.client('s3', region_name=AWS_REGION)
|
|
s3_key = f"cards/cardset-{cardset_id:03d}/player-{player_id}/{card_type}card.png"
|
|
|
|
try:
|
|
with open(file_path, 'rb') as f:
|
|
s3_client.put_object(
|
|
Bucket=AWS_BUCKET,
|
|
Key=s3_key,
|
|
Body=f,
|
|
ContentType='image/png',
|
|
CacheControl='public, max-age=31536000'
|
|
)
|
|
|
|
s3_url = f"https://{AWS_BUCKET}.s3.{AWS_REGION}.amazonaws.com/{s3_key}?d={cache_bust_date}"
|
|
return s3_url
|
|
|
|
except ClientError as e:
|
|
raise Exception(f"S3 upload failed: {e}")
|
|
|
|
|
|
def regenerate_cards_for_players(
|
|
player_ids: List[int],
|
|
cardset_id: int,
|
|
cache_bust_date: Optional[str] = None,
|
|
upload_to_s3_flag: bool = True,
|
|
update_player_records: bool = True,
|
|
environment: str = 'prod',
|
|
card_type: str = 'batting',
|
|
batch_size: int = 50
|
|
) -> Dict:
|
|
"""
|
|
Complete pipeline: fetch cards, upload to S3, update player records
|
|
|
|
Args:
|
|
player_ids: List of player IDs to refresh
|
|
cardset_id: Cardset ID
|
|
cache_bust_date: Date for cache-busting (default: tomorrow)
|
|
upload_to_s3_flag: Whether to upload to S3
|
|
update_player_records: Whether to update player.image URLs
|
|
environment: 'prod' or 'dev'
|
|
card_type: 'batting' or 'pitching'
|
|
batch_size: Number of players per batch
|
|
|
|
Returns:
|
|
Dict with 'success', 'failures', 'total'
|
|
"""
|
|
if cache_bust_date is None:
|
|
cache_bust_date = get_cache_bust_date()
|
|
|
|
# Create output directory
|
|
output_dir = Path("/tmp/card_refresh") / f"{cardset_id}_{cache_bust_date.replace('-', '')}"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
api = PaperDynastyAPI(environment=environment, verbose=False)
|
|
|
|
results = {
|
|
'success': 0,
|
|
'failures': [],
|
|
'total': len(player_ids),
|
|
's3_urls': {}
|
|
}
|
|
|
|
print(f"Regenerating {len(player_ids)} cards for cardset {cardset_id}")
|
|
print(f"Cache-bust date: {cache_bust_date}")
|
|
print(f"Output dir: {output_dir}")
|
|
print()
|
|
|
|
# Process in batches
|
|
for i in range(0, len(player_ids), batch_size):
|
|
batch = player_ids[i:i + batch_size]
|
|
batch_num = (i // batch_size) + 1
|
|
total_batches = (len(player_ids) + batch_size - 1) // batch_size
|
|
|
|
print(f"Batch {batch_num}/{total_batches} ({len(batch)} players)")
|
|
|
|
# Fetch cards
|
|
local_files = asyncio.run(fetch_cards_batch(
|
|
batch, card_type, cache_bust_date, output_dir
|
|
))
|
|
|
|
# Upload to S3 and update records
|
|
for player_id, file_path in local_files.items():
|
|
try:
|
|
if upload_to_s3_flag:
|
|
s3_url = upload_to_s3(
|
|
file_path, player_id, cardset_id, cache_bust_date, card_type
|
|
)
|
|
results['s3_urls'][player_id] = s3_url
|
|
|
|
if update_player_records:
|
|
api.patch('players', object_id=player_id, params=[('image', s3_url)])
|
|
|
|
results['success'] += 1
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Failed to process player {player_id}: {e}")
|
|
results['failures'].append({'player_id': player_id, 'error': str(e)})
|
|
|
|
print()
|
|
|
|
return results
|
|
|
|
|
|
def verify_switch_hitters(cardset_id: int, environment: str = 'prod') -> Dict:
|
|
"""
|
|
Verify all switch hitters in a cardset have correct handedness
|
|
|
|
Args:
|
|
cardset_id: Cardset ID to check
|
|
environment: 'prod' or 'dev'
|
|
|
|
Returns:
|
|
Dict with 'correct', 'incorrect', and 'details'
|
|
"""
|
|
api = PaperDynastyAPI(environment=environment, verbose=False)
|
|
|
|
# Get all batting cards for the cardset
|
|
players = api.get('players', params=[('cardset_id', cardset_id)])['players']
|
|
|
|
results = {
|
|
'correct': [],
|
|
'incorrect': [],
|
|
'total_checked': 0
|
|
}
|
|
|
|
print(f"Checking switch hitters in cardset {cardset_id}...")
|
|
|
|
for player in players:
|
|
player_id = player['player_id']
|
|
|
|
# Check if player has a batting card
|
|
try:
|
|
# Get battingcard record via API v2
|
|
bc_response = api.get('battingcards', params=[
|
|
('player_id', player_id),
|
|
('variant', 0)
|
|
])
|
|
|
|
if bc_response['count'] == 0:
|
|
continue # No batting card
|
|
|
|
battingcard = bc_response['battingcards'][0]
|
|
hand = battingcard.get('hand')
|
|
|
|
if hand == 'S':
|
|
results['total_checked'] += 1
|
|
|
|
# Verify image URL has recent date (not cached with old data)
|
|
image_url = player.get('image', '')
|
|
|
|
# Check if image is from S3 and relatively recent
|
|
if 's3.amazonaws.com' in image_url:
|
|
results['correct'].append({
|
|
'player_id': player_id,
|
|
'name': player['p_name'],
|
|
'hand': hand,
|
|
'image_url': image_url
|
|
})
|
|
else:
|
|
results['incorrect'].append({
|
|
'player_id': player_id,
|
|
'name': player['p_name'],
|
|
'hand': hand,
|
|
'issue': 'No S3 URL',
|
|
'image_url': image_url
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f" Error checking player {player_id}: {e}")
|
|
continue
|
|
|
|
print(f"\nResults:")
|
|
print(f" Total switch hitters: {results['total_checked']}")
|
|
print(f" Correct: {len(results['correct'])}")
|
|
print(f" Needs refresh: {len(results['incorrect'])}")
|
|
|
|
if results['incorrect']:
|
|
print(f"\nSwitch hitters needing refresh:")
|
|
for player in results['incorrect']:
|
|
print(f" - {player['name']} (ID: {player['player_id']}): {player['issue']}")
|
|
|
|
return results
|
|
|
|
|
|
def regenerate_cards_for_cardset(
|
|
cardset_id: int,
|
|
environment: str = 'prod',
|
|
cache_bust_date: Optional[str] = None,
|
|
player_filter: Optional[Dict] = None,
|
|
batch_size: int = 50
|
|
) -> Dict:
|
|
"""
|
|
Regenerate all cards for an entire cardset
|
|
|
|
Args:
|
|
cardset_id: Cardset ID
|
|
environment: 'prod' or 'dev'
|
|
cache_bust_date: Date for cache-busting (default: tomorrow)
|
|
player_filter: Optional dict of filters (e.g., {'pos_include': 'SP'})
|
|
batch_size: Players per batch
|
|
|
|
Returns:
|
|
Dict with results
|
|
"""
|
|
api = PaperDynastyAPI(environment=environment, verbose=False)
|
|
|
|
# Get players
|
|
params = [('cardset_id', cardset_id)]
|
|
if player_filter:
|
|
for key, value in player_filter.items():
|
|
params.append((key, value))
|
|
|
|
players = api.get('players', params=params)['players']
|
|
|
|
# Separate batters and pitchers
|
|
batters = []
|
|
pitchers = []
|
|
|
|
for player in players:
|
|
positions = [player.get(f'pos_{i}') for i in range(1, 9)]
|
|
positions = [p for p in positions if p]
|
|
|
|
if any(pos in ['SP', 'RP', 'CP'] for pos in positions):
|
|
pitchers.append(player['player_id'])
|
|
else:
|
|
batters.append(player['player_id'])
|
|
|
|
print(f"Cardset {cardset_id}: {len(batters)} batters, {len(pitchers)} pitchers")
|
|
|
|
results = {'batters': {}, 'pitchers': {}}
|
|
|
|
if batters:
|
|
print("\n=== REGENERATING BATTING CARDS ===")
|
|
results['batters'] = regenerate_cards_for_players(
|
|
batters, cardset_id, cache_bust_date,
|
|
upload_to_s3_flag=True,
|
|
update_player_records=True,
|
|
environment=environment,
|
|
card_type='batting',
|
|
batch_size=batch_size
|
|
)
|
|
|
|
if pitchers:
|
|
print("\n=== REGENERATING PITCHING CARDS ===")
|
|
results['pitchers'] = regenerate_cards_for_players(
|
|
pitchers, cardset_id, cache_bust_date,
|
|
upload_to_s3_flag=True,
|
|
update_player_records=True,
|
|
environment=environment,
|
|
card_type='pitching',
|
|
batch_size=batch_size
|
|
)
|
|
|
|
return results
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Example usage
|
|
print("Card Utilities - Example Usage\n")
|
|
|
|
# Get tomorrow's date
|
|
tomorrow = get_cache_bust_date()
|
|
print(f"Tomorrow's date: {tomorrow}\n")
|
|
|
|
# Example: Refresh specific players
|
|
# player_ids = [12785, 12788, 12854]
|
|
# results = regenerate_cards_for_players(
|
|
# player_ids=player_ids,
|
|
# cardset_id=27,
|
|
# cache_bust_date=tomorrow,
|
|
# upload_to_s3_flag=True,
|
|
# update_player_records=True
|
|
# )
|
|
# print(f"Success: {results['success']}/{results['total']}")
|
|
|
|
# Example: Verify switch hitters
|
|
# verify_switch_hitters(cardset_id=27, environment='prod')
|
|
|
|
# Example: Refresh entire cardset
|
|
# regenerate_cards_for_cardset(cardset_id=27, batch_size=50)
|