engin 0.0.13__py3-none-any.whl → 0.0.15__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 CHANGED
@@ -2,9 +2,11 @@ from engin import ext
2
2
  from engin._assembler import Assembler
3
3
  from engin._block import Block, invoke, provide
4
4
  from engin._dependency import Entrypoint, Invoke, Provide, Supply
5
- from engin._engin import Engin, Option
5
+ from engin._engin import Engin
6
6
  from engin._exceptions import ProviderError
7
7
  from engin._lifecycle import Lifecycle
8
+ from engin._option import Option
9
+ from engin._type_utils import TypeId
8
10
 
9
11
  __all__ = [
10
12
  "Assembler",
@@ -17,6 +19,7 @@ __all__ = [
17
19
  "Provide",
18
20
  "ProviderError",
19
21
  "Supply",
22
+ "TypeId",
20
23
  "ext",
21
24
  "invoke",
22
25
  "provide",
engin/_assembler.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from collections import defaultdict
4
- from collections.abc import Collection, Iterable
4
+ from collections.abc import Iterable
5
5
  from dataclasses import dataclass
6
6
  from inspect import BoundArguments, Signature
7
7
  from typing import Any, Generic, TypeVar, cast
8
8
 
9
9
  from engin._dependency import Dependency, Provide, Supply
10
10
  from engin._exceptions import ProviderError
11
- from engin._type_utils import TypeId, type_id_of
11
+ from engin._type_utils import TypeId
12
12
 
13
13
  LOG = logging.getLogger("engin")
14
14
 
@@ -39,7 +39,7 @@ class Assembler:
39
39
  A container for Providers that is responsible for building provided types.
40
40
 
41
41
  The Assembler acts as a cache for previously built types, meaning repeat calls
42
- to `get` will produce the same value.
42
+ to `build` will produce the same value.
43
43
 
44
44
  Examples:
45
45
  ```python
@@ -47,15 +47,14 @@ class Assembler:
47
47
  return "foo"
48
48
 
49
49
  a = Assembler([Provide(build_str)])
50
- await a.get(str)
50
+ await a.build(str)
51
51
  ```
52
52
  """
53
53
 
54
54
  def __init__(self, providers: Iterable[Provide]) -> None:
55
55
  self._providers: dict[TypeId, Provide[Any]] = {}
56
56
  self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
57
- self._dependencies: dict[TypeId, Any] = {}
58
- self._consumed_providers: set[Provide[Any]] = set()
57
+ self._assembled_outputs: dict[TypeId, Any] = {}
59
58
  self._lock = asyncio.Lock()
60
59
 
61
60
  for provider in providers:
@@ -86,17 +85,15 @@ class Assembler:
86
85
  bound_args=await self._bind_arguments(dependency.signature),
87
86
  )
88
87
 
89
- async def get(self, type_: type[T]) -> T:
88
+ async def build(self, type_: type[T]) -> T:
90
89
  """
91
- Return the constructed value for the given type.
90
+ Build the type from Assembler's factories.
92
91
 
93
- This method assembles the required Providers and constructs their corresponding
94
- values.
95
-
96
- If the
92
+ If the type has been built previously the value will be cached and will return the
93
+ same instance.
97
94
 
98
95
  Args:
99
- type_: the type of the desired value.
96
+ type_: the type of the desired value to build.
100
97
 
101
98
  Raises:
102
99
  LookupError: When no provider is found for the given type.
@@ -106,13 +103,14 @@ class Assembler:
106
103
  Returns:
107
104
  The constructed value.
108
105
  """
109
- type_id = type_id_of(type_)
110
- if type_id in self._dependencies:
111
- return cast(T, self._dependencies[type_id])
106
+ type_id = TypeId.from_type(type_)
107
+ if type_id in self._assembled_outputs:
108
+ return cast("T", self._assembled_outputs[type_id])
112
109
  if type_id.multi:
113
- out = []
114
110
  if type_id not in self._multiproviders:
115
111
  raise LookupError(f"no provider found for target type id '{type_id}'")
112
+
113
+ out = []
116
114
  for provider in self._multiproviders[type_id]:
117
115
  assembled_dependency = await self.assemble(provider)
118
116
  try:
@@ -123,11 +121,12 @@ class Assembler:
123
121
  error_type=type(err),
124
122
  error_message=str(err),
125
123
  ) from err
126
- self._dependencies[type_id] = out
124
+ self._assembled_outputs[type_id] = out
127
125
  return out # type: ignore[return-value]
128
126
  else:
129
127
  if type_id not in self._providers:
130
128
  raise LookupError(f"no provider found for target type id '{type_id}'")
129
+
131
130
  assembled_dependency = await self.assemble(self._providers[type_id])
132
131
  try:
133
132
  value = await assembled_dependency()
@@ -137,7 +136,7 @@ class Assembler:
137
136
  error_type=type(err),
138
137
  error_message=str(err),
139
138
  ) from err
140
- self._dependencies[type_id] = value
139
+ self._assembled_outputs[type_id] = value
141
140
  return value # type: ignore[return-value]
142
141
 
143
142
  def has(self, type_: type[T]) -> bool:
@@ -150,7 +149,7 @@ class Assembler:
150
149
  Returns:
151
150
  True if the Assembler has a provider for type else False.
152
151
  """
153
- type_id = type_id_of(type_)
152
+ type_id = TypeId.from_type(type_)
154
153
  if type_id.multi:
155
154
  return type_id in self._multiproviders
156
155
  else:
@@ -160,55 +159,64 @@ class Assembler:
160
159
  """
161
160
  Add a provider to the Assembler post-initialisation.
162
161
 
162
+ If this replaces an existing provider, this will clear any previously assembled
163
+ output for the existing Provider.
164
+
163
165
  Args:
164
166
  provider: the Provide instance to add.
165
167
 
166
168
  Returns:
167
169
  None
168
-
169
- Raises:
170
- ValueError: if a provider for this type already exists.
171
170
  """
172
171
  type_id = provider.return_type_id
173
172
  if provider.is_multiprovider:
174
- if type_id in self._multiproviders:
175
- self._multiproviders[type_id].append(provider)
176
- else:
177
- self._multiproviders[type_id] = [provider]
173
+ if type_id in self._assembled_outputs:
174
+ del self._assembled_outputs[type_id]
175
+ self._multiproviders[type_id].append(provider)
178
176
  else:
179
- if type_id in self._providers:
180
- raise ValueError(f"A provider for '{type_id}' already exists")
177
+ if type_id in self._assembled_outputs:
178
+ del self._assembled_outputs[type_id]
181
179
  self._providers[type_id] = provider
182
180
 
183
- def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
181
+ def _resolve_providers(self, type_id: TypeId) -> Iterable[Provide]:
182
+ """
183
+ Resolves the chain of providers required to satisfy the provider of a given type.
184
+ Ordering of the return value is very important!
185
+
186
+ # TODO: performance optimisation, do not recurse for already satisfied providers?
187
+ """
184
188
  if type_id.multi:
185
- providers = self._multiproviders.get(type_id)
189
+ root_providers = self._multiproviders.get(type_id)
186
190
  else:
187
- providers = [provider] if (provider := self._providers.get(type_id)) else None
188
- if not providers:
191
+ root_providers = [provider] if (provider := self._providers.get(type_id)) else None
192
+
193
+ if not root_providers:
189
194
  if type_id.multi:
190
195
  LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
191
- providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
196
+ root_providers = [(Supply([], as_type=list[type_id.type]))] # type: ignore[name-defined]
192
197
  # store default to prevent the warning appearing multiple times
193
- self._multiproviders[type_id] = providers
198
+ self._multiproviders[type_id] = root_providers
194
199
  else:
195
- raise LookupError(f"No Provider registered for dependency '{type_id}'")
196
-
197
- required_providers: list[Provide[Any]] = []
198
- for provider in providers:
199
- required_providers.extend(
200
- provider
201
- for provider_param in provider.parameter_types
202
- for provider in self._resolve_providers(provider_param)
203
- )
204
-
205
- return {*required_providers, *providers}
200
+ available = sorted(str(k) for k in self._providers)
201
+ msg = f"Missing Provider for type '{type_id}', available: {available}"
202
+ raise LookupError(msg)
203
+
204
+ # providers that must be satisfied to satisfy the root level providers
205
+ yield from (
206
+ child_provider
207
+ for root_provider in root_providers
208
+ for root_provider_param in root_provider.parameter_types
209
+ for child_provider in self._resolve_providers(root_provider_param)
210
+ )
211
+ yield from root_providers
206
212
 
207
213
  async def _satisfy(self, target: TypeId) -> None:
208
214
  for provider in self._resolve_providers(target):
209
- if provider in self._consumed_providers:
215
+ if (
216
+ not provider.is_multiprovider
217
+ and provider.return_type_id in self._assembled_outputs
218
+ ):
210
219
  continue
211
- self._consumed_providers.add(provider)
212
220
  type_id = provider.return_type_id
213
221
  bound_args = await self._bind_arguments(provider.signature)
214
222
  try:
@@ -218,12 +226,12 @@ class Assembler:
218
226
  provider=provider, error_type=type(err), error_message=str(err)
219
227
  ) from err
220
228
  if provider.is_multiprovider:
221
- if type_id in self._dependencies:
222
- self._dependencies[type_id].extend(value)
229
+ if type_id in self._assembled_outputs:
230
+ self._assembled_outputs[type_id].extend(value)
223
231
  else:
224
- self._dependencies[type_id] = value
232
+ self._assembled_outputs[type_id] = value
225
233
  else:
226
- self._dependencies[type_id] = value
234
+ self._assembled_outputs[type_id] = value
227
235
 
228
236
  async def _bind_arguments(self, signature: Signature) -> BoundArguments:
229
237
  args = []
@@ -232,11 +240,11 @@ class Assembler:
232
240
  if param_name == "self":
233
241
  args.append(object())
234
242
  continue
235
- param_key = type_id_of(param.annotation)
236
- has_dependency = param_key in self._dependencies
243
+ param_key = TypeId.from_type(param.annotation)
244
+ has_dependency = param_key in self._assembled_outputs
237
245
  if not has_dependency:
238
246
  await self._satisfy(param_key)
239
- val = self._dependencies[param_key]
247
+ val = self._assembled_outputs[param_key]
240
248
  if param.kind == param.POSITIONAL_ONLY:
241
249
  args.append(val)
242
250
  else:
engin/_block.py CHANGED
@@ -1,27 +1,48 @@
1
1
  import inspect
2
- from collections.abc import Iterable, Iterator
3
- from typing import ClassVar
2
+ from collections.abc import Callable, Iterable, Sequence
3
+ from itertools import chain
4
+ from typing import TYPE_CHECKING, ClassVar
4
5
 
5
- from engin._dependency import Func, Invoke, Provide
6
+ from engin._dependency import Dependency, Func, Invoke, Provide
7
+ from engin._option import Option
6
8
 
9
+ if TYPE_CHECKING:
10
+ from engin._engin import Engin
7
11
 
8
- def provide(func: Func) -> Func:
12
+
13
+ def provide(
14
+ func_: Func | None = None, *, override: bool = False
15
+ ) -> Func | Callable[[Func], Func]:
9
16
  """
10
17
  A decorator for defining a Provider in a Block.
11
18
  """
12
- func._opt = Provide(func) # type: ignore[attr-defined]
13
- return func
14
19
 
20
+ def _inner(func: Func) -> Func:
21
+ func._opt = Provide(func, override=override) # type: ignore[attr-defined]
22
+ return func
23
+
24
+ if func_ is None:
25
+ return _inner
26
+ else:
27
+ return _inner(func_)
15
28
 
16
- def invoke(func: Func) -> Func:
29
+
30
+ def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
17
31
  """
18
32
  A decorator for defining an Invocation in a Block.
19
33
  """
20
- func._opt = Invoke(func) # type: ignore[attr-defined]
21
- return func
22
34
 
35
+ def _inner(func: Func) -> Func:
36
+ func._opt = Invoke(func) # type: ignore[attr-defined]
37
+ return func
38
+
39
+ if func_ is None:
40
+ return _inner
41
+ else:
42
+ return _inner(func_)
23
43
 
24
- class Block(Iterable[Provide | Invoke]):
44
+
45
+ class Block(Option):
25
46
  """
26
47
  A Block is a collection of providers and invocations.
27
48
 
@@ -48,23 +69,21 @@ class Block(Iterable[Provide | Invoke]):
48
69
  ```
49
70
  """
50
71
 
51
- options: ClassVar[list[Provide | Invoke]] = []
52
-
53
- def __init__(self, /, block_name: str | None = None) -> None:
54
- self._options: list[Provide | Invoke] = self.options[:]
55
- self._name = block_name or f"{type(self).__name__}"
56
- for _, method in inspect.getmembers(self):
57
- if opt := getattr(method, "_opt", None):
58
- if not isinstance(opt, Provide | Invoke):
72
+ name: ClassVar[str | None] = None
73
+ options: ClassVar[Sequence[Option]] = []
74
+
75
+ @classmethod
76
+ def apply(cls, engin: "Engin") -> None:
77
+ block_name = cls.name or f"{cls.__name__}"
78
+ for option in chain(cls.options, cls._method_options()):
79
+ if isinstance(option, Dependency):
80
+ option._block_name = block_name
81
+ option.apply(engin)
82
+
83
+ @classmethod
84
+ def _method_options(cls) -> Iterable[Provide | Invoke]:
85
+ for _, method in inspect.getmembers(cls):
86
+ if option := getattr(method, "_opt", None):
87
+ if not isinstance(option, Provide | Invoke):
59
88
  raise RuntimeError("Block option is not an instance of Provide or Invoke")
60
- opt.set_block_name(self._name)
61
- self._options.append(opt)
62
- for opt in self.options:
63
- opt.set_block_name(self._name)
64
-
65
- @property
66
- def name(self) -> str:
67
- return self._name
68
-
69
- def __iter__(self) -> Iterator[Provide | Invoke]:
70
- return iter(self._options)
89
+ yield option
engin/_cli/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ try:
2
+ import typer
3
+ except ImportError:
4
+ raise ImportError(
5
+ "Unable to import typer, to use the engin cli please install the"
6
+ " `cli` extra, e.g. pip install engin[cli]"
7
+ ) from None
8
+
9
+ from engin._cli._graph import cli as graph_cli
10
+
11
+ app = typer.Typer()
12
+
13
+ app.add_typer(graph_cli)
@@ -1,63 +1,76 @@
1
+ import contextlib
1
2
  import importlib
2
3
  import logging
3
4
  import socketserver
4
5
  import sys
5
6
  import threading
6
- from argparse import ArgumentParser
7
7
  from http.server import BaseHTTPRequestHandler
8
8
  from time import sleep
9
- from typing import Any
9
+ from typing import Annotated, Any
10
+
11
+ import typer
12
+ from rich import print
10
13
 
11
14
  from engin import Engin, Entrypoint, Invoke
15
+ from engin._cli._utils import print_error
12
16
  from engin._dependency import Dependency, Provide, Supply
13
17
  from engin.ext.asgi import ASGIEngin
14
- from engin.ext.fastapi import APIRouteDependency
18
+
19
+ try:
20
+ from engin.ext.fastapi import APIRouteDependency
21
+ except ImportError:
22
+ APIRouteDependency = None # type: ignore[assignment,misc]
23
+
24
+ cli = typer.Typer()
25
+
15
26
 
16
27
  # mute logging from importing of files + engin's debug logging.
17
28
  logging.disable()
18
29
 
19
- args = ArgumentParser(
20
- prog="engin-graph",
21
- description="Creates a visualisation of your application's dependencies",
22
- )
23
- args.add_argument(
24
- "app",
25
- help=(
26
- "the import path of your Engin instance, in the form "
27
- "'package:application', e.g. 'app.main:engin'"
28
- ),
29
- )
30
30
 
31
31
  _APP_ORIGIN = ""
32
32
 
33
-
34
- def serve_graph() -> None:
33
+ _CLI_HELP = {
34
+ "app": (
35
+ "The import path of your Engin instance, in the form 'package:application'"
36
+ ", e.g. 'app.main:engin'"
37
+ )
38
+ }
39
+
40
+
41
+ @cli.command(name="graph")
42
+ def serve_graph(
43
+ app: Annotated[
44
+ str,
45
+ typer.Argument(help=_CLI_HELP["app"]),
46
+ ],
47
+ ) -> None:
48
+ """
49
+ Creates a visualisation of your application's dependencies.
50
+ """
35
51
  # add cwd to path to enable local package imports
36
52
  sys.path.insert(0, "")
37
53
 
38
- parsed = args.parse_args()
39
-
40
- app = parsed.app
41
-
42
54
  try:
43
55
  module_name, engin_name = app.split(":", maxsplit=1)
44
56
  except ValueError:
45
- raise ValueError(
46
- "Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
47
- ) from None
57
+ print_error("Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'")
48
58
 
49
59
  global _APP_ORIGIN
50
60
  _APP_ORIGIN = module_name.split(".", maxsplit=1)[0]
51
61
 
52
- module = importlib.import_module(module_name)
62
+ try:
63
+ module = importlib.import_module(module_name)
64
+ except ModuleNotFoundError:
65
+ print_error(f"unable to find module '{module_name}'")
53
66
 
54
67
  try:
55
68
  instance = getattr(module, engin_name)
56
- except LookupError:
57
- raise LookupError(f"Module '{module_name}' has no attribute '{engin_name}'") from None
69
+ except AttributeError:
70
+ print_error(f"module '{module_name}' has no attribute '{engin_name}'")
58
71
 
59
72
  if not isinstance(instance, Engin):
60
- raise TypeError(f"'{app}' is not an Engin instance")
73
+ print_error(f"'{app}' is not an Engin instance")
61
74
 
62
75
  nodes = instance.graph()
63
76
 
@@ -96,10 +109,14 @@ def serve_graph() -> None:
96
109
  server_thread.daemon = True # Daemonize the thread so it exits when the main script exits
97
110
  server_thread.start()
98
111
 
99
- try:
100
- sleep(10000)
101
- except KeyboardInterrupt:
102
- print("Exiting the server...")
112
+ with contextlib.suppress(KeyboardInterrupt):
113
+ wait_for_interrupt()
114
+
115
+ print("Exiting the server...")
116
+
117
+
118
+ def wait_for_interrupt() -> None:
119
+ sleep(10000)
103
120
 
104
121
 
105
122
  _BLOCK_IDX: dict[str, int] = {}
engin/_cli/_utils.py ADDED
@@ -0,0 +1,18 @@
1
+ from typing import Never
2
+
3
+ import typer
4
+ from rich import print
5
+ from rich.panel import Panel
6
+
7
+
8
+ def print_error(msg: str) -> Never:
9
+ print(
10
+ Panel(
11
+ title="Error",
12
+ renderable=msg.capitalize(),
13
+ title_align="left",
14
+ border_style="red",
15
+ highlight=True,
16
+ )
17
+ )
18
+ raise typer.Exit(code=1)
engin/_dependency.py CHANGED
@@ -3,8 +3,8 @@ import typing
3
3
  from abc import ABC
4
4
  from collections.abc import Awaitable, Callable
5
5
  from inspect import Parameter, Signature, isclass, iscoroutinefunction
6
- from types import FrameType
7
6
  from typing import (
7
+ TYPE_CHECKING,
8
8
  Any,
9
9
  Generic,
10
10
  ParamSpec,
@@ -14,7 +14,12 @@ from typing import (
14
14
  get_type_hints,
15
15
  )
16
16
 
17
- from engin._type_utils import TypeId, type_id_of
17
+ from engin._introspect import get_first_external_frame
18
+ from engin._option import Option
19
+ from engin._type_utils import TypeId
20
+
21
+ if TYPE_CHECKING:
22
+ from engin._engin import Engin
18
23
 
19
24
  P = ParamSpec("P")
20
25
  T = TypeVar("T")
@@ -24,23 +29,16 @@ Func: TypeAlias = Callable[P, T]
24
29
  def _noop(*args: Any, **kwargs: Any) -> None: ...
25
30
 
26
31
 
27
- def _walk_stack() -> FrameType:
28
- stack = inspect.stack()[1]
29
- frame = stack.frame
30
- while True:
31
- if frame.f_globals["__package__"] != "engin" or frame.f_back is None:
32
- return frame
33
- else:
34
- frame = frame.f_back
35
-
36
-
37
- class Dependency(ABC, Generic[P, T]):
38
- def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
32
+ class Dependency(ABC, Option, Generic[P, T]):
33
+ def __init__(self, func: Func[P, T]) -> None:
39
34
  self._func = func
40
35
  self._is_async = iscoroutinefunction(func)
41
36
  self._signature = inspect.signature(self._func)
42
- self._block_name = block_name
43
- self._source_frame = _walk_stack()
37
+ self._block_name: str | None = None
38
+
39
+ source_frame = get_first_external_frame()
40
+ self._source_package = cast("str", source_frame.frame.f_globals["__package__"])
41
+ self._source_frame = cast("str", source_frame.frame.f_globals["__name__"])
44
42
 
45
43
  @property
46
44
  def source_module(self) -> str:
@@ -50,7 +48,7 @@ class Dependency(ABC, Generic[P, T]):
50
48
  Returns:
51
49
  A string, e.g. "examples.fastapi.app"
52
50
  """
53
- return self._source_frame.f_globals["__name__"] # type: ignore[no-any-return]
51
+ return self._source_frame
54
52
 
55
53
  @property
56
54
  def source_package(self) -> str:
@@ -60,7 +58,7 @@ class Dependency(ABC, Generic[P, T]):
60
58
  Returns:
61
59
  A string, e.g. "engin"
62
60
  """
63
- return self._source_frame.f_globals["__package__"] # type: ignore[no-any-return]
61
+ return self._source_package
64
62
 
65
63
  @property
66
64
  def block_name(self) -> str | None:
@@ -84,18 +82,15 @@ class Dependency(ABC, Generic[P, T]):
84
82
  return []
85
83
  if parameters[0].name == "self":
86
84
  parameters.pop(0)
87
- return [type_id_of(param.annotation) for param in parameters]
85
+ return [TypeId.from_type(param.annotation) for param in parameters]
88
86
 
89
87
  @property
90
88
  def signature(self) -> Signature:
91
89
  return self._signature
92
90
 
93
- def set_block_name(self, name: str) -> None:
94
- self._block_name = name
95
-
96
91
  async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
97
92
  if self._is_async:
98
- return await cast(Awaitable[T], self._func(*args, **kwargs))
93
+ return await cast("Awaitable[T]", self._func(*args, **kwargs))
99
94
  else:
100
95
  return self._func(*args, **kwargs)
101
96
 
@@ -119,8 +114,11 @@ class Invoke(Dependency):
119
114
  ```
120
115
  """
121
116
 
122
- def __init__(self, invocation: Func[P, T], block_name: str | None = None) -> None:
123
- super().__init__(func=invocation, block_name=block_name)
117
+ def __init__(self, invocation: Func[P, T]) -> None:
118
+ super().__init__(func=invocation)
119
+
120
+ def apply(self, engin: "Engin") -> None:
121
+ engin._invocations.append(self)
124
122
 
125
123
  def __str__(self) -> str:
126
124
  return f"Invoke({self.name})"
@@ -133,13 +131,13 @@ class Entrypoint(Invoke):
133
131
  Entrypoints are a short hand for no-op Invocations that can be used to
134
132
  """
135
133
 
136
- def __init__(self, type_: type[Any], *, block_name: str | None = None) -> None:
134
+ def __init__(self, type_: type[Any]) -> None:
137
135
  self._type = type_
138
- super().__init__(invocation=_noop, block_name=block_name)
136
+ super().__init__(invocation=_noop)
139
137
 
140
138
  @property
141
139
  def parameter_types(self) -> list[TypeId]:
142
- return [type_id_of(self._type)]
140
+ return [TypeId.from_type(self._type)]
143
141
 
144
142
  @property
145
143
  def signature(self) -> Signature:
@@ -150,12 +148,21 @@ class Entrypoint(Invoke):
150
148
  )
151
149
 
152
150
  def __str__(self) -> str:
153
- return f"Entrypoint({type_id_of(self._type)})"
151
+ return f"Entrypoint({TypeId.from_type(self._type)})"
154
152
 
155
153
 
156
154
  class Provide(Dependency[Any, T]):
157
- def __init__(self, builder: Func[P, T], block_name: str | None = None) -> None:
158
- super().__init__(func=builder, block_name=block_name)
155
+ def __init__(self, builder: Func[P, T], *, override: bool = False) -> None:
156
+ """
157
+ Provide a type via a builder or factory function.
158
+
159
+ Args:
160
+ builder: the builder function that returns the type.
161
+ override: allow this provider to override existing providers from the same
162
+ package.
163
+ """
164
+ super().__init__(func=builder)
165
+ self._override = override
159
166
  self._is_multi = typing.get_origin(self.return_type) is list
160
167
 
161
168
  # Validate that the provider does to depend on its own output value, as this will
@@ -190,12 +197,36 @@ class Provide(Dependency[Any, T]):
190
197
 
191
198
  @property
192
199
  def return_type_id(self) -> TypeId:
193
- return type_id_of(self.return_type)
200
+ return TypeId.from_type(self.return_type)
194
201
 
195
202
  @property
196
203
  def is_multiprovider(self) -> bool:
197
204
  return self._is_multi
198
205
 
206
+ def apply(self, engin: "Engin") -> None:
207
+ type_id = self.return_type_id
208
+ if self.is_multiprovider:
209
+ engin._multiproviders[type_id].append(self)
210
+ return
211
+
212
+ if type_id not in engin._providers:
213
+ engin._providers[type_id] = self
214
+ return
215
+
216
+ existing_provider = engin._providers[type_id]
217
+ is_same_package = existing_provider.source_package == self.source_package
218
+
219
+ # overwriting a dependency from the same package must be explicit
220
+ if is_same_package and not self._override:
221
+ msg = (
222
+ f"Provider '{self.name}' is implicitly overriding "
223
+ f"'{existing_provider.name}', if this is intended specify "
224
+ "`override=True` for the overriding Provider"
225
+ )
226
+ raise RuntimeError(msg)
227
+
228
+ engin._providers[type_id] = self
229
+
199
230
  def __hash__(self) -> int:
200
231
  return hash(self.return_type_id)
201
232
 
@@ -205,13 +236,27 @@ class Provide(Dependency[Any, T]):
205
236
 
206
237
  class Supply(Provide, Generic[T]):
207
238
  def __init__(
208
- self, value: T, *, type_hint: type | None = None, block_name: str | None = None
239
+ self, value: T, *, as_type: type | None = None, override: bool = False
209
240
  ) -> None:
241
+ """
242
+ Supply a value.
243
+
244
+ This is a shorthand which under the hood creates a Provider with a noop factory
245
+ function.
246
+
247
+ Args:
248
+ value: the value to Supply
249
+ as_type: allows you to specify the provided type, useful for type erasing,
250
+ e.g. Supply a concrete value but specify it as an interface or other
251
+ abstraction.
252
+ override: allow this provider to override existing providers from the same
253
+ package.
254
+ """
210
255
  self._value = value
211
- self._type_hint = type_hint
256
+ self._type_hint = as_type
212
257
  if self._type_hint is not None:
213
- self._get_val.__annotations__["return"] = type_hint
214
- super().__init__(builder=self._get_val, block_name=block_name)
258
+ self._get_val.__annotations__["return"] = as_type
259
+ super().__init__(builder=self._get_val, override=override)
215
260
 
216
261
  @property
217
262
  def return_type(self) -> type[T]:
engin/_engin.py CHANGED
@@ -3,26 +3,21 @@ import logging
3
3
  import os
4
4
  import signal
5
5
  from asyncio import Event, Task
6
- from collections.abc import Iterable
6
+ from collections import defaultdict
7
7
  from contextlib import AsyncExitStack
8
8
  from itertools import chain
9
9
  from types import FrameType
10
- from typing import ClassVar, TypeAlias
10
+ from typing import ClassVar
11
11
 
12
- from engin import Entrypoint
13
12
  from engin._assembler import AssembledDependency, Assembler
14
- from engin._block import Block
15
- from engin._dependency import Dependency, Invoke, Provide, Supply
13
+ from engin._dependency import Invoke, Provide, Supply
16
14
  from engin._graph import DependencyGrapher, Node
17
15
  from engin._lifecycle import Lifecycle
16
+ from engin._option import Option
18
17
  from engin._type_utils import TypeId
19
18
 
20
- LOG = logging.getLogger("engin")
21
-
22
- Option: TypeAlias = Invoke | Provide | Supply | Block
23
- _Opt: TypeAlias = Invoke | Provide | Supply
24
-
25
19
  _OS_IS_WINDOWS = os.name == "nt"
20
+ LOG = logging.getLogger("engin")
26
21
 
27
22
 
28
23
  class Engin:
@@ -88,12 +83,16 @@ class Engin:
88
83
  self._shutdown_task: Task | None = None
89
84
  self._run_task: Task | None = None
90
85
 
91
- # TODO: refactor _destruct_options and related attributes into a dedicated class?
92
- self._providers: dict[TypeId, Provide] = {TypeId.from_type(Engin): Provide(self._self)}
93
- self._multiproviders: dict[TypeId, list[Provide]] = {}
86
+ self._providers: dict[TypeId, Provide] = {
87
+ TypeId.from_type(Engin): Supply(self, as_type=Engin)
88
+ }
89
+ self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
94
90
  self._invocations: list[Invoke] = []
91
+
95
92
  # populates the above
96
- self._destruct_options(chain(self._LIB_OPTIONS, options))
93
+ for option in chain(self._LIB_OPTIONS, options):
94
+ option.apply(self)
95
+
97
96
  multi_providers = [p for multi in self._multiproviders.values() for p in multi]
98
97
  self._assembler = Assembler(chain(self._providers.values(), multi_providers))
99
98
 
@@ -111,7 +110,6 @@ class Engin:
111
110
  await self.start()
112
111
  self._run_task = asyncio.create_task(_wait_for_stop_signal(self._stop_requested_event))
113
112
  await self._stop_requested_event.wait()
114
- await self._shutdown()
115
113
 
116
114
  async def start(self) -> None:
117
115
  """
@@ -134,14 +132,18 @@ class Engin:
134
132
  LOG.error(f"invocation '{name}' errored, exiting", exc_info=err)
135
133
  return
136
134
 
137
- lifecycle = await self._assembler.get(Lifecycle)
135
+ lifecycle = await self._assembler.build(Lifecycle)
138
136
 
139
137
  try:
140
138
  for hook in lifecycle.list():
141
- await self._exit_stack.enter_async_context(hook)
139
+ await asyncio.wait_for(self._exit_stack.enter_async_context(hook), timeout=15)
142
140
  except Exception as err:
143
- LOG.error("lifecycle startup error, exiting", exc_info=err)
144
- await self._exit_stack.aclose()
141
+ if isinstance(err, TimeoutError):
142
+ msg = "lifecycle startup task timed out after 15s, exiting"
143
+ else:
144
+ msg = "lifecycle startup task errored, exiting"
145
+ LOG.error(msg, exc_info=err)
146
+ await self._shutdown()
145
147
  return
146
148
 
147
149
  LOG.info("startup complete")
@@ -175,46 +177,6 @@ class Engin:
175
177
  await self._stop_requested_event.wait()
176
178
  await self._shutdown()
177
179
 
178
- def _destruct_options(self, options: Iterable[Option]) -> None:
179
- for opt in options:
180
- if isinstance(opt, Block):
181
- self._destruct_options(opt)
182
- if isinstance(opt, Provide | Supply):
183
- if not opt.is_multiprovider:
184
- existing = self._providers.get(opt.return_type_id)
185
- self._log_option(opt, overwrites=existing)
186
- self._providers[opt.return_type_id] = opt
187
- else:
188
- self._log_option(opt)
189
- if opt.return_type_id in self._multiproviders:
190
- self._multiproviders[opt.return_type_id].append(opt)
191
- else:
192
- self._multiproviders[opt.return_type_id] = [opt]
193
- elif isinstance(opt, Invoke):
194
- self._log_option(opt)
195
- self._invocations.append(opt)
196
-
197
- @staticmethod
198
- def _log_option(opt: Dependency, overwrites: Dependency | None = None) -> None:
199
- if overwrites is not None:
200
- extra = f"\tOVERWRITES {overwrites.name}"
201
- if overwrites.block_name:
202
- extra += f" [{overwrites.block_name}]"
203
- else:
204
- extra = ""
205
- if isinstance(opt, Supply):
206
- LOG.debug(f"SUPPLY {opt.return_type_id!s:<35}{extra}")
207
- elif isinstance(opt, Provide):
208
- LOG.debug(f"PROVIDE {opt.return_type_id!s:<35} <- {opt.name}() {extra}")
209
- elif isinstance(opt, Entrypoint):
210
- type_id = opt.parameter_types[0]
211
- LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
212
- elif isinstance(opt, Invoke):
213
- LOG.debug(f"INVOKE {opt.name:<35}")
214
-
215
- def _self(self) -> "Engin":
216
- return self
217
-
218
180
 
219
181
  async def _wait_for_stop_signal(stop_requested_event: Event) -> None:
220
182
  try:
engin/_introspect.py ADDED
@@ -0,0 +1,34 @@
1
+ import inspect
2
+ from collections.abc import Iterable
3
+ from inspect import FrameInfo
4
+
5
+
6
+ def walk_stack() -> Iterable[FrameInfo]:
7
+ """
8
+ Fast alternative to `inspect.stack()`
9
+
10
+ Compared to `inspect.stack()`:
11
+ - Does not read source files to load neighboring context
12
+ - Less accurate filename determination, still correct for most cases
13
+ - Does not compute 3.11+ code positions (PEP 657)
14
+ """
15
+
16
+ frame = inspect.currentframe()
17
+
18
+ while frame := frame and frame.f_back:
19
+ yield inspect.FrameInfo(
20
+ frame,
21
+ inspect.getfile(frame),
22
+ frame.f_lineno,
23
+ frame.f_code.co_name,
24
+ None,
25
+ None,
26
+ )
27
+
28
+
29
+ def get_first_external_frame() -> FrameInfo:
30
+ for frame_info in walk_stack():
31
+ frame = frame_info.frame
32
+ if frame.f_globals["__package__"] != "engin" or frame.f_back is None:
33
+ return frame_info
34
+ raise RuntimeError("Unable to find external frame")
engin/_lifecycle.py CHANGED
@@ -1,12 +1,15 @@
1
1
  import asyncio
2
2
  import logging
3
+ from collections.abc import Awaitable, Callable
3
4
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
5
+ from inspect import iscoroutinefunction
4
6
  from types import TracebackType
5
7
  from typing import TypeAlias, TypeGuard, cast
6
8
 
7
9
  LOG = logging.getLogger("engin")
8
10
 
9
11
  _AnyContextManager: TypeAlias = AbstractAsyncContextManager | AbstractContextManager
12
+ _ParameterlessCallable: TypeAlias = Callable[[], None | Awaitable[None]]
10
13
 
11
14
 
12
15
  class Lifecycle:
@@ -44,6 +47,18 @@ class Lifecycle:
44
47
 
45
48
  lifecycle.append(task)
46
49
  ```
50
+
51
+ Defining a custom lifecycle using a LifecycleHook.
52
+
53
+ ```python
54
+ def my_provider(lifecycle: Lifecycle) -> str:
55
+ connection_pool = ConnectionPool()
56
+
57
+ lifecycle.hook(
58
+ on_start=connection_pool.connect,
59
+ on_stop=connection_pool.close,
60
+ )
61
+ ```
47
62
  """
48
63
 
49
64
  def __init__(self) -> None:
@@ -59,6 +74,25 @@ class Lifecycle:
59
74
  suppressed_cm = _AExitSuppressingAsyncContextManager(cm)
60
75
  self._context_managers.append(suppressed_cm)
61
76
 
77
+ def hook(
78
+ self,
79
+ *,
80
+ on_start: _ParameterlessCallable | None = None,
81
+ on_stop: _ParameterlessCallable | None = None,
82
+ ) -> None:
83
+ """
84
+ Append a hook to the Lifecycle.
85
+
86
+ At least one of `on_start` or `on_stop` must be provided.
87
+
88
+ Args:
89
+ on_start: a callable to be executed on Lifecycle startup.
90
+ on_stop: a callable to be executed on Lifecycle shutdown.
91
+ """
92
+ if on_start is None and on_stop is None:
93
+ raise ValueError("At least one of on_start or on_stop must be provided")
94
+ self.append(LifecycleHook(on_start=on_start, on_stop=on_stop))
95
+
62
96
  def list(self) -> list[AbstractAsyncContextManager]:
63
97
  """
64
98
  List all the defined tasks.
@@ -69,6 +103,38 @@ class Lifecycle:
69
103
  return self._context_managers[:]
70
104
 
71
105
 
106
+ class LifecycleHook(AbstractAsyncContextManager):
107
+ def __init__(
108
+ self,
109
+ on_start: _ParameterlessCallable | None = None,
110
+ on_stop: _ParameterlessCallable | None = None,
111
+ ) -> None:
112
+ self._on_start = on_start
113
+ self._on_stop = on_stop
114
+
115
+ async def __aenter__(self) -> None:
116
+ if self._on_start is not None:
117
+ func = self._on_start
118
+ if iscoroutinefunction(func):
119
+ await func()
120
+ else:
121
+ await asyncio.to_thread(func)
122
+
123
+ async def __aexit__(
124
+ self,
125
+ exc_type: type[BaseException] | None,
126
+ exc_value: BaseException | None,
127
+ traceback: TracebackType | None,
128
+ /,
129
+ ) -> None:
130
+ if self._on_stop is not None:
131
+ func = self._on_stop
132
+ if iscoroutinefunction(func):
133
+ await func()
134
+ else:
135
+ await asyncio.to_thread(func)
136
+
137
+
72
138
  class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
73
139
  def __init__(self, cm: _AnyContextManager) -> None:
74
140
  self._cm = cm
@@ -77,7 +143,7 @@ class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
77
143
  if self._is_async_cm(self._cm):
78
144
  await self._cm.__aenter__()
79
145
  else:
80
- await asyncio.to_thread(cast(AbstractContextManager, self._cm).__enter__)
146
+ await asyncio.to_thread(cast("AbstractContextManager", self._cm).__enter__)
81
147
 
82
148
  async def __aexit__(
83
149
  self,
@@ -91,7 +157,7 @@ class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
91
157
  await self._cm.__aexit__(exc_type, exc_value, traceback)
92
158
  else:
93
159
  await asyncio.to_thread(
94
- cast(AbstractContextManager, self._cm).__exit__,
160
+ cast("AbstractContextManager", self._cm).__exit__,
95
161
  exc_type,
96
162
  exc_value,
97
163
  traceback,
engin/_option.py ADDED
@@ -0,0 +1,10 @@
1
+ from abc import abstractmethod
2
+ from typing import TYPE_CHECKING, Protocol
3
+
4
+ if TYPE_CHECKING:
5
+ from engin._engin import Engin
6
+
7
+
8
+ class Option(Protocol):
9
+ @abstractmethod
10
+ def apply(self, engin: "Engin") -> None: ...
engin/_type_utils.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any
6
6
  _implict_modules = ["builtins", "typing", "collections.abc", "types"]
7
7
 
8
8
 
9
- @dataclass(frozen=True, eq=True, slots=True)
9
+ @dataclass(frozen=True, slots=True)
10
10
  class TypeId:
11
11
  """
12
12
  Represents information about a Type in the Dependency Injection framework.
@@ -26,7 +26,7 @@ class TypeId:
26
26
  Returns:
27
27
  The corresponding TypeId for that type.
28
28
  """
29
- if is_multi_type(type_):
29
+ if _is_multi_type(type_):
30
30
  inner_obj = typing.get_args(type_)[0]
31
31
  return TypeId(type=inner_obj, multi=True)
32
32
  else:
@@ -40,6 +40,11 @@ class TypeId:
40
40
  out += "[]"
41
41
  return out
42
42
 
43
+ def __eq__(self, other: Any) -> bool:
44
+ if not isinstance(other, TypeId):
45
+ return False
46
+ return self.type == other.type and self.multi == other.multi
47
+
43
48
 
44
49
  def _args_to_str(type_: Any) -> str:
45
50
  args = typing.get_args(type_)
@@ -65,14 +70,7 @@ def _args_to_str(type_: Any) -> str:
65
70
  return arg_str
66
71
 
67
72
 
68
- def type_id_of(type_: Any) -> TypeId:
69
- """
70
- Generates a string TypeId for any type.
71
- """
72
- return TypeId.from_type(type_)
73
-
74
-
75
- def is_multi_type(type_: Any) -> bool:
73
+ def _is_multi_type(type_: Any) -> bool:
76
74
  """
77
75
  Discriminates a type to determine whether it is the return type of a multiprovider.
78
76
  """
engin/ext/asgi.py CHANGED
@@ -42,6 +42,7 @@ class ASGIEngin(Engin, ASGIType):
42
42
  except Exception as err:
43
43
  exc = "".join(traceback.format_exception(err))
44
44
  await send({"type": "lifespan.startup.failed", "message": exc})
45
+ raise
45
46
 
46
47
  elif message["type"] == "lifespan.shutdown":
47
48
  await self.stop()
@@ -49,8 +50,8 @@ class ASGIEngin(Engin, ASGIType):
49
50
  await self._asgi_app(scope, receive, send)
50
51
 
51
52
  async def _startup(self) -> None:
53
+ self._asgi_app = await self._assembler.build(self._asgi_type)
52
54
  await self.start()
53
- self._asgi_app = await self._assembler.get(self._asgi_type)
54
55
 
55
56
  def graph(self) -> list[Node]:
56
57
  grapher = DependencyGrapher({**self._providers, **self._multiproviders})
engin/ext/fastapi.py CHANGED
@@ -9,7 +9,7 @@ from fastapi.routing import APIRoute
9
9
  from engin import Assembler, Engin, Entrypoint, Invoke, Option
10
10
  from engin._dependency import Dependency, Supply, _noop
11
11
  from engin._graph import DependencyGrapher, Node
12
- from engin._type_utils import TypeId, type_id_of
12
+ from engin._type_utils import TypeId
13
13
  from engin.ext.asgi import ASGIEngin
14
14
 
15
15
  try:
@@ -33,7 +33,10 @@ def _attach_assembler(app: FastAPI, engin: Engin) -> None:
33
33
 
34
34
 
35
35
  class FastAPIEngin(ASGIEngin):
36
- _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_assembler)]
36
+ _LIB_OPTIONS: ClassVar[list[Option]] = [
37
+ *ASGIEngin._LIB_OPTIONS,
38
+ Invoke(_attach_assembler),
39
+ ]
37
40
  _asgi_type = FastAPI
38
41
 
39
42
  def graph(self) -> list[Node]:
@@ -55,7 +58,7 @@ def Inject(interface: type[T]) -> Depends:
55
58
  assembler: Assembler = conn.app.state.assembler
56
59
  except AttributeError:
57
60
  raise RuntimeError("Assembler is not attached to Application state") from None
58
- return await assembler.get(interface)
61
+ return await assembler.build(interface)
59
62
 
60
63
  dep = Depends(inner)
61
64
  dep.__engin__ = True # type: ignore[attr-defined]
@@ -140,7 +143,8 @@ class APIRouteDependency(Dependency):
140
143
  """
141
144
  Warning: this should never be constructed in application code.
142
145
  """
143
- super().__init__(_noop, wraps.block_name)
146
+ super().__init__(_noop)
147
+ self._block_name = wraps.block_name
144
148
  self._wrapped = wraps
145
149
  self._route = route
146
150
  self._signature = inspect.signature(route.endpoint)
@@ -165,7 +169,7 @@ class APIRouteDependency(Dependency):
165
169
  if parameters[0].name == "self":
166
170
  parameters.pop(0)
167
171
  return [
168
- type_id_of(typing.get_args(param.annotation)[0])
172
+ TypeId.from_type(typing.get_args(param.annotation)[0])
169
173
  for param in parameters
170
174
  if self._is_injected_param(param)
171
175
  ]
@@ -183,3 +187,6 @@ class APIRouteDependency(Dependency):
183
187
  def name(self) -> str:
184
188
  methods = ",".join(self._route.methods)
185
189
  return f"{methods} {self._route.path}"
190
+
191
+ def apply(self, engin: Engin) -> None:
192
+ raise NotImplementedError("APIRouteDependency is not a real dependency")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.13
3
+ Version: 0.0.15
4
4
  Summary: An async-first modular application framework
5
5
  Project-URL: Homepage, https://github.com/invokermain/engin
6
6
  Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
@@ -10,6 +10,8 @@ License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: Application Framework,Dependency Injection
12
12
  Requires-Python: >=3.10
13
+ Provides-Extra: cli
14
+ Requires-Dist: typer>=0.15; extra == 'cli'
13
15
  Description-Content-Type: text/markdown
14
16
 
15
17
  [![codecov](https://codecov.io/gh/invokermain/engin/graph/badge.svg?token=4PJOIMV6IB)](https://codecov.io/gh/invokermain/engin)
@@ -0,0 +1,23 @@
1
+ engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
2
+ engin/_assembler.py,sha256=GpTLW9AmGChnwWWK3SUq5AsxJJ8ukH7yWpemBiH87pw,9294
3
+ engin/_block.py,sha256=Ypl6ffU52dgrHHgCcPokzfRD2-Lbu9b2wYMCgAZIx4g,2578
4
+ engin/_dependency.py,sha256=w-MxF6Ju1Rc2umc7pk3bXTlc65NVIs1VEBj8825WEcg,8328
5
+ engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
6
+ engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
7
+ engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
8
+ engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
9
+ engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
10
+ engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
11
+ engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
12
+ engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ engin/_cli/__init__.py,sha256=lp1KiBpcgk_dZU5V9DjgLPwmp0ja444fwLH2CYCscNc,302
14
+ engin/_cli/_graph.py,sha256=1Kj09BnKh5BTmuM4tqaGICS4KVDGNWT4oGFIrUa9xdU,6230
15
+ engin/_cli/_utils.py,sha256=AQFtLO8qjYRCTQc9A8Z1HVf7eZr8iGWogxbYzsgIkS4,360
16
+ engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ engin/ext/asgi.py,sha256=6V5Aad37MyGzkCtU5TlDrm0o5C04Un_LLvcomxnAmHY,3196
18
+ engin/ext/fastapi.py,sha256=e8UV521Mq9Iqr55CT7_jtd51iaIZjWlAacoqFBXsh-k,6356
19
+ engin-0.0.15.dist-info/METADATA,sha256=qYhQHzJ_YrJEaZ_p4ddZL4OZDOtzWHkQFLimPH_XNDE,2354
20
+ engin-0.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ engin-0.0.15.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
22
+ engin-0.0.15.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
23
+ engin-0.0.15.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ engin = engin._cli:app
engin/scripts/__init__.py DELETED
File without changes
@@ -1,20 +0,0 @@
1
- engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
2
- engin/_assembler.py,sha256=1ODW3HenDlIQLetg0LNEPPbsI6HkFnPU_AHzkR9Zxmc,8844
3
- engin/_block.py,sha256=0QJtqyP5uTFjXsdVGr4ZONLI2LhfzUKmQGnNQWouB3o,2121
4
- engin/_dependency.py,sha256=RWOyGpMFp_5aLq5TP2wLKz0HT5f6CRoLvFyWvVif9jY,6825
5
- engin/_engin.py,sha256=MTE4MkLrK45h0Nv7p5H92Kv5URa1nX246B9Pp1JkM3A,9134
6
- engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
7
- engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
8
- engin/_lifecycle.py,sha256=_jQnGFj4RYXsxMpcXPJQagFOwnoTVh7oSN8oUYoYuW0,3246
9
- engin/_type_utils.py,sha256=EGyKZWuE2ZwuMlSgDhM1znF8giaEET1vcVoQcdGxFGQ,2210
10
- engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- engin/ext/asgi.py,sha256=RUxkG03VTlvI6EG19c1nEJY8FnQw6MQwolfJSFnhUFE,3168
13
- engin/ext/fastapi.py,sha256=GO3AIZNQ69MtzbWuACffx_6Pp34wC5a5Fi_fIAaQvTg,6186
14
- engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- engin/scripts/graph.py,sha256=por62FkzcWx72V2Ha9sIoki-o99fe2Ifm1w-mdoHZIQ,5922
16
- engin-0.0.13.dist-info/METADATA,sha256=dLFQgnZcD2c_xfKK-Z-Mx_f6_Q0kCta-XqNgqrEqRmo,2291
17
- engin-0.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- engin-0.0.13.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
- engin-0.0.13.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
- engin-0.0.13.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- engin-graph = engin.scripts.graph:serve_graph
File without changes