Metadata-Version: 2.4
Name: requestguard
Version: 0.3.0
Summary: Framework-agnostic Python rate limiting with six algorithms, sync and async support, and optional Redis storage.
Author: Subhan Adeel
Maintainer: Subhan Adeel
License-Expression: MIT
Project-URL: Homepage, https://github.com/AdeelMalik22/rateguard
Project-URL: Maintainer, https://github.com/AdeelMalik22
Project-URL: LinkedIn, https://www.linkedin.com/in/subhan-adeel-4b6b14326/
Project-URL: Documentation, https://github.com/AdeelMalik22/rateguard#readme
Project-URL: Repository, https://github.com/AdeelMalik22/rateguard
Project-URL: Issues, https://github.com/AdeelMalik22/rateguard/issues
Project-URL: Changelog, https://github.com/AdeelMalik22/rateguard/blob/master/CHANGELOG.md
Keywords: rate-limiting,rate-limiter,requestguard,throttling,middleware,token-bucket,leaky-bucket,fixed-window,sliding-window,sliding-window-counter,gcra,distributed-rate-limiting,in-memory,redis,framework-agnostic,api-security,fastapi,async
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: dev
Requires-Dist: redis>=5.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: httpx>=0.24.0; extra == "dev"
Requires-Dist: uvicorn>=0.20.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=4.0.0; extra == "dev"

# RequestGuard 🛡️

A lightweight, modular **rate limiting library** for Python applications. RequestGuard provides a clean decorator-based API to protect your endpoints from abuse, with pluggable algorithms and storage backends.

---

## Features

- ✅ Simple `@limit` decorator — drop onto any route handler
- ✅ **Fixed Window**, **Token Bucket**, **Leaky Bucket**, **Sliding Window**, **Sliding Window Counter**, and **GCRA** algorithms out of the box
- ✅ Smart key resolution — auto-detects authenticated users or falls back to client IP
- ✅ Custom key resolver support for advanced use cases
- ✅ Pluggable storage backend (thread-safe memory storage by default)
- ✅ Configurable `RequestGuard` with optional atomic Redis storage
- ✅ Sync and async endpoint support
- ✅ Returns `429 Too Many Requests` with `retry_after`, `reset_after`, and `limit` metadata
- ✅ Zero required runtime dependencies in the core package

## Validation and maturity

RequestGuard is currently published as an alpha release while its API and
production behavior continue to mature. The repository runs automated tests
across Python 3.9–3.12, including synchronous and asynchronous endpoints,
thread-concurrency checks, all six algorithms, bounded in-memory storage, and
package build validation.

For production deployments, use `RedisStorage` with a managed or highly
available Redis deployment when running multiple workers or pods. The exact
sliding-window algorithm is intentionally memory-proportional to its configured
limit; use Sliding Window Counter, Token Bucket, Leaky Bucket, or GCRA for
high-throughput routes. Report reproducible issues through the project issue
tracker before upgrading a deployment-critical installation.

---

## Project Structure

```
requestguard/                     ← project root
├── requestguard/                 ← installable Python package
│   ├── __init__.py               # Public API surface
│   ├── py.typed                  # PEP 561 type marker
│   ├── algorithms/
│   │   ├── registry.py           # Algorithm factory/registry
│   │   ├── fixed_window.py       # Fixed Window rate limiting algorithm
│   │   ├── token_bucket.py       # Token Bucket rate limiting algorithm
│   │   ├── leaky_bucket.py       # Leaky Bucket rate limiting algorithm
│   │   ├── sliding_window.py     # Sliding Window rate limiting algorithm
│   │   ├── sliding_window_counter.py
│   │   └── gcra.py                # GCRA rate limiting algorithm
│   ├── core/
│   │   ├── limiter.py            # RateLimiter — orchestrates algorithm checks
│   │   ├── policy.py             # RateLimitPolicy — limit & window config
│   │   ├── resolver.py           # KeyResolver — identifies the client
│   │   ├── exceptions.py         # RateLimitExceeded exception
│   │   └── enums.py              # Algorithm enum
│   ├── decorators/
│   │   └── decorator.py          # @limit decorator — the main public API
│   └── storage/
│       ├── storage.py            # MemoryStorage — in-memory key/value store
│       └── redis.py              # Optional atomic RedisStorage backend
├── examples/
│   └── basic_usage.py            # Example FastAPI app
├── pyproject.toml                # Package metadata & build config
├── setup.py                      # Editable install shim
├── requirements.txt
└── README.md
```

---

## Installation

### From source (recommended for development)

```bash
git clone https://github.com/AdeelMalik22/rateguard.git
cd rateguard
pip install -e .
```

