Metadata-Version: 2.4
Name: jetio-ratelimit
Version: 0.1.1
Summary: Rate limiting plugin for the Jetio framework -- sliding-window, IP/account-keyed, middleware and Depends()-composable.
Author-email: Stephen Burabari Tete <cehtete@gmail.com>
License: BSD-3-Clause
Project-URL: Homepage, https://github.com/cehstephen/jetio-ratelimit
Project-URL: Bug Tracker, https://github.com/cehstephen/jetio-ratelimit/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jetio>=1.2.3
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: uvicorn; extra == "dev"
Requires-Dist: jetio-auth>=0.2.4; extra == "dev"
Dynamic: license-file

# jetio-ratelimit

![PyPI version](https://img.shields.io/pypi/v/jetio-ratelimit)
![Tests](https://github.com/cehstephen/jetio-ratelimit/actions/workflows/tests.yml/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/cehstephen/jetio-ratelimit/badge.svg?branch=main)](https://coveralls.io/github/cehstephen/jetio-ratelimit?branch=main)
![Python versions](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue)
![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)

Rate limiting for [Jetio](https://pypi.org/project/jetio/): sliding-window
by default, IP- or account-keyed (or both, stacked), usable as middleware
for routes you don't own the handler for (like jetio-auth's `/login`) and
as a `Depends()`-composable dependency for routes you do (a `CrudRouter`
policy, a hand-written route).

- **[Usage Guide](https://github.com/cehstephen/jetio-ratelimit/blob/main/docs/USAGE.md)**
  -- the full guide: every mode, every key function, stacking/reusing
  policies, handling a 429, testing your integration, troubleshooting.
  Start here for "how do I do X."
- **[Design & Architecture](https://github.com/cehstephen/jetio-ratelimit/blob/main/DESIGN.md)**
  -- the reasoning: why sliding window over token bucket, why IP-only
  limiting is weak against real credential stuffing, and the real bugs
  found in Jetio/jetio-auth along the way. Start here for "why does it
  work this way."

## Install

```
pip install jetio-ratelimit
```

For local development (running the test suite, contributing):

```
pip install -e .[dev]   # from this directory
```

## Quickstart

```python
from jetio import Jetio, CrudRouter
from jetio_auth import AuthRouter
from jetio_ratelimit import RateLimiter, InMemoryStore, Limit, by_ip, by_field, by_user

app = Jetio()
auth = AuthRouter(User, company_name="My App")
auth.register_routes(app)  # POST /register, POST /login

limiter = RateLimiter(store=InMemoryStore())

# Middleware mode: protects /login, which AuthRouter registers internally --
# there's no Depends() hook to attach to on a route we don't define.
# protect_many() stacks two independent limits in one call; either tripping
# blocks the request. AUTH_POLICY is a plain list, so the same two rules
# can be applied to /register or any other auth-adjacent route with one
# more protect_many() call each -- see "Reusing a policy across routes" below.
AUTH_POLICY = [
    Limit(max_attempts=5, window_seconds=60, key_func=by_ip),
    Limit(max_attempts=3, window_seconds=60, key_func=by_field("username")),
]
limiter.protect_many(app, path="/login", limits=AUTH_POLICY)

# Dependency mode: composes into a CrudRouter policy, keyed by the
# authenticated user rather than IP.
CrudRouter(
    model=Order,
    secure=True,
    policy={
        "POST": limiter.dependency(
            max_attempts=10, window_seconds=60,
            key_func=by_user, identity_dependency=auth.get_auth_dependency(),
        ),
    },
).register_routes(app)
```

Run [examples/demo_app.py](https://github.com/cehstephen/jetio-ratelimit/blob/main/examples/demo_app.py)
and hit it with curl to see both modes working against a real
jetio-auth-backed app. For why `/login` gets two stacked limits instead of
one, how to reuse one policy across many routes, `by_header`-keyed API
endpoints, using dependency mode outside CrudRouter, and more, see the
**[Usage Guide](https://github.com/cehstephen/jetio-ratelimit/blob/main/docs/USAGE.md)**.

## Status

[Published on PyPI](https://pypi.org/project/jetio-ratelimit/) as v0.1.0:
sliding window algorithm, in-memory store, both API modes, IP/account/
header/user keying, `protect_many()` for stacking multiple limits (or
reusing one policy across many routes) in one call. Not yet done: Redis store (for
anything running more than one worker -- InMemoryStore's state is
per-process), progressive lockout on repeat violations, an equivalent
stacking helper for dependency mode, trusted-proxy-aware `X-Forwarded-For`
support. See [Roadmap](https://github.com/cehstephen/jetio-ratelimit/blob/main/DESIGN.md#roadmap).

## Known limitations

- **InMemoryStore is single-process.** Behind multiple workers/replicas,
  each one has its own counters, so the effective limit becomes
  `limit * worker_count`. Don't treat this as sufficient for a
  horizontally-scaled deployment yet.
- **No `X-Forwarded-For` support.** Behind a reverse proxy, `by_ip` sees the
  proxy's IP, not the real client's. This is a deliberate default, not an
  oversight: `X-Forwarded-For` is a client-suppliable header, so trusting it
  unconditionally lets any caller spoof their rate-limit identity -- evading
  their own limit, or framing another IP for one. Other rate limiters that
  do support it require you to explicitly configure how many proxy hops (or
  which ones) to trust, precisely to avoid this; naive header-trusting is a
  known footgun, not just a missing feature. See
  [Roadmap](https://github.com/cehstephen/jetio-ratelimit/blob/main/DESIGN.md#roadmap).
