Canary Framework¶
Dependency injection and lifecycle for plain Python classes. Standard library only, zero third-party dependencies.
Subclass Canary and you have a unit: it declares what it depends on, and what it does in
each phase. Start one unit and its dependencies come up in dependency order; leaving reclaims
them in reverse.
import asyncio
from canary_framework import Canary, dep, init, start, stop
class Config(Canary):
@init
def load(self) -> None:
self.dsn = "postgresql://localhost/dev"
class Database(Canary):
config = dep(Config)
@start
async def connect(self) -> None:
print(f"connecting to {self.config.dsn}")
@stop
async def close(self) -> None:
print("disconnected")
class UserService(Canary):
database = dep(Database)
async def main() -> None:
async with UserService() as service:
print(service.database.config.dsn)
asyncio.run(main())
Two rules¶
The whole framework is two rules.
Entering runs dependencies first: a unit enters a phase only after its dependencies have completed it. One unit runs one phase exactly once no matter how many units depend on it, and independent units enter concurrently.
Releasing runs the unit first: stopping a unit stops it — unless something still uses it —
then tries its dependencies the same way, so what nothing else uses goes down with it. A failed
start() releases what it brought up before it raises.
Both run on the dependency graph, built before any hook runs — which is also where cycles are
caught. init / start / stop are three names for these two rules.
Core concepts¶
| Name | What it is |
|---|---|
Canary |
The unit base class. Subclass it and you get four lifecycle actions. |
dep(Cls) |
A dependency declaration. You choose the attribute name. |
@init / @start / @stop |
Phase markers: which phase a method belongs to. |
Phase |
A phase itself. Phase("migrate") is a fourth one, no registration needed. |
Scope |
The state one run shares. One scope is one graph. |
Invariants¶
- Units are always constructed with no arguments. Anything that needs the outside world happens in a lifecycle hook, because only those have a matching reclamation step.
- One instance per type per scope. Two separately constructed roots are two unrelated graphs.
- Dependencies exist from
@initonward. Reading one in__init__raisesLifecycleError. - Every
@initcompletes before any@startruns. stop()is the single reclamation path. Success and failure share it, it is idempotent, and it leaves the graph ready to start again.
Install¶
Requires Python 3.12 or newer. Installing pulls in no third-party packages.
Next¶
- Why Canary: where it fits, where it does not, and a measured comparison.
- Quick Start: a working example in ten minutes.
- Units: the four actions on
Canary. - Dependencies:
dep()and scopes. - Lifecycle: phases, the barrier, failure and reclamation.
- Patterns: test doubles, configuration, hosting, retry.
- API Reference: every public name.
- Versioning & Compatibility: what 1.x promises.