"""Mixin for Trakt identifier validation in Pydantic models."""
import re
from typing import ClassVar, Self
from pydantic import (
BaseModel,
ConfigDict,
Field,
ValidationInfo,
field_validator,
model_validator,
)
class IdentifierValidatorMixin(BaseModel):
"""Mixin that validates Trakt identifiers.
This mixin provides field and model validators for common Trakt identifier fields.
It validates:
- trakt_id: Must be numeric
- tmdb_id: Must be numeric
- tvdb_id: Must be numeric
- imdb_id: Must match tt + digits format (e.g., tt0468569)
- slug: Any non-empty string
- At least one identifier OR both title+year must be provided
Classes using this mixin must define these fields:
- trakt_id: str | None
- slug: str | None
- imdb_id: str | None
- tmdb_id: str | None
- tvdb_id: str | None
- title: str | None
- year: int | None
Classes can customize the error message by setting:
- _identifier_error_prefix: ClassVar[str] = "Item"
"""
model_config = ConfigDict(
json_schema_extra={
"examples": [
{"trakt_id": "120"},
{"slug": "the-dark-knight-2008"},
{"imdb_id": "tt0468569"},
{"tmdb_id": "155"},
{"title": "The Dark Knight", "year": 2008},
]
}
)
# Fields with default values for mixin compatibility
trakt_id: str | None = Field(
default=None,
min_length=1,
description="Trakt ID (numeric string)",
json_schema_extra={"examples": ["120", "16662"]},
)
slug: str | None = Field(
default=None,
min_length=1,
description="Trakt slug",
json_schema_extra={"examples": ["the-dark-knight-2008", "inception-2010"]},
)
imdb_id: str | None = Field(
default=None,
min_length=1,
description="IMDB ID (format: 'tt' + digits)",
json_schema_extra={"examples": ["tt0468569", "tt1375666"]},
)
tmdb_id: str | None = Field(
default=None,
min_length=1,
description="TMDB ID (numeric string)",
json_schema_extra={"examples": ["155", "27205"]},
)
tvdb_id: str | None = Field(
default=None,
min_length=1,
description="TVDB ID (numeric string, for TV shows only)",
json_schema_extra={"examples": ["81189"]},
)
title: str | None = Field(
default=None,
min_length=1,
description="Title (use with 'year' instead of identifiers)",
json_schema_extra={"examples": ["The Dark Knight", "Inception"]},
)
year: int | None = Field(
default=None,
gt=1800,
description="Release year (use with 'title' instead of identifiers)",
json_schema_extra={"examples": [2008, 2010]},
)
# Class variable for customizable error message prefix
_identifier_error_prefix: ClassVar[str] = "Item"
@field_validator(
"trakt_id", "slug", "imdb_id", "tmdb_id", "tvdb_id", "title", mode="before"
)
@classmethod
def _strip_strings(cls, v: object) -> object:
"""Strip whitespace from string fields."""
return v.strip() if isinstance(v, str) else v
@field_validator("trakt_id", "tmdb_id", "tvdb_id", mode="after")
@classmethod
def _validate_numeric_ids(cls, v: str | None, info: ValidationInfo) -> str | None:
"""Ensure numeric ID fields are numeric if provided."""
if v is not None and not v.isdigit():
raise ValueError(f"{info.field_name} must be numeric, got: '{v}'")
return v
@field_validator("imdb_id", mode="after")
@classmethod
def _validate_imdb_id_format(cls, v: str | None) -> str | None:
"""Ensure imdb_id follows tt + digits format if provided."""
if v is not None and not re.match(r"^tt\d+$", v):
raise ValueError(
f"imdb_id must be in format 'tt' followed by digits (e.g., 'tt0468569'), got: '{v}'"
)
return v
@model_validator(mode="after")
def _validate_identifiers(self) -> Self:
"""Ensure at least one identifier OR both title+year are provided."""
has_id = any(
[self.trakt_id, self.slug, self.imdb_id, self.tmdb_id, self.tvdb_id]
)
has_title_year = self.title and self.year
if not has_id and not has_title_year:
prefix = self._identifier_error_prefix
raise ValueError(
f"{prefix} must include either an identifier (trakt_id, slug, imdb_id, tmdb_id, or tvdb_id) "
+ "or both title and year for proper identification"
)
return self
def build_ids_dict(self) -> dict[str, str | int]:
"""Build IDs dict for Trakt API requests.
Converts identifier fields to API format:
- trakt_id, tmdb_id, tvdb_id → integers
- imdb_id, slug → strings (unchanged)
Returns:
Dict with API-formatted IDs (empty if no identifiers set)
"""
ids: dict[str, str | int] = {}
if self.trakt_id:
ids["trakt"] = int(self.trakt_id)
if self.slug:
ids["slug"] = self.slug
if self.imdb_id:
ids["imdb"] = self.imdb_id
if self.tmdb_id:
ids["tmdb"] = int(self.tmdb_id)
if self.tvdb_id:
ids["tvdb"] = int(self.tvdb_id)
return ids