Skip to main content

Rate-limit declaration toolkit: throttle policies, tier rates, and declaration decorators for Python services

Project description

domain-api-limiter

PyPI CI License Python

Declarative rate-limit policy toolkit for Python services. Attach throttle policies to methods, parse rates, define tier overrides, and collect policies for consumption by framework-level throttling adapters.

Why domain-api-limiter?

Rate-limiting policies are declarations: they define what throttle rules apply to a service method, but they never enforce the limit themselves. Enforcement belongs in the consuming framework's vetted throttling logic (Django REST Framework throttles, Starlette middleware, etc.). domain-api-limiter keeps the declaration layer simple, immutable, and composable. Policies are frozen dataclasses, validated at import time so malformed rates fail fast. The @throttled decorator attaches policy metadata to any method without changing its signature. PolicyRegistry collects declared policies from a service class or module, returning them in definition order so an adapter can build the framework's real throttle classes. Tier overrides let you apply different rate limits by customer plan (free/pro/enterprise) without duplicating policy declarations.

Installation

pip install domain-api-limiter

Or with uv:

uv add domain-api-limiter

Requires Python 3.11+. Depends on domain-errors >= 0.1.0.

Quick Start

1. Declare throttle policies with @throttled

from domain_api_limiter import throttled

class DocumentService:
    @throttled("docs:list", "100/hour", tiers={"free": "10/day", "pro": "1000/hour"})
    def list_documents(self, tenant_id: str) -> list:
        """List all documents for a tenant."""
        return []

    @throttled("docs:upload", "20/minute")
    def upload(self, tenant_id: str) -> None:
        """Upload a document."""
        pass

The decorator validates the rate and tier strings at decoration time and attaches a ThrottlePolicy to the method without changing its behavior.

2. Collect policies for your adapter

from domain_api_limiter import PolicyRegistry

registry = PolicyRegistry()
policies = registry.collect(DocumentService)
print(f"Collected {len(policies)} policies:")
# Output: Collected 2 policies:
#   docs:list: 100/hour (tiers: free=10/day, pro=1000/hour)
#   docs:upload: 20/minute

Your adapter then walks the policies and builds the framework's real throttle classes (DRF throttles, middleware, etc.).

3. Parse rates and inspect tier overrides

from domain_api_limiter import RateLimit, Period

# Parse rate strings
rate = RateLimit.from_rate("100/hour")
print(rate.requests)      # 100
print(rate.period)         # Period.HOUR
print(rate.period_seconds) # 3600
print(rate.as_rate())      # "100/hour"

# Create rates directly
rate = RateLimit(requests=50, period=Period.MINUTE)

# Inspect tier overrides on a collected policy
policy = policies[0]
if policy.has_tiers:
    free_rate = policy.rate_for("free")       # Returns RateLimit for "free" tier
    pro_rate = policy.rate_for("pro")         # Returns RateLimit for "pro" tier
    default_rate = policy.rate_for("unknown") # Returns the base rate

4. Handle declaration errors

from domain_api_limiter import ThrottleDeclarationError

# Errors are raised at decoration time, not at runtime.
try:
    @throttled("scope", "invalid")  # Malformed rate
    def bad_method():
        pass
except ThrottleDeclarationError as e:
    print(f"Invalid policy: {e.message}")

Service Class Example

A typical service class with throttled methods:

from domain_api_limiter import throttled, PolicyRegistry

class DocumentService:
    @throttled("docs:list", "100/hour", tiers={"free": "10/day", "pro": "1000/hour"})
    def list_documents(self, tenant_id: str) -> list:
        """List all documents for a tenant."""
        return []

    @throttled("docs:upload", "20/minute")
    def upload(self, tenant_id: str) -> None:
        """Upload a document."""
        pass

# An adapter collects policies and builds throttle classes
registry = PolicyRegistry()
policies = registry.collect(DocumentService)

