feat: Add validate_series_key() validator for key-based identification (Task 4.6)

- Add validate_series_key() function that validates URL-safe, lowercase,
  hyphen-separated series keys (e.g., 'attack-on-titan')
- Add validate_series_key_or_folder() for backward compatibility during
  transition from folder-based to key-based identification
- Create comprehensive test suite with 99 test cases for all validators
- Update infrastructure.md with validation utilities documentation
- Mark Task 4.6 as complete in instructions.md

Test: conda run -n AniWorld python -m pytest tests/unit/test_validators.py -v
All 99 validator tests pass, 718 total unit tests pass
This commit is contained in:
2025-11-28 07:13:46 +01:00
parent 08c7264d7a
commit c00224467f
4 changed files with 662 additions and 30 deletions

View File

@@ -438,6 +438,107 @@ def validate_series_name(name: str) -> str:
return name.strip()
def validate_series_key(key: str) -> str:
"""
Validate series key format.
Series keys are unique, provider-assigned, URL-safe identifiers.
They should be lowercase, use hyphens for word separation, and contain
only alphanumeric characters and hyphens.
Valid examples:
- "attack-on-titan"
- "one-piece"
- "naruto"
Invalid examples:
- "Attack On Titan" (uppercase, spaces)
- "attack_on_titan" (underscores)
- "attack on titan" (spaces)
- "" (empty)
Args:
key: Series key to validate
Returns:
Validated key (trimmed)
Raises:
ValueError: If key is invalid
"""
if not key or not isinstance(key, str):
raise ValueError("Series key must be a non-empty string")
key = key.strip()
if not key:
raise ValueError("Series key cannot be empty")
if len(key) > 255:
raise ValueError("Series key must be 255 characters or less")
# Key must be lowercase, alphanumeric with hyphens only
# Pattern: starts with letter/number, can contain letters, numbers, hyphens
# Cannot start or end with hyphen, no consecutive hyphens
if not re.match(r'^[a-z0-9]+(?:-[a-z0-9]+)*$', key):
raise ValueError(
"Series key must be lowercase, URL-safe, and use hyphens "
"for word separation (e.g., 'attack-on-titan'). "
"No spaces, underscores, or uppercase letters allowed."
)
return key
def validate_series_key_or_folder(
identifier: str, allow_folder: bool = True
) -> tuple[str, bool]:
"""
Validate an identifier that could be either a series key or folder.
This function provides backward compatibility during the transition
from folder-based to key-based identification.
Args:
identifier: The identifier to validate (key or folder)
allow_folder: Whether to allow folder-style identifiers (default: True)
Returns:
Tuple of (validated_identifier, is_key) where is_key indicates
whether the identifier is a valid key format.
Raises:
ValueError: If identifier is empty or invalid
"""
if not identifier or not isinstance(identifier, str):
raise ValueError("Identifier must be a non-empty string")
identifier = identifier.strip()
if not identifier:
raise ValueError("Identifier cannot be empty")
# Try to validate as key first
try:
validate_series_key(identifier)
return identifier, True
except ValueError:
pass
# If not a valid key, check if folder format is allowed
if not allow_folder:
raise ValueError(
f"Invalid series key format: '{identifier}'. "
"Keys must be lowercase with hyphens (e.g., 'attack-on-titan')."
)
# Validate as folder (more permissive)
if len(identifier) > 1000:
raise ValueError("Identifier too long (max 1000 characters)")
return identifier, False
def validate_backup_name(name: str) -> str:
"""
Validate backup file name.