Source code for animedex.backends.trace

"""High-level Trace.moe Python API.

Exposes screenshot search (URL or upload bytes) plus the quota probe.
The common-projection :class:`~animedex.models.trace.TraceQuota`
returned by :func:`quota` drops the upstream's caller-IP echo (the
``/me`` ``id`` field) — callers who want it can use the rich
:class:`~animedex.backends.trace.models.RawTraceQuota` directly.
"""

from __future__ import annotations

import json as _json
from datetime import datetime, timezone
from typing import List, Optional

from animedex.api import trace as _raw_trace
from animedex.config import Config
from animedex.models.anime import AnimeTitle
from animedex.models.common import ApiError, SourceTag, require_field as _common_require_field
from animedex.models.trace import TraceHit, TraceQuota


def _field(row, key: str, what: str):
    """Trace-flavoured wrapper around :func:`require_field` —
    pre-applies the backend label so call sites stay short."""
    return _common_require_field(row, key, backend="trace", what=what)


def _src(envelope) -> SourceTag:
    return SourceTag(
        backend="trace",
        fetched_at=datetime.now(timezone.utc),
        cached=envelope.cache.hit,
        rate_limited=envelope.timing.rate_limit_wait_ms > 0,
    )


def _parse(envelope) -> dict:
    if envelope.firewall_rejected is not None:  # pragma: no cover
        raise ApiError(
            envelope.firewall_rejected.get("message", "request blocked"),
            backend="trace",
            reason=envelope.firewall_rejected.get("reason", "firewall"),
        )
    if envelope.body_text is None:
        raise ApiError("Trace.moe returned a non-text body", backend="trace", reason="upstream-decode")
    return _json.loads(envelope.body_text)


def _coerce_int(v) -> int:
    if isinstance(v, int):
        return v
    if isinstance(v, str):
        return int(v)
    raise ApiError(f"unexpected non-int value: {v!r}", backend="trace", reason="upstream-shape")


[docs] def quota(*, config: Optional[Config] = None, **kw) -> TraceQuota: """Fetch ``/me``: caller's quota state. Returns the cross-source projection :class:`TraceQuota`, which omits the upstream's ``id`` field by design — the common shape is the lowest-common-denominator across backends and IP echoes don't have a place there. A caller who wants the upstream payload as-is (including ``id``) can reach for the rich :class:`~animedex.backends.trace.models.RawTraceQuota` directly: it round-trips the upstream verbatim. """ raw = _raw_trace.call(path="/me", config=config, **kw) payload = _parse(raw) return TraceQuota( priority=int(_field(payload, "priority", "/me")), concurrency=int(_field(payload, "concurrency", "/me")), quota=int(_field(payload, "quota", "/me")), quota_used=_coerce_int(_field(payload, "quotaUsed", "/me")), source=_src(raw), )
[docs] def selftest() -> bool: """Smoke-test the Trace.moe API (signatures only, no network).""" import inspect for fn in (quota, search): sig = inspect.signature(fn) assert "config" in sig.parameters, f"{fn.__name__} missing config kwarg" return True