Skip to content

Getting Started

Note

For our examples we stick with aioinject as DI provider. Install it directly using your preferred package manager or as extra dependency of waku:

uv add "waku[aioinject]"
# or
uv add aioinject
pip install "waku[aioinject]"
# or
pip install aioinject

Creating Your First waku Application

Let's create a simple application that demonstrates waku core concepts.

Step 1: Create the Basic Structure

Create a new directory for your project and set up your files:

project/
├── app.py
└── services.py

Step 2: Define Your Services

In services.py, let's define a simple service:

services.py
1
2
3
class GreetingService:
    def greet(self, name: str) -> str:
        return f'Hello, {name}!'

Step 3: Create Modules

In app.py, let's define our modules and application setup:

app.py
import asyncio

from waku import Application, ApplicationFactory, module
from waku.di import Scoped, Injected, inject
from waku.di.contrib.aioinject import AioinjectDependencyProvider

from project.services import GreetingService


# Define a feature module
@module(
    providers=[Scoped(GreetingService)],
    exports=[GreetingService],
)
class GreetingModule:
    pass


# Define the root application module
@module(imports=[GreetingModule])
class AppModule:
    pass


# Define a function that will use our service
@inject
async def greet_user(greeting_service: Injected[GreetingService]) -> str:
    return greeting_service.greet('waku')


# Bootstrap the application
def bootstrap() -> Application:
    return ApplicationFactory.create(
        AppModule,
        dependency_provider=AioinjectDependencyProvider(),
    )


# Run the application
async def main() -> None:
    application = bootstrap()

    # Create a context for our application
    async with application, application.container.context():
        # Use our service
        message = await greet_user()  # type: ignore[call-arg]
        print(message)


if __name__ == '__main__':
    asyncio.run(main())

Step 4: Run Your Application

Run the application with:

python app.py

You should see the output:

Hello, waku!

Understanding the Basics

Let's break down what's happening in our simple application:

Modules

Modules are the building blocks of a waku application. Each module encapsulates a specific feature or functionality.

1
2
3
4
5
6
@module(
    providers=[Scoped(GreetingService)],
    exports=[GreetingService],
)
class GreetingModule:
    pass

In this example:

  • providers defines which providers this module creates and manages
  • exports makes these providers (or imported modules) available to other modules that import this one
  • Scoped indicates this provider should be created once for every container context entrance.

Info

For more information on providers and scopes, see Providers.

Application Bootstrap

The application is created using an ApplicationFactory:

1
2
3
4
5
def bootstrap() -> Application:
    return ApplicationFactory.create(
        AppModule,
        dependency_provider=AioinjectDependencyProvider(),
    )

This creates an application instance with:

  • AppModule as the root module
  • AioinjectDependencyProvider as the dependency injection provider

Dependency Injection

Providers are injected into functions using the @inject decorator:

1
2
3
@inject
async def greet_user(greeting_service: Injected[GreetingService]) -> str:
    return greeting_service.greet('waku')

The Injected[GreetingService] type annotation tells waku which provider to inject.

Context Management

waku uses context managers to manage the lifecycle of your application and its providers:

async with application, application.container.context():
    message = await greet_user()

In real applications, you would typically use this context managers in lifespan of your framework.

Creating a More Realistic Application

Let's extend our example to demonstrate a more realistic scenario with multiple modules and configuration.

Step 1: Enhanced Structure

Create a more complete project structure:

app/
├── __init__.py
├── __main__.py
├── application.py
├── modules/
│   ├── __init__.py
│   ├── greetings/
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── services.py
│   │   └── module.py
│   └── users/
│       ├── __init__.py
│       ├── models.py
│       ├── services.py
│       └── module.py
└── settings.py

Step 2: Add Configuration Module

Define an application settings class and configuration module for providing settings object to your application:

app/settings.py
from dataclasses import dataclass
from typing import Literal

from waku import DynamicModule, module
from waku.di import Object

Environment = Literal['dev', 'prod']


# You may consider using `pydantic-settings` or similar libs for settings management
@dataclass(kw_only=True)
class AppSettings:
    environment: Environment
    debug: bool


@module(is_global=True)
class ConfigModule:
    @classmethod
    def register(cls, env: Environment) -> DynamicModule:
        settings = AppSettings(
            environment=env,
            debug=env == 'dev',
        )
        return DynamicModule(
            parent_module=cls,
            providers=[Object(settings)],
        )

Step 3: Create Modules

Greeting Module

app/modules/greetings/models.py
1
2
3
4
5
6
7
from dataclasses import dataclass


@dataclass
class Greeting:
    language: str
    template: str
