--- name: fastapi-redis-sdk description: > fastapi-redis-sdk development skill. Use when writing code, tests, or configuration for the fastapi-redis-sdk library — the official Redis integration for FastAPI. Covers project setup (uv + nox), DI-based caching patterns, connection lifecycle, async/sync endpoints, testing conventions, and CI workflows. Do NOT use for general Redis or FastAPI questions unrelated to this library. license: MIT --- # fastapi-redis-sdk Official Redis integration for FastAPI — connection management and DI-based caching with automatic key consistency. ## Project layout ``` src/redis_fastapi/ # Library source (single flat package) __init__.py # Public API re-exports setup.py # FastAPIRedis fluent builder lifespan.py # redis_lifespan async context manager deps.py # FastAPI DI factories & type aliases config.py # RedisSettings (pydantic-settings) cache.py # cache(), cache_evict(), cache_put() + middleware cache_backend.py # CacheBackend (async) + SyncCacheBackend telemetry.py # Optional OpenTelemetry instrumentation types.py # Coder, KeyBuilder protocols tests/ unit/ # fakeredis-based, no real Redis needed integration/ # Requires Redis on localhost:6379 pyproject.toml # uv build backend, dependency groups, tool config noxfile.py # CI-mirroring sessions (lint, typecheck, security, tests, docs) ``` ## Setup and tooling Package manager: **uv**. Task runner: **nox** (with `uv` venv backend). ```bash uv sync --all-groups # Install all deps uv run nox # Run ALL CI checks locally uv run nox -s lint # Lint + format check only uv run nox -s typecheck # mypy uv run nox -s tests-3.12 # Full suite on a specific Python version uv run nox -s tests_unit-3.12 # Unit suite only (fakeredis, no server) uv run nox -s tests_integration-3.12 # Integration suite only (needs Redis) uv run nox -s fix # Auto-fix lint/format uv run nox -s docs_serve # Live-reload docs at localhost:8000 ``` ## Architecture rules ### Lifespan is mandatory Redis pools MUST be initialised via the app lifespan. There is no fallback. Accessing the pool without a lifespan raises `RuntimeError`. ```python app = FastAPI() FastAPIRedis(app).lifespan().caching() # Always call .lifespan() ``` ### Async-first, sync via bridge - All DI factories (`get_async_redis`, `get_cache_backend`) are async. - FastAPI runs them correctly even for sync endpoints (threadpool). - `SyncCacheBackend` wraps async calls via `anyio.from_thread.run` — use only from sync endpoints running in FastAPI's worker threads. ### Dependency injection types ```python from redis_fastapi import AsyncRedisDep, CacheBackendDep, SyncCacheBackendDep # Async endpoint — use AsyncRedisDep or CacheBackendDep async def endpoint(redis: AsyncRedisDep): ... async def endpoint(cache: CacheBackendDep): ... # Sync endpoint — use SyncCacheBackendDep def endpoint(cache: SyncCacheBackendDep): ... ``` ### Lifespan wrapping `.lifespan()` **wraps** the app's existing lifespan — it does not replace it. Multiple builder calls nest around whatever is already there. For explicit ordering, skip `.lifespan()` and compose manually with `redis_lifespan`: ```python from redis_fastapi import FastAPIRedis, redis_lifespan @asynccontextmanager async def my_lifespan(app): async with redis_lifespan(app): async with db_lifespan(app): yield app = FastAPI(lifespan=my_lifespan) FastAPIRedis(app).caching() # no .lifespan() — user owns it ``` See `docs/guide/architecture.md` § Lifespan wrapping for details. ### Caching patterns Two patterns, same pool: 1. **DI factories** — `cache(ttl=N, eviction_group="x")`, `cache_evict(...)`, `cache_put(...)` as `Depends()`. Requires `.caching()` on setup. - Automatic HTTP semantics (ETag, 304, Cache-Control, X-Redis-Cache). - Only GET requests are cached; non-GET bypasses cache entirely. - Graceful degradation: Redis failures log warnings, never crash. 2. **CacheBackend** — `get`/`set`/`delete`/`has`/`delete_group`. For conditional logic, cascade invalidation, dynamic TTL. - Accepts `timedelta` for TTL (DI factories accept `int` seconds only). - No automatic HTTP headers — add manually if needed. Choose `cache()` for most GET endpoints. Choose `CacheBackend` when you need conditional caching, multi-step invalidation, or custom serializers. `cache_evict()`/`cache_put()` bridge writes back to the same cache keys. ### Cache hit/miss internals - **Hit:** DI dependency raises `CacheHitException` → exception handler returns cached response. Endpoint never executes. - **Miss:** `CacheResponseCaptureMiddleware` buffers the response body and stores it in Redis after the endpoint returns. - No full ASGI middleware for reads — benchmarks showed no gain over DI. See `docs/guide/architecture.md` § Why not a full ASGI middleware. ### Storage model Cached entries are Redis **string** keys with eviction-group prefixes. Namespace deletion uses `SCAN` + `DEL`. Hash-based storage (faster eviction-group deletion via single `DEL`) is a future opt-in gated on Redis ≥ 8.0. See `docs/guide/architecture.md` § Storage model. ### Telemetry Three independent OTel layers (each opt-in): 1. **HTTP spans** — `opentelemetry-instrumentation-fastapi` (external). 2. **Cache operation spans + metrics** — `FastAPIRedis(app)...otel()` or `REDIS_OTEL_ENABLED=true`. Install `fastapi-redis-sdk[otel]`. 3. **Redis command spans** — `REDIS_OTEL_REDIS_ENABLED=true` or `opentelemetry-instrumentation-redis` (not both). See `docs/guide/architecture.md` § Telemetry for span names and metrics. ## Testing conventions - **Unit tests** (`tests/unit/`): use `fakeredis.aioredis`, no real Redis. - **Integration tests** (`tests/integration/`): real Redis, decorated with `@requires_redis` (auto-skip if server unreachable). - `filterwarnings = ["error"]` in pytest config — all warnings are errors. - Coverage threshold: 80% (`--cov-fail-under=80`). The integration-only session lowers this to 70 (`_INTEGRATION_COV_FLOOR` in `noxfile.py`) — 83 integration tests alone cover ~78% of `src/`. - CI runs the two suites in **separate jobs**: `unit-tests` runs `tests_unit` across 3 OSes x 5 Pythons, `integration-tests` runs `tests_integration` against Redis 8.8 and 7.4. See `docs/guide/compatibility.md` § 4. - `asyncio_mode = "auto"` — no need for `@pytest.mark.asyncio`. ### Test fixtures - `fake_async_redis` — fakeredis instance, available in unit tests. - `real_redis` / `real_async_redis` — real Redis clients for integration. - Every integration test flushes the DB on teardown. ## Configuration All settings via env vars prefixed `REDIS_` or `.env` file. Key vars: `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_SSL`, `REDIS_CLUSTER`, `REDIS_PREFIX`, `REDIS_DEFAULT_TTL`. Sentinel mode: `REDIS_SENTINEL=true`, `REDIS_SENTINEL_NODES` (comma-separated `host:port`), `REDIS_SENTINEL_MASTER_NAME`. The primary's pool is a `SentinelConnectionPool` in `_PoolState.async_pool`; `_PoolState.async_sentinel` holds the Sentinel clients, which the lifespan closes. ## Code style - **ruff** for linting + formatting (line-length 88). - **mypy** strict mode, Python 3.10 target. - `from __future__ import annotations` in all source files. - Type aliases use `Union[]` (not `X | Y`) for Python 3.10 compat in `deps.py` and `types.py` (ruff rule UP007 ignored there). ## CI CI is defined in `.github/workflows/ci.yml` and delegates to nox sessions. Do NOT add inline `uv run ruff` / `uv run mypy` commands to CI — use `uv run nox -s ` so CI and local checks stay in sync. ## Do NOT change - **Build system.** The project uses `uv_build` as its build backend. NEVER replace it with `hatchling`, `setuptools`, `flit`, or any other build backend. All build config lives in `pyproject.toml` under `[build-system]` and `[tool.uv.build-backend]`. - **DI approach.** Caching (for example) is implemented as `Depends()` factories (`cache()`, `cache_evict()`, `cache_put()`), NOT as decorators. NEVER refactor DI based solutions to use a decorator-based approach (`@cache`). The DI pattern is a deliberate design decision, see /guide/architecture.md for more information. ### Cache key format Default key: `{prefix}:{eviction_group}:{path}:{sorted_query_params}`. Eviction group is wrapped in hash-tag braces `{ns}` for Redis Cluster slot alignment. Query params are sorted alphabetically for determinism. Headers are NOT part of the key — use a custom `key_builder` for header-dependent responses (e.g. `Accept`). ### TTL behavior Fixed-window expiry. Accessing a cached entry does NOT extend its TTL. `ttl=0` or `ttl=None` means no automatic expiration. ### Error handling All Redis errors (`RedisError`, `OSError`) are caught and logged as warnings. Cache reads return `None`/miss; writes are silently dropped. Exceptions from endpoints are never cached — only successful responses. Corrupted cache data is treated as a miss (auto-fallback). ## Common pitfalls - `anyio.from_thread.run` takes a **zero-arg callable** returning an awaitable, NOT a coroutine object. Wrap with `lambda`. - `SyncCacheBackendDep` must be imported at module level when using `from __future__ import annotations`, or FastAPI cannot resolve the type. - Do not use `uv add --dev` in CI workflows — deps are managed by nox sessions and `pyproject.toml` dependency groups. - `cache()` reads `get_settings()` at dependency-creation time (when the module loads), not per-request. Runtime setting changes won't be picked up by already-registered routes.