animedex.models.common
Ground-floor types every animedex backend, model, and renderer depends on.
This module owns the types that participate in the source-attribution
contract from plans/03-cli-architecture-gh-flavored.md (the
SourceTag carrier; AnimedexModel base) and the
small typed surface the library uses to talk about HTTP-shaped facts
(Pagination, RateLimit, ApiError).
The pydantic v2 base class AnimedexModel fixes a single,
project-wide configuration: models are immutable (frozen=True),
silently ignore unknown upstream fields (extra='ignore'), and
accept both alias and field name on input (populate_by_name=True).
This is enforced once here so individual backend modules do not drift.
The module also exposes a selftest() callable the diagnostic
runner picks up; it instantiates representative samples of every type
to confirm the schema parses end-to-end.
REASONS
- animedex.models.common.REASONS = frozenset({'auth-required', 'bad-args', 'firewall', 'graphql-error', 'jq-failed', 'jq-missing', 'malformed-guidance', 'not-found', 'rate-limited', 'read-only', 'selftest', 'unknown-backend', 'unknown-field', 'upstream-decode', 'upstream-error', 'upstream-shape'})
The complete vocabulary of
ApiErrorreasonslugs. Library callers that want to branch on a typed error can match against this set; new sites that emitApiErrorshould pick one of these values rather than inventing a new slug, so the vocabulary stays stable for downstream consumers.ApiError’s__init__validatesreasonagainst this set so a typo surfaces at construction time rather than at error-handling time.
AnimedexModel
- class animedex.models.common.AnimedexModel[source]
Bases:
BaseModelProject-wide pydantic v2 base for common projection types.
Common types (e.g.
Anime,Character,Staff,Studio) are deliberately the “lowest common denominator” the project surfaces across multiple upstreams. They drop backend-specific fields by design — that is the whole point of a projection.extra='ignore'enforces this policy: any field a backend gives us that the projection didn’t declare is dropped silently.Backend-specific rich types (
AnilistAnime,JikanAnime, etc.) are NOT this. SeeBackendRichModelbelow — they must be lossless.- Variables:
model_config – pydantic configuration; do not override per subclass without a documented reason.
- model_config: ClassVar[ConfigDict] = {'extra': 'ignore', 'frozen': True, 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
BackendRichModel
- class animedex.models.common.BackendRichModel(**extra_data: Any)[source]
Bases:
AnimedexModelProject-wide pydantic v2 base for backend rich dataclasses.
Every per-backend rich type —
AnilistAnime,AnilistCharacter,AnilistStaff,AnilistStudio, the long-tail Anilist* types,JikanAnime,JikanCharacter,JikanPerson,JikanProducer,JikanClub,JikanUser,JikanGenericRow,RawTraceHit,RawTraceQuota, plus their nested helper types (e.g._AnilistTitle,JikanEntity,JikanAired) — inherits from this class.The contract: a rich model is information-lossless. When fed the raw upstream payload via
model_validate, it must retain every key the upstream returned, somodel_dump(by_alias=True, mode='json')reconstructs the original payload field-for-field.Achieved by:
extra='allow'— fields the model didn’t declare are kept on the instance and re-emitted on dump (instead of silently dropped, which is whatAnimedexModeldoes for common projections).populate_by_name=True— alias-renamed fields (e.g.from_: float = Field(alias='from')for the Python keyword conflict) accept either form on input.
The class inherits from
AnimedexModelrather than sitting beside it. Pydantic v2 mergesmodel_configacross inheritance, so theextra='allow'here overrides the parent’sextra='ignore'. The inheritance matters: everyisinstance(x, AnimedexModel)check downstream (the TTY dispatcher inanimedex.render.tty, the JSON renderer, the CLI’s_to_tty_texthelper) must continue to recognise rich instances. Splitting the two roots was a bug that made rich models fall through tostr(x)and dump pydantic__repr__instead of human-friendly TTY text.Backend-rich → common projection happens via the rich type’s
to_common()method. Loss of fields is permitted at and only at that boundary.- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'frozen': True, 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
SourceTag
- class animedex.models.common.SourceTag(*, backend: str, fetched_at: datetime, cached: bool = False, rate_limited: bool = False)[source]
Bases:
AnimedexModelProvenance carrier attached to every backend-returned record.
Per
plans/03-cli-architecture-gh-flavored.md§5, every datum surfaced by animedex must carry the upstream that produced it.SourceTagis the typed form of that contract: the TTY renderer prints[src: <backend>]from it, the JSON renderer emits_sourcefrom it, and the cache layer records it.- Variables:
backend (str) – Short upstream identifier (e.g.
"anilist","jikan","kitsu").fetched_at (datetime.datetime) – When this datum was retrieved (or, for a cache hit, when it was originally retrieved).
cached (bool) –
Truewhen the value was served from local cache rather than a fresh upstream call.rate_limited (bool) –
Truewhen fetching this datum required waiting for a rate-limit window.
- fetched_at: datetime
PartialDate
- class animedex.models.common.PartialDate(*, year: int | None = None, month: int | None = None, day: int | None = None)[source]
Bases:
AnimedexModelA date where any of year/month/day may be unknown.
AniList’s
dateOfBirth/startDate/endDateshapes return{ year, month, day }with each component independently nullable. A character may have a known birth-month but no day; a series may be year-only when the exact air date isn’t recorded.datetime.datecannot represent that, so this lighter- weight type stands in.- Variables:
Pagination
RateLimit
- class animedex.models.common.RateLimit(*, remaining: int | None = None, reset_at: datetime)[source]
Bases:
AnimedexModelSnapshot of an upstream’s rate-limit posture for a single call.
- Variables:
remaining (int or None) – Calls remaining in the current window when reported.
reset_at (datetime.datetime) – When the current window resets.
- reset_at: datetime
ApiError
- class animedex.models.common.ApiError(message: str, *, backend: str | None = None, reason: str | None = None)[source]
Bases:
ExceptionTyped error raised by the transport, cache, and policy layers.
Carries a structured
backendandreasonso the CLI can render a stable, machine-grepable message without parsing free text. The string form of the exception always includes both fields when set.- Parameters:
message (str) – Human-readable description of the failure.
backend (str or None) – Backend identifier when the failure is backend-specific.
reason (str or None) – Short slug categorising the failure. Must be one of the values in
REASONS. The full vocabulary is fixed by the project (so library consumers can branch on a known set); a typo at construction time raisesValueError.
- Raises:
ValueError – When
reasonis set but not inREASONS.
require_field
- animedex.models.common.require_field(row: dict, key: str, *, backend: str, what: str)[source]
Return
row[key]or raiseApiError(upstream-shape).Used inside mapper code where a particular field is expected to be populated on every row (e.g.
idon a search-result row, orpriorityon the/meenvelope). A plainrow[key]would crash withKeyErrorand leak the failure point as an internal exception type; this helper converts that to a typed error the dispatcher / CLI / library callers know how to catch. Surfaces upstream schema drift asreason='upstream-shape'rather than as a Python exception class.- Parameters:
row – One dict from an upstream response (a list-row or a single-entity body).
key – The required field name on the row.
backend – Backend identifier — appears on the raised error.
what – Short label for the row shape (e.g.
"review","/me") — appears in the error message.
- Raises:
ApiError – When
keyis absent fromrow.reason='upstream-shape'.
selftest
- animedex.models.common.selftest() bool[source]
Smoke-test the types end-to-end.
The diagnostic runner (
animedex.diag.run_selftest()) invokes this; the function instantiates each public type so that pydantic schema construction is exercised. Anything that would fail at first use - missing field, wrong default, broken alias - surfaces here in milliseconds, before the CLI tries to render a real backend response.- Returns:
Truewhen the model graph parses. Raises on schema errors so the runner reports them with a traceback.- Return type: