stratae 0.0.1a0__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.
- stratae/__init__.py +31 -0
- stratae/cache/__init__.py +8 -0
- stratae/cache/cache.py +44 -0
- stratae/cache/memory.py +64 -0
- stratae/cache/thread.py +62 -0
- stratae/cache/util.py +8 -0
- stratae/context/__init__.py +5 -0
- stratae/context/context.py +58 -0
- stratae/depends/__init__.py +13 -0
- stratae/depends/_wrappers.py +124 -0
- stratae/depends/depends.py +59 -0
- stratae/depends/exceptions.py +21 -0
- stratae/depends/inject.py +49 -0
- stratae/depends/resolver.py +161 -0
- stratae/integrations/asgi.py +38 -0
- stratae/lifecycle/__init__.py +9 -0
- stratae/lifecycle/_context.py +63 -0
- stratae/lifecycle/_decorators.py +159 -0
- stratae/lifecycle/_scope.py +73 -0
- stratae/lifecycle/_wrappers.py +173 -0
- stratae/lifecycle/async_lifecycle.py +157 -0
- stratae/lifecycle/exceptions.py +21 -0
- stratae/lifecycle/lifecycle.py +151 -0
- stratae/lifecycle/manage.py +33 -0
- stratae/lifecycle/resource.py +168 -0
- stratae/lifecycle/scope.py +73 -0
- stratae-0.0.1a0.dist-info/METADATA +303 -0
- stratae-0.0.1a0.dist-info/RECORD +30 -0
- stratae-0.0.1a0.dist-info/WHEEL +5 -0
- stratae-0.0.1a0.dist-info/top_level.txt +1 -0
stratae/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stratae: Composable tools for building applications in Python.
|
|
3
|
+
|
|
4
|
+
Stratae provides lightweight, high-performance tools for dependency injection,
|
|
5
|
+
lifecycle management, and context variables. Built on Python's native features,
|
|
6
|
+
it works anywhere: APIs, CLIs, workers, and tests.
|
|
7
|
+
|
|
8
|
+
Quick example:
|
|
9
|
+
>>> from stratae.depends import Depends, inject
|
|
10
|
+
>>> from stratae.lifecycle import Lifecycle
|
|
11
|
+
>>>
|
|
12
|
+
>>> lifecycle = Lifecycle(['application', 'request'])
|
|
13
|
+
>>>
|
|
14
|
+
>>> @lifecycle.cache('application')
|
|
15
|
+
>>> def get_database():
|
|
16
|
+
... return Database(url="postgresql://...")
|
|
17
|
+
>>>
|
|
18
|
+
>>> @inject
|
|
19
|
+
>>> def create_user(name: str, db = Depends(get_database)):
|
|
20
|
+
... return db.users.create(name=name)
|
|
21
|
+
>>>
|
|
22
|
+
>>> with lifecycle.start('application'):
|
|
23
|
+
... with lifecycle.start('request'):
|
|
24
|
+
... user = create_user("Alice")
|
|
25
|
+
|
|
26
|
+
Modules:
|
|
27
|
+
cache: Caching protocols and implementations
|
|
28
|
+
depends: Dependency injection and resolution
|
|
29
|
+
lifecycle: Scope-based caching and resource management
|
|
30
|
+
context: Context variables with nested scopes
|
|
31
|
+
"""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Caching tools for managing single or multiple caches."""
|
|
2
|
+
|
|
3
|
+
from stratae.cache.cache import Cache
|
|
4
|
+
from stratae.cache.memory import MemoryCache
|
|
5
|
+
from stratae.cache.thread import ThreadSafeMemoryCache
|
|
6
|
+
from stratae.cache.util import get_function_key
|
|
7
|
+
|
|
8
|
+
__all__ = ["Cache", "MemoryCache", "ThreadSafeMemoryCache", "get_function_key"]
|
stratae/cache/cache.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Protocol class for cache implementations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable, Hashable, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class Cache(Protocol):
|
|
8
|
+
"""Abstract base class for cache implementations."""
|
|
9
|
+
|
|
10
|
+
def has(self, key: Hashable) -> bool:
|
|
11
|
+
"""Check if an item exists in the cache."""
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
def get(self, key: Hashable, default: Any = ...) -> Any:
|
|
15
|
+
"""Retrieve an item from the cache."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def get_or_set[T](self, key: Hashable, factory: Callable[[], T]) -> T:
|
|
19
|
+
"""Get an item from the cache or set it using a factory function."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def aget_or_set[T](self, key: Hashable, factory: Callable[[], Awaitable[T]]) -> T:
|
|
23
|
+
"""Asynchronously get an item from the cache or set it using a factory function."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def set(self, key: Hashable, value: Any) -> None:
|
|
27
|
+
"""Store an item in the cache."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def unset(self, key: Hashable) -> None:
|
|
31
|
+
"""Unset a cached item."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def clear(self) -> None:
|
|
35
|
+
"""Clear the cache."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def aclear(self) -> None:
|
|
39
|
+
"""Asynchronously clear the cache."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def is_empty(self) -> bool:
|
|
43
|
+
"""Check if the cache is empty."""
|
|
44
|
+
...
|
stratae/cache/memory.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""In-memory cache implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable, Hashable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MemoryCache:
|
|
7
|
+
"""A simple in-memory cache."""
|
|
8
|
+
|
|
9
|
+
_missing = object()
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize the memory cache."""
|
|
13
|
+
self._cache: dict[Hashable, Any] = {}
|
|
14
|
+
|
|
15
|
+
def has(self, key: Hashable) -> bool:
|
|
16
|
+
"""Check if an item exists in the cache."""
|
|
17
|
+
return key in self._cache
|
|
18
|
+
|
|
19
|
+
def get(self, key: Hashable, default: Any = _missing) -> Any:
|
|
20
|
+
"""Retrieve an item from the cache."""
|
|
21
|
+
if key in self._cache:
|
|
22
|
+
return self._cache.get(key)
|
|
23
|
+
if default is not self._missing:
|
|
24
|
+
return default
|
|
25
|
+
raise KeyError(key)
|
|
26
|
+
|
|
27
|
+
def get_or_set[T](self, key: Hashable, factory: Callable[[], T]) -> T:
|
|
28
|
+
"""Get an item from the cache or set it using a factory function."""
|
|
29
|
+
if key in self._cache:
|
|
30
|
+
return self._cache[key]
|
|
31
|
+
value = factory()
|
|
32
|
+
self.set(key, value)
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
async def aget_or_set[T](self, key: Hashable, factory: Callable[[], Awaitable[T]]) -> T:
|
|
36
|
+
"""Asynchronously get an item from the cache or set it using a factory function."""
|
|
37
|
+
if key in self._cache:
|
|
38
|
+
return self._cache[key]
|
|
39
|
+
value = await factory()
|
|
40
|
+
self.set(key, value)
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
def set(self, key: Hashable, value: Any) -> None:
|
|
44
|
+
"""Store an item in the cache."""
|
|
45
|
+
self._cache[key] = value
|
|
46
|
+
|
|
47
|
+
def unset(self, key: Hashable) -> None:
|
|
48
|
+
"""Unset a cached item."""
|
|
49
|
+
if key in self._cache:
|
|
50
|
+
del self._cache[key]
|
|
51
|
+
else:
|
|
52
|
+
raise KeyError(key)
|
|
53
|
+
|
|
54
|
+
def clear(self) -> None:
|
|
55
|
+
"""Clear the cache."""
|
|
56
|
+
self._cache.clear()
|
|
57
|
+
|
|
58
|
+
async def aclear(self) -> None:
|
|
59
|
+
"""Asynchronously clear the cache."""
|
|
60
|
+
self.clear()
|
|
61
|
+
|
|
62
|
+
def is_empty(self) -> bool:
|
|
63
|
+
"""Check if the cache is empty."""
|
|
64
|
+
return not self._cache
|
stratae/cache/thread.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Thread-Safe In-memory cache implementation."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any, Awaitable, Callable, Hashable
|
|
5
|
+
|
|
6
|
+
from stratae.cache import MemoryCache
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ThreadSafeMemoryCache:
|
|
10
|
+
"""A thread-safe in-memory cache."""
|
|
11
|
+
|
|
12
|
+
_missing = object()
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initialize the memory cache."""
|
|
16
|
+
self._cache = MemoryCache()
|
|
17
|
+
self._lock = threading.RLock()
|
|
18
|
+
|
|
19
|
+
def has(self, key: Hashable) -> bool:
|
|
20
|
+
"""Check if an item exists in the cache."""
|
|
21
|
+
with self._lock:
|
|
22
|
+
return self._cache.has(key)
|
|
23
|
+
|
|
24
|
+
def get(self, key: Hashable, default: Any = _missing) -> Any:
|
|
25
|
+
"""Retrieve an item from the cache."""
|
|
26
|
+
with self._lock:
|
|
27
|
+
if default is self._missing:
|
|
28
|
+
return self._cache.get(key)
|
|
29
|
+
return self._cache.get(key, default)
|
|
30
|
+
|
|
31
|
+
def get_or_set[T](self, key: Hashable, factory: Callable[[], T]) -> T:
|
|
32
|
+
"""Get an item from the cache or set it using a factory function."""
|
|
33
|
+
with self._lock:
|
|
34
|
+
return self._cache.get_or_set(key, factory)
|
|
35
|
+
|
|
36
|
+
async def aget_or_set[T](self, key: Hashable, factory: Callable[[], Awaitable[T]]) -> T:
|
|
37
|
+
"""Asynchronously get an item from the cache or set it using a factory function."""
|
|
38
|
+
raise NotImplementedError("ThreadSafeMemoryCache does not support async operations.")
|
|
39
|
+
|
|
40
|
+
def set(self, key: Hashable, value: Any) -> None:
|
|
41
|
+
"""Store an item in the cache."""
|
|
42
|
+
with self._lock:
|
|
43
|
+
self._cache.set(key, value)
|
|
44
|
+
|
|
45
|
+
def unset(self, key: Hashable) -> None:
|
|
46
|
+
"""Unset a cached item."""
|
|
47
|
+
with self._lock:
|
|
48
|
+
self._cache.unset(key)
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None:
|
|
51
|
+
"""Clear the cache."""
|
|
52
|
+
with self._lock:
|
|
53
|
+
self._cache.clear()
|
|
54
|
+
|
|
55
|
+
async def aclear(self) -> None:
|
|
56
|
+
"""Asynchronously clear the cache."""
|
|
57
|
+
raise NotImplementedError("ThreadSafeMemoryCache does not support async operations.")
|
|
58
|
+
|
|
59
|
+
def is_empty(self) -> bool:
|
|
60
|
+
"""Check if the cache is empty."""
|
|
61
|
+
with self._lock:
|
|
62
|
+
return self._cache.is_empty()
|
stratae/cache/util.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Utility functions for cache management."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable, Hashable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_function_key(func: Callable[..., Any] | Callable[..., Awaitable[Any]]) -> Hashable:
|
|
7
|
+
"""Generate a unique key for a function based on its name and module."""
|
|
8
|
+
return (func.__module__, func.__qualname__, id(func))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Wrapper around contextvars for named context providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import ContextVar, Token
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _ContextScope[T]:
|
|
9
|
+
"""Stateful context manager for a single context value."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, provider: Context[T], value: T):
|
|
12
|
+
"""Initialize the context scope with provider and value."""
|
|
13
|
+
self._provider = provider
|
|
14
|
+
self._value = value
|
|
15
|
+
self._token: Token[T]
|
|
16
|
+
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
"""Enter the context, setting the value."""
|
|
19
|
+
self._token = self._provider.set(self._value)
|
|
20
|
+
return self._value
|
|
21
|
+
|
|
22
|
+
def __exit__(self, *_):
|
|
23
|
+
"""Exit the context, resetting the value."""
|
|
24
|
+
self._provider.reset(self._token)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Context[T]:
|
|
28
|
+
"""Named context provider using contextvars."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name: str):
|
|
31
|
+
"""Initialize the ContextProvider with a name."""
|
|
32
|
+
self._name = name
|
|
33
|
+
self._var: ContextVar[T] = ContextVar(name)
|
|
34
|
+
|
|
35
|
+
def __call__(self) -> T:
|
|
36
|
+
"""Get the current context value."""
|
|
37
|
+
try:
|
|
38
|
+
return self._var.get()
|
|
39
|
+
except LookupError as lookup_err:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"Context '{self._name}' is not set. Use `with {self._name}.use(value):` to set it."
|
|
42
|
+
) from lookup_err
|
|
43
|
+
|
|
44
|
+
def get(self, default: T | None = None) -> T | None:
|
|
45
|
+
"""Get current value, or default if not set."""
|
|
46
|
+
return self._var.get(default)
|
|
47
|
+
|
|
48
|
+
def set(self, value: T) -> Token[T]:
|
|
49
|
+
"""Set the context value."""
|
|
50
|
+
return self._var.set(value)
|
|
51
|
+
|
|
52
|
+
def reset(self, token: Token[T]) -> None:
|
|
53
|
+
"""Reset the context value to a previous state."""
|
|
54
|
+
self._var.reset(token)
|
|
55
|
+
|
|
56
|
+
def use(self, value: T) -> _ContextScope[T]:
|
|
57
|
+
"""Create a context scope for the given value."""
|
|
58
|
+
return _ContextScope(self, value)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Dependency Injection Module."""
|
|
2
|
+
|
|
3
|
+
from stratae.depends.depends import AUTO, Depends, DependsWrapper
|
|
4
|
+
from stratae.depends.inject import inject
|
|
5
|
+
from stratae.depends.resolver import Resolver
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Resolver",
|
|
9
|
+
"AUTO",
|
|
10
|
+
"Depends",
|
|
11
|
+
"DependsWrapper",
|
|
12
|
+
"inject",
|
|
13
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Wrappers for dependency injection in synchronous and asynchronous functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
7
|
+
|
|
8
|
+
from stratae.depends.exceptions import OverrideNotAllowedError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from stratae.depends.depends import DependsWrapper
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_sync_wrapper(
|
|
15
|
+
func: Callable[..., Any], resolved_deps: dict[str, DependsWrapper], no_override: set[str]
|
|
16
|
+
) -> Callable[..., Any]:
|
|
17
|
+
"""Create a synchronous wrapper function that injects resolved dependencies."""
|
|
18
|
+
deps_items = tuple(resolved_deps.items())
|
|
19
|
+
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def sync_wrapper(*args: Any, **kwargs: Any):
|
|
22
|
+
if not kwargs:
|
|
23
|
+
return func(*args, **{k: v.provide() for k, v in deps_items})
|
|
24
|
+
if no_override and kwargs.keys() & no_override:
|
|
25
|
+
raise OverrideNotAllowedError(
|
|
26
|
+
"Overriding these dependencies is not allowed: "
|
|
27
|
+
f"{', '.join(k for k in kwargs if k in no_override)}"
|
|
28
|
+
)
|
|
29
|
+
return func(*args, **({k: v.provide() for k, v in deps_items if k not in kwargs} | kwargs))
|
|
30
|
+
|
|
31
|
+
return sync_wrapper
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_sync_gen_wrapper(
|
|
35
|
+
func: Callable[..., Any], resolved_deps: dict[str, DependsWrapper], no_override: set[str]
|
|
36
|
+
) -> Callable[..., Any]:
|
|
37
|
+
"""Create a synchronous generator wrapper function that injects resolved dependencies."""
|
|
38
|
+
deps_items = tuple(resolved_deps.items())
|
|
39
|
+
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def sync_gen_wrapper(*args: Any, **kwargs: Any) -> Generator[Any, None, None]:
|
|
42
|
+
if not kwargs:
|
|
43
|
+
yield from func(*args, **{k: v.provide() for k, v in resolved_deps.items()})
|
|
44
|
+
return
|
|
45
|
+
if no_override and any(k in no_override for k in kwargs):
|
|
46
|
+
raise OverrideNotAllowedError(
|
|
47
|
+
"Overriding these dependencies is not allowed: "
|
|
48
|
+
f"{', '.join(k for k in kwargs if k in no_override)}"
|
|
49
|
+
)
|
|
50
|
+
yield from func(
|
|
51
|
+
*args, **({k: v.provide() for k, v in deps_items if k not in kwargs} | kwargs)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return sync_gen_wrapper
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_async_wrapper(
|
|
58
|
+
func: Callable[..., Any], resolved_deps: dict[str, DependsWrapper], no_override: set[str]
|
|
59
|
+
) -> Callable[..., Any]:
|
|
60
|
+
"""Create an asynchronous wrapper function that injects resolved dependencies."""
|
|
61
|
+
deps_items = tuple(resolved_deps.items())
|
|
62
|
+
|
|
63
|
+
@wraps(func)
|
|
64
|
+
async def async_wrapper(*args: Any, **kwargs: Any):
|
|
65
|
+
if not kwargs:
|
|
66
|
+
return await func(
|
|
67
|
+
*args,
|
|
68
|
+
**{k: await v.aprovide() if v.is_async else v.provide() for k, v in deps_items},
|
|
69
|
+
)
|
|
70
|
+
if no_override and kwargs.keys() & no_override:
|
|
71
|
+
raise OverrideNotAllowedError(
|
|
72
|
+
"Overriding these dependencies is not allowed: "
|
|
73
|
+
f"{', '.join(k for k in kwargs if k in no_override)}"
|
|
74
|
+
)
|
|
75
|
+
return await func(
|
|
76
|
+
*args,
|
|
77
|
+
**(
|
|
78
|
+
{
|
|
79
|
+
k: await v.aprovide() if v.is_async else v.provide()
|
|
80
|
+
for k, v in deps_items
|
|
81
|
+
if k not in kwargs
|
|
82
|
+
}
|
|
83
|
+
| kwargs
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return async_wrapper
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_async_gen_wrapper(
|
|
91
|
+
func: Callable[..., Any], resolved_deps: dict[str, DependsWrapper], no_override: set[str]
|
|
92
|
+
):
|
|
93
|
+
"""Create an asynchronous generator wrapper function that injects resolved dependencies."""
|
|
94
|
+
deps_items = tuple(resolved_deps.items())
|
|
95
|
+
|
|
96
|
+
@wraps(func)
|
|
97
|
+
async def async_gen_wrapper(*args: Any, **kwargs: Any):
|
|
98
|
+
if not kwargs:
|
|
99
|
+
async for item in func(
|
|
100
|
+
*args,
|
|
101
|
+
**{k: await v.aprovide() if v.is_async else v.provide() for k, v in deps_items},
|
|
102
|
+
):
|
|
103
|
+
yield item
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if no_override and kwargs.keys() & no_override:
|
|
107
|
+
raise OverrideNotAllowedError(
|
|
108
|
+
"Overriding these dependencies is not allowed: "
|
|
109
|
+
f"{', '.join(k for k in kwargs if k in no_override)}"
|
|
110
|
+
)
|
|
111
|
+
async for item in func(
|
|
112
|
+
*args,
|
|
113
|
+
**(
|
|
114
|
+
{
|
|
115
|
+
k: await v.aprovide() if v.is_async else v.provide()
|
|
116
|
+
for k, v in deps_items
|
|
117
|
+
if k not in kwargs
|
|
118
|
+
}
|
|
119
|
+
| kwargs
|
|
120
|
+
),
|
|
121
|
+
):
|
|
122
|
+
yield item
|
|
123
|
+
|
|
124
|
+
return async_gen_wrapper
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Depends function for dependency injection."""
|
|
2
|
+
|
|
3
|
+
from inspect import iscoroutinefunction, unwrap
|
|
4
|
+
from typing import Any, Awaitable, Callable, cast, overload
|
|
5
|
+
|
|
6
|
+
AUTO: Any = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DependsWrapper:
|
|
10
|
+
"""Class used to wrap the dependency injection."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, dependency: Callable[..., Any], allow_override: bool = True) -> None:
|
|
13
|
+
"""Initialize the Depends instance with an injectable dependency."""
|
|
14
|
+
self.dependency = dependency
|
|
15
|
+
self._is_async = iscoroutinefunction(dependency)
|
|
16
|
+
self.allow_override = allow_override
|
|
17
|
+
|
|
18
|
+
def provide(self):
|
|
19
|
+
"""Provide the dependency."""
|
|
20
|
+
self._fix_outermost()
|
|
21
|
+
self.provide = self._fixed_provide
|
|
22
|
+
return self.dependency()
|
|
23
|
+
|
|
24
|
+
async def aprovide(self):
|
|
25
|
+
"""Asynchronously provide the dependency."""
|
|
26
|
+
self._fix_outermost()
|
|
27
|
+
self.aprovide = self._fixed_aprovide
|
|
28
|
+
return await self.dependency()
|
|
29
|
+
|
|
30
|
+
def _fixed_provide(self):
|
|
31
|
+
return self.dependency()
|
|
32
|
+
|
|
33
|
+
async def _fixed_aprovide(self):
|
|
34
|
+
return await self.dependency()
|
|
35
|
+
|
|
36
|
+
def _fix_outermost(self) -> None:
|
|
37
|
+
"""Fix the dependency to use its outermost version, if applicable."""
|
|
38
|
+
original = unwrap(self.dependency)
|
|
39
|
+
if hasattr(original, "__outermost__"):
|
|
40
|
+
self.dependency = original.__outermost__
|
|
41
|
+
self._resolved_outermost = True
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_async(self) -> bool:
|
|
45
|
+
"""Return True if the dependency is asynchronous, False otherwise."""
|
|
46
|
+
return self._is_async
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def Depends[**P, R](dependency: Callable[P, Awaitable[R]], *, allow_override: bool = True) -> R: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def Depends[**P, R](dependency: Callable[P, R], *, allow_override: bool = True) -> R: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def Depends[**P, R](dependency: Callable[P, R | Awaitable[R]], *, allow_override: bool = True) -> R:
|
|
58
|
+
"""Marker function used to denote a dependency injection."""
|
|
59
|
+
return cast(R, DependsWrapper(dependency=dependency, allow_override=allow_override))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exceptions for errors in dependency injection."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DependencyInjectionError(Exception):
|
|
5
|
+
"""Base class for all dependency injection related exceptions."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DIResolutionError(DependencyInjectionError):
|
|
9
|
+
"""Exception raised when dependency resolution fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CircularDependencyError(DependencyInjectionError):
|
|
13
|
+
"""Exception raised when a circular dependency is detected."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RegistrationError(DependencyInjectionError, ValueError):
|
|
17
|
+
"""Exception raised when a registration error occurs in dependency resolution."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OverrideNotAllowedError(DependencyInjectionError, RuntimeError):
|
|
21
|
+
"""Exception raised when an override is attempted but not allowed."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inject decorator to resolve and inject dependencies into functions.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- A global Resolver instance for managing dependencies.
|
|
6
|
+
- The `inject` decorator for resolving and injecting dependencies into functions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Callable, overload
|
|
12
|
+
|
|
13
|
+
from stratae.depends.resolver import Resolver
|
|
14
|
+
|
|
15
|
+
_resolver = Resolver()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_resolver() -> Resolver:
|
|
19
|
+
"""Get the global dependency resolver."""
|
|
20
|
+
return _resolver
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def inject[**P, R](func: Callable[P, R]) -> Callable[P, R]: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@overload
|
|
28
|
+
def inject[**P, R]() -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def inject[**P, R](func: Callable[P, R], *, allow_override: bool) -> Callable[P, R]: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def inject[**P, R](*, allow_override: bool) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def inject[**P, R](
|
|
40
|
+
func: Callable[P, R] | None = None, *, allow_override: bool = True
|
|
41
|
+
) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]:
|
|
42
|
+
"""Inject decorator to resolve dependencies for a function."""
|
|
43
|
+
|
|
44
|
+
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
|
45
|
+
return get_resolver().resolve_function(f, allow_override=allow_override)
|
|
46
|
+
|
|
47
|
+
if func is None:
|
|
48
|
+
return decorator
|
|
49
|
+
return decorator(func)
|