wirio 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wirio-0.7.0/LICENSE +22 -0
- wirio-0.7.0/PKG-INFO +240 -0
- wirio-0.7.0/README.md +220 -0
- wirio-0.7.0/pyproject.toml +87 -0
- wirio-0.7.0/src/wirio/__init__.py +0 -0
- wirio-0.7.0/src/wirio/_async_concurrent_dictionary.py +32 -0
- wirio-0.7.0/src/wirio/_integrations/__init__.py +0 -0
- wirio-0.7.0/src/wirio/_integrations/_fastapi_dependency_injection.py +203 -0
- wirio-0.7.0/src/wirio/_service_lookup/__init__.py +0 -0
- wirio-0.7.0/src/wirio/_service_lookup/_async_factory_call_site.py +65 -0
- wirio-0.7.0/src/wirio/_service_lookup/_asyncio_reentrant_lock.py +144 -0
- wirio-0.7.0/src/wirio/_service_lookup/_call_site_chain.py +43 -0
- wirio-0.7.0/src/wirio/_service_lookup/_call_site_factory.py +550 -0
- wirio-0.7.0/src/wirio/_service_lookup/_call_site_kind.py +9 -0
- wirio-0.7.0/src/wirio/_service_lookup/_call_site_runtime_resolver.py +330 -0
- wirio-0.7.0/src/wirio/_service_lookup/_call_site_visitor.py +117 -0
- wirio-0.7.0/src/wirio/_service_lookup/_constant_call_site.py +42 -0
- wirio-0.7.0/src/wirio/_service_lookup/_constructor_call_site.py +57 -0
- wirio-0.7.0/src/wirio/_service_lookup/_constructor_information.py +37 -0
- wirio-0.7.0/src/wirio/_service_lookup/_parameter_information.py +110 -0
- wirio-0.7.0/src/wirio/_service_lookup/_result_cache.py +58 -0
- wirio-0.7.0/src/wirio/_service_lookup/_runtime_service_provider_engine.py +34 -0
- wirio-0.7.0/src/wirio/_service_lookup/_service_call_site.py +51 -0
- wirio-0.7.0/src/wirio/_service_lookup/_service_identifier.py +67 -0
- wirio-0.7.0/src/wirio/_service_lookup/_service_provider_call_site.py +28 -0
- wirio-0.7.0/src/wirio/_service_lookup/_service_provider_engine.py +16 -0
- wirio-0.7.0/src/wirio/_service_lookup/_supports_async_context_manager.py +14 -0
- wirio-0.7.0/src/wirio/_service_lookup/_supports_context_manager.py +14 -0
- wirio-0.7.0/src/wirio/_service_lookup/_sync_factory_call_site.py +63 -0
- wirio-0.7.0/src/wirio/_service_lookup/_typed_type.py +102 -0
- wirio-0.7.0/src/wirio/_service_lookup/call_site_result_cache_location.py +8 -0
- wirio-0.7.0/src/wirio/_service_lookup/service_cache_key.py +38 -0
- wirio-0.7.0/src/wirio/_utils/__init__.py +0 -0
- wirio-0.7.0/src/wirio/_utils/_param_utils.py +36 -0
- wirio-0.7.0/src/wirio/_wirio_undefined.py +11 -0
- wirio-0.7.0/src/wirio/abstractions/__init__.py +0 -0
- wirio-0.7.0/src/wirio/abstractions/keyed_service.py +13 -0
- wirio-0.7.0/src/wirio/abstractions/keyed_service_container.py +28 -0
- wirio-0.7.0/src/wirio/abstractions/service_container_is_keyed_service.py +14 -0
- wirio-0.7.0/src/wirio/abstractions/service_container_is_service.py +10 -0
- wirio-0.7.0/src/wirio/abstractions/service_key_lookup_mode.py +7 -0
- wirio-0.7.0/src/wirio/abstractions/service_scope.py +43 -0
- wirio-0.7.0/src/wirio/abstractions/service_scope_factory.py +14 -0
- wirio-0.7.0/src/wirio/annotations.py +81 -0
- wirio-0.7.0/src/wirio/base_service_container.py +82 -0
- wirio-0.7.0/src/wirio/exceptions.py +143 -0
- wirio-0.7.0/src/wirio/py.typed +0 -0
- wirio-0.7.0/src/wirio/service_container.py +994 -0
- wirio-0.7.0/src/wirio/service_descriptor.py +258 -0
- wirio-0.7.0/src/wirio/service_lifetime.py +7 -0
- wirio-0.7.0/src/wirio/service_provider.py +273 -0
- wirio-0.7.0/src/wirio/service_provider_engine_scope.py +199 -0
wirio-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) .NET Foundation and Contributors
|
|
4
|
+
Copyright (c) 2025 Andreu Codina
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
wirio-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wirio
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Powerful Dependency Injection with Python
|
|
5
|
+
Keywords: dependency injection,di,inversion of control,ioc,fastapi
|
|
6
|
+
Author: Andreu Codina
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.118.0
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Project-URL: Homepage, https://github.com/AndreuCodina/wirio
|
|
16
|
+
Project-URL: Repository, https://github.com/AndreuCodina/wirio
|
|
17
|
+
Project-URL: Documentation, https://github.com/AndreuCodina/wirio
|
|
18
|
+
Project-URL: Changelog, https://github.com/AndreuCodina/wirio/releases
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
<img alt="Logo" src="https://raw.githubusercontent.com/AndreuCodina/wirio/refs/heads/main/docs/logo.png" width="522" height="348">
|
|
23
|
+
|
|
24
|
+
[](https://github.com/AndreuCodina/wirio/actions/workflows/main.yaml)
|
|
25
|
+
[](https://coveralls.io/github/AndreuCodina/wirio?branch=main)
|
|
26
|
+
[](https://pypi.org/project/wirio/)
|
|
27
|
+
[](https://github.com/AndreuCodina/wirio)
|
|
28
|
+
[](https://github.com/AndreuCodina/wirio/blob/main/LICENSE)
|
|
29
|
+
[](https://andreucodina.github.io/wirio)
|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Use it everywhere:** Use dependency injection in web servers, background tasks, console applications, Jupyter notebooks, tests, etc.
|
|
36
|
+
- **Lifetimes**: `Singleton` (same instance per application), `Scoped` (same instance per HTTP request scope) and `Transient` (different instance per resolution).
|
|
37
|
+
- **FastAPI integration** out of the box, and pluggable to any web framework.
|
|
38
|
+
- **Automatic resolution and disposal**: Automatically resolve constructor parameters and manage async and non-async context managers. It's no longer our concern to know how to create or dispose services.
|
|
39
|
+
- **Clear design** inspired by one of the most used and battle-tested DI libraries, adding async-native support, important features and good defaults.
|
|
40
|
+
- **Centralized configuration**: Register all services in one place using a clean syntax, and without decorators.
|
|
41
|
+
- **ty** and **Pyright** strict compliant.
|
|
42
|
+
|
|
43
|
+
## 📦 Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add wirio
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ✨ Quickstart with FastAPI
|
|
50
|
+
|
|
51
|
+
Inject services into async endpoints using `Annotated[..., FromServices()]`.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
class EmailService:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class UserService:
|
|
59
|
+
def __init__(self, email_service: EmailService) -> None:
|
|
60
|
+
self.email_service = email_service
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
app = FastAPI()
|
|
64
|
+
|
|
65
|
+
@app.post("/users")
|
|
66
|
+
async def create_user(user_service: Annotated[UserService, FromServices()]) -> None:
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
services = ServiceContainer()
|
|
70
|
+
services.add_transient(EmailService)
|
|
71
|
+
services.add_transient(UserService)
|
|
72
|
+
services.configure_fastapi(app)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## ✨ Quickstart without FastAPI
|
|
76
|
+
|
|
77
|
+
Register services and resolve them.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
class EmailService:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UserService:
|
|
85
|
+
def __init__(self, email_service: EmailService) -> None:
|
|
86
|
+
self.email_service = email_service
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
services = ServiceContainer()
|
|
90
|
+
services.add_transient(EmailService)
|
|
91
|
+
services.add_transient(UserService)
|
|
92
|
+
|
|
93
|
+
user_service = await services.get(UserService)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If we want a scope per operation (e.g., per HTTP request or message from a queue), we can create a scope from the service provider:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
async with services.create_scope() as service_scope:
|
|
100
|
+
user_service = await service_scope.get(UserService)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 🔄 Lifetimes
|
|
104
|
+
|
|
105
|
+
- `Transient`: A new instance is created every time the service is requested. Examples: Services without state, workflows, repositories, service clients...
|
|
106
|
+
- `Singleton`: The same instance is used every time the service is requested. Examples: Settings (`pydantic-settings`), machine learning models, database connection pools, caches.
|
|
107
|
+
- `Scoped`: A new instance is created for each new scope, but the same instance is returned within the same scope. Examples: Database clients, unit of work.
|
|
108
|
+
|
|
109
|
+
## 🏭 Factories
|
|
110
|
+
|
|
111
|
+
Sometimes, we need to use a factory function to create a service. For example, we have settings (a connection string, database name, etc.) stored using the package `pydantic-settings` and we want to provide them to a service `DatabaseClient` to access a database.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
class ApplicationSettings(BaseSettings):
|
|
115
|
+
database_connection_string: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DatabaseClient:
|
|
119
|
+
def __init__(self, connection_string: str) -> None:
|
|
120
|
+
pass
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
In a real `DatabaseClient` implementation, we must use a sync or async context manager, i.e., we instance it with:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
async with DatabaseClient(database_connection_string) as client:
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
And, if we want to reuse it, we create a factory function with yield:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
async def create_database_client(application_settings: ApplicationSettings) -> AsyncGenerator[DatabaseClient]:
|
|
134
|
+
async with DatabaseClient(application_settings.database_connection_string) as database_client:
|
|
135
|
+
yield database_client
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With that factory, we have to provide manually a singleton of `ApplicationSettings`, and to know if `DatabaseClient` implements a sync or async context manager, or neither. Apart from that, if we need a singleton or scoped instance of `DatabaseClient`, it's very complex to manage the disposal of the instance.
|
|
139
|
+
|
|
140
|
+
Then, why don't just return it? With this package, we just have this:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
def inject_database_client(application_settings: ApplicationSettings) -> DatabaseClient:
|
|
144
|
+
return DatabaseClient(
|
|
145
|
+
connection_string=application_settings.database_connection_string
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
services.add_transient(inject_database_client)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The factories can take as parameters other services registered. In this case, `inject_database_client` takes `ApplicationSettings` as a parameter, and the dependency injection mechanism will resolve it automatically.
|
|
152
|
+
|
|
153
|
+
## 🧪 Simplified testing
|
|
154
|
+
|
|
155
|
+
We can substitute dependencies on the fly meanwhile the context manager is active.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
with services.override(EmailService, email_service_mock):
|
|
159
|
+
user_service = await services.get(UserService)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 📝 Interfaces & abstract classes
|
|
163
|
+
|
|
164
|
+
We can register a service by specifying both the service type (interface / abstract class) and the implementation type (concrete class). This is useful when we want to inject services using abstractions.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
class NotificationService(ABC):
|
|
168
|
+
@abstractmethod
|
|
169
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class EmailService(NotificationService):
|
|
174
|
+
@override
|
|
175
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UserService:
|
|
180
|
+
def __init__(self, notification_service: NotificationService) -> None:
|
|
181
|
+
self.notification_service = notification_service
|
|
182
|
+
|
|
183
|
+
async def create_user(self, email: str) -> None:
|
|
184
|
+
user = self.create_user(email)
|
|
185
|
+
await self.notification_service.send_notification(user.id, "Welcome to our service!")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
services.add_transient(NotificationService, EmailService)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 📝 Keyed services
|
|
192
|
+
|
|
193
|
+
We can register a service by specifying both the service type and a key. This is useful when we want to resolve services using abstractions and an explicit key.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class NotificationService(ABC):
|
|
197
|
+
@abstractmethod
|
|
198
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class EmailService(NotificationService):
|
|
203
|
+
@override
|
|
204
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PushNotificationService(NotificationService):
|
|
209
|
+
@override
|
|
210
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class UserService:
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
notification_service: Annotated[NotificationService, FromKeyedServices("email"),
|
|
218
|
+
) -> None:
|
|
219
|
+
self.notification_service = notification_service
|
|
220
|
+
|
|
221
|
+
async def create_user(self, email: str) -> None:
|
|
222
|
+
user = self.create_user(email)
|
|
223
|
+
await self.notification_service.send_notification(user.id, "Welcome to our service!")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
services.add_keyed_transient("email", NotificationService, EmailService)
|
|
227
|
+
services.add_keyed_transient("push", NotificationService, PushNotificationService)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 📝 Auto-activated services
|
|
231
|
+
|
|
232
|
+
We can register a service as auto-activated. This is useful when we want to ensure our FastAPI application doesn't start to serve requests until certain services are fully initialized (e.g., machine learning models, database connection pools and caches).
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
services.add_auto_activated_singleton(MachineLearningModel)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 📚 Documentation
|
|
239
|
+
|
|
240
|
+
For more information, [check out the documentation](https://AndreuCodina.github.io/wirio).
|
wirio-0.7.0/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img alt="Logo" src="https://raw.githubusercontent.com/AndreuCodina/wirio/refs/heads/main/docs/logo.png" width="522" height="348">
|
|
3
|
+
|
|
4
|
+
[](https://github.com/AndreuCodina/wirio/actions/workflows/main.yaml)
|
|
5
|
+
[](https://coveralls.io/github/AndreuCodina/wirio?branch=main)
|
|
6
|
+
[](https://pypi.org/project/wirio/)
|
|
7
|
+
[](https://github.com/AndreuCodina/wirio)
|
|
8
|
+
[](https://github.com/AndreuCodina/wirio/blob/main/LICENSE)
|
|
9
|
+
[](https://andreucodina.github.io/wirio)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Use it everywhere:** Use dependency injection in web servers, background tasks, console applications, Jupyter notebooks, tests, etc.
|
|
16
|
+
- **Lifetimes**: `Singleton` (same instance per application), `Scoped` (same instance per HTTP request scope) and `Transient` (different instance per resolution).
|
|
17
|
+
- **FastAPI integration** out of the box, and pluggable to any web framework.
|
|
18
|
+
- **Automatic resolution and disposal**: Automatically resolve constructor parameters and manage async and non-async context managers. It's no longer our concern to know how to create or dispose services.
|
|
19
|
+
- **Clear design** inspired by one of the most used and battle-tested DI libraries, adding async-native support, important features and good defaults.
|
|
20
|
+
- **Centralized configuration**: Register all services in one place using a clean syntax, and without decorators.
|
|
21
|
+
- **ty** and **Pyright** strict compliant.
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add wirio
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## ✨ Quickstart with FastAPI
|
|
30
|
+
|
|
31
|
+
Inject services into async endpoints using `Annotated[..., FromServices()]`.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
class EmailService:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UserService:
|
|
39
|
+
def __init__(self, email_service: EmailService) -> None:
|
|
40
|
+
self.email_service = email_service
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
app = FastAPI()
|
|
44
|
+
|
|
45
|
+
@app.post("/users")
|
|
46
|
+
async def create_user(user_service: Annotated[UserService, FromServices()]) -> None:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
services = ServiceContainer()
|
|
50
|
+
services.add_transient(EmailService)
|
|
51
|
+
services.add_transient(UserService)
|
|
52
|
+
services.configure_fastapi(app)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## ✨ Quickstart without FastAPI
|
|
56
|
+
|
|
57
|
+
Register services and resolve them.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
class EmailService:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class UserService:
|
|
65
|
+
def __init__(self, email_service: EmailService) -> None:
|
|
66
|
+
self.email_service = email_service
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
services = ServiceContainer()
|
|
70
|
+
services.add_transient(EmailService)
|
|
71
|
+
services.add_transient(UserService)
|
|
72
|
+
|
|
73
|
+
user_service = await services.get(UserService)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If we want a scope per operation (e.g., per HTTP request or message from a queue), we can create a scope from the service provider:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
async with services.create_scope() as service_scope:
|
|
80
|
+
user_service = await service_scope.get(UserService)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 🔄 Lifetimes
|
|
84
|
+
|
|
85
|
+
- `Transient`: A new instance is created every time the service is requested. Examples: Services without state, workflows, repositories, service clients...
|
|
86
|
+
- `Singleton`: The same instance is used every time the service is requested. Examples: Settings (`pydantic-settings`), machine learning models, database connection pools, caches.
|
|
87
|
+
- `Scoped`: A new instance is created for each new scope, but the same instance is returned within the same scope. Examples: Database clients, unit of work.
|
|
88
|
+
|
|
89
|
+
## 🏭 Factories
|
|
90
|
+
|
|
91
|
+
Sometimes, we need to use a factory function to create a service. For example, we have settings (a connection string, database name, etc.) stored using the package `pydantic-settings` and we want to provide them to a service `DatabaseClient` to access a database.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
class ApplicationSettings(BaseSettings):
|
|
95
|
+
database_connection_string: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DatabaseClient:
|
|
99
|
+
def __init__(self, connection_string: str) -> None:
|
|
100
|
+
pass
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
In a real `DatabaseClient` implementation, we must use a sync or async context manager, i.e., we instance it with:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
async with DatabaseClient(database_connection_string) as client:
|
|
107
|
+
...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
And, if we want to reuse it, we create a factory function with yield:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
async def create_database_client(application_settings: ApplicationSettings) -> AsyncGenerator[DatabaseClient]:
|
|
114
|
+
async with DatabaseClient(application_settings.database_connection_string) as database_client:
|
|
115
|
+
yield database_client
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
With that factory, we have to provide manually a singleton of `ApplicationSettings`, and to know if `DatabaseClient` implements a sync or async context manager, or neither. Apart from that, if we need a singleton or scoped instance of `DatabaseClient`, it's very complex to manage the disposal of the instance.
|
|
119
|
+
|
|
120
|
+
Then, why don't just return it? With this package, we just have this:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
def inject_database_client(application_settings: ApplicationSettings) -> DatabaseClient:
|
|
124
|
+
return DatabaseClient(
|
|
125
|
+
connection_string=application_settings.database_connection_string
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
services.add_transient(inject_database_client)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The factories can take as parameters other services registered. In this case, `inject_database_client` takes `ApplicationSettings` as a parameter, and the dependency injection mechanism will resolve it automatically.
|
|
132
|
+
|
|
133
|
+
## 🧪 Simplified testing
|
|
134
|
+
|
|
135
|
+
We can substitute dependencies on the fly meanwhile the context manager is active.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
with services.override(EmailService, email_service_mock):
|
|
139
|
+
user_service = await services.get(UserService)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 📝 Interfaces & abstract classes
|
|
143
|
+
|
|
144
|
+
We can register a service by specifying both the service type (interface / abstract class) and the implementation type (concrete class). This is useful when we want to inject services using abstractions.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
class NotificationService(ABC):
|
|
148
|
+
@abstractmethod
|
|
149
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class EmailService(NotificationService):
|
|
154
|
+
@override
|
|
155
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class UserService:
|
|
160
|
+
def __init__(self, notification_service: NotificationService) -> None:
|
|
161
|
+
self.notification_service = notification_service
|
|
162
|
+
|
|
163
|
+
async def create_user(self, email: str) -> None:
|
|
164
|
+
user = self.create_user(email)
|
|
165
|
+
await self.notification_service.send_notification(user.id, "Welcome to our service!")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
services.add_transient(NotificationService, EmailService)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 📝 Keyed services
|
|
172
|
+
|
|
173
|
+
We can register a service by specifying both the service type and a key. This is useful when we want to resolve services using abstractions and an explicit key.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
class NotificationService(ABC):
|
|
177
|
+
@abstractmethod
|
|
178
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class EmailService(NotificationService):
|
|
183
|
+
@override
|
|
184
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class PushNotificationService(NotificationService):
|
|
189
|
+
@override
|
|
190
|
+
async def send_notification(self, user_id: str, message: str) -> None:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class UserService:
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
notification_service: Annotated[NotificationService, FromKeyedServices("email"),
|
|
198
|
+
) -> None:
|
|
199
|
+
self.notification_service = notification_service
|
|
200
|
+
|
|
201
|
+
async def create_user(self, email: str) -> None:
|
|
202
|
+
user = self.create_user(email)
|
|
203
|
+
await self.notification_service.send_notification(user.id, "Welcome to our service!")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
services.add_keyed_transient("email", NotificationService, EmailService)
|
|
207
|
+
services.add_keyed_transient("push", NotificationService, PushNotificationService)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 📝 Auto-activated services
|
|
211
|
+
|
|
212
|
+
We can register a service as auto-activated. This is useful when we want to ensure our FastAPI application doesn't start to serve requests until certain services are fully initialized (e.g., machine learning models, database connection pools and caches).
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
services.add_auto_activated_singleton(MachineLearningModel)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## 📚 Documentation
|
|
219
|
+
|
|
220
|
+
For more information, [check out the documentation](https://AndreuCodina.github.io/wirio).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wirio"
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
description = "Powerful Dependency Injection with Python"
|
|
5
|
+
authors = [{ name = "Andreu Codina" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Programming Language :: Python :: 3.14",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"dependency injection",
|
|
17
|
+
"di",
|
|
18
|
+
"inversion of control",
|
|
19
|
+
"ioc",
|
|
20
|
+
"fastapi",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.13"
|
|
23
|
+
dependencies = ["fastapi[standard-no-fastapi-cloud-cli]>=0.118.0"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/AndreuCodina/wirio"
|
|
27
|
+
Repository = "https://github.com/AndreuCodina/wirio"
|
|
28
|
+
Documentation = "https://github.com/AndreuCodina/wirio"
|
|
29
|
+
Changelog = "https://github.com/AndreuCodina/wirio/releases"
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"jupyter>=1.1.1",
|
|
34
|
+
"mkdocs-material>=9.7.1",
|
|
35
|
+
"mkdocstrings-python>=2.0.1",
|
|
36
|
+
"pyright[nodejs]>=1.1.408",
|
|
37
|
+
"pytest>=9.0.2",
|
|
38
|
+
"pytest-asyncio>=1.3.0",
|
|
39
|
+
"pytest-cov>=7.0.0",
|
|
40
|
+
"pytest-mock>=3.15.1",
|
|
41
|
+
"ruff>=0.14.14",
|
|
42
|
+
"ty>=0.0.14",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["uv_build"]
|
|
47
|
+
build-backend = "uv_build"
|
|
48
|
+
|
|
49
|
+
[tool.uv]
|
|
50
|
+
required-version = ">=0.9.18,<0.10.0"
|
|
51
|
+
|
|
52
|
+
[tool.pyright]
|
|
53
|
+
typeCheckingMode = "strict"
|
|
54
|
+
|
|
55
|
+
[tool.ty.terminal]
|
|
56
|
+
error-on-warning = true
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["ALL"]
|
|
60
|
+
ignore = [
|
|
61
|
+
"COM812", # missing-trailing-comma
|
|
62
|
+
"D100", # undocumented-public-module
|
|
63
|
+
"D101", # undocumented-public-class
|
|
64
|
+
"D102", # undocumented-public-method
|
|
65
|
+
"D103", # undocumented-public-function
|
|
66
|
+
"D104", # undocumented-public-package
|
|
67
|
+
"D105", # undocumented-magic-method
|
|
68
|
+
"D106", # undocumented-public-nested-class
|
|
69
|
+
"D107", # undocumented-public-init
|
|
70
|
+
"E501", # line-too-long
|
|
71
|
+
"ICN", # flake8-import-conventions
|
|
72
|
+
"S101", # assert
|
|
73
|
+
"TRY003", # raise-vanilla-args
|
|
74
|
+
"FBT001", # boolean-type-hint-positional-argument
|
|
75
|
+
"FBT002", # boolean-default-value-positional-argument
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint.per-file-ignores]
|
|
79
|
+
"docs/**" = [
|
|
80
|
+
"F841", # unused-variable
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
[tool.pytest]
|
|
85
|
+
strict = true
|
|
86
|
+
asyncio_mode = "auto"
|
|
87
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsyncConcurrentDictionary[TKey, TValue]:
|
|
7
|
+
"""Coroutine-safe collection of key/value pairs that can be accessed by multiple coroutines concurrently."""
|
|
8
|
+
|
|
9
|
+
_dict: Final[dict[TKey, TValue]]
|
|
10
|
+
_lock: Final[asyncio.Lock]
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self._dict = {}
|
|
14
|
+
self._lock = asyncio.Lock()
|
|
15
|
+
|
|
16
|
+
async def get_or_add(
|
|
17
|
+
self, key: TKey, value_factory: Callable[[TKey], Awaitable[TValue]]
|
|
18
|
+
) -> TValue:
|
|
19
|
+
if key not in self._dict:
|
|
20
|
+
async with self._lock:
|
|
21
|
+
if key not in self._dict:
|
|
22
|
+
value = await value_factory(key)
|
|
23
|
+
self._dict[key] = value
|
|
24
|
+
|
|
25
|
+
return self._dict[key]
|
|
26
|
+
|
|
27
|
+
def get(self, key: TKey) -> TValue | None:
|
|
28
|
+
return self._dict.get(key)
|
|
29
|
+
|
|
30
|
+
async def upsert(self, key: TKey, value: TValue) -> None:
|
|
31
|
+
async with self._lock:
|
|
32
|
+
self._dict[key] = value
|
|
File without changes
|