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/__init__.py +3 -0
- engin/_assembler.py +12 -12
- engin/_cli/__init__.py +2 -0
- engin/_cli/_check.py +56 -0
- engin/_cli/_common.py +72 -2
- engin/_cli/_graph.html +878 -73
- engin/_cli/_graph.py +128 -65
- engin/_cli/_inspect.py +10 -8
- engin/_dependency.py +18 -11
- engin/_engin.py +142 -71
- engin/_supervisor.py +137 -0
- engin/_type_utils.py +2 -2
- engin/exceptions.py +21 -6
- engin/extensions/asgi.py +2 -0
- engin/extensions/fastapi.py +2 -2
- engin-0.1.0.dist-info/METADATA +122 -0
- engin-0.1.0.dist-info/RECORD +27 -0
- engin-0.0.19.dist-info/METADATA +0 -71
- engin-0.0.19.dist-info/RECORD +0 -25
- {engin-0.0.19.dist-info → engin-0.1.0.dist-info}/WHEEL +0 -0
- {engin-0.0.19.dist-info → engin-0.1.0.dist-info}/entry_points.txt +0 -0
- {engin-0.0.19.dist-info → engin-0.1.0.dist-info}/licenses/LICENSE +0 -0
engin/__init__.py
CHANGED
@@ -4,6 +4,7 @@ from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
|
4
4
|
from engin._engin import Engin
|
5
5
|
from engin._lifecycle import Lifecycle
|
6
6
|
from engin._option import Option
|
7
|
+
from engin._supervisor import OnException, Supervisor
|
7
8
|
from engin._type_utils import TypeId
|
8
9
|
|
9
10
|
__all__ = [
|
@@ -13,8 +14,10 @@ __all__ = [
|
|
13
14
|
"Entrypoint",
|
14
15
|
"Invoke",
|
15
16
|
"Lifecycle",
|
17
|
+
"OnException",
|
16
18
|
"Option",
|
17
19
|
"Provide",
|
20
|
+
"Supervisor",
|
18
21
|
"Supply",
|
19
22
|
"TypeId",
|
20
23
|
"invoke",
|
engin/_assembler.py
CHANGED
@@ -10,7 +10,7 @@ from typing import Any, Generic, TypeVar, cast
|
|
10
10
|
|
11
11
|
from engin._dependency import Dependency, Provide, Supply
|
12
12
|
from engin._type_utils import TypeId
|
13
|
-
from engin.exceptions import NotInScopeError, ProviderError
|
13
|
+
from engin.exceptions import NotInScopeError, ProviderError, TypeNotProvidedError
|
14
14
|
|
15
15
|
LOG = logging.getLogger("engin")
|
16
16
|
|
@@ -65,7 +65,7 @@ class Assembler:
|
|
65
65
|
self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
|
66
66
|
self._assembled_outputs: dict[TypeId, Any] = {}
|
67
67
|
self._lock = asyncio.Lock()
|
68
|
-
self._graph_cache: dict[TypeId,
|
68
|
+
self._graph_cache: dict[TypeId, list[Provide]] = defaultdict(list)
|
69
69
|
|
70
70
|
for provider in providers:
|
71
71
|
type_id = provider.return_type_id
|
@@ -111,7 +111,7 @@ class Assembler:
|
|
111
111
|
type_: the type of the desired value to build.
|
112
112
|
|
113
113
|
Raises:
|
114
|
-
|
114
|
+
TypeNotProvidedError: When no provider is found for the given type.
|
115
115
|
ProviderError: When a provider errors when trying to construct the type or
|
116
116
|
any of its dependent types.
|
117
117
|
|
@@ -123,7 +123,7 @@ class Assembler:
|
|
123
123
|
return cast("T", self._assembled_outputs[type_id])
|
124
124
|
if type_id.multi:
|
125
125
|
if type_id not in self._multiproviders:
|
126
|
-
raise
|
126
|
+
raise TypeNotProvidedError(type_id)
|
127
127
|
|
128
128
|
out = []
|
129
129
|
for provider in self._multiproviders[type_id]:
|
@@ -142,7 +142,7 @@ class Assembler:
|
|
142
142
|
return out # type: ignore[return-value]
|
143
143
|
else:
|
144
144
|
if type_id not in self._providers:
|
145
|
-
raise
|
145
|
+
raise TypeNotProvidedError(type_id)
|
146
146
|
|
147
147
|
provider = self._providers[type_id]
|
148
148
|
if provider.scope and provider.scope not in _get_scope():
|
@@ -206,10 +206,10 @@ class Assembler:
|
|
206
206
|
if provider.scope == scope:
|
207
207
|
self._assembled_outputs.pop(type_id, None)
|
208
208
|
|
209
|
-
def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) ->
|
209
|
+
def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) -> Iterable[Provide]:
|
210
210
|
"""
|
211
211
|
Resolves the chain of providers required to satisfy the provider of a given type.
|
212
|
-
Ordering of the return value is very important!
|
212
|
+
Ordering of the return value is very important here!
|
213
213
|
"""
|
214
214
|
if type_id in self._graph_cache:
|
215
215
|
return self._graph_cache[type_id]
|
@@ -226,18 +226,18 @@ class Assembler:
|
|
226
226
|
# store default to prevent the warning appearing multiple times
|
227
227
|
self._multiproviders[type_id] = root_providers
|
228
228
|
else:
|
229
|
-
|
230
|
-
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
231
|
-
raise LookupError(msg)
|
229
|
+
raise TypeNotProvidedError(type_id)
|
232
230
|
|
233
231
|
# providers that must be satisfied to satisfy the root level providers
|
234
|
-
resolved_providers =
|
232
|
+
resolved_providers = [
|
235
233
|
child_provider
|
236
234
|
for root_provider in root_providers
|
237
235
|
for root_provider_param in root_provider.parameter_type_ids
|
238
236
|
for child_provider in self._resolve_providers(root_provider_param, resolved)
|
239
237
|
if root_provider_param not in resolved
|
240
|
-
|
238
|
+
]
|
239
|
+
|
240
|
+
resolved_providers.extend(root_providers)
|
241
241
|
|
242
242
|
resolved.add(type_id)
|
243
243
|
self._graph_cache[type_id] = resolved_providers
|
engin/_cli/__init__.py
CHANGED
@@ -9,6 +9,7 @@ except ImportError:
|
|
9
9
|
" `cli` extra, e.g. pip install engin[cli]"
|
10
10
|
) from None
|
11
11
|
|
12
|
+
from engin._cli._check import cli as check_cli
|
12
13
|
from engin._cli._graph import cli as graph_cli
|
13
14
|
from engin._cli._inspect import cli as inspect_cli
|
14
15
|
|
@@ -20,5 +21,6 @@ sys.path.insert(0, "")
|
|
20
21
|
|
21
22
|
app = typer.Typer()
|
22
23
|
|
24
|
+
app.add_typer(check_cli)
|
23
25
|
app.add_typer(graph_cli)
|
24
26
|
app.add_typer(inspect_cli)
|
engin/_cli/_check.py
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich.console import Console
|
5
|
+
|
6
|
+
from engin._cli._common import COMMON_HELP, get_engin_instance
|
7
|
+
from engin.exceptions import TypeNotProvidedError
|
8
|
+
|
9
|
+
cli = typer.Typer()
|
10
|
+
|
11
|
+
|
12
|
+
@cli.command(name="check")
|
13
|
+
def check_dependencies(
|
14
|
+
app: Annotated[
|
15
|
+
str | None,
|
16
|
+
typer.Argument(help=COMMON_HELP["app"]),
|
17
|
+
] = None,
|
18
|
+
) -> None:
|
19
|
+
"""
|
20
|
+
Validates that all dependencies are satisfied for the given engin instance.
|
21
|
+
|
22
|
+
This command checks that all providers required by invocations and other providers
|
23
|
+
are available. It's intended for use in CI to catch missing dependencies.
|
24
|
+
|
25
|
+
Examples:
|
26
|
+
|
27
|
+
1. `engin check`
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Exit code 0 if all dependencies are satisfied.
|
31
|
+
Exit code 1 if there are missing providers.
|
32
|
+
"""
|
33
|
+
_, _, instance = get_engin_instance(app)
|
34
|
+
|
35
|
+
console = Console()
|
36
|
+
assembler = instance.assembler
|
37
|
+
missing_providers = set()
|
38
|
+
|
39
|
+
for invocation in instance._invocations:
|
40
|
+
for param_type_id in invocation.parameter_type_ids:
|
41
|
+
try:
|
42
|
+
assembler._resolve_providers(param_type_id, set())
|
43
|
+
except TypeNotProvidedError:
|
44
|
+
missing_providers.add(param_type_id)
|
45
|
+
|
46
|
+
if missing_providers:
|
47
|
+
sorted_missing = sorted(str(type_id) for type_id in missing_providers)
|
48
|
+
|
49
|
+
console.print("❌ Missing providers found:", style="red bold")
|
50
|
+
for missing_type in sorted_missing:
|
51
|
+
console.print(f" • {missing_type}", style="red")
|
52
|
+
|
53
|
+
raise typer.Exit(code=1)
|
54
|
+
else:
|
55
|
+
console.print("✅ All dependencies are satisfied!", style="green bold")
|
56
|
+
raise typer.Exit(code=0)
|
engin/_cli/_common.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
import importlib
|
2
|
+
import sys
|
3
|
+
from pathlib import Path
|
2
4
|
from typing import Never
|
3
5
|
|
4
6
|
import typer
|
@@ -7,6 +9,11 @@ from rich.panel import Panel
|
|
7
9
|
|
8
10
|
from engin import Engin
|
9
11
|
|
12
|
+
if sys.version_info >= (3, 11):
|
13
|
+
import tomllib
|
14
|
+
else:
|
15
|
+
import tomli as tomllib
|
16
|
+
|
10
17
|
|
11
18
|
def print_error(msg: str) -> Never:
|
12
19
|
print(
|
@@ -24,12 +31,75 @@ def print_error(msg: str) -> Never:
|
|
24
31
|
COMMON_HELP = {
|
25
32
|
"app": (
|
26
33
|
"The import path of your Engin instance, in the form 'package:application'"
|
27
|
-
", e.g. 'app.main:engin'"
|
34
|
+
", e.g. 'app.main:engin'. If not provided, will try to use the `default-instance`"
|
35
|
+
" value specified in your pyproject.toml"
|
28
36
|
)
|
29
37
|
}
|
30
38
|
|
31
39
|
|
32
|
-
def
|
40
|
+
def _find_pyproject_toml() -> Path | None:
|
41
|
+
"""Find pyproject.toml file starting from current directory and walking up."""
|
42
|
+
current_path = Path.cwd()
|
43
|
+
|
44
|
+
for path in [current_path, *current_path.parents]:
|
45
|
+
pyproject_path = path / "pyproject.toml"
|
46
|
+
if pyproject_path.exists():
|
47
|
+
return pyproject_path
|
48
|
+
|
49
|
+
return None
|
50
|
+
|
51
|
+
|
52
|
+
def _get_default_engin_from_pyproject() -> str | None:
|
53
|
+
"""Get the default engin instance from pyproject.toml."""
|
54
|
+
pyproject_path = _find_pyproject_toml()
|
55
|
+
if not pyproject_path:
|
56
|
+
return None
|
57
|
+
|
58
|
+
try:
|
59
|
+
with Path(pyproject_path).open("rb") as f:
|
60
|
+
data = tomllib.load(f)
|
61
|
+
|
62
|
+
tool_section = data.get("tool", {})
|
63
|
+
engin_section = tool_section.get("engin", {})
|
64
|
+
instance = engin_section.get("default-instance")
|
65
|
+
|
66
|
+
if instance is None:
|
67
|
+
return None
|
68
|
+
|
69
|
+
if not isinstance(instance, str):
|
70
|
+
print_error("value of `default-instance` is not a string")
|
71
|
+
|
72
|
+
return instance
|
73
|
+
|
74
|
+
except (OSError, tomllib.TOMLDecodeError):
|
75
|
+
print_error("invalid toml detected")
|
76
|
+
|
77
|
+
|
78
|
+
NO_APP_FOUND_ERROR = (
|
79
|
+
"App path not specified and no default instance specified in pyproject.toml"
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
def get_engin_instance(app: str | None = None) -> tuple[str, str, Engin]:
|
84
|
+
"""
|
85
|
+
Get an Engin instance either from the provided value or from pyproject.toml.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
app: Optional string in format 'module:attribute'. If not provided will lookup in
|
89
|
+
pyproject.toml.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Tuple of (module_name, engin_name, engin_instance)
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
typer.Exit: If no app is provided and no default instance is specified in the user's
|
96
|
+
pyproject.toml.
|
97
|
+
"""
|
98
|
+
if app is None:
|
99
|
+
app = _get_default_engin_from_pyproject()
|
100
|
+
if app is None:
|
101
|
+
print_error(NO_APP_FOUND_ERROR)
|
102
|
+
|
33
103
|
try:
|
34
104
|
module_name, engin_name = app.split(":", maxsplit=1)
|
35
105
|
except ValueError:
|