Skip to content

feat: add per-endpoint rate limiting#156

Merged
NandaScott merged 19 commits intodevelopfrom
feature/per-endpoint-rate-limiting
Apr 7, 2026
Merged

feat: add per-endpoint rate limiting#156
NandaScott merged 19 commits intodevelopfrom
feature/per-endpoint-rate-limiting

Conversation

@NandaScott
Copy link
Copy Markdown
Owner

@NandaScott NandaScott commented Apr 6, 2026

Scryfall API rate limit docs: https://scryfall.com/docs/api/rate-limits

Summary

  • Add per-endpoint rate limiting to comply with Scryfall tiered rate limits (2/s for search, named, random, collection; 10/s for all other endpoints)
  • Introduce SlowRateLimiter subclass with 2.0 calls/second default
  • Change RateLimiter singleton to per-class registry so each rate tier maintains independent state
  • Add _rate_limiter_class to ScrythonRequestHandler so endpoint classes declare their rate tier
  • No public API changes; existing rate_limit_per_second kwarg and rate_limit=False still work

Test plan

  • SlowRateLimiter unit tests (default rate, 500ms timing enforcement)
  • Registry tests (per-class isolation, reset clears all tiers)
  • Integration tests (slow handler timing, kwarg override, class default)
  • Endpoint assignment tests (4 slow endpoints verified, 2 fast endpoints verified)
  • Full suite passes (410 passed, 1 pre-existing skip)

Scryfall now enforces tiered rate limits: 2/s for /cards/search,
/cards/named, /cards/random, /cards/collection, and 10/s for all
other endpoints. The four slow endpoints share a single 2/s budget.

- Add SlowRateLimiter subclass with 2.0 calls/second default
- Change RateLimiter singleton to per-class registry so each rate
  tier maintains independent state
- Add _rate_limiter_class to ScrythonRequestHandler so endpoint
  classes declare their rate tier
- Assign SlowRateLimiter to Search, Named, Random, Collection
- rate_limit_per_second kwarg still overrides per-request
- rate_limit=False still disables entirely
- No public API changes; existing user code works without modification
@claude

This comment was marked as outdated.

This comment was marked as resolved.

The rate_limit_per_second kwarg was creating a new RateLimiter per
request, so consecutive calls never throttled against each other.
Use a shared registry keyed by (class, rate) so custom rates enforce
across calls. Also rename reset_global_limiter to reset_all_limiters
to reflect that it clears the entire registry.
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Instead of caching override limiters in the global registry (which
grows unbounded with unique rate values), store the override limiter
on the handler instance. This keeps pagination throttled within a
single handler while avoiding an unbounded global cache.

Removes get_global_limiter_for_rate() and simplifies the registry
type back to dict[type, RateLimiter].
Lower bounds prove correctness (rate limiting works). Upper bounds
only assert "not absurdly slow" and should be generous enough to
pass on heavily loaded CI runners.
@claude

This comment was marked as outdated.

Add __init__ docstring explaining that rate_limit_per_second creates
a per-instance limiter scoped to the handler's lifecycle. Also widen
test_rate_limit_can_be_disabled upper bound for CI reliability.
@claude

This comment was marked as outdated.

…ide throttling

- get_global_limiter() now accepts the old calls_per_second param
  with a DeprecationWarning (previously would TypeError)
- Add test proving per-instance override limiter throttles repeated
  calls within the same handler (e.g., pagination)
- Remove stale rate_limit_per_second reference from iter_all docstring
@claude

This comment was marked as outdated.

The kwarg is only effective at construction time. Passing it to
iter_all() was silently ignored. Now emits a UserWarning and strips
the kwarg. Also adds a note to the README that the override is
scoped to the handler instance.
@claude

This comment was marked as outdated.

Add 2.0.1 changelog entry documenting new rate limiting features,
behavioral changes, and deprecations. Update CLAUDE.md to reflect
built-in rate limiting, correct Python version, and add rate_limiter
and cache modules to the file structure.
@claude

This comment was marked as resolved.

@claude

This comment was marked as resolved.

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

test comment - ignore

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review - Part 1 of 5: Overall, this is a well-structured addition. The per-class registry design is clean, the deprecation warnings are properly implemented, and the test coverage is thorough. A few things worth flagging before merging (continued in follow-up comments):

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Bug: _override_limiter always creates base RateLimiter - In scrython/base.py, when rate_limit_per_second is provided, the override limiter is always a RateLimiter instance regardless of the endpoint's _rate_limiter_class. This means the override limiter ignores any future behavior added to SlowRateLimiter subclasses. A more consistent approach would be: self._override_limiter = self._rate_limiter_class(rate_limit_per_second). At minimum this should be documented.

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

deleting test

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

test - ignore

1 similar comment
@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

test - ignore

@claude

This comment was marked as resolved.

@NandaScott NandaScott merged commit 5f86797 into develop Apr 7, 2026
7 of 8 checks passed
@NandaScott NandaScott deleted the feature/per-endpoint-rate-limiting branch April 7, 2026 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants