Production-ready idempotency keys for FastAPI, Flask, Django and any Python function. Redis & memory backends, async-first.
Project description
pyidempotent
Production-ready idempotency keys for Python backends — in 5 lines.
Stop double charges, double orders, and duplicate webhooks. Exactly-once execution for FastAPI, Flask, Django, Celery — with Redis or in-memory store.
from fastapi import FastAPI, Request
from pyidempotent import idempotent
from pyidempotent.backends.redis import RedisBackend
import redis.asyncio as redis
app = FastAPI()
backend = RedisBackend(redis.from_url("redis://localhost"))
@app.post("/pay")
@idempotent(backend=backend, ttl=24*3600)
async def pay(request: Request, amount: int):
# will run ONCE per Idempotency-Key
return {"status": "charged", "amount": amount}
Send Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000 header. Repeat the request — you get the cached response, no second charge.
Why pyidempotent?
- Async-first, works with sync too
- Zero framework lock-in — pure decorator
- Race-safe — Redis SET NX + atomic claim
- Request fingerprinting — 422 if same key but different body
- Processing state — returns 409 while first request still runs
- Pluggable backends — Redis (prod), Memory (tests)
- <400 LOC, no magic
Install
pip install pyidempotent[redis,fastapi]
Backends
# Production
from pyidempotent.backends.redis import RedisBackend
backend = RedisBackend(redis.from_url("redis://localhost"), prefix="idem:")
# Tests
from pyidempotent.backends.memory import MemoryBackend
backend = MemoryBackend()
FastAPI full example
from fastapi import FastAPI, Request, HTTPException
from pyidempotent import idempotent, IdempotencyConflict, IdempotencyProcessing
from pyidempotent.backends.redis import RedisBackend
import redis.asyncio as redis
app = FastAPI()
backend = RedisBackend(redis.from_url("redis://localhost"))
@app.exception_handler(IdempotencyConflict)
async def conflict_handler(_, exc):
raise HTTPException(422, "Idempotency-Key already used with different payload")
@app.post("/orders")
@idempotent(backend=backend, key_header="Idempotency-Key")
async def create_order(request: Request, item: str):
# your DB write here
return {"order_id": "ord_123", "item": item}
How it works
- Extract key from header
SET key {status:processing} NX EX ttl— atomic claim- Run your function
SET key {status:completed, response:...} EX ttl- Duplicates return cached response
Fingerprint = SHA256 of function arguments (excluding Request). Prevents accidental reuse.
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 pyidempotent-0.1.0.tar.gz.
File metadata
- Download URL: pyidempotent-0.1.0.tar.gz
- Upload date:
- Size: 5.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d30d12ff10891c0ec41844573cd5b8685aff7c2d889070acc730c1715b24b59
|
|
| MD5 |
4325940c6fd7a27f35a459a1e8935c41
|
|
| BLAKE2b-256 |
0c7a9aa47796756a197666fb7f3c8475ce153beeff5f400e29f4e9422f226c2d
|
File details
Details for the file pyidempotent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyidempotent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e457aded1acad41c7ce971a6d92f3c3ef82650536937e635dfdafab331d3ef64
|
|
| MD5 |
cb79066c7634ad705fe66f8566d835fa
|
|
| BLAKE2b-256 |
d8574878a01dc62e326b761bb722fb2194b6def56e21b45768f15f62551b58b2
|