engin 0.0.dev1__tar.gz → 0.0.2__tar.gz

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.
Files changed (63) hide show
  1. engin-0.0.2/.github/workflows/check.yaml +30 -0
  2. engin-0.0.2/CHANGELOG.md +19 -0
  3. engin-0.0.2/PKG-INFO +56 -0
  4. engin-0.0.2/README.md +48 -0
  5. engin-0.0.2/examples/fastapi/app.py +25 -0
  6. engin-0.0.2/examples/fastapi/main.py +14 -0
  7. engin-0.0.2/examples/fastapi/routes/cats/adapters/repository.py +18 -0
  8. engin-0.0.2/examples/fastapi/routes/cats/api.py +42 -0
  9. engin-0.0.2/examples/fastapi/routes/cats/block.py +16 -0
  10. engin-0.0.2/examples/fastapi/routes/cats/domain.py +18 -0
  11. engin-0.0.2/examples/fastapi/routes/cats/ports.py +14 -0
  12. {engin-0.0.dev1 → engin-0.0.2}/pyproject.toml +2 -1
  13. {engin-0.0.dev1 → engin-0.0.2}/src/engin/__init__.py +3 -1
  14. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_assembler.py +8 -3
  15. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_engin.py +4 -5
  16. engin-0.0.2/src/engin/ext/__init__.py +0 -0
  17. {engin-0.0.dev1/src/engin/extensions → engin-0.0.2/src/engin/ext}/asgi.py +7 -4
  18. engin-0.0.2/src/engin/ext/fastapi.py +42 -0
  19. engin-0.0.2/src/engin/py.typed +0 -0
  20. engin-0.0.2/tests/__init__.py +0 -0
  21. engin-0.0.2/tests/conftest.py +0 -0
  22. {engin-0.0.dev1 → engin-0.0.2}/tests/test_assembler.py +20 -0
  23. engin-0.0.2/uv.lock +482 -0
  24. engin-0.0.dev1/PKG-INFO +0 -6
  25. engin-0.0.dev1/uv.lock +0 -424
  26. {engin-0.0.dev1 → engin-0.0.2}/.github/workflows/publish.yaml +0 -0
  27. {engin-0.0.dev1 → engin-0.0.2}/.gitignore +0 -0
  28. {engin-0.0.dev1 → engin-0.0.2}/LICENSE +0 -0
  29. {engin-0.0.dev1/examples/asgi → engin-0.0.2/examples}/__init__.py +0 -0
  30. {engin-0.0.dev1/examples/asgi/common → engin-0.0.2/examples/asgi}/__init__.py +0 -0
  31. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/app.py +0 -0
  32. {engin-0.0.dev1/examples/asgi/common/db → engin-0.0.2/examples/asgi/common}/__init__.py +0 -0
  33. {engin-0.0.dev1/examples/asgi/common/db/adapaters → engin-0.0.2/examples/asgi/common/db}/__init__.py +0 -0
  34. {engin-0.0.dev1/examples/asgi/common/starlette → engin-0.0.2/examples/asgi/common/db/adapaters}/__init__.py +0 -0
  35. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/adapaters/memory.py +0 -0
  36. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/block.py +0 -0
  37. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/ports.py +0 -0
  38. {engin-0.0.dev1/examples/asgi/features → engin-0.0.2/examples/asgi/common/starlette}/__init__.py +0 -0
  39. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/starlette/endpoint.py +0 -0
  40. {engin-0.0.dev1/examples/asgi/features/cats → engin-0.0.2/examples/asgi/features}/__init__.py +0 -0
  41. {engin-0.0.dev1/examples/asgi/features/cats/api → engin-0.0.2/examples/asgi/features/cats}/__init__.py +0 -0
  42. {engin-0.0.dev1/examples/simple → engin-0.0.2/examples/asgi/features/cats/api}/__init__.py +0 -0
  43. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/api/get.py +0 -0
  44. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/api/post.py +0 -0
  45. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/block.py +0 -0
  46. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/domain.py +0 -0
  47. {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/main.py +0 -0
  48. {engin-0.0.dev1/src/engin/extensions → engin-0.0.2/examples/fastapi}/__init__.py +0 -0
  49. {engin-0.0.dev1/tests → engin-0.0.2/examples/fastapi/routes}/__init__.py +0 -0
  50. /engin-0.0.dev1/README.md → /engin-0.0.2/examples/fastapi/routes/cats/__init__.py +0 -0
  51. /engin-0.0.dev1/src/engin/py.typed → /engin-0.0.2/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  52. /engin-0.0.dev1/tests/conftest.py → /engin-0.0.2/examples/simple/__init__.py +0 -0
  53. {engin-0.0.dev1 → engin-0.0.2}/examples/simple/main.py +0 -0
  54. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_block.py +0 -0
  55. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_dependency.py +0 -0
  56. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_exceptions.py +0 -0
  57. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_lifecycle.py +0 -0
  58. {engin-0.0.dev1 → engin-0.0.2}/src/engin/_type_utils.py +0 -0
  59. {engin-0.0.dev1 → engin-0.0.2}/tests/deps.py +0 -0
  60. {engin-0.0.dev1 → engin-0.0.2}/tests/test_dependencies.py +0 -0
  61. {engin-0.0.dev1 → engin-0.0.2}/tests/test_engin.py +0 -0
  62. {engin-0.0.dev1 → engin-0.0.2}/tests/test_modules.py +0 -0
  63. {engin-0.0.dev1 → engin-0.0.2}/tests/test_utils.py +0 -0
@@ -0,0 +1,30 @@
1
+ name: Check
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ publish-to-pypi:
8
+ name: python
9
+ runs-on: ubuntu-latest
10
+
11
+ permissions:
12
+ id-token: write
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v3
19
+
20
+ - name: Set up Python
21
+ run: uv python install
22
+
23
+ - name: Ruff
24
+ run: uv run ruff check src tests
25
+
26
+ - name: Mypy
27
+ run: uv run mypy src
28
+
29
+ - name: Test
30
+ run: uv run pytest tests
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.2] - 2025-01-10
9
+
10
+ ### Added
11
+
12
+ - The `ext` sub-package is now explicitly exported in the package `__init__.py`
13
+
14
+
15
+ ## [0.0.1] - 2024-12-12
16
+
17
+ ### Added
18
+
19
+ - Initial release
engin-0.0.2/PKG-INFO ADDED
@@ -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
+
engin-0.0.2/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Engin 🏎️
2
+
3
+ Engin is a zero-dependency application framework for modern Python.
4
+
5
+ ## Features ✨
6
+
7
+ - **Lightweight**: Engin has no dependencies.
8
+ - **Async First**: Engin provides first-class support for applications.
9
+ - **Dependency Injection**: Engin promotes a modular decoupled architecture in your application.
10
+ - **Lifecycle Management**: Engin provides an simple, portable approach for implememting
11
+ startup and shutdown tasks.
12
+ - **Ecosystem Compatability**: seamlessly integrate with frameworks such as FastAPI without
13
+ having to migrate your dependencies.
14
+ - **Code Reuse**: Engin's modular components work great as packages and distributions. Allowing
15
+ low boiler-plate code reuse within your Organisation.
16
+
17
+ ## Installation
18
+
19
+ Engin is available on PyPI, install using your favourite dependency manager:
20
+
21
+ - **pip**:`pip install engin`
22
+ - **poetry**: `poetry add engin`
23
+ - **uv**: `uv add engin`
24
+
25
+ ## Getting Started
26
+
27
+ A minimal example:
28
+
29
+ ```python
30
+ import asyncio
31
+
32
+ from httpx import AsyncClient
33
+
34
+ from engin import Engin, Invoke, Provide
35
+
36
+
37
+ def httpx_client() -> AsyncClient:
38
+ return AsyncClient()
39
+
40
+
41
+ async def main(http_client: AsyncClient) -> None:
42
+ print(await http_client.get("https://httpbin.org/get"))
43
+
44
+ engin = Engin(Provide(httpx_client), Invoke(main))
45
+
46
+ asyncio.run(engin.run())
47
+ ```
48
+
@@ -0,0 +1,25 @@
1
+ from fastapi import FastAPI
2
+ from pydantic_settings import BaseSettings
3
+
4
+ from engin import Block, invoke, provide
5
+
6
+
7
+ class AppConfig(BaseSettings):
8
+ debug: bool = False
9
+
10
+
11
+ class AppBlock(Block):
12
+ @provide
13
+ def app_factory(self, app_config: AppConfig) -> FastAPI:
14
+ return FastAPI(debug=app_config.debug)
15
+
16
+ @provide
17
+ def default_config(self) -> AppConfig:
18
+ return AppConfig()
19
+
20
+ @invoke
21
+ def add_health_endpoint(self, app: FastAPI) -> None:
22
+ async def health():
23
+ return {"ok": True}
24
+
25
+ app.add_api_route(path="/health", endpoint=health)
@@ -0,0 +1,14 @@
1
+ import logging
2
+
3
+ import uvicorn
4
+
5
+ from engin import Supply
6
+ from engin.extensions.fastapi import FastAPIEngin
7
+ from examples.fastapi.app import AppBlock, AppConfig
8
+ from examples.fastapi.routes.cats.block import CatBlock
9
+
10
+ logging.basicConfig(level=logging.DEBUG)
11
+
12
+ app = FastAPIEngin(AppBlock(), CatBlock(), Supply(AppConfig(debug=True)))
13
+
14
+ uvicorn.run(app)
@@ -0,0 +1,18 @@
1
+ from examples.fastapi.routes.cats.domain import Cat
2
+ from examples.fastapi.routes.cats.ports import CatRepository
3
+
4
+
5
+ class InMemoryCatRepository(CatRepository):
6
+ def __init__(self) -> None:
7
+ self._cats: dict[int, Cat] = {}
8
+
9
+ def get(self, cat_id: int) -> Cat:
10
+ if cat_id in self._cats:
11
+ return self._cats[cat_id]
12
+ raise LookupError(f"No cat found for id: {cat_id}")
13
+
14
+ def set(self, cat: Cat) -> None:
15
+ self._cats[cat.id] = cat
16
+
17
+ def next_id(self) -> int:
18
+ return len(self._cats)
@@ -0,0 +1,42 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import APIRouter
4
+ from pydantic import BaseModel
5
+
6
+ from engin.ext.fastapi import Inject
7
+ from examples.fastapi.routes.cats.domain import Cat, CatPersonality
8
+ from examples.fastapi.routes.cats.ports import CatRepository
9
+
10
+ router = APIRouter(prefix="/cats")
11
+
12
+
13
+ @router.get("/{cat_id}")
14
+ async def get_cat(
15
+ cat_id: int,
16
+ repository: Annotated[CatRepository, Inject(CatRepository)],
17
+ ) -> Cat:
18
+ return repository.get(cat_id=cat_id)
19
+
20
+
21
+ class CatPostModel(BaseModel):
22
+ name: str
23
+ breed: str
24
+ age: float
25
+ personality: CatPersonality
26
+
27
+
28
+ @router.post("/")
29
+ async def get_cat(
30
+ cat: CatPostModel,
31
+ repository: Annotated[CatRepository, Inject(CatRepository)],
32
+ ) -> int:
33
+ cat_id = repository.next_id()
34
+ cat = Cat(
35
+ id=cat_id,
36
+ name=cat.name,
37
+ personality=cat.personality,
38
+ age=cat.age,
39
+ breed=cat.breed,
40
+ )
41
+ repository.set(cat=cat)
42
+ return cat_id
@@ -0,0 +1,16 @@
1
+ from fastapi import FastAPI
2
+
3
+ from engin import Block, invoke, provide
4
+ from examples.fastapi.routes.cats.adapters.repository import InMemoryCatRepository
5
+ from examples.fastapi.routes.cats.api import router
6
+ from examples.fastapi.routes.cats.ports import CatRepository
7
+
8
+
9
+ class CatBlock(Block):
10
+ @provide
11
+ def cat_repository(self) -> CatRepository:
12
+ return InMemoryCatRepository()
13
+
14
+ @invoke
15
+ def attach_router(self, app: FastAPI) -> None:
16
+ app.include_router(router)
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class CatPersonality(Enum):
7
+ CUTE = "CUTE"
8
+ EVIL = "EVIL"
9
+
10
+
11
+ class Cat(BaseModel):
12
+ model_config = ConfigDict(use_enum_values=True)
13
+
14
+ id: int
15
+ name: str
16
+ breed: str
17
+ age: float
18
+ personality: CatPersonality
@@ -0,0 +1,14 @@
1
+ from abc import abstractmethod
2
+
3
+ from examples.fastapi.routes.cats.domain import Cat
4
+
5
+
6
+ class CatRepository:
7
+ @abstractmethod
8
+ def get(self, cat_id: int) -> Cat: ...
9
+
10
+ @abstractmethod
11
+ def set(self, cat: Cat) -> None: ...
12
+
13
+ @abstractmethod
14
+ def next_id(self) -> int: ...
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.0.dev1"
3
+ version = "0.0.2"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -12,6 +12,7 @@ build-backend = "hatchling.build"
12
12
 
