engin 0.0.2__py3-none-any.whl → 0.0.4__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 +2 -2
- engin/_block.py +6 -6
- engin/_dependency.py +14 -16
- engin/_engin.py +128 -13
- engin/_lifecycle.py +30 -13
- engin/ext/asgi.py +7 -7
- engin/ext/fastapi.py +5 -9
- engin-0.0.4.dist-info/METADATA +61 -0
- engin-0.0.4.dist-info/RECORD +16 -0
- engin-0.0.2.dist-info/METADATA +0 -56
- engin-0.0.2.dist-info/RECORD +0 -16
- {engin-0.0.2.dist-info → engin-0.0.4.dist-info}/WHEEL +0 -0
- {engin-0.0.2.dist-info → engin-0.0.4.dist-info}/licenses/LICENSE +0 -0
engin/_assembler.py
CHANGED
@@ -4,7 +4,7 @@ from collections import defaultdict
|
|
4
4
|
from collections.abc import Collection, Iterable
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from inspect import BoundArguments, Signature
|
7
|
-
from typing import Any, Generic, TypeVar
|
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 AssemblyError
|
@@ -113,7 +113,7 @@ class Assembler:
|
|
113
113
|
async def get(self, type_: type[T]) -> T:
|
114
114
|
type_id = type_id_of(type_)
|
115
115
|
if type_id in self._dependencies:
|
116
|
-
return self._dependencies[type_id]
|
116
|
+
return cast(T, self._dependencies[type_id])
|
117
117
|
if type_id.multi:
|
118
118
|
out = []
|
119
119
|
for provider in self._multiproviders[type_id]:
|
engin/_block.py
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
import inspect
|
2
2
|
from collections.abc import Iterable, Iterator
|
3
|
+
from typing import ClassVar
|
3
4
|
|
4
5
|
from engin._dependency import Func, Invoke, Provide
|
5
6
|
|
6
7
|
|
7
8
|
def provide(func: Func) -> Func:
|
8
|
-
func._opt = Provide(func) # type: ignore[
|
9
|
+
func._opt = Provide(func) # type: ignore[attr-defined]
|
9
10
|
return func
|
10
11
|
|
11
12
|
|
12
13
|
def invoke(func: Func) -> Func:
|
13
|
-
func._opt = Invoke(func) # type: ignore[
|
14
|
+
func._opt = Invoke(func) # type: ignore[attr-defined]
|
14
15
|
return func
|
15
16
|
|
16
17
|
|
17
18
|
class Block(Iterable[Provide | Invoke]):
|
18
|
-
|
19
|
-
_options: list[Provide | Invoke]
|
19
|
+
options: ClassVar[list[Provide | Invoke]] = []
|
20
20
|
|
21
21
|
def __init__(self, /, block_name: str | None = None) -> None:
|
22
|
-
self._options: list[Provide | Invoke] = []
|
22
|
+
self._options: list[Provide | Invoke] = self.options[:]
|
23
23
|
self._name = block_name or f"{type(self).__name__}"
|
24
24
|
for _, method in inspect.getmembers(self):
|
25
25
|
if opt := getattr(method, "_opt", None):
|
26
|
-
if not isinstance(opt,
|
26
|
+
if not isinstance(opt, Provide | Invoke):
|
27
27
|
raise RuntimeError("Block option is not an instance of Provide or Invoke")
|
28
28
|
opt.set_block_name(self._name)
|
29
29
|
self._options.append(opt)
|
engin/_dependency.py
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
import inspect
|
2
2
|
import typing
|
3
3
|
from abc import ABC
|
4
|
+
from collections.abc import Awaitable, Callable
|
4
5
|
from inspect import Parameter, Signature, isclass, iscoroutinefunction
|
5
6
|
from typing import (
|
6
7
|
Any,
|
7
|
-
Awaitable,
|
8
|
-
Callable,
|
9
8
|
Generic,
|
10
9
|
ParamSpec,
|
11
|
-
Type,
|
12
10
|
TypeAlias,
|
13
11
|
TypeVar,
|
14
12
|
cast,
|
@@ -19,13 +17,11 @@ from engin._type_utils import TypeId, type_id_of
|
|
19
17
|
|
20
18
|
P = ParamSpec("P")
|
21
19
|
T = TypeVar("T")
|
22
|
-
Func: TypeAlias =
|
23
|
-
Callable[P, T] | Callable[P, Awaitable[T]] | Callable[[], T] | Callable[[], Awaitable[T]]
|
24
|
-
)
|
20
|
+
Func: TypeAlias = Callable[P, T]
|
25
21
|
_SELF = object()
|
26
22
|
|
27
23
|
|
28
|
-
def _noop(*args, **kwargs) -> None: ...
|
24
|
+
def _noop(*args: Any, **kwargs: Any) -> None: ...
|
29
25
|
|
30
26
|
|
31
27
|
class Dependency(ABC, Generic[P, T]):
|
@@ -66,11 +62,11 @@ class Dependency(ABC, Generic[P, T]):
|
|
66
62
|
if self._is_async:
|
67
63
|
return await cast(Awaitable[T], self._func(*args, **kwargs))
|
68
64
|
else:
|
69
|
-
return
|
65
|
+
return self._func(*args, **kwargs)
|
70
66
|
|
71
67
|
|
72
68
|
class Invoke(Dependency):
|
73
|
-
def __init__(self, invocation: Func[P, T], block_name: str | None = None):
|
69
|
+
def __init__(self, invocation: Func[P, T], block_name: str | None = None) -> None:
|
74
70
|
super().__init__(func=invocation, block_name=block_name)
|
75
71
|
|
76
72
|
def __str__(self) -> str:
|
@@ -78,7 +74,7 @@ class Invoke(Dependency):
|
|
78
74
|
|
79
75
|
|
80
76
|
class Entrypoint(Invoke):
|
81
|
-
def __init__(self, type_:
|
77
|
+
def __init__(self, type_: type[Any], *, block_name: str | None = None) -> None:
|
82
78
|
self._type = type_
|
83
79
|
super().__init__(invocation=_noop, block_name=block_name)
|
84
80
|
|
@@ -99,7 +95,7 @@ class Entrypoint(Invoke):
|
|
99
95
|
|
100
96
|
|
101
97
|
class Provide(Dependency[Any, T]):
|
102
|
-
def __init__(self, builder: Func[P, T], block_name: str | None = None):
|
98
|
+
def __init__(self, builder: Func[P, T], block_name: str | None = None) -> None:
|
103
99
|
super().__init__(func=builder, block_name=block_name)
|
104
100
|
self._is_multi = typing.get_origin(self.return_type) is list
|
105
101
|
|
@@ -111,14 +107,16 @@ class Provide(Dependency[Any, T]):
|
|
111
107
|
)
|
112
108
|
|
113
109
|
@property
|
114
|
-
def return_type(self) ->
|
110
|
+
def return_type(self) -> type[T]:
|
115
111
|
if isclass(self._func):
|
116
112
|
return_type = self._func # __init__ returns self
|
117
113
|
else:
|
118
114
|
try:
|
119
115
|
return_type = get_type_hints(self._func)["return"]
|
120
|
-
except KeyError:
|
121
|
-
raise RuntimeError(
|
116
|
+
except KeyError as err:
|
117
|
+
raise RuntimeError(
|
118
|
+
f"Dependency '{self.name}' requires a return typehint"
|
119
|
+
) from err
|
122
120
|
|
123
121
|
return return_type
|
124
122
|
|
@@ -140,7 +138,7 @@ class Provide(Dependency[Any, T]):
|
|
140
138
|
class Supply(Provide, Generic[T]):
|
141
139
|
def __init__(
|
142
140
|
self, value: T, *, type_hint: type | None = None, block_name: str | None = None
|
143
|
-
):
|
141
|
+
) -> None:
|
144
142
|
self._value = value
|
145
143
|
self._type_hint = type_hint
|
146
144
|
if self._type_hint is not None:
|
@@ -148,7 +146,7 @@ class Supply(Provide, Generic[T]):
|
|
148
146
|
super().__init__(builder=self._get_val, block_name=block_name)
|
149
147
|
|
150
148
|
@property
|
151
|
-
def return_type(self) ->
|
149
|
+
def return_type(self) -> type[T]:
|
152
150
|
if self._type_hint is not None:
|
153
151
|
return self._type_hint
|
154
152
|
if isinstance(self._value, list):
|
engin/_engin.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
import asyncio
|
1
2
|
import logging
|
2
|
-
|
3
|
+
import os
|
4
|
+
import signal
|
5
|
+
from asyncio import Event, Task
|
3
6
|
from collections.abc import Iterable
|
7
|
+
from contextlib import AsyncExitStack
|
4
8
|
from itertools import chain
|
9
|
+
from types import FrameType
|
5
10
|
from typing import ClassVar, TypeAlias
|
6
11
|
|
7
12
|
from engin import Entrypoint
|
@@ -16,14 +21,47 @@ LOG = logging.getLogger("engin")
|
|
16
21
|
Option: TypeAlias = Invoke | Provide | Supply | Block
|
17
22
|
_Opt: TypeAlias = Invoke | Provide | Supply
|
18
23
|
|
24
|
+
_OS_IS_WINDOWS = os.name == "nt"
|
25
|
+
|
19
26
|
|
20
27
|
class Engin:
|
28
|
+
"""
|
29
|
+
The Engin class runs your application. It assembles the required dependencies, invokes
|
30
|
+
any invocations and manages your application's lifecycle.
|
31
|
+
|
32
|
+
Examples:
|
33
|
+
```python
|
34
|
+
import asyncio
|
35
|
+
|
36
|
+
from httpx import AsyncClient
|
37
|
+
|
38
|
+
from engin import Engin, Invoke, Provide
|
39
|
+
|
40
|
+
|
41
|
+
def httpx_client() -> AsyncClient:
|
42
|
+
return AsyncClient()
|
43
|
+
|
44
|
+
|
45
|
+
async def main(http_client: AsyncClient) -> None:
|
46
|
+
print(await http_client.get("https://httpbin.org/get"))
|
47
|
+
|
48
|
+
engin = Engin(Provide(httpx_client), Invoke(main))
|
49
|
+
|
50
|
+
asyncio.run(engin.run())
|
51
|
+
```
|
52
|
+
"""
|
53
|
+
|
21
54
|
_LIB_OPTIONS: ClassVar[list[Option]] = [Provide(Lifecycle)]
|
22
55
|
|
23
56
|
def __init__(self, *options: Option) -> None:
|
24
57
|
self._providers: dict[TypeId, Provide] = {TypeId.from_type(Engin): Provide(self._self)}
|
25
58
|
self._invokables: list[Invoke] = []
|
26
|
-
|
59
|
+
|
60
|
+
self._stop_requested_event = Event()
|
61
|
+
self._stop_complete_event = Event()
|
62
|
+
self._exit_stack: AsyncExitStack = AsyncExitStack()
|
63
|
+
self._shutdown_task: Task | None = None
|
64
|
+
self._run_task: Task | None = None
|
27
65
|
|
28
66
|
self._destruct_options(chain(self._LIB_OPTIONS, options))
|
29
67
|
self._assembler = Assembler(self._providers.values())
|
@@ -32,33 +70,69 @@ class Engin:
|
|
32
70
|
def assembler(self) -> Assembler:
|
33
71
|
return self._assembler
|
34
72
|
|
35
|
-
async def run(self):
|
73
|
+
async def run(self) -> None:
|
74
|
+
"""
|
75
|
+
Run the Engin and wait for it to be stopped via an external signal or by calling
|
76
|
+
the `stop` method.
|
77
|
+
"""
|
36
78
|
await self.start()
|
37
|
-
|
38
|
-
|
39
|
-
await self.
|
79
|
+
self._run_task = asyncio.create_task(_raise_on_stop(self._stop_requested_event))
|
80
|
+
await self._stop_requested_event.wait()
|
81
|
+
await self._shutdown()
|
40
82
|
|
41
83
|
async def start(self) -> None:
|
84
|
+
"""
|
85
|
+
Starts the engin, this method waits for the shutdown lifecycle to complete.
|
86
|
+
"""
|
42
87
|
LOG.info("starting engin")
|
43
88
|
assembled_invocations: list[AssembledDependency] = [
|
44
89
|
await self._assembler.assemble(invocation) for invocation in self._invokables
|
45
90
|
]
|
91
|
+
|
46
92
|
for invocation in assembled_invocations:
|
47
|
-
|
93
|
+
try:
|
94
|
+
await invocation()
|
95
|
+
except Exception as err:
|
96
|
+
name = invocation.dependency.name
|
97
|
+
LOG.error(f"invocation '{name}' errored, exiting", exc_info=err)
|
98
|
+
return
|
48
99
|
|
49
100
|
lifecycle = await self._assembler.get(Lifecycle)
|
50
|
-
|
51
|
-
|
101
|
+
|
102
|
+
try:
|
103
|
+
for hook in lifecycle.list():
|
104
|
+
await self._exit_stack.enter_async_context(hook)
|
105
|
+
except Exception as err:
|
106
|
+
LOG.error("lifecycle startup error, exiting", exc_info=err)
|
107
|
+
await self._exit_stack.aclose()
|
108
|
+
return
|
109
|
+
|
52
110
|
LOG.info("startup complete")
|
53
111
|
|
54
|
-
|
55
|
-
self._stop_event.set()
|
112
|
+
self._shutdown_task = asyncio.create_task(self._shutdown_when_stopped())
|
56
113
|
|
57
|
-
def
|
114
|
+
async def stop(self) -> None:
|
115
|
+
"""
|
116
|
+
Stops the engin, this method waits for the shutdown lifecycle to complete.
|
117
|
+
"""
|
118
|
+
self._stop_requested_event.set()
|
119
|
+
await self._stop_complete_event.wait()
|
120
|
+
|
121
|
+
async def _shutdown(self) -> None:
|
122
|
+
LOG.info("stopping engin")
|
123
|
+
await self._exit_stack.aclose()
|
124
|
+
self._stop_complete_event.set()
|
125
|
+
LOG.info("shutdown complete")
|
126
|
+
|
127
|
+
async def _shutdown_when_stopped(self) -> None:
|
128
|
+
await self._stop_requested_event.wait()
|
129
|
+
await self._shutdown()
|
130
|
+
|
131
|
+
def _destruct_options(self, options: Iterable[Option]) -> None:
|
58
132
|
for opt in options:
|
59
133
|
if isinstance(opt, Block):
|
60
134
|
self._destruct_options(opt)
|
61
|
-
if isinstance(opt,
|
135
|
+
if isinstance(opt, Provide | Supply):
|
62
136
|
existing = self._providers.get(opt.return_type_id)
|
63
137
|
self._log_option(opt, overwrites=existing)
|
64
138
|
self._providers[opt.return_type_id] = opt
|
@@ -86,3 +160,44 @@ class Engin:
|
|
86
160
|
|
87
161
|
def _self(self) -> "Engin":
|
88
162
|
return self
|
163
|
+
|
164
|
+
|
165
|
+
class _StopRequested(RuntimeError):
|
166
|
+
pass
|
167
|
+
|
168
|
+
|
169
|
+
async def _raise_on_stop(stop_requested_event: Event) -> None:
|
170
|
+
"""
|
171
|
+
This method is based off of the Temporal Python SDK's Worker class:
|
172
|
+
https://github.com/temporalio/sdk-python/blob/main/temporalio/worker/_worker.py#L488
|
173
|
+
"""
|
174
|
+
try:
|
175
|
+
# try to gracefully handle sigint/sigterm
|
176
|
+
if not _OS_IS_WINDOWS:
|
177
|
+
loop = asyncio.get_running_loop()
|
178
|
+
for signame in (signal.SIGINT, signal.SIGTERM):
|
179
|
+
loop.add_signal_handler(signame, stop_requested_event.set)
|
180
|
+
|
181
|
+
await stop_requested_event.wait()
|
182
|
+
raise _StopRequested()
|
183
|
+
else:
|
184
|
+
should_stop = False
|
185
|
+
|
186
|
+
# windows does not support signal_handlers, so this is the workaround
|
187
|
+
def ctrlc_handler(sig: int, frame: FrameType | None) -> None:
|
188
|
+
nonlocal should_stop
|
189
|
+
if should_stop:
|
190
|
+
raise KeyboardInterrupt("Forced keyboard interrupt")
|
191
|
+
should_stop = True
|
192
|
+
|
193
|
+
signal.signal(signal.SIGINT, ctrlc_handler)
|
194
|
+
|
195
|
+
while not should_stop:
|
196
|
+
# In case engin is stopped via external `stop` call.
|
197
|
+
if stop_requested_event.is_set():
|
198
|
+
return
|
199
|
+
await asyncio.sleep(0.1)
|
200
|
+
|
201
|
+
stop_requested_event.set()
|
202
|
+
except asyncio.CancelledError:
|
203
|
+
pass
|
engin/_lifecycle.py
CHANGED
@@ -1,21 +1,38 @@
|
|
1
|
-
|
2
|
-
from contextlib import AbstractAsyncContextManager
|
1
|
+
import logging
|
2
|
+
from contextlib import AbstractAsyncContextManager
|
3
|
+
from types import TracebackType
|
4
|
+
|
5
|
+
LOG = logging.getLogger("engin")
|
3
6
|
|
4
7
|
|
5
8
|
class Lifecycle:
|
6
9
|
def __init__(self) -> None:
|
7
|
-
self._on_startup: list[Callable] = []
|
8
|
-
self._on_shutdown: list[Callable] = []
|
9
10
|
self._context_managers: list[AbstractAsyncContextManager] = []
|
10
|
-
self._stack: AsyncExitStack = AsyncExitStack()
|
11
11
|
|
12
|
-
def
|
13
|
-
|
12
|
+
def append(self, cm: AbstractAsyncContextManager) -> None:
|
13
|
+
suppressed_cm = _AExitSuppressingAsyncContextManager(cm)
|
14
|
+
self._context_managers.append(suppressed_cm)
|
15
|
+
|
16
|
+
def list(self) -> list[AbstractAsyncContextManager]:
|
17
|
+
return self._context_managers[:]
|
18
|
+
|
19
|
+
|
20
|
+
class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
|
21
|
+
def __init__(self, cm: AbstractAsyncContextManager) -> None:
|
22
|
+
self._cm = cm
|
14
23
|
|
15
|
-
async def
|
16
|
-
self.
|
17
|
-
|
18
|
-
await self._stack.enter_async_context(cm)
|
24
|
+
async def __aenter__(self) -> AbstractAsyncContextManager:
|
25
|
+
await self._cm.__aenter__()
|
26
|
+
return self._cm
|
19
27
|
|
20
|
-
async def
|
21
|
-
|
28
|
+
async def __aexit__(
|
29
|
+
self,
|
30
|
+
exc_type: type[BaseException] | None,
|
31
|
+
exc_value: BaseException | None,
|
32
|
+
traceback: TracebackType | None,
|
33
|
+
/,
|
34
|
+
) -> None:
|
35
|
+
try:
|
36
|
+
await self._cm.__aexit__(exc_type, exc_value, traceback)
|
37
|
+
except Exception as err:
|
38
|
+
LOG.error("error in lifecycle hook stop, ignoring...", exc_info=err)
|
engin/ext/asgi.py
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
import traceback
|
2
|
-
import
|
3
|
-
from typing import ClassVar, Protocol, TypeAlias
|
2
|
+
from collections.abc import Awaitable, Callable, MutableMapping
|
3
|
+
from typing import Any, ClassVar, Protocol, TypeAlias
|
4
4
|
|
5
5
|
from engin import Engin, Option
|
6
6
|
|
7
7
|
__all__ = ["ASGIEngin", "ASGIType"]
|
8
8
|
|
9
9
|
|
10
|
-
_Scope: TypeAlias =
|
11
|
-
_Message: TypeAlias =
|
12
|
-
_Receive: TypeAlias =
|
13
|
-
_Send: TypeAlias =
|
10
|
+
_Scope: TypeAlias = MutableMapping[str, Any]
|
11
|
+
_Message: TypeAlias = MutableMapping[str, Any]
|
12
|
+
_Receive: TypeAlias = Callable[[], Awaitable[_Message]]
|
13
|
+
_Send: TypeAlias = Callable[[_Message], Awaitable[None]]
|
14
14
|
|
15
15
|
|
16
16
|
class ASGIType(Protocol):
|
@@ -54,5 +54,5 @@ class _Rereceive:
|
|
54
54
|
def __init__(self, message: _Message) -> None:
|
55
55
|
self._message = message
|
56
56
|
|
57
|
-
async def __call__(self, *args, **kwargs) -> _Message:
|
57
|
+
async def __call__(self, *args: Any, **kwargs: Any) -> _Message:
|
58
58
|
return self._message
|
engin/ext/fastapi.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import typing
|
2
1
|
from typing import ClassVar, TypeVar
|
3
2
|
|
4
3
|
from engin import Engin, Invoke, Option
|
@@ -8,13 +7,10 @@ try:
|
|
8
7
|
from fastapi import FastAPI
|
9
8
|
from fastapi.params import Depends
|
10
9
|
from starlette.requests import HTTPConnection
|
11
|
-
except ImportError:
|
12
|
-
raise ImportError(
|
13
|
-
|
14
|
-
|
15
|
-
if typing.TYPE_CHECKING:
|
16
|
-
from fastapi import FastAPI
|
17
|
-
from fastapi.params import Depends
|
10
|
+
except ImportError as err:
|
11
|
+
raise ImportError(
|
12
|
+
"fastapi package must be installed to use the fastapi extension"
|
13
|
+
) from err
|
18
14
|
|
19
15
|
__all__ = ["FastAPIEngin", "Inject"]
|
20
16
|
|
@@ -27,7 +23,7 @@ def _attach_engin(
|
|
27
23
|
|
28
24
|
|
29
25
|
class FastAPIEngin(ASGIEngin):
|
30
|
-
_LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
|
26
|
+
_LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
|
31
27
|
_asgi_type = FastAPI
|
32
28
|
|
33
29
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: engin
|
3
|
+
Version: 0.0.4
|
4
|
+
Summary: An async-first modular application framework
|
5
|
+
License-File: LICENSE
|
6
|
+
Requires-Python: >=3.10
|
7
|
+
Description-Content-Type: text/markdown
|
8
|
+
|
9
|
+
# Engin 🏎️
|
10
|
+
|
11
|
+
Engin is a zero-dependency application framework for modern Python.
|
12
|
+
|
13
|
+
**Documentation**: https://engin.readthedocs.io/
|
14
|
+
|
15
|
+
## Features ✨
|
16
|
+
|
17
|
+
- **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
|
18
|
+
powered by type hints.
|
19
|
+
- **Lifecycle Management** - Engin provides a simple & portable approach for attaching
|
20
|
+
startup and shutdown tasks to the application's lifecycle.
|
21
|
+
- **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
|
22
|
+
packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
|
23
|
+
maintaining many services across your organisation.
|
24
|
+
- **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
|
25
|
+
provide their own Dependency Injection, for example FastAPI, allowing you to integrate
|
26
|
+
Engin into existing code bases incrementally.
|
27
|
+
- **Async Native**: Engin is an async framework, meaning first class support for async
|
28
|
+
dependencies. However Engin will happily run synchronous code as well.
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
Engin is available on PyPI, install using your favourite dependency manager:
|
33
|
+
|
34
|
+
- **pip**:`pip install engin`
|
35
|
+
- **poetry**: `poetry add engin`
|
36
|
+
- **uv**: `uv add engin`
|
37
|
+
|
38
|
+
## Getting Started
|
39
|
+
|
40
|
+
A minimal example:
|
41
|
+
|
42
|
+
```python
|
43
|
+
import asyncio
|
44
|
+
|
45
|
+
from httpx import AsyncClient
|
46
|
+
|
47
|
+
from engin import Engin, Invoke, Provide
|
48
|
+
|
49
|
+
|
50
|
+
def httpx_client() -> AsyncClient:
|
51
|
+
return AsyncClient()
|
52
|
+
|
53
|
+
|
54
|
+
async def main(http_client: AsyncClient) -> None:
|
55
|
+
print(await http_client.get("https://httpbin.org/get"))
|
56
|
+
|
57
|
+
engin = Engin(Provide(httpx_client), Invoke(main))
|
58
|
+
|
59
|
+
asyncio.run(engin.run())
|
60
|
+
```
|
61
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
engin/__init__.py,sha256=AIE9wnwvfXwS1mwp6Sa9xtatBYyYzhWa36GKfAkEx_M,508
|
2
|
+
engin/_assembler.py,sha256=UY97tXdHgvRi_iph_kolXZ8YTrxRjHKoIc4tAhqYOcw,5258
|
3
|
+
engin/_block.py,sha256=Sdl9o3ZmmplZgB6pWFCbKjMemWzJXUso63p_bUg6BtM,1152
|
4
|
+
engin/_dependency.py,sha256=G0ooXhYVauWiZ2keisot8iQXjGwdAwyvpwMtE1rjQzw,4754
|
5
|
+
engin/_engin.py,sha256=tkW-QP-CNwKH9tr3xIZh-uvBKgyn9MrSvXjn3xLp7VU,6833
|
6
|
+
engin/_exceptions.py,sha256=nkzTqxrW5nkcNgFDGoZ2TBtnHtO2RLk0qghM5LNAEmU,542
|
7
|
+
engin/_lifecycle.py,sha256=LrDTrxEW6Mo0U4Nol9Rq-49Bi0UCa-7pbSCYjuSmG50,1211
|
8
|
+
engin/_type_utils.py,sha256=naEk-lknC3Fdsd4jiP4YZAxjX3KXZN0MhFde9EV-Fmo,1835
|
9
|
+
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
engin/ext/asgi.py,sha256=6vuC4zIhsvAdmwRn2I6uuUWPYfqobox1dv7skg2OWwE,1940
|
12
|
+
engin/ext/fastapi.py,sha256=CH2Zi7Oh_Va0TJGx05e7_LqAiCsoI1qcu0Z59_rgfRk,899
|
13
|
+
engin-0.0.4.dist-info/METADATA,sha256=CMTjhIBzTSn-PYAAnV6wAVpr-32GqV_BVUg-uQgsKVY,1806
|
14
|
+
engin-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
engin-0.0.4.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
16
|
+
engin-0.0.4.dist-info/RECORD,,
|
engin-0.0.2.dist-info/METADATA
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: engin
|
3
|
-
Version: 0.0.2
|
4
|
-
Summary: An async-first modular application framework
|
5
|
-
License-File: LICENSE
|
6
|
-
Requires-Python: >=3.10
|
7
|
-
Description-Content-Type: text/markdown
|
8
|
-
|
9
|
-
# Engin 🏎️
|
10
|
-
|
11
|
-
Engin is a zero-dependency application framework for modern Python.
|
12
|
-
|
13
|
-
## Features ✨
|
14
|
-
|
15
|
-
- **Lightweight**: Engin has no dependencies.
|
16
|
-
- **Async First**: Engin provides first-class support for applications.
|
17
|
-
- **Dependency Injection**: Engin promotes a modular decoupled architecture in your application.
|
18
|
-
- **Lifecycle Management**: Engin provides an simple, portable approach for implememting
|
19
|
-
startup and shutdown tasks.
|
20
|
-
- **Ecosystem Compatability**: seamlessly integrate with frameworks such as FastAPI without
|
21
|
-
having to migrate your dependencies.
|
22
|
-
- **Code Reuse**: Engin's modular components work great as packages and distributions. Allowing
|
23
|
-
low boiler-plate code reuse within your Organisation.
|
24
|
-
|
25
|
-
## Installation
|
26
|
-
|
27
|
-
Engin is available on PyPI, install using your favourite dependency manager:
|
28
|
-
|
29
|
-
- **pip**:`pip install engin`
|
30
|
-
- **poetry**: `poetry add engin`
|
31
|
-
- **uv**: `uv add engin`
|
32
|
-
|
33
|
-
## Getting Started
|
34
|
-
|
35
|
-
A minimal example:
|
36
|
-
|
37
|
-
```python
|
38
|
-
import asyncio
|
39
|
-
|
40
|
-
from httpx import AsyncClient
|
41
|
-
|
42
|
-
from engin import Engin, Invoke, Provide
|
43
|
-
|
44
|
-
|
45
|
-
def httpx_client() -> AsyncClient:
|
46
|
-
return AsyncClient()
|
47
|
-
|
48
|
-
|
49
|
-
async def main(http_client: AsyncClient) -> None:
|
50
|
-
print(await http_client.get("https://httpbin.org/get"))
|
51
|
-
|
52
|
-
engin = Engin(Provide(httpx_client), Invoke(main))
|
53
|
-
|
54
|
-
asyncio.run(engin.run())
|
55
|
-
```
|
56
|
-
|
engin-0.0.2.dist-info/RECORD
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
engin/__init__.py,sha256=AIE9wnwvfXwS1mwp6Sa9xtatBYyYzhWa36GKfAkEx_M,508
|
2
|
-
engin/_assembler.py,sha256=Py159y4HvI54QuDcmbjL2kfXtbucYDE2KFBOgoC4qCM,5243
|
3
|
-
engin/_block.py,sha256=XrGMFEeps_rvOGsYTlsefUkJGAY6jqdp71G8jE9FbwU,1109
|
4
|
-
engin/_dependency.py,sha256=cL1qQFKDt7Rgg7mx8IXVkCZlUigg5cvhPFF9g2LNrgg,4749
|
5
|
-
engin/_engin.py,sha256=KHjtncm0bG_N4sAFt7haxUBMeuLp0zP8Bxpftp7Zgy8,3109
|
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/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
engin/ext/asgi.py,sha256=7viV6YgEwa0KatypDUrEYZT3YIQ3URQbzfVTGG7OuZE,1931
|
12
|
-
engin/ext/fastapi.py,sha256=vf7eB5no6eVuZyYdJZdvDYZmjJAmoKbrKhskgiSWg5g,1005
|
13
|
-
engin-0.0.2.dist-info/METADATA,sha256=CxEA114Eb2DF_voKUqPITeZiU1KWfSZaqvzIVNXaUAw,1491
|
14
|
-
engin-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
engin-0.0.2.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
16
|
-
engin-0.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|