"""Provider abstraction (section 13). A `MetadataProvider` fetches **raw**, unnormalized data from one external source. It never writes to the database and never returns Aurora entities — that's the normalizer/matcher/ingestion pipeline's job (see `app/services/metadata/` and `app/services/ingestion/`). This keeps adapters replaceable: swapping TMDb for another provider means writing a new class here, not touching ingestion logic. """ from __future__ import annotations from abc import ABC, abstractmethod from typing import Any class ProviderError(Exception): """Raised for any provider-side failure (auth, not-found, rate limit, network). Ingestion jobs catch this and record it, they don't crash.""" class ProviderNotConfiguredError(ProviderError): """Raised when a required credential (e.g. TMDB_API_KEY) is missing.""" class ProviderNotFoundError(ProviderError): """Raised when the provider has no record for the given external ID.""" class MetadataProvider(ABC): slug: str name: str @abstractmethod async def get_movie(self, external_id: str) -> dict[str, Any]: """Fetch raw movie data, including credits/images/videos/keywords/ alternate titles/external IDs/release dates where the provider supports fetching them in one call.""" @abstractmethod async def get_series(self, external_id: str) -> dict[str, Any]: """Fetch raw series data (without season/episode bodies).""" @abstractmethod async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]: """Fetch raw season data, including its episodes.""" @abstractmethod async def get_person(self, external_id: str) -> dict[str, Any]: """Fetch raw person data, including external IDs and images.""" @abstractmethod async def get_collection(self, external_id: str) -> dict[str, Any]: """Fetch raw collection/franchise data: name, overview, poster/ backdrop, and its member movies (TMDb's "parts").""" @abstractmethod async def search_movie(self, query: str) -> list[dict[str, Any]]: """Search this provider's catalog for movies matching `query`. Used to seed ingestion, not exposed directly to API clients.""" async def aclose(self) -> None: """Release any held resources (HTTP clients, ...). Optional."""