for policy in policies:
    print(f"Scope: {policy.scope}")
    print(f"Base rate: {policy.rate.as_rate()}")
    if policy.has_tiers:
        for tier_rate in policy.tier_rates:
            print(f"  {tier_rate.tier}: {tier_rate.rate.as_rate()}")

Output:

Scope: docs:list
Base rate: 100/hour
  free: 10/day
  pro: 1000/hour
Scope: docs:upload
Base rate: 20/minute

Public API

Throttle Declaration

from domain_api_limiter import throttled, ThrottlePolicy, RateLimit, TierRate, Period

# Decorator: attach a policy to a method
@throttled(scope: str, rate: str, tiers: dict[str, str] | None = None)
def my_method() -> None:
    pass

# Value objects (immutable, validated at creation)
policy = ThrottlePolicy(
    scope: str,                      # Policy identifier (e.g., "docs:list")
    rate: RateLimit,                 # Base rate for all tiers
    tier_rates: tuple[TierRate, ...] # Per-tier rate overrides (default: ())
)

rate = RateLimit(
    requests: int,  # Positive count of allowed requests
    period: Period  # Period enum: SECOND, MINUTE, HOUR, DAY
)

tier_rate = TierRate(
    tier: str,       # Non-empty tier label (e.g., "free", "pro")
    rate: RateLimit  # Rate override for this tier
)

# Period enumeration
from domain_api_limiter import Period
period = Period.HOUR  # or SECOND, MINUTE, DAY

Policy Registry

from domain_api_limiter import PolicyRegistry

registry = PolicyRegistry()

# Retrieve the policy from a single callable
policy = registry.policy_of(my_method)  # Returns ThrottlePolicy | None

# Collect all policies from a class or module
policies = registry.collect(DocumentService)  # Returns tuple[ThrottlePolicy, ...]

Error Types

from domain_api_limiter import ThrottleError, RateLimitExceeded, ThrottleDeclarationError

# Base error for all rate-limit domain failures
# domain="api_limiter", code="throttle_error", http_status=429, retryable=True
class ThrottleError(DomainError):
    pass

# Request rejected because a rate limit was exceeded
# code="rate_limit_exceeded", http_status=429, retryable=True
class RateLimitExceeded(ThrottleError):
    pass

# Invalid throttle declaration detected at import time
# code="throttle_declaration_invalid", http_status=500, retryable=False
class ThrottleDeclarationError(ThrottleError):
    pass

All errors inherit from domain-errors DomainError and integrate with structured logging and error tracking systems.

Documentation

For detailed documentation on each feature, see:

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Contributing

This library is maintained by James Ekhator. Contributions welcome via pull requests. Please read CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md for community standards.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

domain_api_limiter-0.1.0.tar.gz (78.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

domain_api_limiter-0.1.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file domain_api_limiter-0.1.0.tar.gz.

File metadata

  • Download URL: domain_api_limiter-0.1.0.tar.gz
  • Upload date:
  • Size: 78.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for domain_api_limiter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4331aa6559ccefee06c977bd51a3429bba7f27da4a60344bb64a6bf246b46a11
MD5 6377c9d6f5bba9f3951add2b30cfabb3
BLAKE2b-256 85bf2842b4b3b966451ed8c837acd69c7a6a6a5321f8886c683dc2a3caa9a11a

See more details on using hashes here.

Provenance

The following attestation bundles were made for domain_api_limiter-0.1.0.tar.gz:

Publisher: publish.yml on jekhator/domain-api-limiter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file domain_api_limiter-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for domain_api_limiter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b49517ea32c73ea9a28a352ce1f8255f460742be904c3e12a8a7eee420a3867c
MD5 a953aabbf694e2e008f6bdb78f0fe176
BLAKE2b-256 3498a208861cf1c62b4193d1eea4753c8fed0f94d3b13c233c4611af076d2071

See more details on using hashes here.

Provenance

The following attestation bundles were made for domain_api_limiter-0.1.0-py3-none-any.whl:

Publisher: publish.yml on jekhator/domain-api-limiter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page