Task 4.5: Update Pydantic models to use key as primary identifier
- Updated AnimeSeriesResponse and SearchResult models in anime.py: - Changed 'id' field to 'key' as the primary series identifier - Added 'folder' as optional metadata field - Added field validator to normalize key to lowercase and strip whitespace - Added comprehensive docstrings explaining identifier usage - Updated DownloadItem and DownloadRequest models in download.py: - Added field validator for serie_id normalization (lowercase, stripped) - Improved documentation for serie_id (primary identifier) vs serie_folder (metadata) - Updated test_anime_models.py with comprehensive tests: - Tests for key normalization and whitespace stripping - Tests for folder as optional metadata - Reorganized tests into proper class structure - Updated test_download_models.py with validator tests: - Tests for serie_id normalization in DownloadItem - Tests for serie_id normalization in DownloadRequest All 885 tests pass.
This commit is contained in:
@@ -1,9 +1,24 @@
|
||||
"""Anime Pydantic models for the Aniworld web application.
|
||||
|
||||
This module defines request/response models used by the anime API
|
||||
and services. Models are focused on serialization, validation,
|
||||
and OpenAPI documentation.
|
||||
|
||||
Note on identifiers:
|
||||
- key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
|
||||
- folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from pydantic import BaseModel, Field, HttpUrl, field_validator
|
||||
|
||||
|
||||
# Regex pattern for valid series keys (URL-safe, lowercase with hyphens)
|
||||
KEY_PATTERN = re.compile(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$')
|
||||
|
||||
|
||||
class EpisodeInfo(BaseModel):
|
||||
@@ -31,10 +46,30 @@ class MissingEpisodeInfo(BaseModel):
|
||||
|
||||
|
||||
class AnimeSeriesResponse(BaseModel):
|
||||
"""Response model for a series with metadata and episodes."""
|
||||
"""Response model for a series with metadata and episodes.
|
||||
|
||||
Note on identifiers:
|
||||
- key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
|
||||
This is the unique key used for all lookups and operations.
|
||||
- folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
|
||||
Used only for display and filesystem operations.
|
||||
"""
|
||||
|
||||
id: str = Field(..., description="Unique series identifier")
|
||||
key: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Series key (primary identifier) - provider-assigned URL-safe "
|
||||
"key (e.g., 'attack-on-titan'). Used for lookups/identification."
|
||||
)
|
||||
)
|
||||
title: str = Field(..., description="Series title")
|
||||
folder: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Series folder name on disk (metadata only) "
|
||||
"(e.g., 'Attack on Titan (2013)'). For display/filesystem ops only."
|
||||
)
|
||||
)
|
||||
alt_titles: List[str] = Field(default_factory=list, description="Alternative titles")
|
||||
description: Optional[str] = Field(None, description="Short series description")
|
||||
total_episodes: Optional[int] = Field(None, ge=0, description="Declared total episode count if known")
|
||||
@@ -42,20 +77,56 @@ class AnimeSeriesResponse(BaseModel):
|
||||
missing_episodes: List[MissingEpisodeInfo] = Field(default_factory=list, description="Detected missing episode ranges")
|
||||
thumbnail: Optional[HttpUrl] = Field(None, description="Optional thumbnail image URL")
|
||||
|
||||
@field_validator('key', mode='before')
|
||||
@classmethod
|
||||
def normalize_key(cls, v: str) -> str:
|
||||
"""Normalize key to lowercase."""
|
||||
if isinstance(v, str):
|
||||
return v.lower().strip()
|
||||
return v
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request payload for searching series."""
|
||||
|
||||
query: str = Field(..., min_length=1)
|
||||
limit: int = Field(10, ge=1, le=100)
|
||||
include_adult: bool = Field(False)
|
||||
query: str = Field(..., min_length=1, description="Search query string")
|
||||
limit: int = Field(10, ge=1, le=100, description="Maximum number of results")
|
||||
include_adult: bool = Field(False, description="Include adult content in results")
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
"""Search result item for a series discovery endpoint."""
|
||||
"""Search result item for a series discovery endpoint.
|
||||
|
||||
Note on identifiers:
|
||||
- key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
|
||||
This is the unique key used for all lookups and operations.
|
||||
- folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
|
||||
Used only for display and filesystem operations.
|
||||
"""
|
||||
|
||||
id: str
|
||||
title: str
|
||||
snippet: Optional[str] = None
|
||||
thumbnail: Optional[HttpUrl] = None
|
||||
score: Optional[float] = None
|
||||
key: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Series key (primary identifier) - provider-assigned URL-safe "
|
||||
"key (e.g., 'attack-on-titan'). Used for lookups/identification."
|
||||
)
|
||||
)
|
||||
title: str = Field(..., description="Series title")
|
||||
folder: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Series folder name on disk (metadata only) "
|
||||
"(e.g., 'Attack on Titan (2013)'). For display/filesystem ops only."
|
||||
)
|
||||
)
|
||||
snippet: Optional[str] = Field(None, description="Short description or snippet")
|
||||
thumbnail: Optional[HttpUrl] = Field(None, description="Thumbnail image URL")
|
||||
score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Search relevance score (0-1)")
|
||||
|
||||
@field_validator('key', mode='before')
|
||||
@classmethod
|
||||
def normalize_key(cls, v: str) -> str:
|
||||
"""Normalize key to lowercase."""
|
||||
if isinstance(v, str):
|
||||
return v.lower().strip()
|
||||
return v
|
||||
|
||||
@@ -126,6 +126,14 @@ class DownloadItem(BaseModel):
|
||||
None, description="Source URL for download"
|
||||
)
|
||||
|
||||
@field_validator('serie_id', mode='before')
|
||||
@classmethod
|
||||
def normalize_serie_id(cls, v: str) -> str:
|
||||
"""Normalize serie_id (key) to lowercase and stripped."""
|
||||
if isinstance(v, str):
|
||||
return v.lower().strip()
|
||||
return v
|
||||
|
||||
|
||||
class QueueStatus(BaseModel):
|
||||
"""Overall status of the download queue system."""
|
||||
@@ -218,6 +226,14 @@ class DownloadRequest(BaseModel):
|
||||
return v.upper()
|
||||
return v
|
||||
|
||||
@field_validator('serie_id', mode='before')
|
||||
@classmethod
|
||||
def normalize_serie_id(cls, v: str) -> str:
|
||||
"""Normalize serie_id (key) to lowercase and stripped."""
|
||||
if isinstance(v, str):
|
||||
return v.lower().strip()
|
||||
return v
|
||||
|
||||
|
||||
class DownloadResponse(BaseModel):
|
||||
"""Response after adding items to the download queue."""
|
||||
|
||||
Reference in New Issue
Block a user