app/modules/greetings/services.py
from app.config import AppConfig
from app.modules.greetings.models import Greeting


class GreetingService:
    def __init__(self, config: AppConfig) -> None:
        self.config = config
        self.greetings: dict[str, Greeting] = {
            'en': Greeting(language='en', template='Hello, {}!'),
            'es': Greeting(language='es', template='¡Hola, {}!'),
            'fr': Greeting(language='fr', template='Bonjour, {}!'),
        }

    def get_greeting(self, language: str = 'en') -> Greeting:
        # If in debug mode and language not found, return default
        if self.config.debug and language not in self.greetings:
            return self.greetings['en']
        return self.greetings.get(language, self.greetings['en'])

    def greet(self, name: str, language: str = 'en') -> str:
        greeting = self.get_greeting(language)
        return greeting.template.format(name)

    def available_languages(self) -> list[str]:
        return list(self.greetings.keys())
app/modules/greetings/module.py
from waku import module
from waku.di import Singleton

from app.modules.greetings.services import GreetingService


@module(
    providers=[Singleton(GreetingService)],
    exports=[GreetingService],
)
class GreetingModule:
    pass

User Module

app/modules/users/models.py
1
2
3
4
5
6
7
8
from dataclasses import dataclass


@dataclass
class User:
    id: str
    name: str
    preferred_language: str = 'en'
app/modules/users/services.py
from app.modules.users.models import User


class UserService:
    def __init__(self) -> None:
        # Mock database
        self.users: dict[str, User] = {
            '1': User(id='1', name='Alice', preferred_language='en'),
            '2': User(id='2', name='Bob', preferred_language='fr'),
            '3': User(id='3', name='Carlos', preferred_language='es'),
        }

    def get_user(self, user_id: str) -> User | None:
        return self.users.get(user_id)
app/modules/users/module.py
from waku import module
from waku.di import Scoped

from app.modules.users.services import UserService


@module(
    providers=[Scoped(UserService)],
    exports=[UserService],
)
class UserModule:
    pass

Step 4: Create the Application Module

Define the application module and bootstrap function for initializing your application:

app/application.py
from waku import Application, ApplicationFactory, module
from waku.di.contrib.aioinject import AioinjectDependencyProvider

from app.settings import ConfigModule
from app.greetings.module import GreetingModule
from app.users.module import UserModule


@module(
    # Import all top-level modules
    imports=[
        ConfigModule.register(env='dev'),
        GreetingModule,
        UserModule,
    ],
)
class AppModule:
    pass


def bootstrap_application() -> Application:
    return ApplicationFactory.create(
        AppModule,
        dependency_provider=AioinjectDependencyProvider(),
    )

Step 5: Create the Main Entrypoint

In real world scenarios, you would use a framework like FastAPI, Flask, etc. for defining your entry points, also known as handlers. For the sake of simplicity, we don't use any framework in this example.

app/__main__.py
import asyncio

from waku.di import Injected, inject

from app.application import bootstrap_application
from app.modules.users.services import UserService
from app.modules.greetings.services import GreetingService


@inject
async def greet_user_by_id(
    user_id: str,
    user_service: Injected[UserService],
    greeting_service: Injected[GreetingService],
) -> str:
    user = user_service.get_user(user_id)
    if not user:
        return f'User {user_id} not found'

    return greeting_service.greet(name=user.name, language=user.preferred_language)


async def main() -> None:
    application = bootstrap_application()

    async with application, application.container.context():
        # Greet different users
        for user_id in ['1', '2', '3', '4']:  # '4' doesn't exist
            greeting = await greet_user_by_id(user_id)  # type: ignore[call-arg]
            print(greeting)

        # Get service directly for demonstration
        greeting_service = application.container.get(GreetingService)
        print(f'Available languages: {greeting_service.available_languages()}')


if __name__ == '__main__':
    asyncio.run(main())

Step 6: Run Your Application

python -m app

Expected output:

Hello, Alice!
Bonjour, Bob!
¡Hola, Carlos!
User 4 not found
Available languages: ['en', 'es', 'fr']

Next Steps

Now that you have a basic understanding of waku, you can:

  1. Explore more advanced features like Mediator (CQRS)
  2. Learn about Extensions for adding functionality to your application
  3. Integrate with web frameworks like FastAPI
  4. Understand Module System in depth
  5. Explore Dependency Injection techniques

waku modular architecture allows your application to grow while maintaining clear separation of concerns and a clean, maintainable codebase.

Note

This guide is a starting point. It's highly recommended to read The Software Architecture Chronicles by Herberto Graça. He distills all popular software architectural styles into a single one to rule them all. It's a great read and will help you understand the principles behind waku.

Happy coding with waku!