engin 0.0.dev1__py3-none-any.whl → 0.0.2__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 +3 -1
- engin/_assembler.py +8 -3
- engin/_engin.py +4 -5
- engin/{extensions → ext}/asgi.py +7 -4
- engin/ext/fastapi.py +42 -0
- engin-0.0.2.dist-info/METADATA +56 -0
- engin-0.0.2.dist-info/RECORD +16 -0
- {engin-0.0.dev1.dist-info → engin-0.0.2.dist-info}/WHEEL +1 -1
- engin-0.0.dev1.dist-info/METADATA +0 -6
- engin-0.0.dev1.dist-info/RECORD +0 -15
- /engin/{extensions → ext}/__init__.py +0 -0
- {engin-0.0.dev1.dist-info → engin-0.0.2.dist-info}/licenses/LICENSE +0 -0
engin/__init__.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from engin import ext
|
1
2
|
from engin._assembler import Assembler
|
2
3
|
from engin._block import Block, invoke, provide
|
3
4
|
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
@@ -8,14 +9,15 @@ from engin._lifecycle import Lifecycle
|
|
8
9
|
__all__ = [
|
9
10
|
"Assembler",
|
10
11
|
"AssemblyError",
|
12
|
+
"Block",
|
11
13
|
"Engin",
|
12
14
|
"Entrypoint",
|
13
15
|
"Invoke",
|
14
16
|
"Lifecycle",
|
15
|
-
"Block",
|
16
17
|
"Option",
|
17
18
|
"Provide",
|
18
19
|
"Supply",
|
20
|
+
"ext",
|
19
21
|
"invoke",
|
20
22
|
"provide",
|
21
23
|
]
|
engin/_assembler.py
CHANGED
@@ -49,7 +49,7 @@ class Assembler:
|
|
49
49
|
if not providers:
|
50
50
|
if type_id.multi:
|
51
51
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
52
|
-
providers = [(Supply([], type_hint=list[type_id.type]))]
|
52
|
+
providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
|
53
53
|
else:
|
54
54
|
raise LookupError(f"No Provider registered for dependency '{type_id}'")
|
55
55
|
|
@@ -112,15 +112,20 @@ class Assembler:
|
|
112
112
|
|
113
113
|
async def get(self, type_: type[T]) -> T:
|
114
114
|
type_id = type_id_of(type_)
|
115
|
+
if type_id in self._dependencies:
|
116
|
+
return self._dependencies[type_id]
|
115
117
|
if type_id.multi:
|
116
118
|
out = []
|
117
119
|
for provider in self._multiproviders[type_id]:
|
118
120
|
assembled_dependency = await self.assemble(provider)
|
119
121
|
out.extend(await assembled_dependency())
|
120
|
-
|
122
|
+
self._dependencies[type_id] = out
|
123
|
+
return out # type: ignore[return-value]
|
121
124
|
else:
|
122
125
|
assembled_dependency = await self.assemble(self._providers[type_id])
|
123
|
-
|
126
|
+
value = await assembled_dependency()
|
127
|
+
self._dependencies[type_id] = value
|
128
|
+
return value # type: ignore[return-value]
|
124
129
|
|
125
130
|
def has(self, type_: type[T]) -> bool:
|
126
131
|
return type_id_of(type_) in self._providers
|
engin/_engin.py
CHANGED
@@ -21,7 +21,7 @@ class Engin:
|
|
21
21
|
_LIB_OPTIONS: ClassVar[list[Option]] = [Provide(Lifecycle)]
|
22
22
|
|
23
23
|
def __init__(self, *options: Option) -> None:
|
24
|
-
self._providers: dict[TypeId, Provide] = {}
|
24
|
+
self._providers: dict[TypeId, Provide] = {TypeId.from_type(Engin): Provide(self._self)}
|
25
25
|
self._invokables: list[Invoke] = []
|
26
26
|
self._stop_event = Event()
|
27
27
|
|
@@ -35,13 +35,9 @@ class Engin:
|
|
35
35
|
async def run(self):
|
36
36
|
await self.start()
|
37
37
|
|
38
|
-
# lifecycle startup
|
39
|
-
|
40
38
|
# wait till stop signal recieved
|
41
39
|
await self._stop_event.wait()
|
42
40
|
|
43
|
-
# lifecycle shutdown
|
44
|
-
|
45
41
|
async def start(self) -> None:
|
46
42
|
LOG.info("starting engin")
|
47
43
|
assembled_invocations: list[AssembledDependency] = [
|
@@ -87,3 +83,6 @@ class Engin:
|
|
87
83
|
LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
|
88
84
|
elif isinstance(opt, Invoke):
|
89
85
|
LOG.debug(f"INVOKE {opt.name:<35}")
|
86
|
+
|
87
|
+
def _self(self) -> "Engin":
|
88
|
+
return self
|
engin/{extensions → ext}/asgi.py
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
import traceback
|
2
2
|
import typing
|
3
|
-
from typing import Protocol, TypeAlias
|
3
|
+
from typing import ClassVar, Protocol, TypeAlias
|
4
4
|
|
5
5
|
from engin import Engin, Option
|
6
6
|
|
@@ -18,13 +18,16 @@ class ASGIType(Protocol):
|
|
18
18
|
|
19
19
|
|
20
20
|
class ASGIEngin(Engin, ASGIType):
|
21
|
+
_asgi_type: ClassVar[type[ASGIType]] = ASGIType # type: ignore[type-abstract]
|
21
22
|
_asgi_app: ASGIType
|
22
23
|
|
23
24
|
def __init__(self, *options: Option) -> None:
|
24
25
|
super().__init__(*options)
|
25
26
|
|
26
|
-
if not self._assembler.has(
|
27
|
-
raise LookupError(
|
27
|
+
if not self._assembler.has(self._asgi_type):
|
28
|
+
raise LookupError(
|
29
|
+
f"A provider for `{self._asgi_type.__name__}` was expected, none found"
|
30
|
+
)
|
28
31
|
|
29
32
|
async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None:
|
30
33
|
if scope["type"] == "lifespan":
|
@@ -44,7 +47,7 @@ class ASGIEngin(Engin, ASGIType):
|
|
44
47
|
|
45
48
|
async def _startup(self) -> None:
|
46
49
|
await self.start()
|
47
|
-
self._asgi_app = await self._assembler.get(
|
50
|
+
self._asgi_app = await self._assembler.get(self._asgi_type)
|
48
51
|
|
49
52
|
|
50
53
|
class _Rereceive:
|
engin/ext/fastapi.py
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import ClassVar, TypeVar
|
3
|
+
|
4
|
+
from engin import Engin, Invoke, Option
|
5
|
+
from engin.ext.asgi import ASGIEngin
|
6
|
+
|
7
|
+
try:
|
8
|
+
from fastapi import FastAPI
|
9
|
+
from fastapi.params import Depends
|
10
|
+
from starlette.requests import HTTPConnection
|
11
|
+
except ImportError:
|
12
|
+
raise ImportError("fastapi must be installed to use the corresponding extension")
|
13
|
+
|
14
|
+
|
15
|
+
if typing.TYPE_CHECKING:
|
16
|
+
from fastapi import FastAPI
|
17
|
+
from fastapi.params import Depends
|
18
|
+
|
19
|
+
__all__ = ["FastAPIEngin", "Inject"]
|
20
|
+
|
21
|
+
|
22
|
+
def _attach_engin(
|
23
|
+
app: FastAPI,
|
24
|
+
engin: Engin,
|
25
|
+
) -> None:
|
26
|
+
app.state.engin = engin
|
27
|
+
|
28
|
+
|
29
|
+
class FastAPIEngin(ASGIEngin):
|
30
|
+
_LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)] # type: ignore[arg-type]
|
31
|
+
_asgi_type = FastAPI
|
32
|
+
|
33
|
+
|
34
|
+
T = TypeVar("T")
|
35
|
+
|
36
|
+
|
37
|
+
def Inject(interface: type[T]) -> Depends:
|
38
|
+
async def inner(conn: HTTPConnection) -> T:
|
39
|
+
engin: Engin = conn.app.state.engin
|
40
|
+
return await engin.assembler.get(interface)
|
41
|
+
|
42
|
+
return Depends(inner)
|
@@ -0,0 +1,56 @@
|
|
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
|
+
|
@@ -0,0 +1,16 @@
|
|
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,,
|
engin-0.0.dev1.dist-info/RECORD
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
engin/__init__.py,sha256=T2tv-pdULMTp-p0PhB_XYmr9CEWOmMzcu0jebZx7-2Q,475
|
2
|
-
engin/_assembler.py,sha256=HPnrTGk6XbO-iG3BZfD65zzOxWC4C1yWOZf7Vv3MMKk,4944
|
3
|
-
engin/_block.py,sha256=XrGMFEeps_rvOGsYTlsefUkJGAY6jqdp71G8jE9FbwU,1109
|
4
|
-
engin/_dependency.py,sha256=cL1qQFKDt7Rgg7mx8IXVkCZlUigg5cvhPFF9g2LNrgg,4749
|
5
|
-
engin/_engin.py,sha256=Sg7sxESuwzTXx-4xgFv869YkpBRxE9d-kjIOTOTtcxU,3071
|
6
|
-
engin/_exceptions.py,sha256=nkzTqxrW5nkcNgFDGoZ2TBtnHtO2RLk0qghM5LNAEmU,542
|
7
|
-
engin/_lifecycle.py,sha256=0hk24fiwaBos5kaZrnG_Qm0VmUhWKnGtwYCjc007XDk,729
|
8
|
-
engin/_type_utils.py,sha256=naEk-lknC3Fdsd4jiP4YZAxjX3KXZN0MhFde9EV-Fmo,1835
|
9
|
-
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
engin/extensions/asgi.py,sha256=EHQhevGJt3UJZp_7Y7AIuYVm-bYUO-seuzE3kMuOJWM,1775
|
12
|
-
engin-0.0.dev1.dist-info/METADATA,sha256=263kjboSCI7Th0GHl7bbpF5cCHAKsB51_56v5xcJThk,152
|
13
|
-
engin-0.0.dev1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
14
|
-
engin-0.0.dev1.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
15
|
-
engin-0.0.dev1.dist-info/RECORD,,
|
File without changes
|
File without changes
|