Skip to main content

Canary Framework

Lightweight Python Async Service Framework — Decorator-Driven, Annotation-Based DI

License Python CI GitHub Stars


Canary Framework is a decorator-driven async service framework for Python. Core philosophy: Services are the smallest unit, modules compose services, and modules themselves are services.

0.5.x is the actively-maintained line. As long as the core design philosophy holds, new features and fixes ship as 0.5.x — see the Versioning Policy in the changelog, and What's New in 0.5.x for the latest router redesign.

Core Features

  • Decorator-Driven — Use @service and @module decorators with explicit base class inheritance
  • Annotation-Based DI — Declare dependencies with type annotations: db: DatabaseService, no boilerplate
  • Topological Startup — Kahn's algorithm ensures dependencies start first
  • Lifecycle Management — @before_startup / @before_shutdown hooks
  • ASGI Compatible — Built on Starlette, works with uvicorn and other ASGI servers
  • Modular Architecture — Hierarchical composition with nested modules
  • OpenAPI Support — Auto-generated Swagger UI and ReDoc documentation

Installation

pip install canary-framework

Quick Start

from pydantic import BaseModel

from canary_framework import service, module
from canary_framework.core.service import ServiceBase
from canary_framework.core.module import ModuleBase
from canary_framework.core.router import Router

class NewUser(BaseModel):
    name: str

@service()
class Database(ServiceBase):
    async def init(self):
        await super().init()
        self.conn = "connected"

@service()
class UserService(ServiceBase):
    db: Database

    async def get_user(self, user_id: int):
        return {"id": user_id, "name": "Alice"}

@service()
class Api(ServiceBase):
    router = Router(prefix="/api", tags=["users"])
    user_service: UserService

    @router.get("/users/{user_id}")
    async def get_user(self, user_id: int) -> dict:
        return self.user_service.get_user(user_id)

    @router.post("/users")
    async def create_user(self, user: NewUser) -> dict:
        # `user` is a BaseModel param, so it's auto-detected as the request body.
        return {"id": 1, "name": user.name}

@module(services=[Database, UserService, Api])
class App(ModuleBase):
    pass

# ---- Entry Point ----

async def setup():
    app = App()
    await app.init()
    return app

if __name__ == "__main__":
    import asyncio
    import uvicorn

    app = asyncio.run(setup())
    uvicorn.run(app, lifespan="on")

Configuration

Use @config with CanaryConfig to customize framework behavior:

from canary_framework import config
from canary_framework.common.config import CanaryConfig

@config()
class AppConfig(CanaryConfig):
    host: str = "0.0.0.0"
    port: int = 8080
    openapi_title: str = "My API"
    log_level: str = "DEBUG"

@module(services=[AppConfig, Database, Api])
class App(ModuleBase):
    config: AppConfig

async def setup():
    app = App()
    await app.init()
    return app, app.config

Web Example with OpenAPI

from canary_framework import service, module
from canary_framework.core.service import ServiceBase
from canary_framework.core.module import ModuleBase
from canary_framework.core.router import Router
from pydantic import BaseModel, Field

class UserRequest(BaseModel):
    name: str = Field(description="User name")
    email: str = Field(description="User email")

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

@service()
class Users(ServiceBase):
    router = Router(prefix="/users", tags=["Users"])

    @router.get("/", summary="List users", description="Get all users")
    async def list_users(self) -> list[UserResponse]:
        return []

    @router.post("/",
          summary="Create user",
          description="Create a new user",
          request_model=UserRequest,
          response_model=UserResponse)
    async def create_user(self, body: UserRequest) -> UserResponse:
        return UserResponse(id=1, name=body.name, email=body.email)

@module(services=[Users])
class App(ModuleBase):
    pass

OpenAPI Documentation

Access automatically generated documentation:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • OpenAPI JSON: http://localhost:8000/openapi.json

Architecture

