engin 0.1.0b5__py3-none-any.whl → 0.1.0rc2__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 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
 
@@ -111,7 +111,7 @@ class Assembler:
111
111
  type_: the type of the desired value to build.
112
112
 
113
113
  Raises:
114
- LookupError: When no provider is found for the given type.
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 LookupError(f"no provider found for target type id '{type_id}'")
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 LookupError(f"no provider found for target type id '{type_id}'")
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():
@@ -226,9 +226,7 @@ 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
- available = sorted(str(k) for k in self._providers)
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
232
  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,63 @@
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
+ available_providers = sorted(
54
+ str(provider.return_type_id) for provider in assembler.providers
55
+ )
56
+ console.print("\nAvailable providers:", style="yellow")
57
+ for available_type in available_providers:
58
+ console.print(f" • {available_type}", style="yellow")
59
+
60
+ raise typer.Exit(code=1)
61
+ else:
62
+ console.print("✅ All dependencies are satisfied!", style="green bold")
63
+ 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 get_engin_instance(app: str) -> tuple[str, str, Engin]:
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: