engin 0.2.0a1__py3-none-any.whl → 0.2.1__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/_assembler.py CHANGED
@@ -8,6 +8,8 @@ from inspect import BoundArguments, Signature
8
8
  from types import TracebackType
9
9
  from typing import Any, Generic, TypeVar, cast
10
10
 
11
+ from typing_extensions import Self
12
+
11
13
  from engin._dependency import Dependency, Provide, Supply
12
14
  from engin._type_utils import TypeId
13
15
  from engin.exceptions import NotInScopeError, ProviderError, TypeNotProvidedError
@@ -76,6 +78,34 @@ class Assembler:
76
78
  else:
77
79
  self._multiproviders[type_id].append(provider)
78
80
 
81
+ @classmethod
82
+ def from_mapped_providers(
83
+ cls,
84
+ providers: dict[TypeId, Provide[Any]],
85
+ multiproviders: dict[TypeId, list[Provide[list[Any]]]],
86
+ ) -> Self:
87
+ """
88
+ Create an Assembler from pre-mapped providers.
89
+
90
+ This method is only exposed for performance reasons in the case that Providers
91
+ have already been mapped, it is recommended to use the `__init__` method if this
92
+ is no the case.
93
+
94
+ Args:
95
+ providers: a dictionary of Providers with the Provider's `return_type_id` as
96
+ the key.
97
+ multiproviders: a dictionary of list of Providers with the Provider's
98
+ `return_type_id` as key. All Providers in the given list must be for the
99
+ related `return_type_id`.
100
+
101
+ Returns:
102
+ An Assembler instance.
103
+ """
104
+ assembler = cls(tuple()) # noqa: C408
105
+ assembler._providers = providers
106
+ assembler._multiproviders = multiproviders
107
+ return assembler
108
+
79
109
  @property
80
110
  def providers(self) -> Sequence[Provide[Any]]:
81
111
  multi_providers = [p for multi in self._multiproviders.values() for p in multi]
engin/_block.py CHANGED
@@ -10,6 +10,8 @@ from engin.exceptions import InvalidBlockError
10
10
  if TYPE_CHECKING:
11
11
  from engin._engin import Engin
12
12
 
13
+ _BUILTIN_CLASS_FUNCTIONS = ("__annotate_func__",)
14
+
13
15
 