src/canary_framework/
├── common/              # Shared infrastructure
│   ├── config.py        # CanaryConfig
│   ├── errors.py        # Framework exceptions
│   ├── logging.py       # Framework logging
│   └── types.py         # Data classes, markers, and type aliases
├── core/                # Base classes
│   ├── module/
│   │   └── _base.py     # ModuleBase — orchestration and DI
│   ├── service/
│   │   ├── _base.py     # ServiceBase — lifecycle and ASGI
│   │   └── _hooks.py    # Lifecycle hook invocation
│   └── router/
│       ├── _base.py     # Router — route collection and ASGI routing
│       └── _utils.py    # Route handler building
├── decorators/          # Decorator implementations
│   ├── module.py        # @module
│   ├── service.py       # @service
│   ├── config.py        # @config
│   └── lifecycle.py     # @before_startup, @before_shutdown
└── engine/              # Runtime engine
    ├── registry.py      # Service registry
    ├── dependencies.py  # Topological sort + resolve_deps
    ├── openapi.py       # OpenAPI schema generation
    └── params.py        # Route parameter resolution

Dependency Injection Flow

@service() class MyService:
    db: DatabaseService      ←  1. User declares dependency via annotation

resolve_deps(MyService)
    → get_type_hints() reads {db: DatabaseService}
    → filters by CF_SERVICE_MARKER
    → returns {"db": DatabaseService}

    ↓ topo sort: Kahn's algorithm builds dependency order
    ↓ instantiation: creates instances in order
    ↓ wiring:

setattr(instance, "db", db_instance)   ←  2. Injected with annotation key name

Lifecycle Flow

app.init()
  ├── Register all services + transitive deps
  ├── Topological sort (Kahn's algorithm)
  ├── Instantiate services
  ├── Inject dependencies (annotation-driven)
  ├── Call init() on each service (topological order)

app.startup()
  ├── Invoke @before_startup hook
  └── Call startup() on each service (topological order)

app.shutdown()
  ├── Invoke @before_shutdown hook
  └── Call shutdown() on each service (reverse topological order)

Examples

The examples/ directory contains runnable, tested examples:

File Description
01_standalone.py Single service with Router, standalone mode
02_module_compose.py Module composing multiple services
03_nested_modules.py Nested module hierarchy
04_module_router.py Module with its own Router
05_config.py Configuration with @config() + CanaryConfig
06_lifecycle.py Lifecycle hooks (before_startup, before_shutdown)
07_validation.py Pydantic request/response validation
08_parameters.py Path, query, body parameter binding
09_openapi.py OpenAPI title/version/description customization
10_full_app.py Complete blog API with nested modules

Testing

# Run all tests
pytest

# Run unit tests
pytest tests/unit/

# Run integration tests
pytest tests/integration/

Community

Contributing

See CONTRIBUTING.md.

License

Apache 2.0 · Copyright 2026 Zhang Wenbo (Canary)

Release files for canary-framework 0.5.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for canary-framework 0.5.2
File Size Uploaded
canary_framework-0.5.2.tar.gz 129.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canary-framework 0.5.2
File Interpreter ABI Platform
canary_framework-0.5.2-py3-none-any.whl Python 3 none any Details

Total release size: 174.5 kB

Release files / canary_framework-0.5.2.tar.gz

Download URL canary_framework-0.5.2.tar.gz
Size 129.6 kB
Tags Source
SHA-256 checksum
How to use checksums
6d1ebcc51b6b3813ade8d478d0181c1be4f997e16a51ea1317ca161583b17eff
BLAKE2b-256 checksum
How to use checksums
481d7bbe64dab88ebb80ebae591a6942e7e609e130f2ecea2456f6ed00abff64
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.

Transparency log

Release files / canary_framework-0.5.2-py3-none-any.whl

Download URL canary_framework-0.5.2-py3-none-any.whl
Size 44.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0aaa3ed9a423741a8e7213afa0e9021f7876d0a21403d23f48c057976c3db53
BLAKE2b-256 checksum
How to use checksums
da494008084791d5ab303fa647503d6bdeff79c30bfcd1a72ee221b8ac3a55eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.

Transparency log

Release history Release notifications | RSS feed

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.10.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.6.1

2 release files

This release

0.5.2 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.13

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page