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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- 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."""
|