13
13
  [tool.uv]
14
14
  dev-dependencies = [
15
+ "fastapi>=0.115.6",
15
16
  "httpx>=0.27.2",
16
17
  "mypy>=1",
17
18
  "pydantic-settings>=2.6.0",
@@ -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
  ]
@@ -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
- return out
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
- return await assembled_dependency()
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
@@ -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
File without changes
@@ -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(ASGIType):
27
- raise LookupError("A provider for `ASGIType` was expected, none found")
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(ASGIType)
50
+ self._asgi_app = await self._assembler.get(self._asgi_type)
48
51
 
49
52
 
50
53
  class _Rereceive:
@@ -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)
File without changes
File without changes
File without changes
@@ -25,3 +25,23 @@ async def test_assembler_with_multiproviders():
25
25
  assembled_dependency = await assembler.assemble(Invoke(assert_all))
26
26
 
27
27
  await assembled_dependency()
28
+
29
+
30
+ async def test_assembler_providers_only_called_once():
31
+ _count = 0
32
+
33
+ def count() -> int:
34
+ nonlocal _count
35
+ _count += 1
36
+ return _count
37
+
38
+ def assert_singleton(some: int) -> None:
39
+ assert some == 1
40
+
41
+ assembler = Assembler([Provide(count)])
42
+
43
+ assembled_dependency = await assembler.assemble(Invoke(assert_singleton))
44
+ await assembled_dependency()
45
+
46
+ assembled_dependency = await assembler.assemble(Invoke(assert_singleton))
47
+ await assembled_dependency()