engin 0.0.19__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
engin/_supervisor.py ADDED
@@ -0,0 +1,137 @@
1
+ import inspect
2
+ import logging
3
+ import typing
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from types import TracebackType
8
+ from typing import TypeAlias, assert_never
9
+
10
+ import anyio
11
+ from anyio import get_cancelled_exc_class
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from anyio.abc import TaskGroup
15
+
16
+ LOG = logging.getLogger("engin")
17
+
18
+ TaskFactory: TypeAlias = Callable[[], Awaitable[None]]
19
+
20
+
21
+ class OnException(Enum):
22
+ SHUTDOWN = 0
23
+ """
24
+ Cancel all other supervised tasks and shutdown the Engin.
25
+ """
26
+
27
+ RETRY = 1
28
+ """
29
+ Retry the task.
30
+ """
31
+
32
+ IGNORE = 2
33
+ """
34
+ The task will be not be retried and the engin will not be stopped, other tasks will
35
+ continue to run.
36
+ """
37
+
38
+
39
+ @dataclass(kw_only=True, slots=True, eq=False)
40
+ class _SupervisorTask:
41
+ """
42
+ Attributes:
43
+ - factory: a coroutine function that can create the task.
44
+ - on_exception: determines the behaviour when task raises an exception.
45
+ - complete: will be set to true if task stops for any reason except cancellation.
46
+ - last_exception: the last exception raised by the task.
47
+ """
48
+
49
+ factory: TaskFactory
50
+ on_exception: OnException
51
+ complete: bool = False
52
+ last_exception: Exception | None = None
53
+
54
+ async def __call__(self) -> None:
55
+ # loop to allow for restarting erroring tasks
56
+ while True:
57
+ try:
58
+ await self.factory()
59
+ self.complete = True
60
+ return
61
+ except get_cancelled_exc_class() as err:
62
+ LOG.debug(f"supervised task '{self.name}' was cancelled", exc_info=err)
63
+ raise
64
+ except Exception as err:
65
+ self.last_exception = err
66
+ if self.on_exception == OnException.IGNORE:
67
+ LOG.warning(
68
+ f"supervisor task '{self.name}' raised {type(err).__name__} "
69
+ "which will be ignored",
70
+ exc_info=err,
71
+ )
72
+ self.complete = True
73
+ return
74
+ if self.on_exception == OnException.RETRY:
75
+ LOG.warning(
76
+ f"supervisor task '{self.name}' raised {type(err).__name__} "
77
+ "which will be retried",
78
+ exc_info=err,
79
+ )
80
+ continue
81
+ if self.on_exception == OnException.SHUTDOWN:
82
+ LOG.error(
83
+ f"supervisor task '{self.name}' raised {type(err).__name__}, "
84
+ "starting shutdown",
85
+ exc_info=err,
86
+ )
87
+ self.complete = True
88
+ raise get_cancelled_exc_class() from None
89
+ assert_never(self.on_exception)
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ factory = self.factory
94
+ if inspect.ismethod(factory):
95
+ return f"{factory.__self__.__class__.__name__}.{factory.__func__.__name__}"
96
+ if inspect.isclass(factory):
97
+ return type(factory).__name__
98
+ if inspect.isfunction(factory):
99
+ return factory.__name__
100
+ return str(factory)
101
+
102
+
103
+ class Supervisor:
104
+ def __init__(self) -> None:
105
+ self._tasks: list[_SupervisorTask] = []
106
+ self._task_group: TaskGroup | None = None
107
+
108
+ def supervise(
109
+ self, func: TaskFactory, *, on_exception: OnException = OnException.SHUTDOWN
110
+ ) -> None:
111
+ self._tasks.append(_SupervisorTask(factory=func, on_exception=on_exception))
112
+
113
+ @property
114
+ def empty(self) -> bool:
115
+ return not self._tasks
116
+
117
+ async def __aenter__(self) -> None:
118
+ if not self._tasks:
119
+ return
120
+
121
+ self._task_group = await anyio.create_task_group().__aenter__()
122
+
123
+ for task in self._tasks:
124
+ LOG.info(f"supervising task: {task.name}")
125
+ self._task_group.start_soon(task, name=task.name)
126
+
127
+ async def __aexit__(
128
+ self,
129
+ exc_type: type[BaseException] | None,
130
+ exc_value: BaseException | None,
131
+ traceback: TracebackType | None,
132
+ /,
133
+ ) -> None:
134
+ if self._task_group:
135
+ if not self._task_group.cancel_scope.cancel_called:
136
+ self._task_group.cancel_scope.cancel()
137
+ await self._task_group.__aexit__(exc_type, exc_value, traceback)
engin/_type_utils.py CHANGED
@@ -33,8 +33,8 @@ class TypeId:
33
33
  return TypeId(type=type_, multi=False)
34
34
 
35
35
  def __str__(self) -> str:
36
- module = self.type.__module__
37
- out = f"{module}." if module not in _implict_modules else ""
36
+ module = getattr(self.type, "__module__", None)
37
+ out = f"{module}." if module and module not in _implict_modules else ""
38
38
  out += _args_to_str(self.type)
39
39
  if self.multi:
40
40
  out += "[]"
engin/exceptions.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from typing import TYPE_CHECKING, Any
2
2
 
3
3
  from engin._dependency import Provide
4
+ from engin._type_utils import TypeId
4
5
 
5
6
  if TYPE_CHECKING:
6
7
  from engin._block import Block
@@ -12,12 +13,6 @@ class EnginError(Exception):
12
13
  """
13
14
 
14
15
 
15
- class AssemblerError(EnginError):
16
- """
17
- Base class for all custom exceptions raised by the Assembler.
18
- """
19
-
20
-
21
16
  class InvalidBlockError(EnginError):
22
17
  """
23
18
  Raised when an invalid block is instantiated.
@@ -32,6 +27,25 @@ class InvalidBlockError(EnginError):
32
27
  return self.message
33
28
 
34
29
 
30
+ class AssemblerError(EnginError):
31
+ """
32
+ Base class for all custom exceptions raised by the Assembler.
33
+ """
34
+
35
+
36
+ class TypeNotProvidedError(AssemblerError):
37
+ """
38
+ Raised when the Assembler cannot assemble a type due to a missing Provider.
39
+ """
40
+
41
+ def __init__(self, type_id: TypeId) -> None:
42
+ self.type_id = type_id
43
+ self.message = f"no provider found for '{type_id}'"
44
+
45
+ def __str__(self) -> str:
46
+ return self.message
47
+
48
+
35
49
  class ProviderError(AssemblerError):
36
50
  """
37
51
  Raised when a Provider errors during Assembly.
@@ -77,4 +91,5 @@ __all__ = [
77
91
  "InvalidBlockError",
78
92
  "NotInScopeError",
79
93
  "ProviderError",
94
+ "TypeNotProvidedError",
80
95
  ]
engin/extensions/asgi.py CHANGED
@@ -21,6 +21,8 @@ class ASGIType(Protocol):
21
21
 
22
22
 
23
23
  class ASGIEngin(Engin, ASGIType):
24
+ _STOP_ON_SINGAL = False # web server implementation is responsible for this
25
+
24
26
  _asgi_type: ClassVar[type[ASGIType]] = ASGIType # type: ignore[type-abstract]
25
27
  _asgi_app: ASGIType
26
28
 
@@ -24,12 +24,12 @@ except ImportError as err:
24
24
  __all__ = ["APIRouteDependency", "FastAPIEngin", "Inject"]
25
25
 
26
26
 
27
- def _attach_assembler(app: FastAPI, engin: Engin) -> None:
27
+ def _attach_assembler(app: FastAPI, assembler: Assembler) -> None:
28
28
  """
29
29
  An invocation that attaches the Engin's Assembler to the FastAPI application, enabling
30
30
  the Inject marker.
