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.15.tar.gz (135.8 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.15-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: cuneus-0.2.15.tar.gz
  • Upload date:
  • Size: 135.8 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.15.tar.gz
Algorithm Hash digest
SHA256 4cf034be6ddac7e82ba67de5ed4f6f3887a1874e99557402fb134dd933933a6a
MD5 b60fabfc9dcbbc1f8c678b82bfe4bbbd
BLAKE2b-256 b48d2e05752afb54bc5a4b0aa057f13b65dab6f465bca64b1d17c6a5c0736604

See more details on using hashes here.

File details

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

File metadata

  • Download URL: cuneus-0.2.15-py3-none-any.whl
  • Upload date:
  • Size: 22.3 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.15-py3-none-any.whl
Algorithm Hash digest
SHA256 974b400fbd4938a4ef24c53ae25b2c050e5f8a47009b2bac47e8d6f56b5a62b9
MD5 786c94fdc2831eb163ea44f416b1a4e7
BLAKE2b-256 0404ae48e121a12da385b006804bbfba83d80e50651cad077319cd238b1ff997

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