Skip to content

Commit 4bbbe46

Browse files
Add Rodi's documentation
1 parent f2720b9 commit 4bbbe46

26 files changed

+2585
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ out
88
.local
99

1010
# temporary
11-
rodi
1211
shared-assets
1312
copy-shared.sh

blacksheep/docs/dependency-injection.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ This page describes:
1111
- [X] Examples of dependency injection.
1212
- [X] How to use alternatives to `rodi`.
1313

14+
!!! info "Rodi's documentation"
15+
Detailed documentation for Rodi can be found at: [_Rodi_](/rodi/).
16+
1417
## Introduction
1518

1619
The `Application` object exposes a `services` property that can be used to

home/docs/img/rodi.png

60.1 KB
Loading

home/docs/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ the documentation of some of the projects.
1111
image: ./img/blacksheep.png
1212
url: /blacksheep/
1313

14+
- title: Rodi
15+
content: |
16+
Non-intrusive Dependency Injection for Python.
17+
image: ./img/rodi.png
18+
url: /rodi/
19+
1420
- title: MkDocs-Plugins
1521
content: |
1622
Plugins for Python Markdown designed for MkDocs and Material for MkDocs.

home/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ edit_uri: ""
99
nav:
1010
- Index: index.md
1111
- BlackSheep: /blacksheep/
12+
- Rodi: /rodi/
1213
- MkDocs-Plugins: /mkdocs-plugins/
1314

1415
theme:

pack.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#! /bin/bash
22
folders=(
33
blacksheep
4+
rodi
45
mkdocs-plugins
56
)
67