31
31
  """
32
- app.state.assembler = engin.assembler
32
+ app.state.assembler = assembler
33
33
 
34
34
 
35
35
  class FastAPIEngin(ASGIEngin):
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: engin
3
+ Version: 0.1.0
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
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: Application Framework,Dependency Injection
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: anyio>=4
14
+ Requires-Dist: exceptiongroup>=1
15
+ 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
+ Description-Content-Type: text/markdown
19
+
20
+ # Engin 🏎️
21
+
22
+ [![codecov](https://codecov.io/gh/invokermain/engin/graph/badge.svg?token=4PJOIMV6IB)](https://codecov.io/gh/invokermain/engin)
23
+
24
+ ---
25
+
26
+ **Documentation**: [https://engin.readthedocs.io/](https://engin.readthedocs.io/)
27
+
28
+ **Source Code**: [https://github.com/invokermain/engin](https://github.com/invokermain/engin)
29
+
30
+ ---
31
+
32
+ Engin is a lightweight application framework powered by dependency injection, it helps
33
+ you build and maintain large monoliths and many microservices.
34
+
35
+
36
+ ## Feature
37
+
38
+ The Engin framework gives you:
39
+
40
+ - A fully-featured dependency injection system.
41
+ - A robust application runtime with lifecycle hooks and supervised background tasks.
42
+ - Zero boilerplate code reuse across applications.
43
+ - Integrations for other frameworks such as FastAPI.
44
+ - Full async support.
45
+ - CLI commands to aid local development.
46
+
47
+
48
+ ## Installation
49
+
50
+ Engin is available on PyPI, install it using your favourite dependency manager:
51
+
52
+ - `pip install engin`
53
+ - `poetry add engin`
54
+ - `uv add engin`
55
+
56
+ ## Example
57
+
58
+ A small example which shows some of the features of Engin. This application
59
+ makes 3 http requests and shuts itself down.
60
+
61
+ ```python
62
+ import asyncio
63
+ from httpx import AsyncClient
64
+ from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
65
+
66
+
67
+ def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
68
+ # create our http client
69
+ client = AsyncClient()
70
+ # this will open and close the AsyncClient as part of the application's lifecycle
71
+ lifecycle.append(client)
72
+ return client
73
+
74
+
75
+ async def main(
76
+ httpx_client: AsyncClient,
77
+ supervisor: Supervisor,
78
+ ) -> None:
79
+ async def http_requests_task():
80
+ # simulate a background task
81
+ for x in range(3):
82
+ await httpx_client.get("https://httpbin.org/get")
83
+ await asyncio.sleep(1.0)
84
+ # raise an error to shutdown the application, normally you wouldn't do this!
85
+ raise RuntimeError("Forcing shutdown")
86
+
87
+ # supervise the http requests as part of the application's lifecycle
88
+ supervisor.supervise(http_requests_task, on_exception=OnException.SHUTDOWN)
89
+
90
+
91
+ # define our modular application
92
+ engin = Engin(Provide(httpx_client_factory), Invoke(main))
93
+
94
+ # run it!
95
+ asyncio.run(engin.run())
96
+ ```
97
+
98
+ With logs enabled this will output:
99
+
100
+ ```shell
101
+ INFO:engin:starting engin
102
+ INFO:engin:startup complete
103
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
104
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
105
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
106
+ ERROR:engin:supervisor task 'http_requests_task' raised RuntimeError, starting shutdown
107
+ Traceback (most recent call last):
108
+ File "C:\dev\python\engin\src\engin\_supervisor.py", line 58, in __call__
109
+ await self.factory()
110
+ File "C:\dev\python\engin\readme_example.py", line 29, in http_requests_task
111
+ raise RuntimeError("Forcing shutdown")
112
+ RuntimeError: Forcing shutdown
113
+ INFO:engin:stopping engin
114
+ INFO:engin:shutdown complete
115
+ ```
116
+
117
+ ## Inspiration
118
+
119
+ Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
120
+ and the [Injector framework for Python](https://github.com/python-injector/injector).
121
+
122
+ They are both great projects, go check them out.
@@ -0,0 +1,27 @@
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=xINk3sudxzsTmkUkNAKQwzBc0G0DfhpnrZli4z3ALBY,9459
5
+ engin/_engin.py,sha256=1bgZmdmRlwHIZ9pz5rysYgvFu5s8e0gzlc9iq-Of97c,9651
6
+ engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
7
+ engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
8
+ engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
9
+ engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
10
+ engin/_supervisor.py,sha256=nRz8GMFBJ5dAzMBO94HL_U1W2c89ZXg_LPeoD4H-s-4,4397
11
+ engin/_type_utils.py,sha256=H3Tl-kJr2wY2RhaTXP9GrMqa2RsXMijHbjHKe1AxGmc,2276
12
+ 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
+ engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ engin/extensions/asgi.py,sha256=7vQFaVs1jxq1KbhHGN8k7x2UFab6SPUq2_hXfX6HiXU,3329
22
+ engin/extensions/fastapi.py,sha256=7N6i-eZUEZRPo7kcvjS7kbRSY5QAPyKJXSeongSQ-OA,6371
23
+ engin-0.1.0.dist-info/METADATA,sha256=Eny-piAImwQB6Ij3djngjTqvg0ILyGlbr9aprE28bNE,3947
24
+ engin-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ engin-0.1.0.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
26
+ engin-0.1.0.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
27
+ engin-0.1.0.dist-info/RECORD,,
@@ -1,71 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: engin
3
- Version: 0.0.19
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
9
- License-Expression: MIT
10
- License-File: LICENSE
11
- Keywords: Application Framework,Dependency Injection
12
- Requires-Python: >=3.10
13
- Provides-Extra: cli
14
- Requires-Dist: typer>=0.15; extra == 'cli'
15
- Description-Content-Type: text/markdown
16
-
17
- [![codecov](https://codecov.io/gh/invokermain/engin/graph/badge.svg?token=4PJOIMV6IB)](https://codecov.io/gh/invokermain/engin)
18
-
19
- # Engin 🏎️
20
-
21
- Engin is a zero-dependency application framework for modern Python.
22
-
23
- **Documentation**: https://engin.readthedocs.io/
24
-
25
- ## Features ✨
26
-
27
- - **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
28
- powered by type hints.
29
- - **Lifecycle Management** - Engin provides a simple & portable approach for attaching
30
- startup and shutdown tasks to the application's lifecycle.
31
- - **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
32
- packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
33
- maintaining many services across your organisation.
34
- - **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
35
- provide their own Dependency Injection, for example FastAPI, allowing you to integrate
36
- Engin into existing code bases incrementally.
37
- - **Async Native**: Engin is an async framework, meaning first class support for async
38
- dependencies. However Engin will happily run synchronous code as well.
39
-
40
- ## Installation
41
-
42
- Engin is available on PyPI, install using your favourite dependency manager:
43
-
44
- - **pip**:`pip install engin`
45
- - **poetry**: `poetry add engin`
46
- - **uv**: `uv add engin`
47
-
48
- ## Getting Started
49
-
50
- A minimal example:
51
-
52
- ```python
53
- import asyncio
54
-
55
- from httpx import AsyncClient
56
-
57
- from engin import Engin, Invoke, Provide
58
-
59
-
60
- def httpx_client() -> AsyncClient:
61
- return AsyncClient()
62
-
63
-
64
- async def main(http_client: AsyncClient) -> None:
65
- print(await http_client.get("https://httpbin.org/get"))
66
-
67
- engin = Engin(Provide(httpx_client), Invoke(main))
68
-
69
- asyncio.run(engin.run())
70
- ```
71
-
@@ -1,25 +0,0 @@
1
- engin/__init__.py,sha256=A8TE_ci7idoR683535YoBrWZbYTgXXS-q7Y2y51nZ5M,486
2
- engin/_assembler.py,sha256=-ENSrXPMWacionIYrTSQO7th9DDBOPyAT8ybPbBRtQw,11318
3
- engin/_block.py,sha256=IacP4PoJKRhSQCbQSdoyCtmu362a4vj6qoUQKyaJwzI,3062
4
- engin/_dependency.py,sha256=Nfq6L92LN4X53QpiMCIF3MjmWfuntYVOnZmmoPYYJEw,9165
5
- engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
6
- engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
7
- engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
8
- engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
9
- engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
10
- engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
11
- engin/exceptions.py,sha256=-VPwPReZb9YEIkrWMR9TW2K5HEwmHHgEO7QWH6wfV8c,1946
12
- engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- engin/_cli/__init__.py,sha256=koD5WTkZXb8QQIiVU5bJiSR1wwPGb5rv2iwd-v-BA7A,564
14
- engin/_cli/_common.py,sha256=zMYb1Bs1yUuR3qf3r6WuVozYzDwHJvTVthVbTQfTF9w,1261
15
- engin/_cli/_graph.html,sha256=rR5dnDKoz7KtSff0ERCi2UKuoH_Z03MRYiXI_W03G5k,2430
16
- engin/_cli/_graph.py,sha256=HMC91nWvTOr6_czPBNx1RU55Ib3qesJRCmbnL2DsdDk,4659
17
- engin/_cli/_inspect.py,sha256=0jm25d4wcbXVNJkyaeECSKY-irsxd-EIYBH1GDW_Yjc,3163
18
- engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- engin/extensions/asgi.py,sha256=d5Z6gtMVWDZdAlvrTaMt987sKyiq__A0X4gJQ7IETmA,3247
20
- engin/extensions/fastapi.py,sha256=e8F4L_nZ9dU9j8mb9lXKwJG6CTu5aIk4N5faRj4EyUA,6369
21
- engin-0.0.19.dist-info/METADATA,sha256=Rb1VPxLjnzVxhnq8Llie5XRxpI0GANR-dK31BEcPAqg,2354
22
- engin-0.0.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- engin-0.0.19.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
24
- engin-0.0.19.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
25
- engin-0.0.19.dist-info/RECORD,,
File without changes