Skip to content

validators

Validation utilities for Music Genre Updater.

SupportsDictConversion

Bases: Protocol

Protocol for objects that can be converted to dict (e.g., Pydantic models).

model_dump

model_dump()

Convert model to dictionary.

Source code in src/core/models/validators.py
def model_dump(self) -> dict[str, Any]:
    """Convert model to dictionary."""
    ...

SecurityValidationError

SecurityValidationError(
    message, field=None, dangerous_pattern=None
)

Bases: Exception

Exception raised when security validation fails.

Initialize the security validation error.

Parameters:

Name Type Description Default
message str

Error message describing the validation failure

required
field str | None

The field that failed validation

None
dangerous_pattern str | None

The specific pattern that triggered the error

None
Source code in src/core/models/validators.py
def __init__(
    self,
    message: str,
    field: str | None = None,
    dangerous_pattern: str | None = None,
) -> None:
    """Initialize the security validation error.

    Args:
        message: Error message describing the validation failure
        field: The field that failed validation
        dangerous_pattern: The specific pattern that triggered the error

    """
    super().__init__(message)
    self.field = field
    self.dangerous_pattern = dangerous_pattern

SecurityValidator

SecurityValidator(logger=None)

Comprehensive security validator for input sanitization and validation.

This class provides methods to validate and sanitize various types of input to prevent security vulnerabilities including injection attacks, path traversal, and malicious content.

Initialize the security validator.

Parameters:

Name Type Description Default
logger Logger | None

Optional logger instance for security event logging

None
Source code in src/core/models/validators.py
def __init__(self, logger: logging.Logger | None = None) -> None:
    """Initialize the security validator.

    Args:
        logger: Optional logger instance for security event logging

    """
    self.logger = logger or logging.getLogger(__name__)
    self._sql_patterns = [re.compile(pattern) for pattern in SQL_INJECTION_PATTERNS]
    self._xss_patterns = [re.compile(pattern) for pattern in XSS_PATTERNS]

validate_track_data

validate_track_data(track_data)

Validate and sanitize track data dictionary.

Parameters:

Name Type Description Default
track_data dict[str, Any]

Track data dictionary to validate

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Validated and sanitized track data

Raises:

Type Description
SecurityValidationError

If validation fails

Source code in src/core/models/validators.py
def validate_track_data(self, track_data: dict[str, Any]) -> dict[str, Any]:
    """Validate and sanitize track data dictionary.

    Args:
        track_data: Track data dictionary to validate

    Returns:
        dict[str, Any]: Validated and sanitized track data

    Raises:
        SecurityValidationError: If validation fails

    """
    validated_data: dict[str, Any] = {}

    # Required string fields (matching TrackDict model)
    required_fields = ["id", "artist", "name", "album"]
    for field in required_fields:
        if field not in track_data:
            msg = f"Required field '{field}' is missing"
            raise SecurityValidationError(msg, field)

        value = track_data[field]
        if not isinstance(value, str):
            msg = f"Field '{field}' must be a string"
            raise SecurityValidationError(msg, field)

        validated_data[field] = self.sanitize_string(value, field)

    # Optional string fields (matching TrackDict model)
    optional_fields = [
        "genre",
        "year",
        "date_added",
        "track_status",
        "year_before_mgu",
        "release_year",
        "year_set_by_mgu",
        "album_artist",
    ]
    for field in optional_fields:
        if field in track_data and track_data[field] is not None:
            value = track_data[field]
            if isinstance(value, str):
                validated_data[field] = self.sanitize_string(value, field)
            else:
                validated_data[field] = str(value)
        elif field in track_data:
            validated_data[field] = None

    # Special validation for track ID
    if "id" in validated_data:
        SecurityValidator._validate_track_id_format(validated_data["id"])

    self.logger.debug("Validated track data for ID: %s", validated_data.get("id", "unknown"))
    return validated_data

sanitize_string staticmethod

sanitize_string(value, field_name=None)

Sanitize a string value by removing dangerous characters and patterns.

Parameters:

Name Type Description Default
value str

The string value to sanitize

required
field_name str | None

Optional field name for logging

None

Returns:

Type Description
str

The sanitized string

Raises:

Type Description
SecurityValidationError

If validation fails

