Getting started
This walkthrough will introduce you to the most common use cases for a typical application.
We'll build a simple weather forecast application that calls a remote weather service and uses a distributed key-value store to cache results.
1. Setup
Installation
Install wireup using pip or your favorite package manager.
$ pip install wireup
Configuration
Use Wireup the way you prefer
The container can be configured through annotations or programmatically. It was designed with annotations in mind but all features are available with either approach.
Sections below show how to achieve the same result using each method. Learn more.
The first step is to set up the container by exposing configuration on startup. In this example, we will store the Redis URL and an API key for the weather service.
import os
from wireup import container, initialize_container
from myapp import services
def create_app():
app = ...
# Expose configuration by populating container.params.
container.params.put("redis_url", os.environ["APP_REDIS_URL"])
container.params.put("weather_api_key", os.environ["APP_WEATHER_API_KEY"])
# Bulk update is also possible.
container.params.update(Settings().model_dump())
# Start the container: This registers + initializes services.
# `service_modules` contains top-level modules containing registrations.
initialize_container(container, service_modules=[services])
return app
from pydantic import Field, PostgresDsn
from pydantic_settings import BaseSettings
from wireup import container, initialize_container
from myapp.services import factories
class Settings(BaseSettings):
redis_url: str = Field(alias="APP_REDIS_URL")
weather_api_key: str = Field(alias="APP_WEATHER_API_KEY")
def create_app():
app = ...
# Expose configuration as a service in the container.
container.register(Settings)
# Start the container: This registers + initializes services
# service_modules contains top-level modules containing registrations.
initialize_container(container, service_modules=[factories])
return app
Now that the setup is complete, let's move on to the next step.
2. Define services
KeyValueStore
First, let's add a KeyValueStore
service. We wrap Redis with a class that abstracts it.
While we have the option to inject Redis directly, in this example, we've chosen the abstraction route.
The Redis client requires specific configuration details to establish a connection with the server, which we fetch from the configuration.
With a declarative approach, the container uses configuration metadata provided from decorators and annotations to define services and the dependencies between them. This means that the service declaration is self-contained and does not require additional setup.
from wireup import service, Inject
from typing_extensions import Annotated
@service #(1)!
class KeyValueStore:
def __init__(self, dsn: Annotated[str, Inject(param="redis_url")]) -> None: #(2)!
self.client = redis.from_url(dsn)
def get(self, key: str) -> Any: ...
def set(self, key: str, value: Any): ...
- Decorators do not modify the classes in any way and only serve to collect metadata. This makes testing simpler, as you can still instantiate this like a regular class in your tests.
- Parameters must be annotated with the
Inject(param=name)
syntax. This tells the container which parameter to inject.
The @service
decorator marks this class as a service to be registered in the container.
Decorators and annotations are read once during the call to initialize_container
.
With this approach, services are devoid of container references. Registration and creation is handled by factory functions.
class KeyValueStore:
def __init__(self, dsn: str) -> None:
self.client = redis.from_url(dsn)
def get(self, key: str) -> Any: ...
def set(self, key: str, value: Any): ...
The @service
decorator makes this factory known with the container.. Decorators/annotations
are read once during the call to initialize_container
.
Return type is mandatory and denotes what will be built.
from wireup import service
@service
def key_value_store_factory(settings: Settings) -> KeyValueStore:
return KeyValueStore(dsn=settings.redis_url)
WeatherService
Next, we add a weather service that will perform requests against a remote server and cache results as necessary.
The api_key
field contains the value of the weather_api_key
parameter as specified in the annotation.
KeyValueStore
will be automatically injected without requiring additional metadata.
from wireup import service
@service #(1)!
@dataclass
class WeatherService:
api_key: Annotated[str, Inject(param="weather_api_key")]
kv_store: KeyValueStore
async def get_forecast(self, lat: float, lon: float) -> WeatherForecast:
# implementation omitted for brevity
pass
-
- Injection is supported for regular classes as well as dataclasses.
- When using dataclasses it is important that the
@dataclass
decorator is applied before@service
.
-
- Use type hints to indicate which dependency to inject.
- Dependencies are automatically discovered and injected.
@dataclass
class WeatherService:
api_key: str
kv: KeyValueStore
async def get_forecast(self, lat: float, lon: float) -> WeatherForecast:
# implementation omitted for brevity
pass
from wireup import service
@service
def weather_service_factory(settings: Settings, kv_store: KeyValueStore) -> WeatherService:
return WeatherService(api_key=settings.weather_api_key, kv=kv_store)
That concludes service creation. The container knows how to build services and inject them as necessary.
3. Inject
The final step would be to decorate functions where the container needs to perform injection.
Decorate injection targets with @container.autowire
.
@app.get("/weather/forecast")
@container.autowire # (1)!
async def get_forecast_view(weather_service: WeatherService):
return await weather_service.get_forecast(...)
- Decorate methods where the library must perform injection.
Conclusion
This concludes the "Getting Started" walkthrough, covering the most common dependency injection use cases.
Good to know
- The
@container.autowire
decorator is not needed for services. - Wireup can perform injection on both sync and async functions.