swiftpy-framework 0.1.0__py3-none-any.whl

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 (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,596 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import logging
6
+ from collections.abc import AsyncIterator, Callable, Iterator
7
+ from contextlib import asynccontextmanager, contextmanager
8
+ from contextvars import ContextVar
9
+ from dataclasses import dataclass
10
+ from enum import StrEnum
11
+ from types import TracebackType
12
+ from typing import Any, TypeVar, cast, get_type_hints
13
+
14
+ from swiftpy.core.context import Context
15
+
16
+ from .container_exceptions import (
17
+ BindingNotFoundError,
18
+ CircularDependencyError,
19
+ PrimitiveResolutionError,
20
+ ResolutionError,
21
+ )
22
+
23
+ logger = logging.getLogger("swiftpy.core.container")
24
+
25
+ T = TypeVar("T")
26
+
27
+ Factory = Callable[["Container"], Any]
28
+
29
+ PRIMITIVE_TYPES: tuple[type[Any], ...] = (
30
+ str,
31
+ int,
32
+ float,
33
+ bool,
34
+ dict,
35
+ list,
36
+ set,
37
+ tuple,
38
+ bytes,
39
+ )
40
+
41
+ _DI_RESOLVING_KEY = "__swiftpy_di_resolving__"
42
+ _DI_SCOPED_INSTANCES_KEY = "__swiftpy_di_scoped_instances__"
43
+ _NONE_SENTINEL = object()
44
+
45
+
46
+ @dataclass(slots=True, frozen=True)
47
+ class _CallableParamInfo:
48
+ name: str
49
+ param_type: Any
50
+ default: Any
51
+
52
+
53
+ _callable_reflection_cache: dict[tuple[Any, bool], tuple[_CallableParamInfo, ...]] = {}
54
+ _can_resolve_init_cache: dict[type[Any], tuple[tuple[str, Any], ...] | None] = {}
55
+
56
+
57
+ def _inspect_callable_specs(
58
+ func: Callable[..., Any], *, skip_first: bool = False
59
+ ) -> tuple[_CallableParamInfo, ...]:
60
+ cache_key = (func, skip_first)
61
+ cached = _callable_reflection_cache.get(cache_key)
62
+ if cached is not None:
63
+ return cached
64
+
65
+ try:
66
+ signature = inspect.signature(func)
67
+ type_hints = get_type_hints(func)
68
+ except (NameError, TypeError, ValueError) as exc:
69
+ callable_name = getattr(
70
+ func, "__qualname__", getattr(func, "__name__", str(func))
71
+ )
72
+ raise ResolutionError(
73
+ f"Failed to resolve type hints for '{callable_name}': {exc}"
74
+ ) from exc
75
+
76
+ parameters = list(signature.parameters.items())
77
+ if skip_first and parameters:
78
+ parameters = parameters[1:]
79
+
80
+ specs: list[_CallableParamInfo] = []
81
+ for name, parameter in parameters:
82
+ param_type = type_hints.get(name, parameter.annotation)
83
+ specs.append(
84
+ _CallableParamInfo(
85
+ name=name,
86
+ param_type=param_type,
87
+ default=parameter.default,
88
+ )
89
+ )
90
+
91
+ res = tuple(specs)
92
+ _callable_reflection_cache[cache_key] = res
93
+ return res
94
+
95
+
96
+ def _inspect_can_resolve_params(
97
+ target: type[Any],
98
+ ) -> tuple[tuple[str, Any], ...] | None:
99
+ if target in _can_resolve_init_cache:
100
+ return _can_resolve_init_cache[target]
101
+
102
+ init_method = target.__init__
103
+ if init_method is object.__init__:
104
+ _can_resolve_init_cache[target] = ()
105
+ return ()
106
+
107
+ try:
108
+ signature = inspect.signature(init_method)
109
+ type_hints = get_type_hints(init_method)
110
+ except (TypeError, ValueError, NameError):
111
+ _can_resolve_init_cache[target] = None
112
+ return None
113
+
114
+ parameters = list(signature.parameters.items())
115
+ if parameters:
116
+ parameters = parameters[1:]
117
+
118
+ required: list[tuple[str, Any]] = []
119
+ for name, parameter in parameters:
120
+ if parameter.kind in (
121
+ inspect.Parameter.VAR_POSITIONAL,
122
+ inspect.Parameter.VAR_KEYWORD,
123
+ ):
124
+ continue
125
+ if parameter.default is not inspect.Parameter.empty:
126
+ continue
127
+ param_type = type_hints.get(name, parameter.annotation)
128
+ required.append((name, param_type))
129
+
130
+ res = tuple(required)
131
+ _can_resolve_init_cache[target] = res
132
+ return res
133
+
134
+
135
+ def _current_task_or_none() -> asyncio.Task[Any] | None:
136
+ try:
137
+ return asyncio.current_task()
138
+ except RuntimeError:
139
+ return None
140
+
141
+
142
+ def _scoped_context_key(interface: type[Any]) -> str:
143
+ return f"__swiftpy_di__:{interface.__module__}.{interface.__qualname__}"
144
+
145
+ _container_var: ContextVar[Container | None] = ContextVar(
146
+ "swiftpy_container", default=None
147
+ )
148
+ _container_fallback: Container | None = None
149
+
150
+
151
+ def set_container(container: Container) -> None:
152
+ """Set the active application container context-wide and globally."""
153
+ global _container_fallback
154
+ _container_var.set(container)
155
+ _container_fallback = container
156
+
157
+
158
+ def get_container() -> Container:
159
+ """Retrieve the currently active application container."""
160
+ container = _container_var.get()
161
+ if container is None:
162
+ container = _container_fallback
163
+ if container is None:
164
+ raise RuntimeError("Application container not bootstrapped")
165
+ return container
166
+
167
+
168
+ @contextmanager
169
+ def bind_container(container: Container) -> Iterator[None]:
170
+ """Bind `container` to the current asyncio context without mutating global fallback."""
171
+ token = _container_var.set(container)
172
+ try:
173
+ yield
174
+ finally:
175
+ _container_var.reset(token)
176
+
177
+
178
+ class Scope(StrEnum):
179
+ TRANSIENT = "transient"
180
+ SINGLETON = "singleton"
181
+ SCOPED = "scoped"
182
+
183
+
184
+ @dataclass(slots=True, frozen=True)
185
+ class Binding:
186
+ """Immutable metadata describing a container registration."""
187
+
188
+ interface: type[Any]
189
+ factory: Factory
190
+ scope: Scope = Scope.TRANSIENT
191
+
192
+
193
+ class Container:
194
+ """
195
+ Lightweight dependency injection container for SwiftPY.
196
+
197
+ Supports transient and singleton bindings, constructor auto-wiring,
198
+ callable dependency injection, and circular dependency detection.
199
+ """
200
+
201
+ def __init__(self) -> None:
202
+ self._bindings: dict[type[Any], Binding] = {}
203
+ self._singletons: dict[type[Any], Any] = {}
204
+
205
+ self.instance(Container, self)
206
+
207
+ def bind(
208
+ self,
209
+ interface: type[T],
210
+ factory: Factory | type[T] | None = None,
211
+ scope: Scope = Scope.TRANSIENT,
212
+ ) -> None:
213
+ """
214
+ Register a binding.
215
+ """
216
+
217
+ if factory is None:
218
+ factory = interface
219
+
220
+ resolved_factory: Factory
221
+
222
+ if inspect.isclass(factory):
223
+ target_cls = factory
224
+
225
+ def class_factory(container: Container) -> Any:
226
+ return container._auto_wire(target_cls)
227
+
228
+ resolved_factory = class_factory
229
+
230
+ else:
231
+ resolved_factory = factory
232
+
233
+ if interface in self._singletons:
234
+ self._singletons.pop(interface, None)
235
+ logger.debug(
236
+ "Rebinding '%s' (%s); invalidated cached singleton instance.",
237
+ getattr(interface, "__name__", str(interface)),
238
+ scope,
239
+ )
240
+
241
+ scoped_key = _scoped_context_key(interface)
242
+ if Context.has(scoped_key):
243
+ Context.set(scoped_key, None)
244
+
245
+ self._bindings[interface] = Binding(
246
+ interface=interface,
247
+ factory=resolved_factory,
248
+ scope=scope,
249
+ )
250
+
251
+ def singleton(
252
+ self,
253
+ interface: type[T],
254
+ factory: Factory | type[T] | None = None,
255
+ ) -> None:
256
+ """Register a singleton binding."""
257
+
258
+ self.bind(
259
+ interface=interface,
260
+ factory=factory,
261
+ scope=Scope.SINGLETON,
262
+ )
263
+
264
+ def scoped(
265
+ self,
266
+ interface: type[T],
267
+ factory: Callable[[Container], T] | type[T] | None = None,
268
+ ) -> None:
269
+ """Register a task-scoped binding."""
270
+
271
+ self.bind(
272
+ interface=interface,
273
+ factory=factory,
274
+ scope=Scope.SCOPED,
275
+ )
276
+
277
+ def instance(
278
+ self,
279
+ interface: type[T],
280
+ instance_obj: T,
281
+ ) -> None:
282
+ """Register an already-created singleton instance."""
283
+
284
+ self._bindings[interface] = Binding(
285
+ interface=interface,
286
+ factory=lambda _: instance_obj,
287
+ scope=Scope.SINGLETON,
288
+ )
289
+
290
+ self._singletons[interface] = (
291
+ _NONE_SENTINEL if instance_obj is None else instance_obj
292
+ )
293
+
294
+ def get_singleton(self, interface: type[T]) -> T | None:
295
+ """Return an already-instantiated singleton if present, or None."""
296
+ if interface not in self._singletons:
297
+ return None
298
+ val = self._singletons[interface]
299
+ return None if val is _NONE_SENTINEL else cast(T, val)
300
+
301
+ def has(self, interface: type[Any]) -> bool:
302
+ """Determine whether an interface or service has been registered in the container."""
303
+ return interface in self._bindings or interface in self._singletons
304
+
305
+ def can_resolve(
306
+ self,
307
+ target: Any,
308
+ _seen: frozenset[type[Any]] | None = None,
309
+ ) -> bool:
310
+ """
311
+ Determine whether `target` can be satisfied by this container
312
+ (either via an explicit binding or constructor auto-wiring).
313
+ """
314
+ if target is inspect.Parameter.empty or target is Any or target is object:
315
+ return False
316
+
317
+ try:
318
+ if self.has(target):
319
+ return True
320
+ except TypeError:
321
+ return False
322
+
323
+ if target in PRIMITIVE_TYPES or not inspect.isclass(target):
324
+ return False
325
+
326
+ if inspect.isabstract(target):
327
+ return False
328
+
329
+ seen = _seen or frozenset()
330
+ if target in seen:
331
+ return False
332
+
333
+ required_params = _inspect_can_resolve_params(target)
334
+ if required_params is None:
335
+ return False
336
+
337
+ next_seen = seen | {target}
338
+ for _, param_type in required_params:
339
+ if not self.can_resolve(param_type, next_seen):
340
+ return False
341
+
342
+ return True
343
+
344
+ def resolve(self, target: type[T]) -> T:
345
+ """
346
+ Resolve a dependency from the container.
347
+
348
+ Registered bindings are resolved according to their configured
349
+ lifetime. Unregistered concrete classes are auto-wired.
350
+ """
351
+ resolving_stack = Context.get(_DI_RESOLVING_KEY, [])
352
+
353
+ if target in PRIMITIVE_TYPES:
354
+ raise PrimitiveResolutionError(
355
+ f"Cannot auto-wire primitive type "
356
+ f"'{target.__name__}'. "
357
+ f"Provide a default value or explicit binding."
358
+ )
359
+
360
+ if target in self._singletons:
361
+ item = self._singletons[target]
362
+ return cast(T, None if item is _NONE_SENTINEL else item)
363
+
364
+ if target in resolving_stack:
365
+ chain = " -> ".join(dependency.__name__ for dependency in resolving_stack)
366
+
367
+ raise CircularDependencyError(
368
+ f"Circular dependency detected: {chain} -> {target.__name__}"
369
+ )
370
+
371
+ Context.push(_DI_RESOLVING_KEY, target)
372
+
373
+ try:
374
+ binding = self._bindings.get(target)
375
+
376
+ if binding is not None:
377
+ if binding.scope is Scope.SINGLETON:
378
+ if target in self._singletons:
379
+ cached = self._singletons[target]
380
+ return cast(T, None if cached is _NONE_SENTINEL else cached)
381
+
382
+ instance = binding.factory(self)
383
+ self._singletons[target] = (
384
+ _NONE_SENTINEL if instance is None else instance
385
+ )
386
+
387
+ return cast(T, instance)
388
+
389
+ if binding.scope is Scope.SCOPED:
390
+ context_key = _scoped_context_key(target)
391
+ cur_task = _current_task_or_none()
392
+
393
+ if Context.has(context_key):
394
+ cached_entry = Context.get(context_key)
395
+ if (
396
+ isinstance(cached_entry, tuple)
397
+ and len(cached_entry) == 2
398
+ and cached_entry[0] is cur_task
399
+ ):
400
+ return cast(T, cached_entry[1])
401
+
402
+ instance = binding.factory(self)
403
+ if getattr(instance, "_cache_in_scope", True) is not False:
404
+ Context.set(context_key, (cur_task, instance))
405
+ scoped_list: list[
406
+ tuple[asyncio.Task[Any] | None, str, Any]
407
+ ] = list(Context.get(_DI_SCOPED_INSTANCES_KEY, []))
408
+ scoped_list.append((cur_task, context_key, instance))
409
+ Context.set(_DI_SCOPED_INSTANCES_KEY, scoped_list)
410
+
411
+ return cast(T, instance)
412
+
413
+ return cast(T, binding.factory(self))
414
+
415
+ if inspect.isclass(target):
416
+ return self._auto_wire(target)
417
+
418
+ raise BindingNotFoundError(f"No binding registered for '{target}'.")
419
+
420
+ finally:
421
+ Context.pop(_DI_RESOLVING_KEY)
422
+
423
+ async def aclose_scoped(
424
+ self,
425
+ exc_type: type[BaseException] | None = None,
426
+ exc_val: BaseException | None = None,
427
+ exc_tb: TracebackType | None = None,
428
+ ) -> None:
429
+ """
430
+ Dispose all task-scoped instances created in the current task in reverse creation order.
431
+ Supports `aclose()`, `__aexit__()`, `close()`, and `__exit__()`.
432
+ """
433
+ raw_list = Context.get(_DI_SCOPED_INSTANCES_KEY)
434
+ if not raw_list or not isinstance(raw_list, list):
435
+ return
436
+
437
+ cur_task = _current_task_or_none()
438
+ owned = [
439
+ entry
440
+ for entry in raw_list
441
+ if isinstance(entry, tuple) and len(entry) == 3 and entry[0] is cur_task
442
+ ]
443
+ remaining = [
444
+ entry
445
+ for entry in raw_list
446
+ if not (
447
+ isinstance(entry, tuple) and len(entry) == 3 and entry[0] is cur_task
448
+ )
449
+ ]
450
+ Context.set(_DI_SCOPED_INSTANCES_KEY, remaining)
451
+
452
+ for _, context_key, inst in reversed(owned):
453
+ Context.set(context_key, None)
454
+ if inst is None:
455
+ continue
456
+ try:
457
+ if hasattr(inst, "aclose") and callable(inst.aclose):
458
+ res = inst.aclose()
459
+ if inspect.isawaitable(res):
460
+ await res
461
+ elif hasattr(inst, "__aexit__") and callable(inst.__aexit__):
462
+ await inst.__aexit__(exc_type, exc_val, exc_tb)
463
+ elif hasattr(inst, "close") and callable(inst.close):
464
+ res = inst.close()
465
+ if inspect.isawaitable(res):
466
+ await res
467
+ elif hasattr(inst, "__exit__") and callable(inst.__exit__):
468
+ inst.__exit__(exc_type, exc_val, exc_tb)
469
+ except Exception:
470
+ logger.exception("Error disposing scoped instance %r", inst)
471
+
472
+ @asynccontextmanager
473
+ async def scope(self) -> AsyncIterator[Container]:
474
+ """Async context manager that isolates and disposes task-scoped dependencies on exit."""
475
+ cur_task = _current_task_or_none()
476
+ raw_list = Context.get(_DI_SCOPED_INSTANCES_KEY)
477
+ saved_owned: list[tuple[asyncio.Task[Any] | None, str, Any]] = []
478
+ saved_keys: dict[str, Any] = {}
479
+ if isinstance(raw_list, list) and raw_list:
480
+ other_entries: list[Any] = []
481
+ for entry in raw_list:
482
+ if (
483
+ isinstance(entry, tuple)
484
+ and len(entry) == 3
485
+ and entry[0] is cur_task
486
+ ):
487
+ saved_owned.append(entry)
488
+ ctx_key = entry[1]
489
+ if ctx_key not in saved_keys:
490
+ saved_keys[ctx_key] = Context.get(ctx_key)
491
+ else:
492
+ other_entries.append(entry)
493
+ if saved_owned:
494
+ Context.set(_DI_SCOPED_INSTANCES_KEY, other_entries)
495
+ for ctx_key in saved_keys:
496
+ Context.set(ctx_key, None)
497
+
498
+ exc_info: tuple[
499
+ type[BaseException] | None,
500
+ BaseException | None,
501
+ TracebackType | None,
502
+ ] = (None, None, None)
503
+ try:
504
+ yield self
505
+ except BaseException as exc:
506
+ exc_info = (type(exc), exc, exc.__traceback__)
507
+ raise
508
+ finally:
509
+ await self.aclose_scoped(*exc_info)
510
+ if saved_owned:
511
+ remaining = Context.get(_DI_SCOPED_INSTANCES_KEY)
512
+ restored = list(remaining) if isinstance(remaining, list) else []
513
+ Context.set(_DI_SCOPED_INSTANCES_KEY, [*restored, *saved_owned])
514
+ for ctx_key, saved_val in saved_keys.items():
515
+ Context.set(ctx_key, saved_val)
516
+
517
+ def call(
518
+ self,
519
+ func: Callable[..., T],
520
+ **extra_kwargs: Any,
521
+ ) -> T:
522
+ """Invoke a callable with dependency injection."""
523
+
524
+ kwargs = self._resolve_callable_dependencies(
525
+ func,
526
+ extra_kwargs=extra_kwargs,
527
+ )
528
+
529
+ return func(**kwargs)
530
+
531
+ def _auto_wire(self, target_cls: type[T]) -> T:
532
+ """Construct a class using constructor injection."""
533
+
534
+ init_method = target_cls.__init__
535
+
536
+ if init_method is object.__init__:
537
+ return target_cls()
538
+
539
+ kwargs = self._resolve_callable_dependencies(
540
+ init_method,
541
+ skip_first=True,
542
+ )
543
+
544
+ return target_cls(**kwargs)
545
+
546
+ def _resolve_callable_dependencies(
547
+ self,
548
+ func: Callable[..., Any],
549
+ *,
550
+ skip_first: bool = False,
551
+ extra_kwargs: dict[str, Any] | None = None,
552
+ ) -> dict[str, Any]:
553
+ """Resolve dependencies required by a callable."""
554
+
555
+ if extra_kwargs is None:
556
+ extra_kwargs = {}
557
+
558
+ specs = _inspect_callable_specs(func, skip_first=skip_first)
559
+ resolved_kwargs: dict[str, Any] = {}
560
+
561
+ for spec in specs:
562
+ name = spec.name
563
+ if name in extra_kwargs:
564
+ resolved_kwargs[name] = extra_kwargs[name]
565
+ continue
566
+
567
+ parameter_type = spec.param_type
568
+
569
+ if (
570
+ parameter_type is not inspect.Parameter.empty
571
+ and parameter_type not in PRIMITIVE_TYPES
572
+ ):
573
+ try:
574
+ resolved_kwargs[name] = self.resolve(parameter_type)
575
+ continue
576
+ except CircularDependencyError:
577
+ raise
578
+ except ResolutionError:
579
+ if spec.default is not inspect.Parameter.empty:
580
+ resolved_kwargs[name] = spec.default
581
+ continue
582
+
583
+ if spec.default is not inspect.Parameter.empty:
584
+ resolved_kwargs[name] = spec.default
585
+ continue
586
+
587
+ callable_name = getattr(
588
+ func, "__qualname__", getattr(func, "__name__", str(func))
589
+ )
590
+ raise ResolutionError(
591
+ f"Cannot resolve parameter '{name}' "
592
+ f"of type '{parameter_type}' "
593
+ f"for callable '{callable_name}'."
594
+ )
595
+
596
+ return resolved_kwargs
@@ -0,0 +1,18 @@
1
+ class ContainerError(Exception):
2
+ """Base exception for all container-related errors."""
3
+
4
+
5
+ class ResolutionError(ContainerError):
6
+ """Raised when the container fails to resolve a dependency or auto-wire a target."""
7
+
8
+
9
+ class CircularDependencyError(ResolutionError):
10
+ """Raised when a circular dependency graph is detected during resolution."""
11
+
12
+
13
+ class BindingNotFoundError(ResolutionError):
14
+ """Raised when an interface has no registered binding and cannot be auto-wired."""
15
+
16
+
17
+ class PrimitiveResolutionError(ResolutionError):
18
+ """Raised when trying to auto-wire primitive built-in types."""