rodi/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Rodi docs 📜
2+
3+
[www.neoteroi.dev](https://www.neoteroi.dev/rodi/).

rodi/docs/about.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# About Rodi
2+
3+
Rodi born from the desire of using a `non-intrusive` implementation of
4+
Dependency Injection for Python, that does not require modifying the code of
5+
types it resolved using decorators, like most others existing implementations
6+
of DI for Python. Type annotations make explicit decorators superfluous as
7+
the DI container can inspect the code to obtain all information it needs to
8+
resolve types.
9+
10+
Rodi is the built-in DI framework in the [BlackSheep](/blacksheep/) web
11+
framework, although it can be replaced with alternative solutions if desired.
12+
13+
## The project's home
14+
15+
The project is hosted in [GitHub](https://github.com/Neoteroi/rodi),
16+
handled following DevOps good practices, and is published to
17+
[pypi.org](https://pypi.org/project/rodi/).

rodi/docs/async.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
As explained in [_Getting Started_](./getting-started.md), Rodi's objective is to
2+
simplify constructing objects based on constructors and class properties.
3+
Support for async resolution is intentionally out of the scope of the library because
4+
constructing objects should be lightweight.
5+
6+
This page provides guidelines for working with objects that require asynchronous
7+
initialization.
8+
9+
## A common example
10+
11+
A common example of this situation are objects that handle TCP/IP connection pooling,
12+
such as `HTTP` clients and database clients. These objects are usually implemented as
13+
*context managers* in Python because they need to implement connection pooling and
14+
gracefully close TCP connections when disposed.
15+
16+
Python supports [`asynchronous` context managers](https://peps.python.org/pep-0492/#asynchronous-context-managers-and-async-with) for this kind of scenario.
17+
18+
Consider the following example, of a `SendGrid` API client to send emails using the
19+
SendGrid API, with asynchronous code and using [`httpx`](https://www.python-httpx.org/async/).
20+
21+
```python {linenums="1"}
22+
# domain/emails.py
23+
from abc import ABC, abstractmethod
24+
from dataclasses import dataclass
25+
26+
27+
# TODO: use Pydantic for the Email object.
28+
@dataclass
29+
class Email:
30+
recipients: list[str]
31+
sender: str
32+
sender_name: str
33+
subject: str
34+
body: str
35+
cc: list[str] = None
36+
bcc: list[str] = None
37+
38+
39+
class EmailHandler(ABC): # interface
40+
@abstractmethod
41+
async def send(self, email: Email) -> None:
42+
pass
43+
```
44+
45+
```python {linenums="1", hl_lines="24 32"}
46+
# data/apis/sendgrid.py
47+
import os
48+
from dataclasses import dataclass
49+
50+
import httpx
51+
52+
from domain.emails import Email, EmailHandler
53+
54+
55+
@dataclass
56+
class SendGridClientSettings:
57+
api_key: str
58+
59+
@classmethod
60+
def from_env(cls):
61+
api_key = os.environ.get("SENDGRID_API_KEY")
62+
if not api_key:
63+
raise ValueError("SENDGRID_API_KEY environment variable is required")
64+
return cls(api_key=api_key)
65+
66+
67+
class SendGridClient(EmailHandler):
68+
def __init__(
69+
self, settings: SendGridClientSettings, http_client: httpx.AsyncClient
70+
):
71+
if not settings.api_key:
72+
raise ValueError("API key is required")
73+
self.http_client = http_client
74+
self.api_key = settings.api_key
75+
76+
async def send(self, email: Email) -> None:
77+
response = await self.http_client.post(
78+
"https://api.sendgrid.com/v3/mail/send",
79+
headers={
80+
"Authorization": f"Bearer {self.api_key}",
81+
"Content-Type": "application/json",
82+
},
83+
json=self.get_body(email),
84+
)
85+
# Note: in case of error, inspect response.text
86+
response.raise_for_status() # Raise an error for bad responses
87+
88+
def get_body(self, email: Email) -> dict:
89+
return {
90+
"personalizations": [
91+
{
92+
"to": [{"email": recipient} for recipient in email.recipients],
93+
"subject": email.subject,
94+
"cc": [{"email": cc} for cc in email.cc] if email.cc else None,
95+
"bcc": [{"email": bcc} for bcc in email.bcc] if email.bcc else None,
96+
}
97+
],
98+
"from": {"email": email.sender, "name": email.sender_name},
99+
"content": [{"type": "text/html", "value": email.body}],
100+
}
101+
```
102+
103+
/// details | The official SendGrid Python SDK does not support async.
104+
type: danger
105+
106+
At the time of this writing, the official SendGrid Python SDK does not support `async`.
107+
Its documentation provides a wrong example for `async` code (see [_issue #988_](https://github.com/sendgrid/sendgrid-python/issues/988)).
108+
The SendGrid REST API is very well documented and comfortable to use! Use a class like
109+
the one shown on this page to send emails using SendGrid in async code.
110+
///
111+
112+
The **SendGridClient** depends on an instance of `SendGridClientSettings` (providing a
113+
SendGrid API Key), and on an instance of `httpx.AsyncClient` able to make HTTP requests.
114+
115+
The code below shows how to register the object that requires asynchronous
116+
initialization and use it across the lifetime of your application.
117+
118+
```python {linenums="1", hl_lines="12-20 25 40-41 44-46 48"}
119+
# main.py
120+
import asyncio
121+
from contextlib import asynccontextmanager
122+
123+
import httpx
124+
from rodi import Container
125+
126+
from data.apis.sendgrid import SendGridClient, SendGridClientSettings
127+
from domain.emails import EmailHandler
128+
129+
130+
@asynccontextmanager
131+
async def register_http_client(container: Container):
132+
133+
async with httpx.AsyncClient() as http_client:
134+
print("HTTP client initialized")
135+
container.add_instance(http_client)
136+
yield
137+
138+
print("HTTP client disposed")
139+
140+
141+
async def application_runtime(container: Container):
142+
# Entry point for what your application does
143+
email_handler = container.resolve(EmailHandler)
144+
assert isinstance(email_handler, SendGridClient)
145+
assert isinstance(email_handler.http_client, httpx.AsyncClient)
146+
147+
# We can use the HTTP Client during the lifetime of the Application
148+
print("All is good! ✨")
149+
150+
151+
def sendgrid_settings_factory() -> SendGridClientSettings:
152+
return SendGridClientSettings.from_env()
153+
154+
155+
async def main():
156+
# Bootstrap code for the application
157+
container = Container()
158+
container.add_singleton_by_factory(sendgrid_settings_factory)
159+
container.add_singleton(EmailHandler, SendGridClient)
160+
161+
async with register_http_client(container) as http_client:
162+
container.add_instance(
163+
http_client
164+
) # <-- Configure the HTTP client as singleton
165+
166+
await application_runtime(container)
167+
168+
169+
if __name__ == "__main__":
170+
asyncio.run(main())
171+
```
172+
173+
The above code displays the following:
174+
175+
```bash
176+
$ SENDGRID_API_KEY="***" python main.py
177+
178+
HTTP client initialized
179+
All is good!
180+
HTTP client disposed
181+
```
182+
183+
## Considerations
184+
185+
- It is not Rodi's responsibility to administer the lifecycle of the application. It is
186+
the responsibility of the code that bootstrap the application, to handle objects that
187+
require asynchronous initialization and disposal.
188+
- Python's `asynccontextmanager` is convenient for these scenarios.
189+
- In the example above, the HTTP Client is configured as singleton to benefit from TCP
190+
connection pooling. It would also be possible to configure it as transient or scoped
191+
service, as long as all instances share the same connection pool. In the case of
192+
`httpx`, you can read on this subject here: [Why use a Client?](https://www.python-httpx.org/advanced/clients/#why-use-a-client).
193+
- Dependency Injection likes custom classes to describe _settings_ for types,
194+
because registering simple types (`str`, `int`, `float`, etc.) in the container does
195+
not scale and should be avoided.
196+
197+
The next page explains how Rodi handles [context managers](./context-managers.md).

rodi/docs/context-managers.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
This page describes how to work with Rodi and context managers.
2+
3+
## How Rodi handles context managers
4+
5+
When a class implements the context manager protocol (`__enter__`, `__exit__`),
6+
Rodi instantiates the class but does **not** enter nor exit the instance
7+
automatically.
8+
9+
```python {linenums="1", hl_lines="4 10 15 25-26 41"}
10+
from rodi import Container
11+
12+
13+
class A:
14+
def __init__(self) -> None:
15+
print("A created")
16+
self.initialized = False
17+
self.disposed = False
18+
19+
def __enter__(self) -> "A":
20+
print("A initialized")
21+
self.initialized = True
22+
return self
23+
24+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
25+
self.disposed = True
26+
print("A destroyed")
27+
28+
29+
class B:
30+
def __init__(self, dependency: A) -> None:
31+
self.dependency = dependency
32+
33+
def do_work(self):
34+
with self.dependency:
35+
print("Do work")
36+
37+
38+
container = Container()
39+
40+
container.register(A)
41+
container.register(B)
42+
43+
b = container.resolve(B)
44+
45+
# b.dependency is instantiated and provided as is, it is not entered
46+
# automatically
47+
assert b.dependency.initialized is False
48+
assert b.dependency.disposed is False
49+
50+
b.do_work()
51+
assert b.dependency.initialized is True
52+
assert b.dependency.disposed is True
53+
```
54+
55+
/// admonition | Rodi does not enter and exit contexts.
56+
type: into
57+
58+
There is no way to unambiguously know the intentions of the developer:
59+
should a context be entered automatically and disposed automatically?
60+
///
61+
62+
## Async context managers
63+
64+
As described above for context managers, Rodi does not handle async context
65+
managers in any special way either.
66+
67+
```python {linenums="1", hl_lines="6 12 17 26-27 41"}
68+
import asyncio
69+
70+
from rodi import Container
71+
72+
73+
class A:
74+
def __init__(self) -> None:
75+
print("A created")
76+
self.initialized = False
77+
self.disposed = False
78+
79+
async def __aenter__(self) -> "A":
80+
print("A initialized")
81+
self.initialized = True
82+
return self
83+
84+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
85+
self.disposed = True
86+
print("A destroyed")
87+
88+
89+
class B:
90+
def __init__(self, dependency: A) -> None:
91+
self.dependency = dependency
92+
93+
async def do_work(self):
94+
async with self.dependency:
95+
print("Do work")
96+
97+
98+
container = Container()
99+
100+
container.register(A)
101+
container.register(B)
102+
103+
b = container.resolve(B)
104+
assert b.dependency.initialized is False
105+
assert b.dependency.disposed is False
106+
107+
108+
asyncio.run(b.do_work())
109+
assert b.dependency.initialized is True
110+
assert b.dependency.disposed is True
111+
```
112+
113+
The next page describes support for [_Union types_](./union-types.md) in Rodi.

0 commit comments

Comments
 (0)