Source code in src/core/models/validators.py
@staticmethod
def sanitize_string(value: str, field_name: str | None = None) -> str:
    """Sanitize a string value by removing dangerous characters and patterns.

    Args:
        value: The string value to sanitize
        field_name: Optional field name for logging

    Returns:
        The sanitized string

    Raises:
        SecurityValidationError: If validation fails

    """
    # Check string length
    if len(value) > MAX_STRING_LENGTH:
        msg = f"String too long: {len(value)} characters (max: {MAX_STRING_LENGTH})"
        raise SecurityValidationError(
            msg,
            field_name,
        )

    # Remove control characters
    sanitized = value
    for char in CONTROL_CHARS:
        sanitized = sanitized.replace(char, "")

    return sanitized.strip()

validate_file_path

validate_file_path(file_path, allowed_base_paths=None)

Validate a file path to prevent path traversal attacks.

Parameters:

Name Type Description Default
file_path str

The file path to validate

required
allowed_base_paths list[str] | None

Optional list of allowed base paths

None

Returns:

Type Description
str

The validated and normalized file path

Raises:

Type Description
SecurityValidationError

If validation fails

Source code in src/core/models/validators.py
def validate_file_path(self, file_path: str, allowed_base_paths: list[str] | None = None) -> str:
    """Validate a file path to prevent path traversal attacks.

    Args:
        file_path: The file path to validate
        allowed_base_paths: Optional list of allowed base paths

    Returns:
        The validated and normalized file path

    Raises:
        SecurityValidationError: If validation fails

    """
    if not file_path.strip():
        msg = "File path cannot be empty"
        raise SecurityValidationError(msg)

    # Check path length
    if len(file_path) > MAX_PATH_LENGTH:
        msg = f"File path too long: {len(file_path)} characters"
        raise SecurityValidationError(msg)

    try:
        return self._validate_and_normalize_path(
            file_path,
            allowed_base_paths,
        )
    except (ValueError, OSError) as e:
        msg = f"Invalid file path: {e}"
        raise SecurityValidationError(msg) from e

validate_api_input

validate_api_input(data, max_depth=10, _current_depth=0)

Validate and sanitize API input data.

Parameters:

Name Type Description Default
data dict[str, Any]

Input data dictionary to validate

required
max_depth int

Maximum recursion depth allowed (default: 10)

10
_current_depth int

Internal parameter for tracking recursion depth

0

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Validated and sanitized data

Raises:

Type Description
SecurityValidationError

If validation fails or max depth exceeded

Source code in src/core/models/validators.py
def validate_api_input(
    self,
    data: dict[str, Any],
    max_depth: int = 10,
    _current_depth: int = 0,
) -> dict[str, Any]:
    """Validate and sanitize API input data.

    Args:
        data: Input data dictionary to validate
        max_depth: Maximum recursion depth allowed (default: 10)
        _current_depth: Internal parameter for tracking recursion depth

    Returns:
        dict[str, Any]: Validated and sanitized data

    Raises:
        SecurityValidationError: If validation fails or max depth exceeded

    """
    # Check recursion depth to prevent deeply nested or cyclic structures
    if _current_depth >= max_depth:
        msg = f"Maximum nesting depth ({max_depth}) exceeded"
        raise SecurityValidationError(msg)

    validated_data: dict[str, Any] = {}

    for key, value in data.items():
        # Validate key
        sanitized_key = self.sanitize_string(str(key), "api_key")

        # Validate value based on type
        sanitized_value: Any
        if isinstance(value, str):
            sanitized_value = self.sanitize_string(value, sanitized_key)
        elif isinstance(value, int | float | bool):
            sanitized_value = value
        elif isinstance(value, list):
            sanitized_value = [(self.sanitize_string(str(item), f"{sanitized_key}_item") if isinstance(item, str) else item) for item in value]
        elif isinstance(value, dict):
            sanitized_value = self.validate_api_input(
                value,
                max_depth=max_depth,
                _current_depth=_current_depth + 1,
            )
        elif value is None:
            sanitized_value = None
        else:
            sanitized_value = self.sanitize_string(str(value), sanitized_key)

        validated_data[sanitized_key] = sanitized_value

    return validated_data

is_valid_year

is_valid_year(year_str, min_year=None, current_year=None)

Check if the given value is a valid 4-digit year.

Parameters:

Name Type Description Default
year_str str | float | None

