Version control Claude Code configuration including: - Global instructions (CLAUDE.md) - User settings (settings.json) - Custom agents (architect, designer, engineer, etc.) - Custom skills (create-skill templates and workflows) Excludes session data, secrets, cache, and temporary files per .gitignore. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
649 lines
21 KiB
Python
Executable File
649 lines
21 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Paper Dynasty CLI - Baseball Card Game Management
|
|
|
|
A command-line interface for the Paper Dynasty API, primarily for use with Claude Code.
|
|
|
|
Usage:
|
|
pd status
|
|
pd team list
|
|
pd team get SKB
|
|
pd team cards SKB
|
|
pd pack today
|
|
pd pack distribute --num 10
|
|
pd gauntlet list --event-id 8 --active
|
|
pd gauntlet cleanup Gauntlet-SKB --event-id 8 --yes
|
|
|
|
Environment:
|
|
API_TOKEN: Required. Bearer token for API authentication.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Annotated, List, Optional
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
# Import the existing API client from same directory
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from api_client import PaperDynastyAPI
|
|
|
|
# ============================================================================
|
|
# App Setup
|
|
# ============================================================================
|
|
|
|
app = typer.Typer(
|
|
name="pd",
|
|
help="Paper Dynasty Baseball Card Game CLI",
|
|
no_args_is_help=True,
|
|
)
|
|
team_app = typer.Typer(help="Team operations")
|
|
pack_app = typer.Typer(help="Pack operations")
|
|
gauntlet_app = typer.Typer(help="Gauntlet operations")
|
|
player_app = typer.Typer(help="Player operations")
|
|
|
|
app.add_typer(team_app, name="team")
|
|
app.add_typer(pack_app, name="pack")
|
|
app.add_typer(gauntlet_app, name="gauntlet")
|
|
app.add_typer(player_app, name="player")
|
|
|
|
console = Console()
|
|
|
|
|
|
class State:
|
|
"""Global state for API client and settings"""
|
|
api: Optional[PaperDynastyAPI] = None
|
|
json_output: bool = False
|
|
|
|
|
|
state = State()
|
|
|
|
|
|
# ============================================================================
|
|
# Output Helpers
|
|
# ============================================================================
|
|
|
|
def output_json(data):
|
|
"""Output data as formatted JSON"""
|
|
console.print_json(json.dumps(data, indent=2, default=str))
|
|
|
|
|
|
def output_table(title: str, columns: List[str], rows: List[List], show_lines: bool = False):
|
|
"""Output data as a rich table"""
|
|
table = Table(title=title, show_header=True, header_style="bold cyan", show_lines=show_lines)
|
|
for col in columns:
|
|
table.add_column(col)
|
|
for row in rows:
|
|
table.add_row(*[str(cell) if cell is not None else "" for cell in row])
|
|
console.print(table)
|
|
|
|
|
|
def handle_error(e: Exception, context: str = ""):
|
|
"""Graceful error handling with helpful messages"""
|
|
error_str = str(e)
|
|
if "401" in error_str:
|
|
console.print("[red]Error:[/red] Unauthorized. Check your API_TOKEN.")
|
|
elif "404" in error_str:
|
|
console.print(f"[red]Error:[/red] Not found. {context}")
|
|
elif "Connection" in error_str or "ConnectionError" in error_str:
|
|
console.print("[red]Error:[/red] Cannot connect to API. Check network and --env setting.")
|
|
else:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
|
|
|
|
# ============================================================================
|
|
# Main Callback (Global Options)
|
|
# ============================================================================
|
|
|
|
@app.callback()
|
|
def main(
|
|
env: Annotated[str, typer.Option("--env", help="Environment: prod or dev")] = "prod",
|
|
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Verbose output")] = False,
|
|
):
|
|
"""Paper Dynasty Baseball Card Game CLI"""
|
|
try:
|
|
state.api = PaperDynastyAPI(environment=env, verbose=verbose)
|
|
state.json_output = json_output
|
|
except ValueError as e:
|
|
console.print(f"[red]Configuration Error:[/red] {e}")
|
|
console.print("\nSet API_TOKEN environment variable:")
|
|
console.print(" export API_TOKEN='your-token-here'")
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Status & Health Commands
|
|
# ============================================================================
|
|
|
|
@app.command()
|
|
def status():
|
|
"""Show packs opened today summary"""
|
|
try:
|
|
result = state.api.get_packs_opened_today()
|
|
|
|
if state.json_output:
|
|
output_json(result)
|
|
return
|
|
|
|
console.print(f"\n[bold cyan]Packs Opened Today ({result['date']})[/bold cyan]\n")
|
|
console.print(f"[bold]Total:[/bold] {result['total']} packs\n")
|
|
|
|
if result['teams']:
|
|
rows = []
|
|
for t in result['teams']:
|
|
rows.append([t['abbrev'], t['name'], t['packs']])
|
|
output_table("By Team", ["Abbrev", "Team", "Packs"], rows)
|
|
else:
|
|
console.print("[dim]No packs opened today[/dim]")
|
|
|
|
if result.get('note'):
|
|
console.print(f"\n[yellow]Note:[/yellow] {result['note']}")
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@app.command()
|
|
def health():
|
|
"""Check API health status"""
|
|
try:
|
|
# Try to list teams as a health check
|
|
teams = state.api.list_teams()
|
|
console.print(f"[green]API is healthy[/green] ({state.api.base_url})")
|
|
console.print(f"[dim]Found {len(teams)} teams[/dim]")
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Team Commands
|
|
# ============================================================================
|
|
|
|
@team_app.command("list")
|
|
def team_list(
|
|
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Filter by season")] = None,
|
|
):
|
|
"""List all teams"""
|
|
try:
|
|
teams = state.api.list_teams(season=season)
|
|
|
|
if state.json_output:
|
|
output_json(teams)
|
|
return
|
|
|
|
if not teams:
|
|
console.print("[yellow]No teams found[/yellow]")
|
|
return
|
|
|
|
# Filter out gauntlet teams for cleaner display
|
|
regular_teams = [t for t in teams if 'Gauntlet' not in t.get('abbrev', '')]
|
|
|
|
rows = []
|
|
for t in regular_teams:
|
|
rows.append([
|
|
t['abbrev'],
|
|
t.get('sname', ''),
|
|
t.get('season', ''),
|
|
t.get('wallet', 0),
|
|
t.get('ranking', 'N/A'),
|
|
'AI' if t.get('is_ai') else 'Human'
|
|
])
|
|
|
|
title = "Teams"
|
|
if season:
|
|
title += f" - Season {season}"
|
|
output_table(title, ["Abbrev", "Name", "Season", "Wallet", "Rank", "Type"], rows)
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@team_app.command("get")
|
|
def team_get(
|
|
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
|
|
):
|
|
"""Get team details"""
|
|
try:
|
|
team = state.api.get_team(abbrev=abbrev.upper())
|
|
|
|
if state.json_output:
|
|
output_json(team)
|
|
return
|
|
|
|
panel = Panel(
|
|
f"[bold]ID:[/bold] {team['id']}\n"
|
|
f"[bold]Abbreviation:[/bold] {team['abbrev']}\n"
|
|
f"[bold]Short Name:[/bold] {team.get('sname', 'N/A')}\n"
|
|
f"[bold]Full Name:[/bold] {team.get('lname', 'N/A')}\n"
|
|
f"[bold]Season:[/bold] {team.get('season', 'N/A')}\n"
|
|
f"[bold]Wallet:[/bold] ${team.get('wallet', 0)}\n"
|
|
f"[bold]Ranking:[/bold] {team.get('ranking', 'N/A')}\n"
|
|
f"[bold]Type:[/bold] {'AI' if team.get('is_ai') else 'Human'}",
|
|
title=f"Team: {team.get('lname', abbrev)}",
|
|
border_style="green",
|
|
)
|
|
console.print(panel)
|
|
|
|
except ValueError as e:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
handle_error(e, f"Team '{abbrev}' may not exist.")
|
|
|
|
|
|
@team_app.command("cards")
|
|
def team_cards(
|
|
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
|
|
limit: Annotated[int, typer.Option("--limit", "-n", help="Max cards to show")] = 50,
|
|
):
|
|
"""List team's cards"""
|
|
try:
|
|
team = state.api.get_team(abbrev=abbrev.upper())
|
|
cards = state.api.list_cards(team_id=team['id'])
|
|
|
|
if state.json_output:
|
|
output_json(cards)
|
|
return
|
|
|
|
if not cards:
|
|
console.print(f"[yellow]Team {abbrev} has no cards[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for c in cards[:limit]:
|
|
player = c.get('player', {})
|
|
rows.append([
|
|
c['id'],
|
|
player.get('p_name', 'Unknown'),
|
|
player.get('rarity', ''),
|
|
c.get('value', 0)
|
|
])
|
|
|
|
output_table(
|
|
f"Cards for {team.get('lname', abbrev)} ({len(cards)} total)",
|
|
["Card ID", "Player", "Rarity", "Value"],
|
|
rows
|
|
)
|
|
|
|
if len(cards) > limit:
|
|
console.print(f"\n[dim]Showing {limit} of {len(cards)} cards. Use --limit to see more.[/dim]")
|
|
|
|
except ValueError as e:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Pack Commands
|
|
# ============================================================================
|
|
|
|
@pack_app.command("list")
|
|
def pack_list(
|
|
team: Annotated[Optional[str], typer.Option("--team", "-t", help="Filter by team abbrev")] = None,
|
|
opened: Annotated[Optional[bool], typer.Option("--opened/--unopened", help="Filter by opened status")] = None,
|
|
limit: Annotated[int, typer.Option("--limit", "-n", help="Max packs to show")] = 50,
|
|
):
|
|
"""List packs"""
|
|
try:
|
|
team_id = None
|
|
team_name = None
|
|
if team:
|
|
team_obj = state.api.get_team(abbrev=team.upper())
|
|
team_id = team_obj['id']
|
|
team_name = team_obj.get('sname', team)
|
|
|
|
packs = state.api.list_packs(team_id=team_id, opened=opened, new_to_old=True, limit=limit)
|
|
|
|
if state.json_output:
|
|
output_json(packs)
|
|
return
|
|
|
|
if not packs:
|
|
console.print("[yellow]No packs found[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for p in packs:
|
|
pack_team = p.get('team', {})
|
|
pack_type = p.get('pack_type', {})
|
|
is_opened = "Yes" if p.get('open_time') else "No"
|
|
rows.append([
|
|
p['id'],
|
|
pack_team.get('abbrev', 'N/A'),
|
|
pack_type.get('name', 'Unknown'),
|
|
is_opened
|
|
])
|
|
|
|
title = "Packs"
|
|
if team_name:
|
|
title += f" - {team_name}"
|
|
if opened is True:
|
|
title += " (Opened)"
|
|
elif opened is False:
|
|
title += " (Unopened)"
|
|
|
|
output_table(title, ["Pack ID", "Team", "Type", "Opened"], rows)
|
|
|
|
except ValueError as e:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@pack_app.command("today")
|
|
def pack_today():
|
|
"""Show packs opened today analytics"""
|
|
# Reuse status command
|
|
status()
|
|
|
|
|
|
@pack_app.command("distribute")
|
|
def pack_distribute(
|
|
num: Annotated[int, typer.Option("--num", "-n", help="Number of packs per team")] = 5,
|
|
exclude: Annotated[Optional[List[str]], typer.Option("--exclude", "-x", help="Team abbrevs to exclude")] = None,
|
|
pack_type: Annotated[int, typer.Option("--pack-type", help="Pack type ID (1=Standard)")] = 1,
|
|
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be done")] = False,
|
|
):
|
|
"""Distribute packs to all human teams"""
|
|
try:
|
|
if dry_run:
|
|
# Get qualifying teams to show preview
|
|
current = state.api.get('current')
|
|
season = current['season']
|
|
all_teams = state.api.list_teams(season=season)
|
|
|
|
exclude_upper = [e.upper() for e in (exclude or [])]
|
|
qualifying = [
|
|
t for t in all_teams
|
|
if not t['is_ai']
|
|
and 'gauntlet' not in t['abbrev'].lower()
|
|
and t['abbrev'].upper() not in exclude_upper
|
|
]
|
|
|
|
console.print(f"\n[bold cyan]Pack Distribution Preview (DRY RUN)[/bold cyan]\n")
|
|
console.print(f"[bold]Packs per team:[/bold] {num}")
|
|
console.print(f"[bold]Pack type:[/bold] {pack_type}")
|
|
console.print(f"[bold]Teams:[/bold] {len(qualifying)}")
|
|
console.print(f"[bold]Total packs:[/bold] {num * len(qualifying)}")
|
|
|
|
if exclude:
|
|
console.print(f"[bold]Excluded:[/bold] {', '.join(exclude)}")
|
|
|
|
console.print("\n[bold]Qualifying teams:[/bold]")
|
|
for t in qualifying:
|
|
console.print(f" - {t['abbrev']}: {t['sname']}")
|
|
|
|
return
|
|
|
|
result = state.api.distribute_packs(
|
|
num_packs=num,
|
|
exclude_team_abbrev=exclude,
|
|
pack_type_id=pack_type
|
|
)
|
|
|
|
if state.json_output:
|
|
output_json(result)
|
|
return
|
|
|
|
console.print(f"\n[green]Distribution complete![/green]")
|
|
console.print(f"[bold]Total packs:[/bold] {result['total_packs']}")
|
|
console.print(f"[bold]Teams:[/bold] {result['teams_count']}")
|
|
|
|
if exclude:
|
|
console.print(f"[bold]Excluded:[/bold] {', '.join(exclude)}")
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Gauntlet Commands
|
|
# ============================================================================
|
|
|
|
@gauntlet_app.command("list")
|
|
def gauntlet_list(
|
|
event_id: Annotated[Optional[int], typer.Option("--event-id", "-e", help="Filter by event ID")] = None,
|
|
active: Annotated[bool, typer.Option("--active", "-a", help="Only active runs")] = False,
|
|
):
|
|
"""List gauntlet runs"""
|
|
try:
|
|
runs = state.api.list_gauntlet_runs(event_id=event_id, active_only=active)
|
|
|
|
if state.json_output:
|
|
output_json(runs)
|
|
return
|
|
|
|
if not runs:
|
|
console.print("[yellow]No gauntlet runs found[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for r in runs:
|
|
team = r.get('team', {})
|
|
is_active = "Active" if r.get('ended', 0) == 0 else "Ended"
|
|
rows.append([
|
|
r['id'],
|
|
team.get('abbrev', 'N/A'),
|
|
r.get('wins', 0),
|
|
r.get('losses', 0),
|
|
r.get('gauntlet_id', 'N/A'),
|
|
is_active
|
|
])
|
|
|
|
title = "Gauntlet Runs"
|
|
if event_id:
|
|
title += f" - Event {event_id}"
|
|
if active:
|
|
title += " (Active Only)"
|
|
|
|
output_table(title, ["Run ID", "Team", "W", "L", "Event", "Status"], rows)
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@gauntlet_app.command("teams")
|
|
def gauntlet_teams(
|
|
event_id: Annotated[Optional[int], typer.Option("--event-id", "-e", help="Filter by event ID")] = None,
|
|
active: Annotated[bool, typer.Option("--active", "-a", help="Only teams with active runs")] = False,
|
|
):
|
|
"""List gauntlet teams"""
|
|
try:
|
|
teams = state.api.find_gauntlet_teams(event_id=event_id, active_only=active)
|
|
|
|
if state.json_output:
|
|
output_json(teams)
|
|
return
|
|
|
|
if not teams:
|
|
console.print("[yellow]No gauntlet teams found[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for t in teams:
|
|
run = t.get('active_run') or t.get('run', {})
|
|
wins = run.get('wins', '-') if run else '-'
|
|
losses = run.get('losses', '-') if run else '-'
|
|
rows.append([
|
|
t['id'],
|
|
t['abbrev'],
|
|
t.get('sname', ''),
|
|
wins,
|
|
losses
|
|
])
|
|
|
|
title = "Gauntlet Teams"
|
|
if active:
|
|
title += " (Active)"
|
|
|
|
output_table(title, ["Team ID", "Abbrev", "Name", "W", "L"], rows)
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
@gauntlet_app.command("cleanup")
|
|
def gauntlet_cleanup(
|
|
team_abbrev: Annotated[str, typer.Argument(help="Team abbreviation (e.g., Gauntlet-SKB)")],
|
|
event_id: Annotated[int, typer.Option("--event-id", "-e", help="Event ID (required)")],
|
|
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
|
|
):
|
|
"""Clean up a gauntlet team (wipe cards, delete packs, end run)"""
|
|
try:
|
|
# Find the team
|
|
team = state.api.get_team(abbrev=team_abbrev)
|
|
team_id = team['id']
|
|
|
|
# Get cards and packs count
|
|
cards = state.api.list_cards(team_id=team_id)
|
|
packs = state.api.list_packs(team_id=team_id, opened=False)
|
|
|
|
# Find active run
|
|
runs = state.api.list_gauntlet_runs(event_id=event_id, team_id=team_id, active_only=True)
|
|
active_run = runs[0] if runs else None
|
|
|
|
console.print(f"\n[bold cyan]Gauntlet Cleanup: {team_abbrev}[/bold cyan]\n")
|
|
console.print(f"[bold]Team ID:[/bold] {team_id}")
|
|
console.print(f"[bold]Cards to wipe:[/bold] {len(cards)}")
|
|
console.print(f"[bold]Packs to delete:[/bold] {len(packs)}")
|
|
console.print(f"[bold]Active run:[/bold] {'Yes (ID: ' + str(active_run['id']) + ')' if active_run else 'No'}")
|
|
|
|
if not yes:
|
|
console.print("\n[yellow]This is a destructive operation![/yellow]")
|
|
console.print("Use --yes flag to confirm.")
|
|
raise typer.Exit(0)
|
|
|
|
# Perform cleanup
|
|
results = []
|
|
|
|
# 1. Wipe cards
|
|
if cards:
|
|
state.api.wipe_team_cards(team_id)
|
|
results.append(f"Wiped {len(cards)} cards")
|
|
|
|
# 2. Delete packs
|
|
for pack in packs:
|
|
state.api.delete_pack(pack['id'])
|
|
if packs:
|
|
results.append(f"Deleted {len(packs)} packs")
|
|
|
|
# 3. End gauntlet run
|
|
if active_run:
|
|
state.api.end_gauntlet_run(active_run['id'])
|
|
results.append(f"Ended run {active_run['id']}")
|
|
|
|
console.print(f"\n[green]Cleanup complete![/green]")
|
|
for r in results:
|
|
console.print(f" - {r}")
|
|
|
|
except ValueError as e:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Player Commands
|
|
# ============================================================================
|
|
|
|
@player_app.command("get")
|
|
def player_get(
|
|
player_id: Annotated[int, typer.Argument(help="Player ID")],
|
|
):
|
|
"""Get player by ID"""
|
|
try:
|
|
player = state.api.get_player(player_id=player_id)
|
|
|
|
if state.json_output:
|
|
output_json(player)
|
|
return
|
|
|
|
# Get positions
|
|
positions = []
|
|
for i in range(1, 9):
|
|
pos = player.get(f'pos{i}')
|
|
if pos:
|
|
positions.append(pos)
|
|
|
|
cardset = player.get('cardset', {})
|
|
|
|
panel = Panel(
|
|
f"[bold]ID:[/bold] {player['player_id']}\n"
|
|
f"[bold]Name:[/bold] {player.get('p_name', 'Unknown')}\n"
|
|
f"[bold]Rarity:[/bold] {player.get('rarity', 'N/A')}\n"
|
|
f"[bold]Cost:[/bold] {player.get('cost', 0)}\n"
|
|
f"[bold]Positions:[/bold] {', '.join(positions) if positions else 'N/A'}\n"
|
|
f"[bold]Cardset:[/bold] {cardset.get('name', 'N/A')} (ID: {cardset.get('id', 'N/A')})\n"
|
|
f"[bold]Bats/Throws:[/bold] {player.get('bats', 'N/A')}/{player.get('throws', 'N/A')}",
|
|
title=f"Player: {player.get('p_name', 'Unknown')}",
|
|
border_style="blue",
|
|
)
|
|
console.print(panel)
|
|
|
|
except Exception as e:
|
|
handle_error(e, f"Player ID {player_id} may not exist.")
|
|
|
|
|
|
@player_app.command("list")
|
|
def player_list(
|
|
rarity: Annotated[Optional[str], typer.Option("--rarity", "-r", help="Filter by rarity")] = None,
|
|
cardset: Annotated[Optional[int], typer.Option("--cardset", "-c", help="Filter by cardset ID")] = None,
|
|
limit: Annotated[int, typer.Option("--limit", "-n", help="Max players to show")] = 50,
|
|
):
|
|
"""List players"""
|
|
try:
|
|
players = state.api.list_players(cardset_id=cardset, rarity=rarity)
|
|
|
|
if state.json_output:
|
|
output_json(players)
|
|
return
|
|
|
|
if not players:
|
|
console.print("[yellow]No players found[/yellow]")
|
|
return
|
|
|
|
rows = []
|
|
for p in players[:limit]:
|
|
cs = p.get('cardset', {})
|
|
rows.append([
|
|
p['player_id'],
|
|
p.get('p_name', 'Unknown'),
|
|
p.get('rarity', ''),
|
|
p.get('cost', 0),
|
|
cs.get('name', 'N/A')
|
|
])
|
|
|
|
title = "Players"
|
|
if rarity:
|
|
title += f" - {rarity}"
|
|
if cardset:
|
|
title += f" - Cardset {cardset}"
|
|
|
|
output_table(title, ["ID", "Name", "Rarity", "Cost", "Cardset"], rows)
|
|
|
|
if len(players) > limit:
|
|
console.print(f"\n[dim]Showing {limit} of {len(players)} players. Use --limit to see more.[/dim]")
|
|
|
|
except Exception as e:
|
|
handle_error(e)
|
|
|
|
|
|
# ============================================================================
|
|
# Entry Point
|
|
# ============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
app()
|