Add scripts to test models in dev database

This commit is contained in:
Cal Corum 2025-10-23 09:10:55 -05:00
parent 9284162682
commit 04a5538447
2 changed files with 606 additions and 0 deletions

308
backend/clean_test_data.py Normal file
View File

@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
Database Cleanup Script
Clear out test data from database tables.
Use with caution - this will DELETE data!
Usage:
python clean_test_data.py # Interactive mode
python clean_test_data.py --all # Delete all data
python clean_test_data.py --game <id> # Delete specific game
"""
import asyncio
import logging
import sys
from uuid import UUID
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import AsyncSessionLocal
from app.models.db_models import (
Game,
Play,
Lineup,
GameSession,
RosterLink,
GameCardsetLink
)
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DatabaseCleaner:
"""Clean test data from database."""
async def count_records(self, session: AsyncSession) -> dict:
"""Count records in each table."""
counts = {}
# Count games
result = await session.execute(select(Game))
counts['games'] = len(list(result.scalars().all()))
# Count plays
result = await session.execute(select(Play))
counts['plays'] = len(list(result.scalars().all()))
# Count lineups
result = await session.execute(select(Lineup))
counts['lineups'] = len(list(result.scalars().all()))
# Count roster links
result = await session.execute(select(RosterLink))
counts['roster_links'] = len(list(result.scalars().all()))
# Count game sessions
result = await session.execute(select(GameSession))
counts['game_sessions'] = len(list(result.scalars().all()))
# Count cardset links
result = await session.execute(select(GameCardsetLink))
counts['game_cardset_links'] = len(list(result.scalars().all()))
return counts
async def delete_game(self, game_id: UUID) -> None:
"""
Delete a specific game and all related data.
Cascade deletes will automatically remove:
- Plays
- Lineups
- RosterLinks
- GameSession
- GameCardsetLinks
"""
async with AsyncSessionLocal() as session:
try:
# Check if game exists
result = await session.execute(
select(Game).where(Game.id == game_id)
)
game = result.scalar_one_or_none()
if not game:
logger.warning(f"Game {game_id} not found")
return
logger.info(f"Deleting game {game_id} ({game.league_id})...")
# Delete game (cascade will handle related records)
await session.delete(game)
await session.commit()
logger.info(f"✅ Successfully deleted game {game_id}")
except Exception as e:
await session.rollback()
logger.error(f"❌ Error deleting game: {e}", exc_info=True)
raise
async def delete_all(self, confirm: bool = False) -> None:
"""
Delete ALL data from all tables.
WARNING: This is destructive and cannot be undone!
"""
if not confirm:
logger.warning("⚠️ delete_all() requires confirm=True parameter")
return
async with AsyncSessionLocal() as session:
try:
logger.info("🗑️ Deleting ALL data from database...")
# Get counts before deletion
before_counts = await self.count_records(session)
logger.info(f"Current record counts: {before_counts}")
# Delete in correct order (children first)
logger.info("Deleting plays...")
await session.execute(delete(Play))
logger.info("Deleting lineups...")
await session.execute(delete(Lineup))
logger.info("Deleting roster links...")
await session.execute(delete(RosterLink))
logger.info("Deleting game sessions...")
await session.execute(delete(GameSession))
logger.info("Deleting game cardset links...")
await session.execute(delete(GameCardsetLink))
logger.info("Deleting games...")
await session.execute(delete(Game))
await session.commit()
# Verify deletion
after_counts = await self.count_records(session)
logger.info(f"After deletion: {after_counts}")
logger.info("✅ Successfully deleted all data")
except Exception as e:
await session.rollback()
logger.error(f"❌ Error deleting data: {e}", exc_info=True)
raise
async def list_games(self) -> None:
"""List all games in the database."""
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Game).order_by(Game.created_at.desc())
)
games = result.scalars().all()
if not games:
logger.info("No games found in database")
return
logger.info(f"\n{'='*80}")
logger.info(f"Found {len(games)} game(s) in database:")
logger.info(f"{'='*80}")
for game in games:
logger.info(f"\nGame ID: {game.id}")
logger.info(f" League: {game.league_id}")
logger.info(f" Status: {game.status}")
logger.info(f" Mode: {game.game_mode}")
logger.info(f" Teams: {game.away_team_id} @ {game.home_team_id}")
logger.info(f" Score: {game.away_score}-{game.home_score}")
if game.current_inning:
logger.info(f" State: {game.current_half} {game.current_inning}")
logger.info(f" AI: Home={game.home_team_is_ai}, Away={game.away_team_is_ai}")
logger.info(f" Created: {game.created_at}")
async def show_stats(self) -> None:
"""Show database statistics."""
async with AsyncSessionLocal() as session:
counts = await self.count_records(session)
logger.info(f"\n{'='*80}")
logger.info("DATABASE STATISTICS")
logger.info(f"{'='*80}")
logger.info(f"Games: {counts['games']:>6}")
logger.info(f"Plays: {counts['plays']:>6}")
logger.info(f"Lineups: {counts['lineups']:>6}")
logger.info(f"Roster Links: {counts['roster_links']:>6}")
logger.info(f"Game Sessions: {counts['game_sessions']:>6}")
logger.info(f"Cardset Links: {counts['game_cardset_links']:>6}")
logger.info(f"{'='*80}\n")
async def interactive_menu():
"""Interactive menu for cleanup operations."""
cleaner = DatabaseCleaner()
while True:
print("\n" + "="*60)
print("DATABASE CLEANUP MENU")
print("="*60)
print("1. Show database statistics")
print("2. List all games")
print("3. Delete specific game")
print("4. Delete ALL data (⚠️ DANGER)")
print("5. Exit")
print("="*60)
choice = input("\nEnter your choice (1-5): ").strip()
if choice == "1":
await cleaner.show_stats()
elif choice == "2":
await cleaner.list_games()
elif choice == "3":
game_id_str = input("Enter game UUID: ").strip()
try:
game_id = UUID(game_id_str)
confirm = input(f"Delete game {game_id}? (yes/no): ").strip().lower()
if confirm == "yes":
await cleaner.delete_game(game_id)
else:
logger.info("Cancelled")
except ValueError:
logger.error("Invalid UUID format")
elif choice == "4":
await cleaner.show_stats()
print("\n⚠️ WARNING: This will DELETE ALL DATA from the database!")
confirm1 = input("Are you absolutely sure? (type 'DELETE ALL'): ").strip()
if confirm1 == "DELETE ALL":
confirm2 = input("Type 'yes' to confirm: ").strip().lower()
if confirm2 == "yes":
await cleaner.delete_all(confirm=True)
else:
logger.info("Cancelled")
else:
logger.info("Cancelled")
elif choice == "5":
logger.info("Exiting...")
break
else:
logger.warning("Invalid choice")
async def main():
"""Main entry point."""
cleaner = DatabaseCleaner()
# Parse command line arguments
if len(sys.argv) == 1:
# No arguments - run interactive mode
await interactive_menu()
elif "--help" in sys.argv or "-h" in sys.argv:
print(__doc__)
elif "--stats" in sys.argv:
await cleaner.show_stats()
elif "--list" in sys.argv:
await cleaner.list_games()
elif "--all" in sys.argv:
await cleaner.show_stats()
print("\n⚠️ WARNING: This will DELETE ALL DATA!")
confirm = input("Type 'yes' to confirm: ").strip().lower()
if confirm == "yes":
await cleaner.delete_all(confirm=True)
else:
logger.info("Cancelled")
elif "--game" in sys.argv:
idx = sys.argv.index("--game")
if idx + 1 < len(sys.argv):
try:
game_id = UUID(sys.argv[idx + 1])
await cleaner.delete_game(game_id)
except ValueError:
logger.error("Invalid UUID format")
else:
logger.error("--game requires a UUID argument")
else:
print("Unknown arguments. Use --help for usage information.")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("\nInterrupted by user")
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
sys.exit(1)

View File

@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
Interactive Database Playground
Test and experiment with database models and operations.
Run this script to create test games and explore the database operations.
Usage:
python test_db_playground.py
"""
import asyncio
import logging
from uuid import uuid4, UUID
from typing import Optional
from app.database.operations import DatabaseOperations
from app.models.roster_models import PdRosterLinkData, SbaRosterLinkData
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DatabasePlayground:
"""Interactive playground for testing database operations."""
def __init__(self):
self.db_ops = DatabaseOperations()
self.test_game_id: Optional[UUID] = None
async def demo_pd_game(self):
"""Create and populate a PD league game with roster and lineup."""
logger.info("=" * 60)
logger.info("DEMO: Creating PD League Game")
logger.info("=" * 60)
# Create game
self.test_game_id = uuid4()
game = await self.db_ops.create_game(
game_id=self.test_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public",
home_team_is_ai=False,
away_team_is_ai=True,
ai_difficulty="balanced"
)
logger.info(f"✅ Created PD game: {game.id}")
logger.info(f" Home Team: {game.home_team_id} (Human)")
logger.info(f" Away Team: {game.away_team_id} (AI - {game.ai_difficulty})")
# Add cards to roster
logger.info("\n📋 Adding cards to roster...")
home_cards = [101, 102, 103, 104, 105, 106, 107, 108, 109]
away_cards = [201, 202, 203, 204, 205, 206, 207, 208, 209]
for card_id in home_cards:
roster_link = await self.db_ops.add_pd_roster_card(
game_id=self.test_game_id,
card_id=card_id,
team_id=1
)
logger.info(f" Added card {card_id} to home roster (link_id={roster_link.id})")
for card_id in away_cards:
roster_link = await self.db_ops.add_pd_roster_card(
game_id=self.test_game_id,
card_id=card_id,
team_id=2
)
logger.info(f" Added card {card_id} to away roster (link_id={roster_link.id})")
# Get roster
home_roster = await self.db_ops.get_pd_roster(self.test_game_id, team_id=1)
away_roster = await self.db_ops.get_pd_roster(self.test_game_id, team_id=2)
logger.info(f"\n✅ Home roster: {len(home_roster)} cards")
logger.info(f"✅ Away roster: {len(away_roster)} cards")
# Set lineup
logger.info("\n⚾ Setting starting lineup...")
positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
for i, (card_id, position) in enumerate(zip(home_cards, positions), start=1):
lineup = await self.db_ops.add_pd_lineup_card(
game_id=self.test_game_id,
team_id=1,
card_id=card_id,
position=position,
batting_order=i if position != 'P' else None,
is_starter=True
)
logger.info(f" {position}: Card {card_id} (batting {lineup.batting_order or 'N/A'})")
# Get active lineup
home_lineup = await self.db_ops.get_active_lineup(self.test_game_id, team_id=1)
logger.info(f"\n✅ Active lineup: {len(home_lineup)} players")
# Update game state
logger.info("\n🎮 Simulating game progress...")
await self.db_ops.update_game_state(
game_id=self.test_game_id,
inning=3,
half="bottom",
home_score=2,
away_score=1,
status="active"
)
logger.info(" Updated to: Bottom 3rd, Home 2 - Away 1")
# Save a play
play_data = {
'game_id': self.test_game_id,
'play_number': 1,
'inning': 3,
'half': 'bottom',
'outs_before': 1,
'batting_order': 3,
'away_score': 1,
'home_score': 2,
'batter_id': home_lineup[2].id,
'pitcher_id': home_lineup[0].id,
'catcher_id': home_lineup[1].id,
'dice_roll': '14+6',
'hit_type': 'FB',
'result_description': 'Deep fly ball to center field - Home Run!',
'outs_recorded': 0,
'runs_scored': 1,
'pa': 1,
'ab': 1,
'hit': 1,
'homerun': 1,
'rbi': 1,
'is_go_ahead': False,
'complete': True,
'locked': False
}
play = await self.db_ops.save_play(play_data)
logger.info(f"\n⚾ Saved play #{play.play_number}: {play.result_description}")
# Load game state
logger.info("\n🔄 Loading complete game state...")
game_state = await self.db_ops.load_game_state(self.test_game_id)
if game_state:
logger.info(f"✅ Loaded game state:")
logger.info(f" Status: {game_state['game']['status']}")
logger.info(f" Score: {game_state['game']['away_score']}-{game_state['game']['home_score']}")
logger.info(f" Lineups: {len(game_state['lineups'])} players")
logger.info(f" Plays: {len(game_state['plays'])} plays recorded")
return self.test_game_id
async def demo_sba_game(self):
"""Create and populate an SBA league game with roster and lineup."""
logger.info("\n" + "=" * 60)
logger.info("DEMO: Creating SBA League Game")
logger.info("=" * 60)
# Create game
game_id = uuid4()
game = await self.db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=10,
away_team_id=11,
game_mode="ranked",
visibility="public",
home_team_is_ai=False,
away_team_is_ai=False
)
logger.info(f"✅ Created SBA game: {game.id}")
logger.info(f" Home Team: {game.home_team_id}")
logger.info(f" Away Team: {game.away_team_id}")
logger.info(f" Mode: {game.game_mode}")
# Add players to roster
logger.info("\n📋 Adding players to roster...")
home_players = [501, 502, 503, 504, 505, 506, 507, 508, 509]
away_players = [601, 602, 603, 604, 605, 606, 607, 608, 609]
for player_id in home_players:
roster_link = await self.db_ops.add_sba_roster_player(
game_id=game_id,
player_id=player_id,
team_id=10
)
logger.info(f" Added player {player_id} to home roster (link_id={roster_link.id})")
for player_id in away_players:
roster_link = await self.db_ops.add_sba_roster_player(
game_id=game_id,
player_id=player_id,
team_id=11
)
logger.info(f" Added player {player_id} to away roster (link_id={roster_link.id})")
# Get roster
home_roster = await self.db_ops.get_sba_roster(game_id, team_id=10)
away_roster = await self.db_ops.get_sba_roster(game_id, team_id=11)
logger.info(f"\n✅ Home roster: {len(home_roster)} players")
logger.info(f"✅ Away roster: {len(away_roster)} players")
# Set lineup
logger.info("\n⚾ Setting starting lineup...")
positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
for i, (player_id, position) in enumerate(zip(home_players, positions), start=1):
lineup = await self.db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=10,
player_id=player_id,
position=position,
batting_order=i if position != 'P' else None,
is_starter=True
)
logger.info(f" {position}: Player {player_id} (batting {lineup.batting_order or 'N/A'})")
# Create game session
session = await self.db_ops.create_game_session(game_id)
logger.info(f"\n✅ Created game session for game: {session.game_id}")
# Update session snapshot
await self.db_ops.update_session_snapshot(
game_id=game_id,
state_snapshot={'test': 'data', 'connected_users': ['user1', 'user2']}
)
logger.info("✅ Updated session snapshot")
return game_id
async def demo_cleanup(self):
"""Demonstrate querying capabilities."""
if not self.test_game_id:
logger.warning("No test game created yet")
return
logger.info("\n" + "=" * 60)
logger.info("DEMO: Query Capabilities")
logger.info("=" * 60)
# Get game
game = await self.db_ops.get_game(self.test_game_id)
if game:
logger.info(f"\n📊 Game Details:")
logger.info(f" ID: {game.id}")
logger.info(f" League: {game.league_id}")
logger.info(f" Status: {game.status}")
logger.info(f" Inning: {game.current_half} {game.current_inning}")
logger.info(f" Score: {game.away_score}-{game.home_score}")
# Get plays
plays = await self.db_ops.get_plays(self.test_game_id)
logger.info(f"\n⚾ Plays: {len(plays)} total")
for play in plays:
logger.info(f" Play #{play.play_number}: {play.result_description}")
# Get active lineup
home_lineup = await self.db_ops.get_active_lineup(self.test_game_id, team_id=1)
logger.info(f"\n👥 Active Lineup: {len(home_lineup)} players")
for player in sorted(home_lineup, key=lambda x: x.batting_order or 99):
logger.info(f" {player.position}: Card {player.card_id} (batting {player.batting_order or 'N/A'})")
async def main():
"""Run interactive playground demos."""
playground = DatabasePlayground()
try:
# Run PD game demo
pd_game_id = await playground.demo_pd_game()
# Run SBA game demo
sba_game_id = await playground.demo_sba_game()
# Show query capabilities
await playground.demo_cleanup()
logger.info("\n" + "=" * 60)
logger.info("✅ ALL DEMOS COMPLETED SUCCESSFULLY!")
logger.info("=" * 60)
logger.info(f"\nCreated games:")
logger.info(f" PD Game: {pd_game_id}")
logger.info(f" SBA Game: {sba_game_id}")
logger.info("\nYou can query these games in the database to explore the data!")
except Exception as e:
logger.error(f"\n❌ Error during demo: {e}", exc_info=True)
raise
if __name__ == "__main__":
asyncio.run(main())