animedex.transport.ratelimit

Per-backend rate limiting for animedex’s outgoing HTTP traffic.

This module provides three pieces:

  • TokenBucket - a small, monotonic-clock-driven token bucket with both non-blocking (TokenBucket.try_acquire()) and blocking (TokenBucket.acquire()) variants. Backends use the blocking form on the request hot path; try_acquire() is for transparent fast-fail in tests and back-pressure logic.

  • RateLimitRegistry - a name-keyed map from backend identifier to its configured bucket. The substrate ships exactly one default registry (default_registry()) so the per-backend caps from plans/01 and the P1 obligations from plans/02 live in one place.

  • default_registry() - the wired-up registry with the caps every backend documented in plans/01-public-apis-anime-survey.md honours.

The Rate.slow override (CLI flag --rate slow) halves the refill rate; we never expose a faster-than-default mode because the upstream contract is a P1 ceiling, not a preference.

The module pulls its clock primitives through two indirection points, _monotonic and _sleep, so unit tests can substitute a deterministic fake without monkeypatching the standard library globally.

TokenBucket

class animedex.transport.ratelimit.TokenBucket(capacity: int, refill_per_second: float)[source]

Bases: object

A monotonic-clock token bucket.

The bucket holds at most capacity tokens and accumulates them at refill_per_second per second. Each acquire consumes one token. The implementation is thread-safe and uses _monotonic / _sleep rather than direct time calls so tests can swap in a fake clock.

Parameters:
  • capacity (int) – Maximum number of tokens the bucket can hold. Must be positive.

  • refill_per_second (float) – Steady-state refill rate, in tokens per second. Must be positive.

Raises:

ValueError – When capacity or refill_per_second is not strictly positive.

__init__(capacity: int, refill_per_second: float) None[source]
try_acquire() bool[source]

Consume a token without blocking.

Returns:

True if a token was available and consumed, False otherwise.

Return type:

bool

acquire() None[source]

Consume a token, blocking via _sleep until one is available.

Used by every real-request hot path. Sleep is computed from the deficit and the refill rate, so we wake exactly when the next token arrives - there is no polling.

Returns:

None.

Return type:

None

with_rate(mode: str) TokenBucket[source]

Return a bucket with the requested rate-mode applied.

"normal" returns self unchanged. "slow" returns a new bucket with the refill rate halved. We never support a faster mode because that would violate the upstream P1 ceiling.

Parameters:

mode (str) – "normal" or "slow".

Returns:

A bucket honouring the requested mode.

Return type:

TokenBucket

Raises:

ValueError – When mode is unrecognised.

RateLimitRegistry

class animedex.transport.ratelimit.RateLimitRegistry[source]

Bases: object

Per-backend bucket map.

A backend identifier (the same short string used in SourceTag) maps to one TokenBucket. register() is idempotent across the same name; get() raises ApiError when the backend is unknown so a typo at the call site fails loudly.

Variables:

_buckets (dict) – Internal mapping from backend identifier to bucket.

__init__() None[source]
register(name: str, *, capacity: int, refill_per_second: float) TokenBucket[source]

Register or replace a backend’s bucket.

Parameters:
  • name (str) – Backend identifier.

  • capacity (int) – Bucket capacity.

  • refill_per_second (float) – Refill rate.

Returns:

The registered bucket.

Return type:

TokenBucket

get(name: str) TokenBucket[source]

Look up a backend’s bucket.

Parameters:

name (str) – Backend identifier.

Returns:

The registered bucket.

Return type:

TokenBucket

Raises:

ApiError – When name is not registered.

default_registry

animedex.transport.ratelimit.default_registry() RateLimitRegistry[source]

Build the project-wide default rate-limit registry.

The caps reflect what each upstream actually enforces, not what we wish for; see plans/01 per-source notes. Anything not listed here either has no documented cap or ships its own persistent scheduler (AniDB).

Returns:

A registry pre-populated with every public backend.

Return type:

RateLimitRegistry

selftest

animedex.transport.ratelimit.selftest() bool[source]

Smoke-test the bucket and the default registry.

Builds a small bucket, exercises both acquire variants, builds the default registry, and verifies every plan-01 backend resolves. Does not call real time.sleep; the bucket is sized to fit the capacity so no waits are required.

Returns:

True on success.

Return type:

bool