engin 0.0.11__tar.gz → 0.0.13__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 (82) hide show
  1. {engin-0.0.11 → engin-0.0.13}/CHANGELOG.md +22 -0
  2. {engin-0.0.11 → engin-0.0.13}/PKG-INFO +1 -1
  3. {engin-0.0.11 → engin-0.0.13}/mkdocs.yaml +1 -1
  4. {engin-0.0.11 → engin-0.0.13}/pyproject.toml +10 -7
  5. {engin-0.0.11 → engin-0.0.13}/src/engin/_assembler.py +38 -1
  6. {engin-0.0.11 → engin-0.0.13}/src/engin/_dependency.py +9 -0
  7. {engin-0.0.11 → engin-0.0.13}/src/engin/_type_utils.py +3 -2
  8. {engin-0.0.11 → engin-0.0.13}/src/engin/ext/asgi.py +9 -2
  9. {engin-0.0.11 → engin-0.0.13}/src/engin/ext/fastapi.py +14 -10
  10. engin-0.0.13/tests/acceptance/test_fastapi.py +72 -0
  11. {engin-0.0.11 → engin-0.0.13}/tests/test_assembler.py +37 -0
  12. {engin-0.0.11 → engin-0.0.13}/tests/test_dependencies.py +16 -0
  13. {engin-0.0.11 → engin-0.0.13}/tests/test_utils.py +7 -0
  14. {engin-0.0.11 → engin-0.0.13}/uv.lock +1 -1
  15. {engin-0.0.11 → engin-0.0.13}/.github/workflows/check.yaml +0 -0
  16. {engin-0.0.11 → engin-0.0.13}/.github/workflows/publish.yaml +0 -0
  17. {engin-0.0.11 → engin-0.0.13}/.gitignore +0 -0
  18. {engin-0.0.11 → engin-0.0.13}/.readthedocs.yaml +0 -0
  19. {engin-0.0.11 → engin-0.0.13}/LICENSE +0 -0
  20. {engin-0.0.11 → engin-0.0.13}/README.md +0 -0
  21. {engin-0.0.11 → engin-0.0.13}/docs/concepts/engin.md +0 -0
  22. {engin-0.0.11 → engin-0.0.13}/docs/concepts/invocations.md +0 -0
  23. {engin-0.0.11 → engin-0.0.13}/docs/concepts/lifecycle.md +0 -0
  24. {engin-0.0.11 → engin-0.0.13}/docs/concepts/providers.md +0 -0
  25. {engin-0.0.11 → engin-0.0.13}/docs/getting-started.md +0 -0
  26. {engin-0.0.11 → engin-0.0.13}/docs/guides/dependency_injection.md +0 -0
  27. {engin-0.0.11 → engin-0.0.13}/docs/guides/fastapi-graph.png +0 -0
  28. {engin-0.0.11 → engin-0.0.13}/docs/guides/fastapi.md +0 -0
  29. {engin-0.0.11 → engin-0.0.13}/docs/index.md +0 -0
  30. {engin-0.0.11 → engin-0.0.13}/docs/js/readthedocs.js +0 -0
  31. {engin-0.0.11 → engin-0.0.13}/docs/overrides/main.html +0 -0
  32. {engin-0.0.11 → engin-0.0.13}/docs/reference.md +0 -0
  33. {engin-0.0.11 → engin-0.0.13}/examples/__init__.py +0 -0
  34. {engin-0.0.11 → engin-0.0.13}/examples/asgi/__init__.py +0 -0
  35. {engin-0.0.11 → engin-0.0.13}/examples/asgi/app.py +0 -0
  36. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/__init__.py +0 -0
  37. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/db/__init__.py +0 -0
  38. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  39. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/db/adapaters/memory.py +0 -0
  40. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/db/block.py +0 -0
  41. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/db/ports.py +0 -0
  42. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/starlette/__init__.py +0 -0
  43. {engin-0.0.11 → engin-0.0.13}/examples/asgi/common/starlette/endpoint.py +0 -0
  44. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/__init__.py +0 -0
  45. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/__init__.py +0 -0
  46. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/api/__init__.py +0 -0
  47. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/api/get.py +0 -0
  48. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/api/post.py +0 -0
  49. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/block.py +0 -0
  50. {engin-0.0.11 → engin-0.0.13}/examples/asgi/features/cats/domain.py +0 -0
  51. {engin-0.0.11 → engin-0.0.13}/examples/asgi/main.py +0 -0
  52. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/__init__.py +0 -0
  53. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/app.py +0 -0
  54. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/main.py +0 -0
  55. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/__init__.py +0 -0
  56. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/__init__.py +0 -0
  57. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  58. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  59. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/api.py +0 -0
  60. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/block.py +0 -0
  61. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/domain.py +0 -0
  62. {engin-0.0.11 → engin-0.0.13}/examples/fastapi/routes/cats/ports.py +0 -0
  63. {engin-0.0.11 → engin-0.0.13}/examples/simple/__init__.py +0 -0
  64. {engin-0.0.11 → engin-0.0.13}/examples/simple/main.py +0 -0
  65. {engin-0.0.11 → engin-0.0.13}/src/engin/__init__.py +0 -0
  66. {engin-0.0.11 → engin-0.0.13}/src/engin/_block.py +0 -0
  67. {engin-0.0.11 → engin-0.0.13}/src/engin/_engin.py +0 -0
  68. {engin-0.0.11 → engin-0.0.13}/src/engin/_exceptions.py +0 -0
  69. {engin-0.0.11 → engin-0.0.13}/src/engin/_graph.py +0 -0
  70. {engin-0.0.11 → engin-0.0.13}/src/engin/_lifecycle.py +0 -0
  71. {engin-0.0.11 → engin-0.0.13}/src/engin/ext/__init__.py +0 -0
  72. {engin-0.0.11 → engin-0.0.13}/src/engin/py.typed +0 -0
  73. {engin-0.0.11 → engin-0.0.13}/src/engin/scripts/__init__.py +0 -0
  74. {engin-0.0.11 → engin-0.0.13}/src/engin/scripts/graph.py +0 -0
  75. {engin-0.0.11 → engin-0.0.13}/tests/__init__.py +0 -0
  76. {engin-0.0.11 → engin-0.0.13}/tests/acceptance/__init__.py +0 -0
  77. {engin-0.0.11 → engin-0.0.13}/tests/acceptance/test_error_in_shutdown.py +0 -0
  78. {engin-0.0.11 → engin-0.0.13}/tests/acceptance/test_error_in_start_up.py +0 -0
  79. {engin-0.0.11 → engin-0.0.13}/tests/conftest.py +0 -0
  80. {engin-0.0.11 → engin-0.0.13}/tests/deps.py +0 -0
  81. {engin-0.0.11 → engin-0.0.13}/tests/test_engin.py +0 -0
  82. {engin-0.0.11 → engin-0.0.13}/tests/test_modules.py +0 -0
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.0.13] - 2025-03-12
10
+
11
+ ### Changed
12
+
13
+ - `Provide` now supports union types.
14
+
15
+
16
+ ## [0.0.12] - 2025-03-03
17
+
18
+ ### Added
19
+
20
+ - `Assembler` has a new method `add(provider: Provide) -> None` which allows adding a
21
+ provider to the Assembler post initialisation.
22
+
23
+ ### Changed
24
+
25
+ - `Provide` now raises a `ValueError` if the factory function is circular, i.e. one of its
26
+ parameters is the same as its return type as the behaviour of this is undefined.
27
+ - The ASGI utility method `engin_to_lifespan` has been improved so that it works "out of
28
+ the box" for more use cases now.
29
+
30
+
9
31
  ## [0.0.11] - 2025-03-02
