redis-infra
Safe, typed, reusable production patterns on top of redis-py — not a thin wrapper. Every capability turns a Redis primitive into application infrastructure you can drop in without re-deriving the correctness details (atomicity, clock skew, key namespacing, delivery guarantees).
Matching sync and async APIs across the board. Python 3.11+.
Install
pip install redis-infra # core
pip install "redis-infra[orjson]" # faster JSON
What's in the box
| Module | Sync / Async | Guarantee |
|---|---|---|
Cache |
Cache / AsyncCache |
TTL cache with optional stampede guard |
RateLimiter |
RateLimiter / AsyncRateLimiter |
atomic (single Lua script), server clock |
Leaderboard |
Leaderboard / AsyncLeaderboard |
sorted-set ranking with around() windows |
RedisLock |
RedisLock / AsyncRedisLock |
single-node lock, token compare-and-delete |
Idempotency |
Idempotency / AsyncIdempotency |
run-once-per-key, result cached |
Geo |
Geo / AsyncGeo |
validated geospatial add/search |
Publisher/Subscriber |
+ Async* |
fire-and-forget pub/sub |
StreamQueue |
StreamQueue / AsyncStreamQueue |
at-least-once queue, DLQ, graceful worker |
Quick start
from redis_infra import RedisConfig, redis_client, Cache, RateLimiter
client = redis_client(RedisConfig(host="localhost", port=6379, namespace="app"))
# Cache with a compute-on-miss factory (stampede-guarded)
cache = Cache(client, namespace="app")
user = cache.get_or_set("user:1", lambda: load_user(1), ttl=300, stampede=True)
# Atomic rate limit — being limited is a normal result, not an exception
limiter = RateLimiter(client, limit=100, window=60) # 100 / 60s, sliding window
r = limiter.allow("user:1")
if not r.allowed:
raise TooManyRequests(retry_after=r.retry_after)
Async mirrors it exactly:
from redis_infra import async_redis_client, AsyncCache
client = async_redis_client(RedisConfig(host="localhost"))
cache = AsyncCache(client, namespace="app")
user = await cache.get_or_set("user:1", load_user_async, ttl=300)
Durable queue (Redis Streams)
from redis_infra import StreamQueue
q = StreamQueue(client, "jobs", group="workers", consumer="w1")
q.ensure_group()
q.publish({"task": "resize", "id": 42})
def handle(msg):
do_work(msg.data) # MUST be idempotent — at-least-once delivery
q.run(handle, should_stop=lambda: shutting_down) # ack on success, DLQ on poison
Guarantees & non-guarantees
- Rate limiting is atomic. Each decision is one server-side Lua script reading the Redis
TIMEclock — correct across many app instances and clock skew. No Python read-modify-write. burstis token-bucket only. Only that algorithm has a bucket capacity to raise, so passingbursttofixed_windoworsliding_windowraisesRateLimitErrorinstead of being quietly ignored.- Queue is at-least-once, never exactly-once. A message stays pending until acked; a crashed worker's in-flight messages are reclaimed (
XAUTOCLAIM) and redelivered. Handlers must be idempotent. Poison messages (redelivered pastmax_deliveries) are dead-lettered. - Pub/Sub is not durable. Messages published with no subscriber connected are lost. Use
StreamQueuewhen you need delivery. - Locks are single-node.
RedisLockis a token-ownedSET NX PXlock with Lua compare-and-delete release — safe against releasing someone else's lock, and auto-expiring so a crash can't deadlock. It is not Redlock; don't rely on it across independent masters. - Errors are never swallowed. redis-py errors are wrapped in this package's exception tree (
RedisInfraErrorand subclasses) with__cause__preserved.
Configuration
RedisConfig is frozen/validated. Build from code or the environment:
RedisConfig.from_env() # REDIS_URL, REDIS_HOST, REDIS_PORT, REDIS_NAMESPACE, REDIS_SSL, ...
No credentials are ever read from source — supply them via RedisConfig or env vars.
Development
uv pip install -e ".[dev,orjson]"
ruff check . && ruff format --check .
mypy src
pytest tests/unit # pure, no Redis needed
pytest -m integration # requires a live Redis (see repo docker-compose.yml)
License
MIT
Release files for redis-infra 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| redis_infra-0.2.0.tar.gz | 23.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| redis_infra-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.0 kB
Release files / redis_infra-0.2.0.tar.gz
| Download URL | redis_infra-0.2.0.tar.gz |
|---|---|
| Size | 23.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6789d1cd71649fd5b701ee7a7ed8a78089c84bcc54440add3b735b57495bf4ea
|
|
BLAKE2b-256 checksum How to use checksums |
4b22bfb104f8e38b6a15c8e73b6d062f27c4a681d9bf0eaaa7d28339b9656c52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.7
|
Release files / redis_infra-0.2.0-py3-none-any.whl
| Download URL | redis_infra-0.2.0-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e233fc420929b09ab3edce1819aa2c09c67bff53e4d9ecfc5905a377d12d7057
|
|
BLAKE2b-256 checksum How to use checksums |
ebe0342e072eceb2881a4a877311716a9e7b82e1a78c6c321fadd01def9616cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.7
|