The `-e` flag installs it in **editable mode** — any changes you make to the source are reflected immediately without reinstalling.

### From PyPI

```bash
pip install requestguard
```

---

## Quick Start

```python
from fastapi import FastAPI, Request
from requestguard import Algorithm, RateLimitExceeded, limit
from requestguard.integrations.fastapi import rate_limit_exception_handler

app = FastAPI()
app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler)

@app.get("/hello")
@limit(requests=5, window=60, algorithm=Algorithm.FIXED_WINDOW)
async def hello(request: Request):
    return {"message": "Hello!"}
```

The first five requests per client are allowed. Further requests receive a
`429` response with `Retry-After` and `RateLimit-*` headers.

For custom storage configuration:

```python
from requestguard import RequestGuard, RedisStorage
import redis

guard = RequestGuard(RedisStorage(
    redis.Redis.from_url("redis://localhost"),
    retries=3,
    failure_mode="closed",  # use "open" only if availability is preferred
))

@guard.limit(requests=5, window=60)
def protected(request: Request):
    return {"ok": True}
```

`MemoryStorage` is process-local and suitable for development, testing, and
single-process applications. Use `RedisStorage` for multi-worker or
distributed deployments.

### Run the server

```bash
uvicorn examples.basic_usage:app --reload
```

---

## Usage

### `@limit(requests, window, key=None, algorithm=Algorithm.FIXED_WINDOW)`

| Parameter      | Type         | Description                                                   |
|----------------|--------------|---------------------------------------------------------------|
| `requests`     | `int`        | Maximum number of requests allowed (capacity)                 |
| `window`       | `int`        | Time window in **seconds**                                    |
| `key`          | `callable`   | *(Optional)* Custom function to resolve the client identifier |
| `algorithm`    | `Algorithm`  | *(Optional)* The algorithm to use. Default is `FIXED_WINDOW`. |

The clearer aliases `requests` and `window` are also supported. Decorated
`async def` functions remain asynchronous and are awaited by the wrapper.

Supported built-in algorithms are:

- `Algorithm.FIXED_WINDOW`
- `Algorithm.SLIDING_WINDOW`
- `Algorithm.SLIDING_WINDOW_COUNTER`
- `Algorithm.TOKEN_BUCKET`
- `Algorithm.LEAKY_BUCKET`
- `Algorithm.GCRA`

#### Basic — 3 requests per 10 seconds (Fixed Window)

```python
from requestguard import limit

@limit(requests=3, window=10)
def my_endpoint(request: Request):
    return {"status": "ok"}
```

#### Token Bucket

```python
from requestguard import limit, Algorithm

# requests acts as the capacity (maximum burst size)
# window acts as the refill window (refill rate = requests / window)
# Example below: Burst of 10, refills at 10/60 tokens per second
@limit(requests=10, window=60, algorithm=Algorithm.TOKEN_BUCKET)
def smooth_endpoint(request: Request):
    return {"status": "ok"}
```

#### Custom Key Resolver

```python
from requestguard import limit

def resolve_by_api_key(*args, **kwargs):
    request = kwargs.get("request")
    return request.headers.get("X-API-Key", "anonymous")

@limit(requests=100, window=60, key=resolve_by_api_key)
def protected_endpoint(request: Request):
    return {"data": "..."}
```

---

## How It Works

```
Request
  │
  ▼
@limit decorator
  │
  ├─► KeyResolver.resolve()       → Identifies client (user ID or IP)
  │
  ├─► get_algorithm(algorithm)    → Fetches the requested Algorithm class
  │
  ├─► RateLimiter.check()         → Delegates to the algorithm instance
  │
  ├─► Limiter.allow()             → Uses time.monotonic() to evaluate rate limit
  │     ├─ Fetch record from MemoryStorage
  │     ├─ Update buckets/windows
  │     ├─ Block if limit reached → raise HTTPException(429)
  │     └─ Increment/Decrement & save to storage
  │
  └─► Route handler executes normally
```

### Key Resolution Priority

1. **Custom resolver** — if a `key` function is passed to `@limit`
2. **Authenticated user** — reads `request.scope["user"].id` (set by auth middleware)
3. **Client IP** — falls back to `request.client.host`

---

## Algorithms

### Fixed Window (`Algorithm.FIXED_WINDOW`)

Counts requests within a fixed time window. Once the window expires, the counter resets entirely.

- **Pros**: Simple, predictable, low memory usage
- **Cons**: Burst traffic possible at window boundaries

### Token Bucket (`Algorithm.TOKEN_BUCKET`)

Allows up to a maximum capacity of tokens (requests), continuously refilling tokens at a constant rate over time.

- **Pros**: Extremely smooth rate limiting, allows for bursts while maintaining a steady long-term rate
- **Cons**: Slightly more floating-point math overhead

### Leaky Bucket (`Algorithm.LEAKY_BUCKET`)

The algorithm tracks virtual bucket occupancy that drains at a constant rate. Requests are accepted while capacity is available and rejected when the bucket is full. It does not delay or queue application requests for later processing.

- **Pros**: Enforces a strict, steady output rate without bursts
- **Cons**: Can penalize bursty traffic immediately if the bucket is full

### Sliding Window (`Algorithm.SLIDING_WINDOW`)

Counts exact request timestamps in a rolling window. It is precise and avoids
fixed-window boundary bursts, with storage proportional to active requests.

```python
@limit(requests=100, window=60, algorithm=Algorithm.SLIDING_WINDOW)
def security_sensitive_endpoint(request):
    return {"ok": True}
```

### Sliding Window Counter (`Algorithm.SLIDING_WINDOW_COUNTER`)

Uses weighted current and previous windows to approximate a rolling count with
constant storage.

```python
@limit(requests=100, window=60, algorithm=Algorithm.SLIDING_WINDOW_COUNTER)
def high_traffic_endpoint(request):
    return {"ok": True}
```

### GCRA (`Algorithm.GCRA`)

Tracks theoretical arrival time to enforce a smooth rate while allowing the
configured burst tolerance.

```python
@limit(requests=100, window=60, algorithm=Algorithm.GCRA)
def smooth_endpoint(request):
    return {"ok": True}
```

### Returned State

All algorithms implement a consistent interface returning:

| Field           | Description                          |
|-----------------|--------------------------------------|
| `allowed`       | `bool` — whether the request passes  |
| `remaining`     | `int` — requests left for this client|
| `retry_after`   | `float` — seconds until at least 1 request can be made (only on `429`) |
| `reset_after`   | `float` — seconds until the rate limit fully resets |
| `limit`         | `int` — the total limit configured   |

The FastAPI integration provides standard `Retry-After`, `RateLimit-Limit`,
`RateLimit-Remaining`, and `RateLimit-Reset` headers:

```python
from requestguard import RateLimitExceeded
from requestguard.integrations.fastapi import rate_limit_exception_handler

app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler)
```

---

## Storage Backends

### `MemoryStorage` (default)

In-memory dictionary store. Fast and dependency-free, but **not shared** across multiple processes or workers.

```python
from requestguard import MemoryStorage

storage = MemoryStorage()
storage.set("key", {"tokens": 10, "last_refill": 1234567890.0})
storage.get("key")     # → {"tokens": 10, "last_refill": ...}
storage.delete("key")
```

> **Production guidance:** `MemoryStorage` is process-local and intended for development, testing, and single-process applications. It bounds resident keys with LRU eviction and cleans TTL-backed records, but it does not share state between workers. Use the optional `RedisStorage` backend for shared state, and configure it with an atomic Redis deployment.

`RedisStorage` retries transient connection failures with exponential backoff.
Its default `failure_mode="closed"` raises `StorageUnavailableError` when
Redis remains unavailable, preserving rate-limit enforcement expectations. Set
`failure_mode="open"` only when serving requests is more important than
enforcing limits during a Redis outage.

### ASGI middleware

Use middleware when protecting an entire Starlette or FastAPI application or
when a route cannot be decorated:

```python
from fastapi import FastAPI
from requestguard import RateLimitMiddleware

app = FastAPI()
app.add_middleware(RateLimitMiddleware, requests=100, window=60)
```

The middleware uses the request client address by default. Flask and Django
applications can use the decorator API with their framework exception handlers.

---

## Response Behavior

| Scenario           | HTTP Status | Response Body                                           |
|--------------------|-------------|----------------------------------------------------------|
| Request allowed    | `2xx`       | Normal route response                                    |
| Limit exceeded     | `429`       | `{"error": "Too many requests", "retry_after": <float>, "reset_after": <float>, "limit": <int>}` |

---

## Publishing to PyPI

```bash
# Install build tools
pip install build twine

# Build the distribution
python -m build

# Upload to PyPI
twine upload dist/*
```

---

## Requirements

| Package            | Version   |
|--------------------|-----------|
| fastapi            | ≥ 0.100.0 |
| starlette          | ≥ 0.27.0  |

---

## Contributing

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/sliding-window`
3. Commit your changes: `git commit -m "feat: add sliding window algorithm"`
4. Push to the branch: `git push origin feature/sliding-window`
5. Open a Pull Request

---

## License

This project is open-source and available under the MIT License.
