The wedge stone that locks the arch together
cuneus is a lightweight lifespan manager for FastAPI applications. It provides a simple pattern for composing extensions that handle startup/shutdown and service registration.
The name comes from Roman architecture: a cuneus is the wedge-shaped stone in a Roman arch. Each stone is simple on its own, but together they lock under pressure to create structures that have stood for millennia—no rebar required.
uv add cuneusor
pip install cuneus# app.py
from fastapi import FastAPI
from cuneus import build_lifespan, Settings
from cuneus.middleware.logging import LoggingMiddleware
from myapp.extensions import DatabaseExtension
settings = Settings()
lifespan = build_lifespan(
settings,
DatabaseExtension(settings),
)
app = FastAPI(lifespan=lifespan, title="My App", version="1.0.0")
# Add middleware directly to FastAPI
app.add_middleware(LoggingMiddleware)That's it. Extensions handle their lifecycle, FastAPI handles the rest.
Use BaseExtension for simple cases:
from cuneus import BaseExtension
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
import svcs
class DatabaseExtension(BaseExtension):
def __init__(self, settings):
self.settings = settings
self.engine: AsyncEngine | None = None
async def startup(self, registry: svcs.Registry, app: FastAPI) -> dict[str, Any]:
self.engine = create_async_engine(self.settings.database_url)
# Register with svcs for dependency injection
registry.register_value(AsyncEngine, self.engine)
# Add routes
app.include_router(health_router, prefix="/health")
# Add exception handlers
app.add_exception_handler(DBError, self.handle_db_error)
# Return state (accessible via request.state.db)
return {"db": self.engine}
async def shutdown(self, app: FastAPI) -> None:
if self.engine:
await self.engine.dispose()For full control, override register() directly:
from contextlib import asynccontextmanager
class RedisExtension(BaseExtension):
def __init__(self, settings):
self.settings = settings
@asynccontextmanager
async def register(self, registry: svcs.Registry, app: FastAPI):
redis = await aioredis.from_url(self.settings.redis_url)
registry.register_value(Redis, redis)
try:
yield {"redis": redis}
finally:
await redis.close()The lifespan exposes a .registry attribute for test overrides:
# test_app.py
from unittest.mock import Mock
from starlette.testclient import TestClient
from myapp import app, lifespan, Database
def test_db_error_handling():
with TestClient(app) as client:
# Override after app startup
mock_db = Mock(spec=Database)
mock_db.get_user.side_effect = Exception("boom")
lifespan.registry.register_value(Database, mock_db)
resp = client.get("/users/42")
assert resp.status_code == 500cuneus includes a base Settings class that loads from multiple sources:
from cuneus import Settings
class AppSettings(Settings):
database_url: str = "sqlite+aiosqlite:///./app.db"
redis_url: str = "redis://localhost"
model_config = SettingsConfigDict(env_prefix="APP_")Load priority (highest wins):
- Environment variables
.envfilepyproject.tomlunder[tool.cuneus]
Creates a lifespan context manager for FastAPI.
settings: Your settings instance (subclass ofSettings)*extensions: Extension instances to register
Returns a lifespan with a .registry attribute for testing.
Base class with startup() and shutdown() hooks:
startup(registry, app) -> dict[str, Any]: Setup resources, return stateshutdown(app) -> None: Cleanup resources
For full control, implement the protocol directly:
def register(self, registry: svcs.Registry, app: FastAPI) -> AsyncContextManager[dict[str, Any]]aget(request, *types)- Async get services from svcsget(request, *types)- Sync get services from svcsget_settings(request)- Get settings from request stateget_request_id(request)- Get request ID from request state
- Simple — one function,
build_lifespan(), does what you need - No magic — middleware added directly to FastAPI, not hidden
- Testable — registry exposed via
lifespan.registry - Composable — extensions are just async context managers
- Built on svcs — proper dependency injection, not global state
MIT