engin 0.0.dev1__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.
- engin/__init__.py +21 -0
- engin/_assembler.py +126 -0
- engin/_block.py +36 -0
- engin/_dependency.py +162 -0
- engin/_engin.py +89 -0
- engin/_exceptions.py +19 -0
- engin/_lifecycle.py +21 -0
- engin/_type_utils.py +65 -0
- engin/extensions/__init__.py +0 -0
- engin/extensions/asgi.py +55 -0
- engin/py.typed +0 -0
- engin-0.0.dev1.dist-info/METADATA +6 -0
- engin-0.0.dev1.dist-info/RECORD +15 -0
- engin-0.0.dev1.dist-info/WHEEL +4 -0
- engin-0.0.dev1.dist-info/licenses/LICENSE +21 -0
engin/__init__.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from engin._assembler import Assembler
|
2
|
+
from engin._block import Block, invoke, provide
|
3
|
+
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
4
|
+
from engin._engin import Engin, Option
|
5
|
+
from engin._exceptions import AssemblyError
|
6
|
+
from engin._lifecycle import Lifecycle
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"Assembler",
|
10
|
+
"AssemblyError",
|
11
|
+
"Engin",
|
12
|
+
"Entrypoint",
|
13
|
+
"Invoke",
|
14
|
+
"Lifecycle",
|
15
|
+
"Block",
|
16
|
+
"Option",
|
17
|
+
"Provide",
|
18
|
+
"Supply",
|
19
|
+
"invoke",
|
20
|
+
"provide",
|
21
|
+
]
|
engin/_assembler.py
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from collections import defaultdict
|
4
|
+
from collections.abc import Collection, Iterable
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from inspect import BoundArguments, Signature
|
7
|
+
from typing import Any, Generic, TypeVar
|
8
|
+
|
9
|
+
from engin._dependency import Dependency, Provide, Supply
|
10
|
+
from engin._exceptions import AssemblyError
|
11
|
+
from engin._type_utils import TypeId, type_id_of
|
12
|
+
|
13
|
+
LOG = logging.getLogger("engin")
|
14
|
+
|
15
|
+
T = TypeVar("T")
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass(slots=True, kw_only=True, frozen=True)
|
19
|
+
class AssembledDependency(Generic[T]):
|
20
|
+
dependency: Dependency[Any, T]
|
21
|
+
bound_args: BoundArguments
|
22
|
+
|
23
|
+
async def __call__(self) -> T:
|
24
|
+
return await self.dependency(*self.bound_args.args, **self.bound_args.kwargs)
|
25
|
+
|
26
|
+
|
27
|
+
class Assembler:
|
28
|
+
def __init__(self, providers: Iterable[Provide]) -> None:
|
29
|
+
self._providers: dict[TypeId, Provide[Any]] = {}
|
30
|
+
self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
|
31
|
+
self._dependencies: dict[TypeId, Any] = {}
|
32
|
+
self._consumed_providers: set[Provide[Any]] = set()
|
33
|
+
self._lock = asyncio.Lock()
|
34
|
+
|
35
|
+
for provider in providers:
|
36
|
+
type_id = provider.return_type_id
|
37
|
+
if not provider.is_multiprovider:
|
38
|
+
if type_id in self._providers:
|
39
|
+
raise RuntimeError(f"A Provider already exists for '{type_id}'")
|
40
|
+
self._providers[type_id] = provider
|
41
|
+
else:
|
42
|
+
self._multiproviders[type_id].append(provider)
|
43
|
+
|
44
|
+
def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
|
45
|
+
if type_id.multi:
|
46
|
+
providers = self._multiproviders.get(type_id)
|
47
|
+
else:
|
48
|
+
providers = [provider] if (provider := self._providers.get(type_id)) else None
|
49
|
+
if not providers:
|
50
|
+
if type_id.multi:
|
51
|
+
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
52
|
+
providers = [(Supply([], type_hint=list[type_id.type]))]
|
53
|
+
else:
|
54
|
+
raise LookupError(f"No Provider registered for dependency '{type_id}'")
|
55
|
+
|
56
|
+
required_providers: list[Provide[Any]] = []
|
57
|
+
for provider in providers:
|
58
|
+
required_providers.extend(
|
59
|
+
provider
|
60
|
+
for provider_param in provider.parameter_types
|
61
|
+
for provider in self._resolve_providers(provider_param)
|
62
|
+
)
|
63
|
+
|
64
|
+
return {*required_providers, *providers}
|
65
|
+
|
66
|
+
async def _satisfy(self, target: TypeId) -> None:
|
67
|
+
for provider in self._resolve_providers(target):
|
68
|
+
if provider in self._consumed_providers:
|
69
|
+
continue
|
70
|
+
self._consumed_providers.add(provider)
|
71
|
+
type_id = provider.return_type_id
|
72
|
+
bound_args = await self._bind_arguments(provider.signature)
|
73
|
+
try:
|
74
|
+
value = await provider(*bound_args.args, **bound_args.kwargs)
|
75
|
+
except Exception as err:
|
76
|
+
raise AssemblyError(
|
77
|
+
provider=provider, error_type=type(err), error_message=str(err)
|
78
|
+
) from err
|
79
|
+
if provider.is_multiprovider:
|
80
|
+
if type_id in self._dependencies:
|
81
|
+
self._dependencies[type_id].extend(value)
|
82
|
+
else:
|
83
|
+
self._dependencies[type_id] = value
|
84
|
+
else:
|
85
|
+
self._dependencies[type_id] = value
|
86
|
+
|
87
|
+
async def _bind_arguments(self, signature: Signature) -> BoundArguments:
|
88
|
+
args = []
|
89
|
+
kwargs = {}
|
90
|
+
for param_name, param in signature.parameters.items():
|
91
|
+
if param_name == "self":
|
92
|
+
args.append(object())
|
93
|
+
continue
|
94
|
+
param_key = type_id_of(param.annotation)
|
95
|
+
has_dependency = param_key in self._dependencies
|
96
|
+
if not has_dependency:
|
97
|
+
await self._satisfy(param_key)
|
98
|
+
val = self._dependencies[param_key]
|
99
|
+
if param.kind == param.POSITIONAL_ONLY:
|
100
|
+
args.append(val)
|
101
|
+
else:
|
102
|
+
kwargs[param.name] = val
|
103
|
+
|
104
|
+
return signature.bind(*args, **kwargs)
|
105
|
+
|
106
|
+
async def assemble(self, dependency: Dependency[Any, T]) -> AssembledDependency[T]:
|
107
|
+
async with self._lock:
|
108
|
+
return AssembledDependency(
|
109
|
+
dependency=dependency,
|
110
|
+
bound_args=await self._bind_arguments(dependency.signature),
|
111
|
+
)
|
112
|
+
|
113
|
+
async def get(self, type_: type[T]) -> T:
|
114
|
+
type_id = type_id_of(type_)
|
115
|
+
if type_id.multi:
|
116
|
+
out = []
|
117
|
+
for provider in self._multiproviders[type_id]:
|
118
|
+
assembled_dependency = await self.assemble(provider)
|
119
|
+
out.extend(await assembled_dependency())
|
120
|
+
return out
|
121
|
+
else:
|
122
|
+
assembled_dependency = await self.assemble(self._providers[type_id])
|
123
|
+
return await assembled_dependency()
|
124
|
+
|
125
|
+
def has(self, type_: type[T]) -> bool:
|
126
|
+
return type_id_of(type_) in self._providers
|
engin/_block.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
import inspect
|
2
|
+
from collections.abc import Iterable, Iterator
|
3
|
+
|
4
|
+
from engin._dependency import Func, Invoke, Provide
|
5
|
+
|
6
|
+
|
7
|
+
def provide(func: Func) -> Func:
|
8
|
+
func._opt = Provide(func) # type: ignore[union-attr]
|
9
|
+
return func
|
10
|
+
|
11
|
+
|
12
|
+
def invoke(func: Func) -> Func:
|
13
|
+
func._opt = Invoke(func) # type: ignore[union-attr]
|
14
|
+
return func
|
15
|
+
|
16
|
+
|
17
|
+
class Block(Iterable[Provide | Invoke]):
|
18
|
+
_name: str
|
19
|
+
_options: list[Provide | Invoke]
|
20
|
+
|
21
|
+
def __init__(self, /, block_name: str | None = None) -> None:
|
22
|
+
self._options: list[Provide | Invoke] = []
|
23
|
+
self._name = block_name or f"{type(self).__name__}"
|
24
|
+
for _, method in inspect.getmembers(self):
|
25
|
+
if opt := getattr(method, "_opt", None):
|
26
|
+
if not isinstance(opt, (Provide, Invoke)):
|
27
|
+
raise RuntimeError("Block option is not an instance of Provide or Invoke")
|
28
|
+
opt.set_block_name(self._name)
|
29
|
+
self._options.append(opt)
|
30
|
+
|
31
|
+
@property
|
32
|
+
def name(self) -> str:
|
33
|
+
return self._name
|
34
|
+
|
35
|
+
def __iter__(self) -> Iterator[Provide | Invoke]:
|
36
|
+
return iter(self._options)
|
engin/_dependency.py
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
import inspect
|
2
|
+
import typing
|
3
|
+
from abc import ABC
|
4
|
+
from inspect import Parameter, Signature, isclass, iscoroutinefunction
|
5
|
+
from typing import (
|
6
|
+
Any,
|
7
|
+
Awaitable,
|
8
|
+
Callable,
|
9
|
+
Generic,
|
10
|
+
ParamSpec,
|
11
|
+
Type,
|
12
|
+
TypeAlias,
|
13
|
+
TypeVar,
|
14
|
+
cast,
|
15
|
+
get_type_hints,
|
16
|
+
)
|
17
|
+
|
18
|
+
from engin._type_utils import TypeId, type_id_of
|
19
|
+
|
20
|
+
P = ParamSpec("P")
|
21
|
+
T = TypeVar("T")
|
22
|
+
Func: TypeAlias = (
|
23
|
+
Callable[P, T] | Callable[P, Awaitable[T]] | Callable[[], T] | Callable[[], Awaitable[T]]
|
24
|
+
)
|
25
|
+
_SELF = object()
|
26
|
+
|
27
|
+
|
28
|
+
def _noop(*args, **kwargs) -> None: ...
|
29
|
+
|
30
|
+
|
31
|
+
class Dependency(ABC, Generic[P, T]):
|
32
|
+
def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
|
33
|
+
self._func = func
|
34
|
+
self._is_async = iscoroutinefunction(func)
|
35
|
+
self._signature = inspect.signature(self._func)
|
36
|
+
self._block_name = block_name
|
37
|
+
|
38
|
+
@property
|
39
|
+
def block_name(self) -> str | None:
|
40
|
+
return self._block_name
|
41
|
+
|
42
|
+
@property
|
43
|
+
def name(self) -> str:
|
44
|
+
if self._block_name:
|
45
|
+
return f"{self._block_name}.{self._func.__name__}"
|
46
|
+
else:
|
47
|
+
return f"{self._func.__module__}.{self._func.__name__}"
|
48
|
+
|
49
|
+
@property
|
50
|
+
def parameter_types(self) -> list[TypeId]:
|
51
|
+
parameters = list(self._signature.parameters.values())
|
52
|
+
if not parameters:
|
53
|
+
return []
|
54
|
+
if parameters[0].name == "self":
|
55
|
+
parameters.pop(0)
|
56
|
+
return [type_id_of(param.annotation) for param in parameters]
|
57
|
+
|
58
|
+
@property
|
59
|
+
def signature(self) -> Signature:
|
60
|
+
return self._signature
|
61
|
+
|
62
|
+
def set_block_name(self, name: str) -> None:
|
63
|
+
self._block_name = name
|
64
|
+
|
65
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
66
|
+
if self._is_async:
|
67
|
+
return await cast(Awaitable[T], self._func(*args, **kwargs))
|
68
|
+
else:
|
69
|
+
return cast(T, self._func(*args, **kwargs))
|
70
|
+
|
71
|
+
|
72
|
+
class Invoke(Dependency):
|
73
|
+
def __init__(self, invocation: Func[P, T], block_name: str | None = None):
|
74
|
+
super().__init__(func=invocation, block_name=block_name)
|
75
|
+
|
76
|
+
def __str__(self) -> str:
|
77
|
+
return f"Invoke({self.name})"
|
78
|
+
|
79
|
+
|
80
|
+
class Entrypoint(Invoke):
|
81
|
+
def __init__(self, type_: Type[Any], *, block_name: str | None = None) -> None:
|
82
|
+
self._type = type_
|
83
|
+
super().__init__(invocation=_noop, block_name=block_name)
|
84
|
+
|
85
|
+
@property
|
86
|
+
def parameter_types(self) -> list[TypeId]:
|
87
|
+
return [type_id_of(self._type)]
|
88
|
+
|
89
|
+
@property
|
90
|
+
def signature(self) -> Signature:
|
91
|
+
return Signature(
|
92
|
+
parameters=[
|
93
|
+
Parameter(name="x", kind=Parameter.POSITIONAL_ONLY, annotation=self._type)
|
94
|
+
]
|
95
|
+
)
|
96
|
+
|
97
|
+
def __str__(self) -> str:
|
98
|
+
return f"Entrypoint({type_id_of(self._type)})"
|
99
|
+
|
100
|
+
|
101
|
+
class Provide(Dependency[Any, T]):
|
102
|
+
def __init__(self, builder: Func[P, T], block_name: str | None = None):
|
103
|
+
super().__init__(func=builder, block_name=block_name)
|
104
|
+
self._is_multi = typing.get_origin(self.return_type) is list
|
105
|
+
|
106
|
+
if self._is_multi:
|
107
|
+
args = typing.get_args(self.return_type)
|
108
|
+
if len(args) != 1:
|
109
|
+
raise ValueError(
|
110
|
+
f"A multiprovider must be of the form list[X], not '{self.return_type}'"
|
111
|
+
)
|
112
|
+
|
113
|
+
@property
|
114
|
+
def return_type(self) -> Type[T]:
|
115
|
+
if isclass(self._func):
|
116
|
+
return_type = self._func # __init__ returns self
|
117
|
+
else:
|
118
|
+
try:
|
119
|
+
return_type = get_type_hints(self._func)["return"]
|
120
|
+
except KeyError:
|
121
|
+
raise RuntimeError(f"Dependency '{self.name}' requires a return typehint")
|
122
|
+
|
123
|
+
return return_type
|
124
|
+
|
125
|
+
@property
|
126
|
+
def return_type_id(self) -> TypeId:
|
127
|
+
return type_id_of(self.return_type)
|
128
|
+
|
129
|
+
@property
|
130
|
+
def is_multiprovider(self) -> bool:
|
131
|
+
return self._is_multi
|
132
|
+
|
133
|
+
def __hash__(self) -> int:
|
134
|
+
return hash(self.return_type_id)
|
135
|
+
|
136
|
+
def __str__(self) -> str:
|
137
|
+
return f"Provide({self.return_type_id})"
|
138
|
+
|
139
|
+
|
140
|
+
class Supply(Provide, Generic[T]):
|
141
|
+
def __init__(
|
142
|
+
self, value: T, *, type_hint: type | None = None, block_name: str | None = None
|
143
|
+
):
|
144
|
+
self._value = value
|
145
|
+
self._type_hint = type_hint
|
146
|
+
if self._type_hint is not None:
|
147
|
+
self._get_val.__annotations__["return"] = type_hint
|
148
|
+
super().__init__(builder=self._get_val, block_name=block_name)
|
149
|
+
|
150
|
+
@property
|
151
|
+
def return_type(self) -> Type[T]:
|
152
|
+
if self._type_hint is not None:
|
153
|
+
return self._type_hint
|
154
|
+
if isinstance(self._value, list):
|
155
|
+
return list[type(self._value[0])] # type: ignore[misc,return-value]
|
156
|
+
return type(self._value)
|
157
|
+
|
158
|
+
def _get_val(self) -> T:
|
159
|
+
return self._value
|
160
|
+
|
161
|
+
def __str__(self) -> str:
|
162
|
+
return f"Supply({self.return_type_id})"
|
engin/_engin.py
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
import logging
|
2
|
+
from asyncio import Event
|
3
|
+
from collections.abc import Iterable
|
4
|
+
from itertools import chain
|
5
|
+
from typing import ClassVar, TypeAlias
|
6
|
+
|
7
|
+
from engin import Entrypoint
|
8
|
+
from engin._assembler import AssembledDependency, Assembler
|
9
|
+
from engin._block import Block
|
10
|
+
from engin._dependency import Dependency, Invoke, Provide, Supply
|
11
|
+
from engin._lifecycle import Lifecycle
|
12
|
+
from engin._type_utils import TypeId
|
13
|
+
|
14
|
+
LOG = logging.getLogger("engin")
|
15
|
+
|
16
|
+
Option: TypeAlias = Invoke | Provide | Supply | Block
|
17
|
+
_Opt: TypeAlias = Invoke | Provide | Supply
|
18
|
+
|
19
|
+
|
20
|
+
class Engin:
|
21
|
+
_LIB_OPTIONS: ClassVar[list[Option]] = [Provide(Lifecycle)]
|
22
|
+
|
23
|
+
def __init__(self, *options: Option) -> None:
|
24
|
+
self._providers: dict[TypeId, Provide] = {}
|
25
|
+
self._invokables: list[Invoke] = []
|
26
|
+
self._stop_event = Event()
|
27
|
+
|
28
|
+
self._destruct_options(chain(self._LIB_OPTIONS, options))
|
29
|
+
self._assembler = Assembler(self._providers.values())
|
30
|
+
|
31
|
+
@property
|
32
|
+
def assembler(self) -> Assembler:
|
33
|
+
return self._assembler
|
34
|
+
|
35
|
+
async def run(self):
|
36
|
+
await self.start()
|
37
|
+
|
38
|
+
# lifecycle startup
|
39
|
+
|
40
|
+
# wait till stop signal recieved
|
41
|
+
await self._stop_event.wait()
|
42
|
+
|
43
|
+
# lifecycle shutdown
|
44
|
+
|
45
|
+
async def start(self) -> None:
|
46
|
+
LOG.info("starting engin")
|
47
|
+
assembled_invocations: list[AssembledDependency] = [
|
48
|
+
await self._assembler.assemble(invocation) for invocation in self._invokables
|
49
|
+
]
|
50
|
+
for invocation in assembled_invocations:
|
51
|
+
await invocation()
|
52
|
+
|
53
|
+
lifecycle = await self._assembler.get(Lifecycle)
|
54
|
+
await lifecycle.startup()
|
55
|
+
self._stop_event = Event()
|
56
|
+
LOG.info("startup complete")
|
57
|
+
|
58
|
+
async def stop(self) -> None:
|
59
|
+
self._stop_event.set()
|
60
|
+
|
61
|
+
def _destruct_options(self, options: Iterable[Option]):
|
62
|
+
for opt in options:
|
63
|
+
if isinstance(opt, Block):
|
64
|
+
self._destruct_options(opt)
|
65
|
+
if isinstance(opt, (Provide, Supply)):
|
66
|
+
existing = self._providers.get(opt.return_type_id)
|
67
|
+
self._log_option(opt, overwrites=existing)
|
68
|
+
self._providers[opt.return_type_id] = opt
|
69
|
+
elif isinstance(opt, Invoke):
|
70
|
+
self._log_option(opt)
|
71
|
+
self._invokables.append(opt)
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
def _log_option(opt: Dependency, overwrites: Dependency | None = None) -> None:
|
75
|
+
if overwrites is not None:
|
76
|
+
extra = f"\tOVERWRITES {overwrites.name}"
|
77
|
+
if overwrites.block_name:
|
78
|
+
extra += f" [{overwrites.block_name}]"
|
79
|
+
else:
|
80
|
+
extra = ""
|
81
|
+
if isinstance(opt, Supply):
|
82
|
+
LOG.debug(f"SUPPLY {opt.return_type_id!s:<35}{extra}")
|
83
|
+
elif isinstance(opt, Provide):
|
84
|
+
LOG.debug(f"PROVIDE {opt.return_type_id!s:<35} <- {opt.name}() {extra}")
|
85
|
+
elif isinstance(opt, Entrypoint):
|
86
|
+
type_id = opt.parameter_types[0]
|
87
|
+
LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
|
88
|
+
elif isinstance(opt, Invoke):
|
89
|
+
LOG.debug(f"INVOKE {opt.name:<35}")
|
engin/_exceptions.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from engin._dependency import Provide
|
4
|
+
|
5
|
+
|
6
|
+
class AssemblyError(Exception):
|
7
|
+
def __init__(
|
8
|
+
self, provider: Provide[Any], error_type: type[Exception], error_message: str
|
9
|
+
) -> None:
|
10
|
+
self.provider = provider
|
11
|
+
self.error_type = error_type
|
12
|
+
self.error_message = error_message
|
13
|
+
self.message = (
|
14
|
+
f"provider '{provider.name}' errored with error "
|
15
|
+
f"({error_type.__name__}): '{error_message}'"
|
16
|
+
)
|
17
|
+
|
18
|
+
def __str__(self) -> str:
|
19
|
+
return self.message
|
engin/_lifecycle.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from contextlib import AbstractAsyncContextManager, AsyncExitStack
|
3
|
+
|
4
|
+
|
5
|
+
class Lifecycle:
|
6
|
+
def __init__(self) -> None:
|
7
|
+
self._on_startup: list[Callable] = []
|
8
|
+
self._on_shutdown: list[Callable] = []
|
9
|
+
self._context_managers: list[AbstractAsyncContextManager] = []
|
10
|
+
self._stack: AsyncExitStack = AsyncExitStack()
|
11
|
+
|
12
|
+
def register_context(self, cm: AbstractAsyncContextManager):
|
13
|
+
self._context_managers.append(cm)
|
14
|
+
|
15
|
+
async def startup(self) -> None:
|
16
|
+
self._stack = AsyncExitStack()
|
17
|
+
for cm in self._context_managers:
|
18
|
+
await self._stack.enter_async_context(cm)
|
19
|
+
|
20
|
+
async def shutdown(self) -> None:
|
21
|
+
await self._stack.aclose()
|
engin/_type_utils.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
import typing
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
_implict_modules = ["builtins", "typing", "collections.abc"]
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass(frozen=True, eq=True)
|
9
|
+
class TypeId:
|
10
|
+
type: type
|
11
|
+
multi: bool
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def from_type(cls, type_: Any) -> "TypeId":
|
15
|
+
if is_multi_type(type_):
|
16
|
+
inner_obj = typing.get_args(type_)[0]
|
17
|
+
return TypeId(type=inner_obj, multi=True)
|
18
|
+
else:
|
19
|
+
return TypeId(type=type_, multi=False)
|
20
|
+
|
21
|
+
def __str__(self) -> str:
|
22
|
+
module = self.type.__module__
|
23
|
+
out = f"{module}." if module not in _implict_modules else ""
|
24
|
+
out += _args_to_str(self.type)
|
25
|
+
if self.multi:
|
26
|
+
out += "[]"
|
27
|
+
return out
|
28
|
+
|
29
|
+
|
30
|
+
def _args_to_str(type_: Any) -> str:
|
31
|
+
args = typing.get_args(type_)
|
32
|
+
if args:
|
33
|
+
arg_str = f"{type_.__name__}["
|
34
|
+
for idx, arg in enumerate(args):
|
35
|
+
if isinstance(arg, list):
|
36
|
+
arg_str += "["
|
37
|
+
for inner_idx, inner_arg in enumerate(arg):
|
38
|
+
arg_str += _args_to_str(inner_arg)
|
39
|
+
if inner_idx < len(arg) - 1:
|
40
|
+
arg_str += ", "
|
41
|
+
arg_str += "]"
|
42
|
+
elif typing.get_args(arg):
|
43
|
+
arg_str += _args_to_str(arg)
|
44
|
+
else:
|
45
|
+
arg_str += getattr(arg, "__name__", str(arg))
|
46
|
+
if idx < len(args) - 1:
|
47
|
+
arg_str += ", "
|
48
|
+
arg_str += "]"
|
49
|
+
else:
|
50
|
+
arg_str = type_.__name__
|
51
|
+
return arg_str
|
52
|
+
|
53
|
+
|
54
|
+
def type_id_of(type_: Any) -> TypeId:
|
55
|
+
"""
|
56
|
+
Generates a string TypeId for any type.
|
57
|
+
"""
|
58
|
+
return TypeId.from_type(type_)
|
59
|
+
|
60
|
+
|
61
|
+
def is_multi_type(type_: Any) -> bool:
|
62
|
+
"""
|
63
|
+
Discriminates a type to determine whether it is the return type of a multiprovider.
|
64
|
+
"""
|
65
|
+
return typing.get_origin(type_) is list
|
File without changes
|
engin/extensions/asgi.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
import traceback
|
2
|
+
import typing
|
3
|
+
from typing import Protocol, TypeAlias
|
4
|
+
|
5
|
+
from engin import Engin, Option
|
6
|
+
|
7
|
+
__all__ = ["ASGIEngin", "ASGIType"]
|
8
|
+
|
9
|
+
|
10
|
+
_Scope: TypeAlias = typing.MutableMapping[str, typing.Any]
|
11
|
+
_Message: TypeAlias = typing.MutableMapping[str, typing.Any]
|
12
|
+
_Receive: TypeAlias = typing.Callable[[], typing.Awaitable[_Message]]
|
13
|
+
_Send: TypeAlias = typing.Callable[[_Message], typing.Awaitable[None]]
|
14
|
+
|
15
|
+
|
16
|
+
class ASGIType(Protocol):
|
17
|
+
async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None: ...
|
18
|
+
|
19
|
+
|
20
|
+
class ASGIEngin(Engin, ASGIType):
|
21
|
+
_asgi_app: ASGIType
|
22
|
+
|
23
|
+
def __init__(self, *options: Option) -> None:
|
24
|
+
super().__init__(*options)
|
25
|
+
|
26
|
+
if not self._assembler.has(ASGIType):
|
27
|
+
raise LookupError("A provider for `ASGIType` was expected, none found")
|
28
|
+
|
29
|
+
async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None:
|
30
|
+
if scope["type"] == "lifespan":
|
31
|
+
message = await receive()
|
32
|
+
receive = _Rereceive(message)
|
33
|
+
if message["type"] == "lifespan.startup":
|
34
|
+
try:
|
35
|
+
await self._startup()
|
36
|
+
except Exception as err:
|
37
|
+
exc = "".join(traceback.format_exception(err))
|
38
|
+
await send({"type": "lifespan.startup.failed", "message": exc})
|
39
|
+
|
40
|
+
elif message["type"] == "lifespan.shutdown":
|
41
|
+
await self.stop()
|
42
|
+
|
43
|
+
await self._asgi_app(scope, receive, send)
|
44
|
+
|
45
|
+
async def _startup(self) -> None:
|
46
|
+
await self.start()
|
47
|
+
self._asgi_app = await self._assembler.get(ASGIType)
|
48
|
+
|
49
|
+
|
50
|
+
class _Rereceive:
|
51
|
+
def __init__(self, message: _Message) -> None:
|
52
|
+
self._message = message
|
53
|
+
|
54
|
+
async def __call__(self, *args, **kwargs) -> _Message:
|
55
|
+
return self._message
|
engin/py.typed
ADDED
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
engin/__init__.py,sha256=T2tv-pdULMTp-p0PhB_XYmr9CEWOmMzcu0jebZx7-2Q,475
|
2
|
+
engin/_assembler.py,sha256=HPnrTGk6XbO-iG3BZfD65zzOxWC4C1yWOZf7Vv3MMKk,4944
|
3
|
+
engin/_block.py,sha256=XrGMFEeps_rvOGsYTlsefUkJGAY6jqdp71G8jE9FbwU,1109
|
4
|
+
engin/_dependency.py,sha256=cL1qQFKDt7Rgg7mx8IXVkCZlUigg5cvhPFF9g2LNrgg,4749
|
5
|
+
engin/_engin.py,sha256=Sg7sxESuwzTXx-4xgFv869YkpBRxE9d-kjIOTOTtcxU,3071
|
6
|
+
engin/_exceptions.py,sha256=nkzTqxrW5nkcNgFDGoZ2TBtnHtO2RLk0qghM5LNAEmU,542
|
7
|
+
engin/_lifecycle.py,sha256=0hk24fiwaBos5kaZrnG_Qm0VmUhWKnGtwYCjc007XDk,729
|
8
|
+
engin/_type_utils.py,sha256=naEk-lknC3Fdsd4jiP4YZAxjX3KXZN0MhFde9EV-Fmo,1835
|
9
|
+
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
engin/extensions/asgi.py,sha256=EHQhevGJt3UJZp_7Y7AIuYVm-bYUO-seuzE3kMuOJWM,1775
|
12
|
+
engin-0.0.dev1.dist-info/METADATA,sha256=263kjboSCI7Th0GHl7bbpF5cCHAKsB51_56v5xcJThk,152
|
13
|
+
engin-0.0.dev1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
14
|
+
engin-0.0.dev1.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
15
|
+
engin-0.0.dev1.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Tim OSullivan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|