Skip to main content

ASGI application wrapper

Project description

cuneus

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.

Installation

uv add cuneus

or

pip install cuneus

Quick Start

# app/main.py
from fastapi import FastAPI
from cuneus import build_app, Settings

from myapp.extensions import DatabaseExtension

class MyAppSettings(Settings):
    my_mood: str = "extatic"

app, cli = build_app(
    DatabaseExtension,
    settings=MyAppSettings(),
)

app.include_router(my_router)

__all__ = ["app", "cli"]

That's it. Extensions handle their lifecycle, registration, and middleware.

Creating Extensions

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()

    def middleware(self) -> list[Middleware]:
        return [Middleware(DatabaseLoggingMiddleware, level=INFO)]

    def register_cli(self, app_cli: click.Group) -> None:
        @app_cli.command()
        @click.option("--workers", default=1, type=int, help="Number of workers")
        def blow_up_db(workers: int): ...

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()

Testing

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 == 500

Settings

cuneus 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):

  1. Environment variables
  2. .env file
  3. pyproject.toml under [tool.cuneus]

API Reference

build_lifespan(settings, *extensions)

Creates a lifespan context manager for FastAPI.

  • settings: Your settings instance (subclass of Settings)
  • *extensions: Extension instances to register

Returns a lifespan with a .registry attribute for testing.

BaseExtension

Base class with startup() and shutdown() hooks:

  • startup(registry, app) -> dict[str, Any]: Setup resources, return state
  • shutdown(app) -> None: Cleanup resources
  • middleware() -> list[Middleware]: Optional middleware to configure
  • register_cli(group) -> None: Optional hook to add click commands

Extension Protocol

For full control, implement the protocol directly:

def register(self, registry: svcs.Registry, app: FastAPI) -> AsyncContextManager[dict[str, Any]]

Accessors

  • aget(request, *types) - Async get services from svcs
  • get(request, *types) - Sync get services from svcs
  • get_settings(request) - Get settings from request state
  • get_request_id(request) - Get request ID from request state

Why cuneus?

  • Simple — one function, build_app(), does what you need
  • Testable — registry exposed via lifespan.registry
  • Composable — extensions are just async context managers
  • Built on svcs — proper dependency injection, not global state

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cuneus-0.2.9.tar.gz (96.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cuneus-0.2.9-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

File details

Details for the file cuneus-0.2.9.tar.gz.

File metadata

  • Download URL: cuneus-0.2.9.tar.gz
  • Upload date:
  • Size: 96.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cuneus-0.2.9.tar.gz
Algorithm Hash digest
SHA256 cb8960dd2739c87be31e199ff5cda95b8f10c0030675ed429b9648d900f00b4e
MD5 dc2ae1d9591b76e3d6726ce46c4673d0
BLAKE2b-256 ad95ab1081ac946ec1a82c7e8313ce56e6736582e38d74f3d7af8dcc9782f88c

See more details on using hashes here.

File details

Details for the file cuneus-0.2.9-py3-none-any.whl.

File metadata

  • Download URL: cuneus-0.2.9-py3-none-any.whl
  • Upload date:
  • Size: 15.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cuneus-0.2.9-py3-none-any.whl
Algorithm Hash digest
SHA256 36b2edde6d9b6540dbc9f3ae56beb960dacec01cdb2d2a596fec9af3a6c62151
MD5 0307053b16b69549ab2136e3f8e2fe91
BLAKE2b-256 db3c46fac4e9c7c0012dddfd547f85081e53de20dabe4c34ae18fa4266fb9f61

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page