Modules¶
Modules are containers that organize and compose services together. They manage the lifecycle of their child services and provide a way to structure your application hierarchically.
Defining a Module¶
Use the @module() decorator to define a module:
from canary_framework import module
from canary_framework.core.module import ModuleBase
@module(services=[Database, UserRepo, AuthApi])
class Auth(ModuleBase):
pass
@module(services=[...])— onlyservicesparameter needed- Name is auto-generated from the class name (
ClassName+"Module") - Module is automatically named
AuthModule
Module Composition¶
Modules can contain services and other modules, creating a hierarchical structure:
from canary_framework import module, service
from canary_framework.core.service import ServiceBase
from canary_framework.core.module import ModuleBase
from canary_framework.core.router import Router
# Core services
@service()
class Database(ServiceBase):
pass
@service()
class Cache(ServiceBase):
pass
# Auth module
@service()
class AuthService(ServiceBase):
db: Database
@service()
class AuthApi(ServiceBase):
router = Router(prefix="/auth")
auth: AuthService
@module(services=[AuthService, AuthApi])
class Auth(ModuleBase):
pass
# Posts module
@service()
class PostsService(ServiceBase):
db: Database
cache: Cache
@service()
class PostsApi(ServiceBase):
router = Router(prefix="/posts")
posts: PostsService
@module(services=[PostsService, PostsApi])
class Posts(ModuleBase):
pass
# Main application module
@module(services=[Database, Cache, Auth, Posts])
class App(ModuleBase):
pass
Module Children Access¶
Child services and sub-modules are accessible directly by their class name on the module instance:
app = App()
await app.init()
# Access child services by class name (not snake_case)
app.Database # Database service instance
app.Cache # Cache service instance
app.Auth # Auth sub-module instance
app.Posts # Posts sub-module instance
Module Lifecycle¶
Modules coordinate the lifecycle of their child services. When a module's lifecycle methods are called, they propagate to all child services in topological order.
app = App()
# 1. Init phase: initializes all services in dependency order
await app.init()
# 2. Startup phase: starts all services
await app.startup()
# ... application runs ...
# 3. Shutdown phase: shuts down all services in reverse order
await app.shutdown()
Module as ASGI App¶
A module can be used directly as an ASGI application. It automatically mounts all child routers:
@module(services=[...])
class App(ModuleBase):
pass
async def setup():
app = App()
await app.init()
return app
import asyncio
import uvicorn
app = asyncio.run(setup())
uvicorn.run(app, host="0.0.0.0", port=8080, lifespan="on")
The module will: 1. Collect all routers from its services 2. Mount them at paths based on their prefix 3. Handle ASGI requests
Module Base Class¶
Classes decorated with @module() must explicitly inherit from ModuleBase, which provides:
init()method: Initializes the module and all servicesstartup()method: Starts the module and all servicesshutdown()method: Shuts down the module and all servicesasgi_appproperty: Access to the ASGI application
Dependency Sharing¶
Services in a module share dependencies. If multiple services depend on the same service, only one instance is created and shared:
@service()
class Database(ServiceBase):
pass
@service()
class ServiceA(ServiceBase):
db: Database
@service()
class ServiceB(ServiceBase):
db: Database
@module(services=[Database, ServiceA, ServiceB])
class App(ModuleBase):
pass
# Both ServiceA and ServiceB receive the same Database instance
Complete Example¶
from canary_framework import module, service
from canary_framework.core.service import ServiceBase
from canary_framework.core.module import ModuleBase
from canary_framework.core.router import Router
# Services
@service()
class Database(ServiceBase):
async def query(self, sql):
pass
@service()
class UserRepo(ServiceBase):
db: Database
@service()
class UserService(ServiceBase):
repo: UserRepo
# Router
@service()
class Users(ServiceBase):
router = Router(prefix="/api/users")
user: UserService
@router.get("/")
async def list_users(self):
return {"users": []}
# Modules
@module(services=[UserRepo, UserService, Users])
class UsersMod(ModuleBase):
pass
@module(services=[Database, UsersMod])
class App(ModuleBase):
pass
Best Practices¶
- Layered Architecture: Organize modules by feature (e.g., auth, users, posts)
- Single Responsibility: Each module focuses on one functional area
- Module Composition: Build large applications by composing smaller modules
- Config Isolation: Provide isolated configuration space for each module
- Test Isolation: Each module can be tested independently
- Use descriptive annotation names:
db,repo,service— notd1,d2