10
32
 
11
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.11
3
+ Version: 0.0.13
4
4
  Summary: An async-first modular application framework
5
5
  Project-URL: Homepage, https://github.com/invokermain/engin
6
6
  Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
@@ -57,7 +57,7 @@ plugins:
57
57
  show_signature_annotations: true
58
58
  show_source: false
59
59
  signature_crossrefs: true
60
- import:
60
+ inventories:
61
61
  - url: https://docs.python.org/3/objects.inv
62
62
  domains: [py, std]
63
63
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.0.11"
3
+ version = "0.0.13"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -32,6 +32,7 @@ dev-dependencies = [
32
32
  "ruff>=0",
33
33
  "starlette>=0.39.2",
34
34
  "uvicorn>=0.31.1",
35
+ "pytest-cov>=6.0.0",
35
36
  ]
36
37
 
37
38
 
@@ -40,9 +41,6 @@ docs = [
40
41
  "mkdocs-material>=9.5.50",
41
42
  "mkdocstrings[python]>=0.27.0",
42
43
  ]
43
- dev = [
44
- "pytest-cov>=6.0.0",
45
- ]
46
44
 
47
45
 
48
46
  [project.scripts]
@@ -75,12 +73,17 @@ ignore = [
75
73
 
76
74
 
77
75
  [tool.pytest.ini_options]
78
- log_cli = true
76
+ log_cli = false
79
77
  log_cli_level = "DEBUG"
80
78
  asyncio_mode = "auto"
81
79
  asyncio_default_fixture_loop_scope = "session"
82
80
 
83
81
 
82
+ [tool.coverage.run]
83
+ source = ["src"]
84
+ omit = ["src/engin/scripts/**"]
85
+
86
+
84
87
  [tool.mypy]
85
88
  strict = true
86
89
  disable_error_code = [
@@ -105,6 +108,6 @@ check.sequence = [
105
108
  fix.default_item_type = "cmd"
106
109
  fix.sequence = ["ruff check src tests examples --fix"]
107
110
 
108
- test = "pytest -s tests"
109
- ci-test = "pytest -s tests --cov-branch --cov-report=xml"
111
+ test = "pytest tests"
112
+ ci-test = "pytest --cov=engin --cov-branch --cov-report=xml tests"
110
113
  docs = "mkdocs serve"
@@ -141,7 +141,44 @@ class Assembler:
141
141
  return value # type: ignore[return-value]
142
142
 
143
143
  def has(self, type_: type[T]) -> bool:
144
- return type_id_of(type_) in self._providers
144
+ """
145
+ Returns True if this Assembler has a provider for the given type.
146
+
147
+ Args:
148
+ type_: the type to check.
149
+
150
+ Returns:
151
+ True if the Assembler has a provider for type else False.
152
+ """
153
+ type_id = type_id_of(type_)
154
+ if type_id.multi:
155
+ return type_id in self._multiproviders
156
+ else:
157
+ return type_id in self._providers
158
+
159
+ def add(self, provider: Provide) -> None:
160
+ """
161
+ Add a provider to the Assembler post-initialisation.
162
+
163
+ Args:
164
+ provider: the Provide instance to add.
165
+
166
+ Returns:
167
+ None
168
+
169
+ Raises:
170
+ ValueError: if a provider for this type already exists.
171
+ """
172
+ type_id = provider.return_type_id
173
+ if provider.is_multiprovider:
174
+ if type_id in self._multiproviders:
175
+ self._multiproviders[type_id].append(provider)
176
+ else:
177
+ self._multiproviders[type_id] = [provider]
178
+ else:
179
+ if type_id in self._providers:
180
+ raise ValueError(f"A provider for '{type_id}' already exists")
181
+ self._providers[type_id] = provider
145
182
 
146
183
  def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
147
184
  if type_id.multi:
@@ -158,6 +158,15 @@ class Provide(Dependency[Any, T]):
158
158
  super().__init__(func=builder, block_name=block_name)
159
159
  self._is_multi = typing.get_origin(self.return_type) is list
160
160
 
161
+ # Validate that the provider does to depend on its own output value, as this will
162
+ # cause a recursion error and is undefined behaviour wise.
163
+ if any(
164
+ self.return_type == param.annotation
165
+ for param in self.signature.parameters.values()
166
+ ):
167
+ raise ValueError("A provider cannot depend on its own return type")
168
+
169
+ # Validate that multiproviders only return a list of one type.
161
170
  if self._is_multi:
162
171
  args = typing.get_args(self.return_type)
163
172
  if len(args) != 1:
@@ -1,8 +1,9 @@
1
1
  import typing
2
2
  from dataclasses import dataclass
3
+ from types import UnionType
3
4
  from typing import Any
4
5
 
5
- _implict_modules = ["builtins", "typing", "collections.abc"]
6
+ _implict_modules = ["builtins", "typing", "collections.abc", "types"]
6
7
 
7
8
 
8
9
  @dataclass(frozen=True, eq=True, slots=True)
@@ -43,7 +44,7 @@ class TypeId:
43
44
  def _args_to_str(type_: Any) -> str:
44
45
  args = typing.get_args(type_)
45
46
  if args:
46
- arg_str = f"{type_.__name__}["
47
+ arg_str = "Union[" if isinstance(type_, UnionType) else f"{type_.__name__}["
47
48
  for idx, arg in enumerate(args):
48
49
  if isinstance(arg, list):
49
50
  arg_str += "["
@@ -1,9 +1,10 @@
1
+ import contextlib
1
2
  import traceback
2
3
  from collections.abc import AsyncIterator, Awaitable, Callable, MutableMapping
3
4
  from contextlib import AbstractAsyncContextManager, asynccontextmanager
4
5
  from typing import Any, ClassVar, Protocol, TypeAlias
5
6
 
6
- from engin import Engin, Entrypoint, Option
7
+ from engin import Engin, Entrypoint, Option, Supply
7
8
 
8
9
  __all__ = ["ASGIEngin", "ASGIType", "engin_to_lifespan"]
9
10
 
@@ -79,7 +80,13 @@ def engin_to_lifespan(engin: Engin) -> Callable[[ASGIType], AbstractAsyncContext
79
80
  """
80
81
 
81
82
  @asynccontextmanager
82
- async def engin_lifespan(_: ASGIType) -> AsyncIterator[None]:
83
+ async def engin_lifespan(app: ASGIType) -> AsyncIterator[None]:
84
+ # ensure the Engin
85
+ with contextlib.suppress(ValueError):
86
+ engin.assembler.add(Supply(app))
87
+
88
+ app.state.assembler = engin.assembler # type: ignore[attr-defined]
89
+
83
90
  await engin.start()
84
91
  yield
85
92
  await engin.stop()
@@ -6,7 +6,7 @@ from typing import ClassVar, TypeVar
6
6
 
7
7
  from fastapi.routing import APIRoute
8
8
 
9
- from engin import Engin, Entrypoint, Invoke, Option
9
+ from engin import Assembler, Engin, Entrypoint, Invoke, Option
10
10
  from engin._dependency import Dependency, Supply, _noop
11
11
  from engin._graph import DependencyGrapher, Node
12
12
  from engin._type_utils import TypeId, type_id_of
@@ -24,15 +24,16 @@ except ImportError as err:
24
24
  __all__ = ["APIRouteDependency", "FastAPIEngin", "Inject"]
25
25
 
26
26
 
27
- def _attach_engin(
28
- app: FastAPI,
29
- engin: Engin,
30
- ) -> None:
31
- app.state.engin = engin
27
+ def _attach_assembler(app: FastAPI, engin: Engin) -> None:
28
+ """
29
+ An invocation that attaches the Engin's Assembler to the FastAPI application, enabling
30
+ the Inject marker.
31
+ """
32
+ app.state.assembler = engin.assembler
32
33
 
33
34
 
34
35
  class FastAPIEngin(ASGIEngin):
35
- _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
36
+ _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_assembler)]
36
37
  _asgi_type = FastAPI
37
38
 
38
39
  def graph(self) -> list[Node]:
@@ -40,7 +41,7 @@ class FastAPIEngin(ASGIEngin):
40
41
  return grapher.resolve(
41
42
  [
42
43
  Entrypoint(self._asgi_type),
43
- *[i for i in self._invocations if i.func_name != "_attach_engin"],
44
+ *[i for i in self._invocations if i.func_name != "_attach_assembler"],
44
45
  ]
45
46
  )
46
47
 
@@ -50,8 +51,11 @@ T = TypeVar("T")
50
51
 
51
52
  def Inject(interface: type[T]) -> Depends:
52
53
  async def inner(conn: HTTPConnection) -> T:
53
- engin: Engin = conn.app.state.engin
54
- return await engin.assembler.get(interface)
54
+ try:
55
+ assembler: Assembler = conn.app.state.assembler
56
+ except AttributeError:
57
+ raise RuntimeError("Assembler is not attached to Application state") from None
58
+ return await assembler.get(interface)
55
59
 
56
60
  dep = Depends(inner)
57
61
  dep.__engin__ = True # type: ignore[attr-defined]
@@ -0,0 +1,72 @@
1
+ from typing import Annotated
2
+
3
+ import pytest
4
+ import starlette.testclient
5
+ from fastapi import APIRouter, FastAPI
6
+
7
+ from engin import Engin, Provide, Supply
8
+ from engin.ext.asgi import engin_to_lifespan
9
+ from engin.ext.fastapi import APIRouteDependency, FastAPIEngin, Inject
10
+
11
+ ROUTER = APIRouter(prefix="")
12
+
13
+
14
+ @ROUTER.get("/")
15
+ async def hello_world() -> str:
16
+ return "hello world"
17
+
18
+
19
+ @ROUTER.get("/inject")
20
+ async def route_with_dep(some_int: Annotated[int, Inject(int)]) -> int:
21
+ return some_int
22
+
23
+
24
+ def app_factory(routers: list[APIRouter]) -> FastAPI:
25
+ app = FastAPI()
26
+ for router in routers:
27
+ app.include_router(router)
28
+ return app
29
+
30
+
31
+ async def test_fastapi():
32
+ engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]))
33
+
34
+ with starlette.testclient.TestClient(engin) as client:
35
+ result = client.get("http://127.0.0.1:8000/")
36
+
37
+ assert result.json() == "hello world"
38
+
39
+
40
+ async def test_inject():
41
+ engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]), Supply(10))
42
+
43
+ with starlette.testclient.TestClient(engin) as client:
44
+ result = client.get("http://127.0.0.1:8000/inject")
45
+
46
+ assert result.json() == 10
47
+
48
+
49
+ async def test_graph():
50
+ engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]), Supply(10))
51
+
52
+ nodes = engin.graph()
53
+
54
+ assert len(nodes) == 5
55
+ assert len([node for node in nodes if isinstance(node.node, APIRouteDependency)]) == 2
56
+
57
+
58
+ async def test_invalid_engin():
59
+ with pytest.raises(LookupError, match="FastAPI"):
60
+ FastAPIEngin()
61
+
62
+
63
+ async def test_engin_to_lifespan():
64
+ engin = Engin(Supply(10))
65
+
66
+ app = FastAPI(lifespan=engin_to_lifespan(engin))
67
+ app.include_router(ROUTER)
68
+
69
+ with starlette.testclient.TestClient(app) as client:
70
+ result = client.get("http://127.0.0.1:8000/inject")
71
+
72
+ assert result.json() == 10
@@ -83,3 +83,40 @@ async def test_annotations():
83
83
 
84
84
  assert await assembler.get(Annotated[str, "1"]) == "bar"
85
85
  assert await assembler.get(Annotated[str, "2"]) == "foo"
86
+
87
+
88
+ async def test_assembler_has():
89
+ def make_str() -> str:
90
+ raise RuntimeError("foo")
91
+
92
+ assembler = Assembler([Provide(make_str)])
93
+
94
+ assert assembler.has(str)
95
+ assert not assembler.has(int)
96
+ assert not assembler.has(list[str])
97
+
98
+
99
+ async def test_assembler_has_multi():
100
+ def make_str() -> list[str]:
101
+ raise RuntimeError("foo")
102
+
103
+ assembler = Assembler([Provide(make_str)])
104
+
105
+ assert assembler.has(list[str])
106
+ assert not assembler.has(int)
107
+ assert not assembler.has(str)
108
+
109
+
110
+ async def test_assembler_add():
111
+ assembler = Assembler([])
112
+ assembler.add(Provide(make_int))
113
+ assembler.add(Provide(make_many_int))
114
+
115
+ assert assembler.has(int)
116
+ assert assembler.has(list[int])
117
+
118
+ with pytest.raises(ValueError, match="exists"):
119
+ assembler.add(Provide(make_int))
120
+
121
+ # can always add more multiproviders
122
+ assembler.add(Provide(make_many_int))
@@ -1,5 +1,7 @@
1
1
  from typing import Annotated
2
2
 
3
+ import pytest
4
+
3
5
  from engin import Provide
4
6
  from engin._dependency import Entrypoint, Supply
5
7
  from tests.deps import make_aliased_int, make_int
@@ -75,3 +77,17 @@ def test_dependency_sources():
75
77
  entrypoint = Entrypoint(3)
76
78
  assert entrypoint.source_module == "tests.test_dependencies"
77
79
  assert entrypoint.source_package == "tests"
80
+
81
+
82
+ def test_provider_cannot_depend_on_self():
83
+ def invalid_provider_1(a: int) -> int:
84
+ return 1
85
+
86
+ def invalid_provider_2(a: list[int]) -> list[int]:
87
+ return [1]
88
+
89
+ with pytest.raises(ValueError, match="return type"):
90
+ Provide(invalid_provider_1)
91
+
92
+ with pytest.raises(ValueError, match="return type"):
93
+ Provide(invalid_provider_2)
@@ -60,3 +60,10 @@ def test_type_id_of_complex_annotation():
60
60
  type_id = type_id_of(annotated)
61
61
  assert type_id == TypeId(type=Annotated[Callable[[int], str], "Activity"], multi=False)
62
62
  assert str(type_id) == "Annotated[Callable[[int], str], Activity]"
63
+
64
+
65
+ def test_type_id_of_union():
66
+ union = int | str
67
+ type_id = type_id_of(union)
68
+ assert type_id == TypeId(type=int | str, multi=False)
69
+ assert str(type_id) == "Union[int, str]"
@@ -206,7 +206,7 @@ toml = [
206
206
 
207
207
  [[package]]
208
208
  name = "engin"
209
- version = "0.0.11"
209
+ version = "0.0.13"
210
210
  source = { editable = "." }
211
211
 
212
212
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes