No project description provided
Project description
idempot
Idempotency for APIs via decorators. Framework-agnostic core with adapters for FastAPI and Flask.
Install
pip install idempot # core + backends (pyyaml, redis)
pip install "idempot[fastapi]" # core + fastapi
pip install "idempot[flask]" # core + flask
pip install "idempot[dev]" # everything + test/lint deps
Zero-config Quickstart
No config needed. Import, decorate, done.
FastAPI
# app.py
from fastapi import FastAPI, Request
from idempot.contrib.fastapi import idempotent
app = FastAPI()
@app.post("/charge")
@idempotent
async def charge(request: Request):
body = await request.json()
return {"charged": body["amount"]}
uvicorn app:app --reload
Flask
# app.py
import flask
from idempot.contrib.flask import idempotent
app = flask.Flask(__name__)
@app.post("/charge")
@idempotent
def charge():
data = flask.request.get_json()
return flask.jsonify({"charged": data["amount"]})
flask --app app run
Defaults: memory backend, 24h TTL, Idempotency-Key header, 5s lock timeout.
Client Usage
Send Idempotency-Key header with every request:
curl -X POST http://localhost:8000/charge \
-H "Idempotency-Key: abc-123" \
-H "Content-Type: application/json" \
-d '{"amount": 99}'
# → {"charged": 99}
# Same key → replayed response (handler not called)
curl -X POST http://localhost:8000/charge \
-H "Idempotency-Key: abc-123" \
-H "Content-Type: application/json" \
-d '{"amount": 50}' # different body, ignored
# → {"charged": 99} # same reply
Configuration
Option A — configure() at startup
In your app entry point, before registering routes:
from idempot import configure
# Redis in production:
configure(
backend="redis",
redis_url="redis://localhost:6379",
redis_key_prefix="myapp:",
default_ttl=86400,
header_name="Idempotency-Key",
lock_timeout=5,
cache_on_status=[200, 201],
)
# Or override just one thing:
configure(backend="memory", default_ttl=3600)
Option B — YAML file
Create idempot-config.yaml in your project root:
backend: redis
redis:
url: redis://localhost:6379
key_prefix: "myapp:"
default_ttl: 86400
header_name: Idempotency-Key
lock_timeout: 5
cache_on_status: [200, 201]
from idempot import configure
configure(config_path="idempot-config.yaml")
A working example file: idempot-config.example.yaml
The config file is not auto-generated. If no config file is found and configure() is never called, memory backend + defaults are used.
Config reference
| Key | Default | Description |
|---|---|---|
backend |
memory |
memory or redis |
redis.url |
redis://localhost:6379 |
Redis connection URL |
redis.key_prefix |
idempot: |
Prefix for Redis keys |
default_ttl |
86400 (24h) |
Seconds before cached response expires |
header_name |
Idempotency-Key |
HTTP header to read the key from |
lock_timeout |
5 |
Max seconds to wait for concurrent same-key request |
cache_on_status |
[200] |
Which HTTP status codes get cached |
max_body_size |
10485760 (10 MB) |
Largest response body to cache |
Per-endpoint Overrides
@app.post("/payments")
@idempotent(
ttl=3600, # cache 1 hour (instead of default 24h)
required=True, # reject requests without key → 400
cache_on_status=[200, 201], # cache on 200 or 201
lock_timeout=10, # wait up to 10s for concurrent
)
async def process_payment(request: Request):
...
Override params: ttl, required, lock_timeout, cache_on_status, max_body_size.
Import Cheatsheet
| Context | Import |
|---|---|
| Config & setup | from idempot import configure, get_config |
| FastAPI decorator | from idempot.contrib.fastapi import idempotent |
| Flask decorator | from idempot.contrib.flask import idempotent |
| Base decorator (no framework) | from idempot import idempotent |
| Exceptions | from idempot import IdempotencyKeyMissingError, KeyConflictError |
| Backends (custom) | from idempot.backends.memory import MemoryBackend |
How It Works
- Decorator reads
Idempotency-Keyfrom request headers - Key missing → 400 if
required=True, otherwise handler runs normally (not cached) - Key in cache → replays stored response verbatim (same status, headers, body)
- Key new → acquires distributed lock (Redis
SETNX) → runs handler → caches response → releases lock - Concurrent same-key → second request polls for cached response, returns it. If first request exceeds
lock_timeout→ 409 Conflict
Backends
- Memory (default, dev): dict-backed, no persistence
- Redis (prod):
SETNXlocks,SETEXcaching with TTL, key prefix isolation
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file idempot-0.1.0.tar.gz.
File metadata
- Download URL: idempot-0.1.0.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a986c0aaf6dc77d46da3011835fd91df1a3b969b9657e92c322b68e587f87fca
|
|
| MD5 |
78cdd8c7d137d4957c42863aaf350542
|
|
| BLAKE2b-256 |
6512c26a7618366e5c037d52b1396ba7248653373733468f9c8594de415d0ed4
|
File details
Details for the file idempot-0.1.0-py3-none-any.whl.
File metadata
- Download URL: idempot-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
290ccad5515518811876685c728880e408a6d8185b802bde15a5a7129beaa9a2
|
|
| MD5 |
b1bbf12f2fbfe9efe0ac05519bbfcabc
|
|
| BLAKE2b-256 |
e8069784da1bf6be892f28ebc59f0a948462ded1b6918ab14242e061e3a098c5
|