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 initialize the container on application startup.
import os
from wireup import container, initialize_container
from myapp import services
def create_app():
app = ...
# ⬇️ Start the container: Register and initialize services.
initialize_container(
container,
# Parameters serve as service configuration.
parameters={
"redis_url": os.environ["APP_REDIS_URL"],
"weather_api_key": os.environ["APP_WEATHER_API_KEY"]
},
# Top-level modules containing service registrations.
service_modules=[services]
)
return app
Register application settings as a service.
from pydantic_settings import BaseSettings
from wireup import service
@service
class Settings(BaseSettings):
redis_url: str = Field(alias="APP_REDIS_URL")
weather_api_key: str = Field(alias="APP_WEATHER_API_KEY")
In the application's entrypoint initialize wireup.
from pydantic import Field
from wireup import container, initialize_container
from myapp import services
def create_app():
app = ...
# ⬇️ Start the container: Register and initialize services.
# service_modules is a list of top-level modules containing registrations.
initialize_container(container, service_modules=[services])
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 # TIP: Use alongside dataclasses to simplify init code.
class WeatherService:
api_key: Annotated[str, Inject(param="weather_api_key")]
kv_store: KeyValueStore
async def get_forecast(self, lat: float, lon: float) -> WeatherForecast:
raise NotImplementedError
-
- Injection is supported for regular classes as well as dataclasses.
- With 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:
raise NotImplementedError
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
Next Steps
While Wireup is framework-agnostic, usage can be further simplified in following frameworks: