Skip to main content

Lightweight decorator-driven Python async service framework with dependency injection

Project description

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.

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 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

@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, body: dict) -> dict:
        return {"id": 1, **body}

@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 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)

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

canary_framework-0.5.1.tar.gz (106.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

canary_framework-0.5.1-py3-none-any.whl (44.8 kB view details)

Uploaded Python 3

File details

Details for the file canary_framework-0.5.1.tar.gz.

File metadata

  • Download URL: canary_framework-0.5.1.tar.gz
  • Upload date:
  • Size: 106.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for canary_framework-0.5.1.tar.gz
Algorithm Hash digest
SHA256 0e2b9d04205f8c56af5fb7d3e2d55aa08a4b088fb2f304b3c750b9f8de33c0f9
MD5 71b7f7a21def31a899c7d2c14b680e11
BLAKE2b-256 1298fe975c2bd607cee5bbf522263d60a94c1eff571928225e8636752084c792

See more details on using hashes here.

Provenance

The following attestation bundles were made for canary_framework-0.5.1.tar.gz:

Publisher: publish.yml on HotcocoaCanary/Canary-Framework

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file canary_framework-0.5.1-py3-none-any.whl.

File metadata

File hashes

Hashes for canary_framework-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 01b9460e4b7cf2957d7ec2071affc8022e4a99a15c364944d39c69d9ab7d9a07
MD5 6561681ba165f2c00473a9191ede7f71
BLAKE2b-256 bf0270123b39c294d469ddf15f22ef00194b6cf0d1513de7356ec11d18a4c1a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for canary_framework-0.5.1-py3-none-any.whl:

Publisher: publish.yml on HotcocoaCanary/Canary-Framework

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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