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.
Files changed (52) hide show
  1. wirio-0.7.0/LICENSE +22 -0
  2. wirio-0.7.0/PKG-INFO +240 -0
  3. wirio-0.7.0/README.md +220 -0
  4. wirio-0.7.0/pyproject.toml +87 -0
  5. wirio-0.7.0/src/wirio/__init__.py +0 -0
  6. wirio-0.7.0/src/wirio/_async_concurrent_dictionary.py +32 -0
  7. wirio-0.7.0/src/wirio/_integrations/__init__.py +0 -0
  8. wirio-0.7.0/src/wirio/_integrations/_fastapi_dependency_injection.py +203 -0
  9. wirio-0.7.0/src/wirio/_service_lookup/__init__.py +0 -0
  10. wirio-0.7.0/src/wirio/_service_lookup/_async_factory_call_site.py +65 -0
  11. wirio-0.7.0/src/wirio/_service_lookup/_asyncio_reentrant_lock.py +144 -0
  12. wirio-0.7.0/src/wirio/_service_lookup/_call_site_chain.py +43 -0
  13. wirio-0.7.0/src/wirio/_service_lookup/_call_site_factory.py +550 -0
  14. wirio-0.7.0/src/wirio/_service_lookup/_call_site_kind.py +9 -0
  15. wirio-0.7.0/src/wirio/_service_lookup/_call_site_runtime_resolver.py +330 -0
  16. wirio-0.7.0/src/wirio/_service_lookup/_call_site_visitor.py +117 -0
  17. wirio-0.7.0/src/wirio/_service_lookup/_constant_call_site.py +42 -0
  18. wirio-0.7.0/src/wirio/_service_lookup/_constructor_call_site.py +57 -0
  19. wirio-0.7.0/src/wirio/_service_lookup/_constructor_information.py +37 -0
  20. wirio-0.7.0/src/wirio/_service_lookup/_parameter_information.py +110 -0
  21. wirio-0.7.0/src/wirio/_service_lookup/_result_cache.py +58 -0
  22. wirio-0.7.0/src/wirio/_service_lookup/_runtime_service_provider_engine.py +34 -0
  23. wirio-0.7.0/src/wirio/_service_lookup/_service_call_site.py +51 -0
  24. wirio-0.7.0/src/wirio/_service_lookup/_service_identifier.py +67 -0
  25. wirio-0.7.0/src/wirio/_service_lookup/_service_provider_call_site.py +28 -0
  26. wirio-0.7.0/src/wirio/_service_lookup/_service_provider_engine.py +16 -0
  27. wirio-0.7.0/src/wirio/_service_lookup/_supports_async_context_manager.py +14 -0
  28. wirio-0.7.0/src/wirio/_service_lookup/_supports_context_manager.py +14 -0
  29. wirio-0.7.0/src/wirio/_service_lookup/_sync_factory_call_site.py +63 -0
  30. wirio-0.7.0/src/wirio/_service_lookup/_typed_type.py +102 -0
  31. wirio-0.7.0/src/wirio/_service_lookup/call_site_result_cache_location.py +8 -0
  32. wirio-0.7.0/src/wirio/_service_lookup/service_cache_key.py +38 -0
  33. wirio-0.7.0/src/wirio/_utils/__init__.py +0 -0
  34. wirio-0.7.0/src/wirio/_utils/_param_utils.py +36 -0
  35. wirio-0.7.0/src/wirio/_wirio_undefined.py +11 -0
  36. wirio-0.7.0/src/wirio/abstractions/__init__.py +0 -0
  37. wirio-0.7.0/src/wirio/abstractions/keyed_service.py +13 -0
  38. wirio-0.7.0/src/wirio/abstractions/keyed_service_container.py +28 -0
  39. wirio-0.7.0/src/wirio/abstractions/service_container_is_keyed_service.py +14 -0
  40. wirio-0.7.0/src/wirio/abstractions/service_container_is_service.py +10 -0
  41. wirio-0.7.0/src/wirio/abstractions/service_key_lookup_mode.py +7 -0
  42. wirio-0.7.0/src/wirio/abstractions/service_scope.py +43 -0
  43. wirio-0.7.0/src/wirio/abstractions/service_scope_factory.py +14 -0
  44. wirio-0.7.0/src/wirio/annotations.py +81 -0
  45. wirio-0.7.0/src/wirio/base_service_container.py +82 -0
  46. wirio-0.7.0/src/wirio/exceptions.py +143 -0
  47. wirio-0.7.0/src/wirio/py.typed +0 -0
  48. wirio-0.7.0/src/wirio/service_container.py +994 -0
  49. wirio-0.7.0/src/wirio/service_descriptor.py +258 -0
  50. wirio-0.7.0/src/wirio/service_lifetime.py +7 -0
  51. wirio-0.7.0/src/wirio/service_provider.py +273 -0
  52. 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
+ [![CI](https://img.shields.io/github/actions/workflow/status/AndreuCodina/wirio/main.yaml?branch=main&logo=github&label=CI)](https://github.com/AndreuCodina/wirio/actions/workflows/main.yaml)
25
+ [![Coverage status](https://coveralls.io/repos/github/AndreuCodina/wirio/badge.svg?branch=main)](https://coveralls.io/github/AndreuCodina/wirio?branch=main)
26
+ [![PyPI - version](https://img.shields.io/pypi/v/wirio?color=blue&label=pypi)](https://pypi.org/project/wirio/)
27
+ [![Python - versions](https://img.shields.io/pypi/pyversions/wirio.svg)](https://github.com/AndreuCodina/wirio)
28
+ [![License](https://img.shields.io/github/license/AndreuCodina/wirio.svg)](https://github.com/AndreuCodina/wirio/blob/main/LICENSE)
29
+ [![Documentation](https://img.shields.io/badge/📚_documentation-3D9970)](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
+ [![CI](https://img.shields.io/github/actions/workflow/status/AndreuCodina/wirio/main.yaml?branch=main&logo=github&label=CI)](https://github.com/AndreuCodina/wirio/actions/workflows/main.yaml)
5
+ [![Coverage status](https://coveralls.io/repos/github/AndreuCodina/wirio/badge.svg?branch=main)](https://coveralls.io/github/AndreuCodina/wirio?branch=main)
6
+ [![PyPI - version](https://img.shields.io/pypi/v/wirio?color=blue&label=pypi)](https://pypi.org/project/wirio/)
7
+ [![Python - versions](https://img.shields.io/pypi/pyversions/wirio.svg)](https://github.com/AndreuCodina/wirio)
8
+ [![License](https://img.shields.io/github/license/AndreuCodina/wirio.svg)](https://github.com/AndreuCodina/wirio/blob/main/LICENSE)
9
+ [![Documentation](https://img.shields.io/badge/📚_documentation-3D9970)](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