CRITICAL BUG FIX: Removed code that was appending asterisks to left-handed players' names and hash symbols to switch hitters' names in production. ## Changes ### Core Fix (retrosheet_data.py) - Removed name_suffix code from new_player_payload() (lines 1103-1108) - Players names now stored cleanly without visual indicators - Affected 20 left-handed batters in 2005 Live cardset ### New Utility Scripts - fix_player_names.py: PATCH player names to remove symbols (uses 'name' param) - check_player_names.py: Verify all players for asterisks/hashes - regenerate_lefty_cards.py: Update image URLs with cache-busting dates - upload_lefty_cards_to_s3.py: Fetch fresh cards and upload to S3 ### Documentation (CRITICAL - READ BEFORE WORKING WITH CARDS) - docs/LESSONS_LEARNED_ASTERISK_REGRESSION.md: Comprehensive guide * API parameter is 'name' NOT 'p_name' * Card generation caching requires timestamp cache-busting * S3 keys must not include query parameters * Player names only in 'players' table * Never append visual indicators to stored data - CLAUDE.md: Added critical warnings section at top ## Key Learnings 1. API param for player name is 'name', not 'p_name' 2. Cards are cached - use timestamp in ?d= parameter 3. S3 keys != S3 URLs (no query params in keys) 4. Fix data BEFORE generating/uploading cards 5. Visual indicators belong in UI, not database ## Impact - Fixed 20 player records in production - Regenerated and uploaded 20 clean cards to S3 - Documented to prevent future regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
"""
|
|
Fetch updated card images for the 20 fixed left-handed players,
|
|
upload to AWS S3, and update player image URLs
|
|
"""
|
|
import asyncio
|
|
import datetime
|
|
import boto3
|
|
import aiohttp
|
|
from io import BytesIO
|
|
|
|
from db_calls import db_get, db_patch, url_get, DB_URL
|
|
from exceptions import logger
|
|
|
|
# AWS Configuration
|
|
AWS_BUCKET_NAME = 'paper-dynasty'
|
|
AWS_REGION = 'us-east-1'
|
|
S3_BASE_URL = f'https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com'
|
|
CARDSET_ID = 27
|
|
|
|
# Initialize S3 client
|
|
s3_client = boto3.client('s3', region_name=AWS_REGION)
|
|
|
|
# List of player IDs that were fixed
|
|
FIXED_PLAYER_IDS = [
|
|
13015, 13017, 13020, 13030, 13032, 13034, 13037, 13045, 13047, 13048,
|
|
13053, 13058, 13062, 13068, 13070, 13071, 13077, 13082, 13084, 13090
|
|
]
|
|
|
|
async def fetch_card_image(session, card_url: str, timeout: int = 6) -> bytes:
|
|
"""Fetch card image from URL and return raw bytes."""
|
|
try:
|
|
async with session.get(card_url, timeout=timeout) as r:
|
|
if r.status == 200:
|
|
return await r.read()
|
|
else:
|
|
error_text = await r.text()
|
|
raise ValueError(f'Status {r.status}: {error_text}')
|
|
except Exception as e:
|
|
raise ValueError(f'Failed to fetch card: {str(e)}')
|
|
|
|
def upload_to_s3(image_bytes: bytes, s3_key: str, content_type: str = 'image/png') -> str:
|
|
"""Upload image bytes to S3 and return the URL."""
|
|
try:
|
|
s3_client.put_object(
|
|
Bucket=AWS_BUCKET_NAME,
|
|
Key=s3_key,
|
|
Body=image_bytes,
|
|
ContentType=content_type,
|
|
CacheControl='public, max-age=31536000' # 1 year cache
|
|
)
|
|
s3_url = f'{S3_BASE_URL}/{s3_key}'
|
|
return s3_url
|
|
except Exception as e:
|
|
raise ValueError(f'Failed to upload to S3: {str(e)}')
|
|
|
|
async def process_player(session, player_id: int, release_date: str):
|
|
"""Fetch card, upload to S3, and update player URL."""
|
|
try:
|
|
print(f"\nProcessing player {player_id}...")
|
|
|
|
# Fetch current player data
|
|
player_data = await db_get('players', object_id=player_id)
|
|
player_name = player_data.get('p_name', 'Unknown')
|
|
print(f" Name: {player_name}")
|
|
|
|
# Build card URL (API endpoint)
|
|
card_api_url = f'{DB_URL}/v2/players/{player_id}/battingcard?d={release_date}'
|
|
|
|
# Fetch the card image
|
|
print(f" Fetching card image...")
|
|
image_bytes = await fetch_card_image(session, card_api_url)
|
|
print(f" ✅ Fetched {len(image_bytes)} bytes")
|
|
|
|
# Build S3 key (without query parameters!)
|
|
s3_key = f'cards/cardset-{CARDSET_ID:03d}/player-{player_id}/battingcard.png'
|
|
|
|
# Upload to S3
|
|
print(f" Uploading to S3...")
|
|
s3_url_base = upload_to_s3(image_bytes, s3_key)
|
|
# Add cache-busting query parameter to the URL
|
|
s3_url = f'{s3_url_base}?d={release_date}'
|
|
print(f" ✅ Uploaded to S3")
|
|
|
|
# Update player record with S3 URL
|
|
print(f" Updating player image URL...")
|
|
await db_patch('players', object_id=player_id, params=[('image', s3_url)])
|
|
print(f" ✅ Updated player record")
|
|
|
|
return {'success': True, 'player_id': player_id, 'name': player_name, 's3_url': s3_url}
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Error: {str(e)}")
|
|
return {'success': False, 'player_id': player_id, 'error': str(e)}
|
|
|
|
async def main():
|
|
# Use timestamp to bust cache completely
|
|
import time
|
|
timestamp = int(time.time())
|
|
release_date = f'2025-11-25-{timestamp}'
|
|
|
|
print(f"{'='*60}")
|
|
print(f"Uploading cards to S3 for 20 left-handed players")
|
|
print(f"Release date: {release_date}")
|
|
print(f"{'='*60}")
|
|
|
|
successes = []
|
|
errors = []
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
for player_id in FIXED_PLAYER_IDS:
|
|
result = await process_player(session, player_id, release_date)
|
|
|
|
if result['success']:
|
|
successes.append(result)
|
|
else:
|
|
errors.append(result)
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"SUMMARY")
|
|
print(f"{'='*60}")
|
|
print(f"Successes: {len(successes)}")
|
|
print(f"Errors: {len(errors)}")
|
|
print(f"Total: {len(FIXED_PLAYER_IDS)}")
|
|
|
|
if errors:
|
|
print(f"\nErrors:")
|
|
for err in errors:
|
|
print(f" Player {err['player_id']}: {err.get('error', 'Unknown error')}")
|
|
|
|
if successes:
|
|
print(f"\nFirst S3 URL: {successes[0]['s3_url']}")
|
|
|
|
print(f"{'='*60}")
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|