Skip to content

Quick Start

Install

pip install canary-framework

Requires Python 3.12 or newer.

Your first unit

A unit is a plain class that subclasses Canary. It is constructed with no arguments, and its behaviour lives in phase hooks.

from canary_framework import Canary, init


class Config(Canary):
    @init
    def load(self) -> None:
        self.dsn = "postgresql://localhost/dev"

A method marked @init runs during the init phase. Hooks may be synchronous or async def; the framework decides whether to await by looking at the return value.

Declare dependencies

Use dep(). You choose the attribute name:

from canary_framework import Canary, dep, start, stop


class Database(Canary):
    config = dep(Config)

    @start
    async def connect(self) -> None:
        self.pool = await open_pool(self.config.dsn)

    @stop
    async def close(self) -> None:
        await self.pool.close()

self.config has type Config — type checkers and IDEs see it.

Run it

import asyncio


class UserService(Canary):
    database = dep(Database)


async def main() -> None:
    async with UserService() as service:
        rows = await service.database.pool.fetch("select 1")
        print(rows)


asyncio.run(main())

Entering async with enters init then start; leaving reclaims. The whole graph (Config → Database → UserService) comes up in dependency order on its own.

The four actions

For step-by-step control, use the explicit form — it behaves exactly like async with:

service = UserService()
await service.init()     # every @init
await service.start()    # every @start
await service.stop()     # every @stop, in reverse

Calling start() without init() raises LifecycleError rather than silently skipping a phase.

What belongs in which phase

  • Construction: nothing. A unit must be constructible with no arguments, and dependencies are not available yet.
  • @init: preparation that needs only dependencies and acquires nothing external — validation, building indexes, deriving values.
  • @start: acquire resources, start background tasks. Only what is acquired here is reclaimed by @stop.
  • @stop: release what @start acquired.

Complete examples

Canary-Framework-Example holds two real projects, deliberately different in shape. It is also checked out at examples/ as a submodule of this repository.

  • library — a library management system with RAG: a request-driven HTTP API on FastAPI.
  • telemetry — device telemetry and alerting: a long-running daemon on the core alone.
git clone https://github.com/HotcocoaCanary/Canary-Framework-Example.git
cd Canary-Framework-Example/library
uv sync && uv run pytest && uv run python main.py

Hosting

The framework knows nothing about shells. ASGI, a CLI or a message consumer all wire up the same way:

from contextlib import asynccontextmanager

from fastapi import FastAPI

service = UserService()


@asynccontextmanager
async def lifespan(_app: FastAPI):
    async with service:
        yield


app = FastAPI(lifespan=lifespan)

More hosts, test doubles and retry: see Patterns.