Value to validate as a year

required
min_year int | None

Minimum valid year (default: 1900)

None
current_year int | None

Current year for upper bound (default: current year)

None

Returns:

Type Description
bool

True if valid year, False otherwise.

Source code in src/core/models/validators.py
def is_valid_year(year_str: str | float | None, min_year: int | None = None, current_year: int | None = None) -> bool:
    """Check if the given value is a valid 4-digit year.

    Args:
        year_str: Value to validate as a year
        min_year: Minimum valid year (default: 1900)
        current_year: Current year for upper bound (default: current year)

    Returns:
        True if valid year, False otherwise.

    """
    if year_str is None:
        return False

    try:
        # Handle different input types
        year_int = int(year_str) if isinstance(year_str, int | float) else int(str(year_str).strip())

        # Use defaults if not provided
        min_year_val = min_year or 1900
        current_year_val = current_year or datetime.now(tz=UTC).year

        # Trust the system - if datetime accepts it, it's valid
        datetime(year_int, 1, 1, tzinfo=UTC)
        return min_year_val <= year_int <= current_year_val

    except (ValueError, TypeError, OverflowError, OSError):
        return False

is_empty_year

is_empty_year(year_value)

Check if a year value is considered empty.

Parameters:

Name Type Description Default
year_value Any

Year value to check

required

Returns:

Type Description
bool

True if the year is empty (None, empty string, or whitespace-only)

Source code in src/core/models/validators.py
def is_empty_year(year_value: Any) -> bool:
    """Check if a year value is considered empty.

    Args:
        year_value: Year value to check

    Returns:
        True if the year is empty (None, empty string, or whitespace-only)

    """
    return not year_value or not str(year_value).strip()

is_valid_track_item

is_valid_track_item(item)

Validate that the given object is a track dictionary.

This performs runtime type checking to ensure the object has the required structure of a TrackDict.

Parameters:

Name Type Description Default
item Any

Object to validate

required

Returns:

Type Description
TypeGuard[TrackDict]

True if the object is a valid TrackDict, False otherwise

Source code in src/core/models/validators.py
def is_valid_track_item(item: Any) -> TypeGuard[TrackDict]:
    """Validate that the given object is a track dictionary.

    This performs runtime type checking to ensure the object
    has the required structure of a TrackDict.

    Args:
        item: Object to validate

    Returns:
        True if the object is a valid TrackDict, False otherwise

    """
    track_data = _convert_to_track_dict(item)
    return track_data is not None and _validate_track_fields(track_data)

validate_track_ids

validate_track_ids(track_ids, year)

Return list of numeric track IDs that are not equal to the year value.

Parameters:

Name Type Description Default
track_ids list[str]

List of track IDs to validate

required
year str

Year value to check against

required

Returns:

Type Description
list[str]

List of valid track IDs

Source code in src/core/models/validators.py
def validate_track_ids(track_ids: list[str], year: str) -> list[str]:
    """Return list of numeric track IDs that are not equal to the year value.

    Args:
        track_ids: List of track IDs to validate
        year: Year value to check against

    Returns:
        List of valid track IDs

    """
    valid_ids: list[str] = []
    valid_ids.extend(track_id for track_id in track_ids if track_id.isdigit() and track_id != year)
    return valid_ids

validate_artist_name

validate_artist_name(artist)

Validate artist name.

Parameters:

Name Type Description Default
artist str | None

Artist name to validate

required

Returns:

Type Description
bool

True if valid, False otherwise

Source code in src/core/models/validators.py
def validate_artist_name(artist: str | None) -> bool:
    """Validate artist name.

    Args:
        artist: Artist name to validate

    Returns:
        True if valid, False otherwise

    """
    if not artist:
        return False

    # Strip and check if not empty
    cleaned = artist.strip()
    return len(cleaned) > 0

validate_album_name

validate_album_name(album)

Validate album name.

Parameters:

Name Type Description Default
album str | None

Album name to validate

required

Returns:

Type Description
bool

True if valid, False otherwise

Source code in src/core/models/validators.py
def validate_album_name(album: str | None) -> bool:
    """Validate album name.

    Args:
        album: Album name to validate

    Returns:
        True if valid, False otherwise

    """
    if not album:
        return False

    # Strip and check if not empty
    cleaned = album.strip()
    return len(cleaned) > 0