14
16
  def provide(
15
17
  func_: Func | None = None, *, scope: str | None = None, override: bool = False
@@ -84,6 +86,8 @@ class Block:
84
86
  @classmethod
85
87
  def _method_options(cls) -> Iterable[Provide | Invoke]:
86
88
  for name, method in inspect.getmembers(cls, inspect.isfunction):
89
+ if name in _BUILTIN_CLASS_FUNCTIONS:
90
+ continue
87
91
  if option := getattr(method, "_opt", None):
88
92
  if not isinstance(option, Provide | Invoke):
89
93
  raise InvalidBlockError(
engin/_cli/_common.py CHANGED
@@ -1,11 +1,11 @@
1
1
  import importlib
2
2
  import sys
3
3
  from pathlib import Path
4
- from typing import Never
5
4
 
6
5
  import typer
7
6
  from rich import print
8
7
  from rich.panel import Panel
8
+ from typing_extensions import Never
9
9
 
10
10
  from engin import Engin
11
11
 
engin/_dependency.py CHANGED
@@ -184,7 +184,9 @@ class Provide(Dependency[Any, T]):
184
184
  if self._explicit_type is not None:
185
185
  self._signature = self._signature.replace(return_annotation=self._explicit_type)
186
186
 
187
- self._is_multi = typing.get_origin(self._return_type) is list
187
+ self._is_multi = (
188
+ typing.get_origin(self._return_type) is list or self._return_type is list
189
+ )
188
190
 
189
191
  # Validate that the provider does to depend on its own output value, as this will
190
192
  # cause a recursion error and is undefined behaviour wise.
@@ -198,9 +200,11 @@ class Provide(Dependency[Any, T]):
198
200
  if self._is_multi:
199
201
  args = typing.get_args(self._return_type)
200
202
  if len(args) != 1:
201
- raise ValueError(
202
- f"A multiprovider must be of the form list[X], not '{self._return_type}'"
203
+ msg = (
204
+ "A multiprovider must be of the form list[X], not "
205
+ f"'{self._return_type_id}'"
203
206
  )
207
+ raise ValueError(msg)
204
208
 
205
209
  @property
206
210
  def return_type(self) -> type[T]:
engin/_engin.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import asyncio
2
2
  import logging
3
- import os
4
3
  import signal
4
+ import sys
5
5
  from asyncio import Event
6
6
  from collections import defaultdict
7
7
  from contextlib import AsyncExitStack
8
8
  from enum import Enum
9
9
  from itertools import chain
10
10
  from types import FrameType
11
- from typing import ClassVar
11
+ from typing import TYPE_CHECKING, ClassVar
12
12
 
13
13
  from anyio import create_task_group, open_signal_receiver
14
14
 
@@ -18,10 +18,12 @@ from engin._graph import DependencyGrapher, Node
18
18
  from engin._lifecycle import Lifecycle
19
19
  from engin._option import Option
20
20
  from engin._supervisor import Supervisor
21
- from engin._type_utils import TypeId
22
21
  from engin.exceptions import EnginError
23
22
 
24
- _OS_IS_WINDOWS = os.name == "nt"
23
+ if TYPE_CHECKING:
24
+ from engin._type_utils import TypeId
25
+
26
+ _OS_IS_WINDOWS = sys.platform == "win32"
25
27
  LOG = logging.getLogger("engin")
26
28
 
27
29
 
@@ -107,12 +109,9 @@ class Engin:
107
109
  self._stop_requested_event = Event()
108
110
  self._stop_complete_event = Event()
109
111
  self._exit_stack = AsyncExitStack()
110
- self._assembler = Assembler([])
111
112
  self._async_context_run_task: asyncio.Task | None = None
112
113
 
113
- self._providers: dict[TypeId, Provide] = {
114
- TypeId.from_type(Assembler): Supply(self._assembler),
115
- }
114
+ self._providers: dict[TypeId, Provide] = {}
116
115
  self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
117
116
  self._invocations: list[Invoke] = []
118
117
 
@@ -120,10 +119,12 @@ class Engin:
120
119
  for option in chain(self._LIB_OPTIONS, options):
121
120
  option.apply(self)
122
121
 
123
- multi_providers = [p for multi in self._multiproviders.values() for p in multi]
124
-
125
- for provider in chain(self._providers.values(), multi_providers):
126
- self._assembler.add(provider)
122
+ # initialise Assembler
123
+ self._assembler = Assembler.from_mapped_providers(
124
+ providers=self._providers,
125
+ multiproviders=self._multiproviders,
126
+ )
127
+ self._assembler.add(Supply(self._assembler))
127
128
 
128
129
  @property
129
130
  def assembler(self) -> Assembler:
@@ -253,29 +254,37 @@ class Engin:
253
254
  async def _stop_engin_on_signal(stop_requested_event: Event) -> None:
254
255
  """
255
256
  A task that waits for a stop signal (SIGINT/SIGTERM) and notifies the given event.
257
+
258
+ On unix-like systems we can use asyncio's `loop.add_signal_handler` method but this
259
+ does not work on Windows as `signal.set_wakeup_fd` is not supported.
260
+
261
+ Therefore on Windows we fallback to using `signal.signal` directly.
256
262
  """
257
263
  if not _OS_IS_WINDOWS:
258
- with open_signal_receiver(signal.SIGINT, signal.SIGTERM) as recieved_signals:
259
- async for signum in recieved_signals:
260
- LOG.debug(f"received {signum.name} signal")
264
+ with open_signal_receiver(signal.SIGINT, signal.SIGTERM) as received_signals:
265
+ async for signum in received_signals:
266
+ LOG.debug(f"received signal: {signum.name}")
261
267
  stop_requested_event.set()
268
+ break
262
269
  else:
263
- should_stop = False
264
270
 
265
- # windows does not support signal_handlers, so this is the workaround
266
271
  def ctrlc_handler(sig: int, frame: FrameType | None) -> None:
267
- LOG.debug(f"received {signal.SIGINT.name} signal")
268
- nonlocal should_stop
269
- if should_stop:
272
+ LOG.debug(f"received signal: {signal.Signals(sig).name}")
273
+ if stop_requested_event.is_set():
270
274
  raise KeyboardInterrupt("Forced keyboard interrupt")
271
- should_stop = True
275
+ else:
276
+ stop_requested_event.set()
272
277
 
273
- signal.signal(signal.SIGINT, ctrlc_handler)
278
+ previous_signal_handler_sigint = signal.signal(signal.SIGINT, ctrlc_handler)
274
279
 
275
- while not should_stop:
276
- # In case engin is stopped via external `stop` call.
277
- if stop_requested_event.is_set():
278
- return
279
- await asyncio.sleep(0.1)
280
+ # technically not needed due to the _OS_IS_WINDOWS checks but keeps mypy happy
281
+ if hasattr(signal, "SIGBREAK"):
282
+ previous_signal_handler_sigbreak = signal.signal(signal.SIGBREAK, ctrlc_handler)
280
283
 
281
- stop_requested_event.set()
284
+ try:
285
+ await stop_requested_event.wait()
286
+ finally:
287
+ # restore orginal signal handlers
288
+ signal.signal(signal.SIGINT, previous_signal_handler_sigint)
289
+ if hasattr(signal, "SIGBREAK"):
290
+ signal.signal(signal.SIGBREAK, previous_signal_handler_sigbreak)
engin/_supervisor.py CHANGED
@@ -5,10 +5,11 @@ from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from types import TracebackType
8
- from typing import TypeAlias, assert_never
8
+ from typing import TypeAlias
9
9
 
10
10
  import anyio
11
11
  from anyio import get_cancelled_exc_class
12
+ from typing_extensions import assert_never
12
13
 
13
14
  if typing.TYPE_CHECKING:
14
15
  from anyio.abc import TaskGroup
@@ -1,20 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.2.0a1
4
- Summary: An async-first modular application framework
5
- Project-URL: Homepage, https://github.com/invokermain/engin
6
- Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
7
- Project-URL: Repository, https://github.com/invokermain/engin.git
8
- Project-URL: Changelog, https://github.com/invokermain/engin/blob/main/CHANGELOG.md
3
+ Version: 0.2.1
4
+ Summary: A dependency-injection powered application framework
5
+ Keywords: Dependency Injection,Application Framework
9
6
  License-Expression: MIT
10
- License-File: LICENSE
11
- Keywords: Application Framework,Dependency Injection
12
- Requires-Python: >=3.10
13
7
  Requires-Dist: anyio>=4
14
8
  Requires-Dist: exceptiongroup>=1
9
+ Requires-Dist: typing-extensions>=4
10
+ Requires-Dist: typer>=0.15 ; extra == 'cli'
11
+ Requires-Dist: tomli>=2.0 ; python_full_version < '3.11' and extra == 'cli'
12
+ Requires-Python: >=3.10
13
+ Project-URL: Changelog, https://github.com/invokermain/engin/blob/main/CHANGELOG.md
14
+ Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
15
+ Project-URL: Homepage, https://github.com/invokermain/engin
16
+ Project-URL: Repository, https://github.com/invokermain/engin.git
15
17
  Provides-Extra: cli
16
- Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'cli'
17
- Requires-Dist: typer>=0.15; extra == 'cli'
18
18
  Description-Content-Type: text/markdown
19
19
 
20
20
  # Engin 🏎️
@@ -29,11 +29,11 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  ---
31
31
 
32
- Engin is a lightweight application framework powered by dependency injection, it helps
33
- you build and maintain large monoliths and many microservices.
32
+ Engin is a lightweight application framework powered by dependency injection. It helps
33
+ you build and maintain everything from large monoliths to hundreds of microservices.
34
34
 
35
35
 
36
- ## Feature
36
+ ## Features
37
37
 
38
38
  The Engin framework gives you:
39
39
 
@@ -120,3 +120,8 @@ Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/ube
120
120
  and the [Injector framework for Python](https://github.com/python-injector/injector).
121
121
 
122
122
  They are both great projects, go check them out.
123
+
124
+ ## Benchmarks
125
+
126
+ Automated benchmarks for the Engin framework can be viewed
127
+ [here](https://invokermain.github.io/engin/dev/bench/).
@@ -1,27 +1,26 @@
1
1
  engin/__init__.py,sha256=O0vS570kZFBq7Kwy4FgeJFIhfo4aIg5mv_Z_9vAQRio,577
2
- engin/_assembler.py,sha256=0uXgtcO5M3EHg0I-TQK9y7LOzfxkLFmKia-zLyVHaxA,11178
3
- engin/_block.py,sha256=IacP4PoJKRhSQCbQSdoyCtmu362a4vj6qoUQKyaJwzI,3062
4
- engin/_dependency.py,sha256=Ie-z0obe7Ut6TF1O7BjP3Z7i216ufKqSeT_D82krdAc,9615
5
- engin/_engin.py,sha256=1bgZmdmRlwHIZ9pz5rysYgvFu5s8e0gzlc9iq-Of97c,9651
2
+ engin/_assembler.py,sha256=Afi9i5wcbhbzqhJlCB_oa9LcR6RMYaQoR40JfSfnuGo,12235
3
+ engin/_block.py,sha256=Rwcpg9j_-tVd0lNJJYZOBgFi0cNKOJ_04MZzMIk9hAc,3187
4
+ engin/_cli/__init__.py,sha256=Ixk3NoZeIN8Bj53I625uqJdLyyT9Gpbe_4GtNy-KQwM,636
5
+ engin/_cli/_check.py,sha256=w9GA9RmCgSflvSU7EQqXKiOCqgrZB-pLS_UoJOqV56E,1666
6
+ engin/_cli/_common.py,sha256=dY9YEqoo1yNIZNPptM-GZjU1LaPcnkSqmIUcWkSii54,3293
7
+ engin/_cli/_graph.html,sha256=5Dw5eyhsrU8KrpdhGn1mxo5kTUTJLMSzT-MBKvSv13g,29073
8
+ engin/_cli/_graph.py,sha256=MsxsNpL1v0v1AUT57ZS97l1diwacqRaPdVBObHHIGJE,6753
9
+ engin/_cli/_inspect.py,sha256=_uzldpHA51IX4srpUGzL4lZNiepqucsO9M3Zo83XBBM,3159
10
+ engin/_dependency.py,sha256=Z2eS-w_5TM_JgnOtrzCbwtQT6yGmkg-o0-zG02Nk1XU,9722
11
+ engin/_engin.py,sha256=ip4kazNp7pz7tLLi2jFwFJ7oCLLC9bj0p3sWU0EE0dA,10129
6
12
  engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
7
13
  engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
8
14
  engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
9
15
  engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
10
- engin/_supervisor.py,sha256=WGio-mfPHo2Wdfqo5ZB66WAoAU2ey0urwM0Dl4vRUTA,4824
16
+ engin/_supervisor.py,sha256=E7bGOGP11qIcbueMUkn-felJdP_bLMJmTBrenQtNdTU,4853
11
17
  engin/_type_utils.py,sha256=H3Tl-kJr2wY2RhaTXP9GrMqa2RsXMijHbjHKe1AxGmc,2276
12
18
  engin/exceptions.py,sha256=lSMOJI4Yl-VIM0yDzFWbPhC0mQm4f0WvGULr9SldIaY,2353
13
- engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- engin/_cli/__init__.py,sha256=Ixk3NoZeIN8Bj53I625uqJdLyyT9Gpbe_4GtNy-KQwM,636
15
- engin/_cli/_check.py,sha256=w9GA9RmCgSflvSU7EQqXKiOCqgrZB-pLS_UoJOqV56E,1666
16
- engin/_cli/_common.py,sha256=6tyjxAkROCViw0LOFdx-X1U-iSXKyeW5CoE9UxWRybI,3282
17
- engin/_cli/_graph.html,sha256=5Dw5eyhsrU8KrpdhGn1mxo5kTUTJLMSzT-MBKvSv13g,29073
18
- engin/_cli/_graph.py,sha256=MsxsNpL1v0v1AUT57ZS97l1diwacqRaPdVBObHHIGJE,6753
19
- engin/_cli/_inspect.py,sha256=_uzldpHA51IX4srpUGzL4lZNiepqucsO9M3Zo83XBBM,3159
20
19
  engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
20
  engin/extensions/asgi.py,sha256=7vQFaVs1jxq1KbhHGN8k7x2UFab6SPUq2_hXfX6HiXU,3329
22
21
  engin/extensions/fastapi.py,sha256=7N6i-eZUEZRPo7kcvjS7kbRSY5QAPyKJXSeongSQ-OA,6371
23
- engin-0.2.0a1.dist-info/METADATA,sha256=OntxY2pdrpJuvpqb-zvj2SJwpohHmEjbjLmqc7w1zqE,3949
24
- engin-0.2.0a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- engin-0.2.0a1.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
26
- engin-0.2.0a1.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
27
- engin-0.2.0a1.dist-info/RECORD,,
22
+ engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ engin-0.2.1.dist-info/WHEEL,sha256=ZbtZh9LqsQoZs-WmwRO6z-tavdkb5LzNxvrOv2F_OXE,78
24
+ engin-0.2.1.dist-info/entry_points.txt,sha256=MpK_1W7JQVbtK2hJ0ehBWQG2O6C5m_HVIAhC_LpGmv0,42
25
+ engin-0.2.1.dist-info/METADATA,sha256=l92ASKlLENk4-Ng1yVV-Nw8Fq83q-DvcD8VtmRoFd_0,4128
26
+ engin-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  engin = engin._cli:app
3
+
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any
@@ -1,21 +0,0 @@
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.