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.2.tar.gz (91.4 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.2-py3-none-any.whl (14.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: cuneus-0.2.2.tar.gz
  • Upload date:
  • Size: 91.4 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.2.tar.gz
Algorithm Hash digest
SHA256 525f92303c9c95d4becfa22621cc4226f1867956fae6d680074db530461be9f5
MD5 be75ad22d7f61842b76d55c965d90bd9
BLAKE2b-256 07efc9c586797a978bd4e98f14fb37be92f8e9c25d4977051fe67114b2c6effd

See more details on using hashes here.

File details

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

File metadata

  • Download URL: cuneus-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 14.6 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 88ede38b3e054f9250073e5daae2c73969589c4699ef9b3f6f1e81c871e0a19a
MD5 662882105eb7af505b80527e52dd1e46
BLAKE2b-256 d5d86dda6ec779a6e03b3b900960a3f70e540af7f3429079884301294ff4e992

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