Backend refactor complete - removed all deprecated parameters and replaced with clean action-based system. Changes: - OffensiveDecision model: Added 'action' field (6 choices), removed deprecated 'hit_and_run' and 'bunt_attempt' boolean fields - Validators: Added action-specific validation (squeeze_bunt, check_jump, sac_bunt, hit_and_run situational constraints) - WebSocket handler: Updated submit_offensive_decision to use action field - Terminal client: Updated CLI, REPL, arg parser, and display for actions - Tests: Updated all 739 unit tests (100% passing) Action field values: - swing_away (default) - steal (requires steal_attempts parameter) - check_jump (requires runner on base) - hit_and_run (requires runner on base) - sac_bunt (cannot use with 2 outs) - squeeze_bunt (requires R3, not with bases loaded, not with 2 outs) Breaking changes: - Removed: hit_and_run boolean → use action="hit_and_run" - Removed: bunt_attempt boolean → use action="sac_bunt" or "squeeze_bunt" - Removed: approach field → use action field Files modified: - app/models/game_models.py - app/core/validators.py - app/websocket/handlers.py - terminal_client/main.py - terminal_client/arg_parser.py - terminal_client/commands.py - terminal_client/repl.py - terminal_client/display.py - tests/unit/models/test_game_models.py - tests/unit/core/test_validators.py - tests/unit/terminal_client/test_arg_parser.py - tests/unit/terminal_client/test_commands.py Test results: 739/739 passing (100%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
216 lines
7.0 KiB
Python
216 lines
7.0 KiB
Python
"""
|
|
Argument parsing utilities for terminal client commands.
|
|
|
|
Provides robust parsing for both REPL and CLI commands using shlex
|
|
to handle quoted strings, spaces, and complex arguments.
|
|
|
|
Author: Claude
|
|
Date: 2025-10-27
|
|
"""
|
|
import shlex
|
|
import logging
|
|
from typing import Dict, Any, List, Optional, Tuple
|
|
|
|
logger = logging.getLogger(f'{__name__}.arg_parser')
|
|
|
|
|
|
class ArgumentParseError(Exception):
|
|
"""Raised when argument parsing fails."""
|
|
pass
|
|
|
|
|
|
class CommandArgumentParser:
|
|
"""Parse command-line style arguments for terminal client."""
|
|
|
|
@staticmethod
|
|
def parse_args(arg_string: str, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Parse argument string according to schema.
|
|
|
|
Args:
|
|
arg_string: Raw argument string from command
|
|
schema: Dictionary defining expected arguments
|
|
{
|
|
'league': {'type': str, 'default': 'sba'},
|
|
'home_team': {'type': int, 'default': 1},
|
|
'count': {'type': int, 'default': 1, 'positional': True},
|
|
'verbose': {'type': bool, 'flag': True}
|
|
}
|
|
|
|
Returns:
|
|
Dictionary of parsed arguments with defaults applied
|
|
|
|
Raises:
|
|
ArgumentParseError: If parsing fails or validation fails
|
|
"""
|
|
try:
|
|
# Use shlex for robust parsing
|
|
tokens = shlex.split(arg_string) if arg_string.strip() else []
|
|
except ValueError as e:
|
|
raise ArgumentParseError(f"Invalid argument syntax: {e}")
|
|
|
|
# Initialize result with defaults
|
|
result = {}
|
|
for key, spec in schema.items():
|
|
if 'default' in spec:
|
|
result[key] = spec['default']
|
|
|
|
# Track which positional arg we're on
|
|
positional_keys = [k for k, v in schema.items() if v.get('positional', False)]
|
|
positional_index = 0
|
|
|
|
i = 0
|
|
while i < len(tokens):
|
|
token = tokens[i]
|
|
|
|
# Handle flags (--option or -o)
|
|
if token.startswith('--'):
|
|
option_name = token[2:]
|
|
|
|
# Convert hyphen to underscore for Python compatibility
|
|
option_key = option_name.replace('-', '_')
|
|
|
|
if option_key not in schema:
|
|
raise ArgumentParseError(f"Unknown option: {token}")
|
|
|
|
spec = schema[option_key]
|
|
|
|
# Boolean flags don't need a value
|
|
if spec.get('flag', False):
|
|
result[option_key] = True
|
|
i += 1
|
|
continue
|
|
|
|
# Option requires a value
|
|
if i + 1 >= len(tokens):
|
|
raise ArgumentParseError(f"Option {token} requires a value")
|
|
|
|
value_str = tokens[i + 1]
|
|
|
|
# Type conversion
|
|
try:
|
|
if spec['type'] == int:
|
|
result[option_key] = int(value_str)
|
|
elif spec['type'] == float:
|
|
result[option_key] = float(value_str)
|
|
elif spec['type'] == list:
|
|
# Parse comma-separated list
|
|
result[option_key] = [item.strip() for item in value_str.split(',')]
|
|
elif spec['type'] == 'int_list':
|
|
# Parse comma-separated integers
|
|
result[option_key] = [int(item.strip()) for item in value_str.split(',')]
|
|
else:
|
|
result[option_key] = value_str
|
|
except ValueError as e:
|
|
raise ArgumentParseError(
|
|
f"Invalid value for {token}: expected {spec['type'].__name__ if hasattr(spec['type'], '__name__') else spec['type']}, got '{value_str}'"
|
|
)
|
|
|
|
i += 2
|
|
|
|
# Handle positional arguments
|
|
else:
|
|
if positional_index >= len(positional_keys):
|
|
raise ArgumentParseError(f"Unexpected positional argument: {token}")
|
|
|
|
key = positional_keys[positional_index]
|
|
spec = schema[key]
|
|
|
|
try:
|
|
if spec['type'] == int:
|
|
result[key] = int(token)
|
|
elif spec['type'] == float:
|
|
result[key] = float(token)
|
|
else:
|
|
result[key] = token
|
|
except ValueError as e:
|
|
raise ArgumentParseError(
|
|
f"Invalid value for {key}: expected {spec['type'].__name__}, got '{token}'"
|
|
)
|
|
|
|
positional_index += 1
|
|
i += 1
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def parse_game_id(arg_string: str) -> Optional[str]:
|
|
"""
|
|
Parse a game ID from argument string.
|
|
|
|
Args:
|
|
arg_string: Raw argument string
|
|
|
|
Returns:
|
|
Game ID string or None
|
|
"""
|
|
try:
|
|
tokens = shlex.split(arg_string) if arg_string.strip() else []
|
|
|
|
# Look for --game-id option
|
|
for i, token in enumerate(tokens):
|
|
if token == '--game-id' and i + 1 < len(tokens):
|
|
return tokens[i + 1]
|
|
|
|
# If no option, check if there's a positional UUID-like argument
|
|
if tokens and len(tokens[0]) == 36: # UUID length
|
|
return tokens[0]
|
|
|
|
return None
|
|
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
# Predefined schemas for common commands
|
|
NEW_GAME_SCHEMA = {
|
|
'league': {'type': str, 'default': 'sba'},
|
|
'home_team': {'type': int, 'default': 1},
|
|
'away_team': {'type': int, 'default': 2}
|
|
}
|
|
|
|
DEFENSIVE_SCHEMA = {
|
|
'alignment': {'type': str, 'default': 'normal'},
|
|
'infield': {'type': str, 'default': 'normal'},
|
|
'outfield': {'type': str, 'default': 'normal'},
|
|
'hold': {'type': 'int_list', 'default': []}
|
|
}
|
|
|
|
OFFENSIVE_SCHEMA = {
|
|
'action': {'type': str, 'default': 'swing_away'}, # Session 2: changed from approach
|
|
'steal': {'type': 'int_list', 'default': []}
|
|
}
|
|
|
|
QUICK_PLAY_SCHEMA = {
|
|
'count': {'type': int, 'default': 1, 'positional': True}
|
|
}
|
|
|
|
USE_GAME_SCHEMA = {
|
|
'game_id': {'type': str, 'positional': True}
|
|
}
|
|
|
|
|
|
def parse_new_game_args(arg_string: str) -> Dict[str, Any]:
|
|
"""Parse arguments for new_game command."""
|
|
return CommandArgumentParser.parse_args(arg_string, NEW_GAME_SCHEMA)
|
|
|
|
|
|
def parse_defensive_args(arg_string: str) -> Dict[str, Any]:
|
|
"""Parse arguments for defensive command."""
|
|
return CommandArgumentParser.parse_args(arg_string, DEFENSIVE_SCHEMA)
|
|
|
|
|
|
def parse_offensive_args(arg_string: str) -> Dict[str, Any]:
|
|
"""Parse arguments for offensive command."""
|
|
return CommandArgumentParser.parse_args(arg_string, OFFENSIVE_SCHEMA)
|
|
|
|
|
|
def parse_quick_play_args(arg_string: str) -> Dict[str, Any]:
|
|
"""Parse arguments for quick_play command."""
|
|
return CommandArgumentParser.parse_args(arg_string, QUICK_PLAY_SCHEMA)
|
|
|
|
|
|
def parse_use_game_args(arg_string: str) -> Dict[str, Any]:
|
|
"""Parse arguments for use_game command."""
|
|
return CommandArgumentParser.parse_args(arg_string, USE_GAME_SCHEMA)
|