cmdbox-cli 1.0.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.
- cmdbox/__init__.py +0 -0
- cmdbox/cli/__init__.py +0 -0
- cmdbox/cli/app.py +125 -0
- cmdbox/cli/commands/__init__.py +0 -0
- cmdbox/cli/commands/alias_fallback.py +102 -0
- cmdbox/cli/commands/command_crud.py +429 -0
- cmdbox/cli/commands/command_run.py +255 -0
- cmdbox/cli/commands/history.py +109 -0
- cmdbox/cli/commands/init.py +54 -0
- cmdbox/cli/commands/settings.py +62 -0
- cmdbox/cli/commands/tag_crud.py +277 -0
- cmdbox/cli/commands/variable_crud.py +349 -0
- cmdbox/cli/common/__init__.py +0 -0
- cmdbox/cli/common/errors.py +58 -0
- cmdbox/cli/common/update_fields.py +88 -0
- cmdbox/cli/completions/__init__.py +0 -0
- cmdbox/cli/completions/commands.py +26 -0
- cmdbox/cli/completions/fields.py +31 -0
- cmdbox/cli/completions/tags.py +24 -0
- cmdbox/cli/completions/variables.py +26 -0
- cmdbox/cli/handlers/__init__.py +0 -0
- cmdbox/cli/handlers/command_handlers.py +357 -0
- cmdbox/cli/handlers/common_handlers.py +15 -0
- cmdbox/cli/handlers/history_handlers.py +94 -0
- cmdbox/cli/handlers/init_handler.py +127 -0
- cmdbox/cli/handlers/run_handler.py +178 -0
- cmdbox/cli/handlers/settings_handler.py +59 -0
- cmdbox/cli/handlers/tag_handlers.py +220 -0
- cmdbox/cli/handlers/variable_handlers.py +272 -0
- cmdbox/cli/prompts/__init__.py +0 -0
- cmdbox/cli/prompts/completers.py +161 -0
- cmdbox/cli/prompts/prompts.py +108 -0
- cmdbox/cli/prompts/validators.py +46 -0
- cmdbox/cli/ui/__init__.py +0 -0
- cmdbox/cli/ui/console.py +31 -0
- cmdbox/cli/ui/editor.py +141 -0
- cmdbox/cli/ui/presenters/__init__.py +0 -0
- cmdbox/cli/ui/presenters/app_presenter.py +8 -0
- cmdbox/cli/ui/presenters/command_presenter.py +168 -0
- cmdbox/cli/ui/presenters/history_presenter.py +83 -0
- cmdbox/cli/ui/presenters/init_instructions.py +52 -0
- cmdbox/cli/ui/presenters/init_presenter.py +57 -0
- cmdbox/cli/ui/presenters/result_presenter.py +144 -0
- cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
- cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
- cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
- cmdbox/cli/ui/primitives.py +410 -0
- cmdbox/cli/ui/theme.py +43 -0
- cmdbox/cli/ui/theme_builder.py +49 -0
- cmdbox/common/__init__.py +0 -0
- cmdbox/common/io.py +34 -0
- cmdbox/container.py +156 -0
- cmdbox/core/__init__.py +0 -0
- cmdbox/core/fields.py +48 -0
- cmdbox/core/paths.py +52 -0
- cmdbox/database.py +65 -0
- cmdbox/exceptions.py +10 -0
- cmdbox/init/__init__.py +0 -0
- cmdbox/init/detect.py +82 -0
- cmdbox/init/integrations/bash.sh +10 -0
- cmdbox/init/integrations/cmd.bat +14 -0
- cmdbox/init/integrations/fish.fish +11 -0
- cmdbox/init/integrations/powershell.ps1 +14 -0
- cmdbox/init/integrations/zsh.sh +10 -0
- cmdbox/init/io.py +68 -0
- cmdbox/init/specs.py +54 -0
- cmdbox/logging_setup/__init__.py +0 -0
- cmdbox/logging_setup/log_config.py +123 -0
- cmdbox/logging_setup/log_decorators.py +40 -0
- cmdbox/logging_setup/log_handlers.py +94 -0
- cmdbox/migrations/__init__.py +1 -0
- cmdbox/migrations/errors.py +10 -0
- cmdbox/migrations/runner.py +127 -0
- cmdbox/migrations/versions/__init__.py +0 -0
- cmdbox/models.py +165 -0
- cmdbox/repositories/__init__.py +0 -0
- cmdbox/repositories/base_repository.py +181 -0
- cmdbox/repositories/command_repository.py +391 -0
- cmdbox/repositories/errors.py +120 -0
- cmdbox/repositories/history_repository.py +155 -0
- cmdbox/repositories/results.py +37 -0
- cmdbox/repositories/tag_repository.py +91 -0
- cmdbox/repositories/validators.py +256 -0
- cmdbox/repositories/variable_repository.py +324 -0
- cmdbox/resolve/__init__.py +0 -0
- cmdbox/resolve/errors.py +65 -0
- cmdbox/resolve/lookup.py +137 -0
- cmdbox/resolve/resolver.py +402 -0
- cmdbox/resolve/type_defs.py +96 -0
- cmdbox/runtime/__init__.py +0 -0
- cmdbox/runtime/executor.py +454 -0
- cmdbox/runtime/results.py +25 -0
- cmdbox/runtime/shell.py +90 -0
- cmdbox/services/__init__.py +0 -0
- cmdbox/services/command_services.py +261 -0
- cmdbox/services/errors.py +37 -0
- cmdbox/services/field_selection.py +162 -0
- cmdbox/services/history_service.py +68 -0
- cmdbox/services/run_service.py +204 -0
- cmdbox/services/tag_services.py +134 -0
- cmdbox/services/variable_services.py +224 -0
- cmdbox/settings/__init__.py +0 -0
- cmdbox/settings/models.py +129 -0
- cmdbox/settings/settings_repository.py +36 -0
- cmdbox/settings/settings_service.py +144 -0
- cmdbox/version.py +1 -0
- cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
- cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
- cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
- cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
cmdbox/resolve/errors.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from cmdbox.exceptions import CmdboxError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ResolutionError(CmdboxError):
|
|
5
|
+
"""
|
|
6
|
+
Represents an error encountered during resolution processes.
|
|
7
|
+
|
|
8
|
+
This exception is raised when an error is encountered during the command
|
|
9
|
+
template resolution process. For example: a circular reference in which
|
|
10
|
+
command A references command B, which references command A.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandSyntaxError(ResolutionError):
|
|
17
|
+
"""
|
|
18
|
+
Represents an error related to command syntax.
|
|
19
|
+
|
|
20
|
+
This class is used to handle exceptions that occur due to syntax errors
|
|
21
|
+
in command parsing or execution.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnknownReference(ResolutionError):
|
|
28
|
+
"""
|
|
29
|
+
Represents an error related to references in a string being resolved.
|
|
30
|
+
|
|
31
|
+
This class is used to handle exceptions when a reference is stored in a
|
|
32
|
+
string being resolved, but the reference cannot be found in the database.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, kind: str, key: str):
|
|
36
|
+
super().__init__(f"Unknown {kind}: {key}")
|
|
37
|
+
self.kind = kind
|
|
38
|
+
self.key = key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MaxDepthExceeded(ResolutionError):
|
|
42
|
+
"""
|
|
43
|
+
Exception raised when the maximum depth is exceeded.
|
|
44
|
+
|
|
45
|
+
Represents a specific error condition where a resolution process exceeds
|
|
46
|
+
the allowed or configured maximum depth. This exception is typically used
|
|
47
|
+
to prevent excessively deep recursion by enforcing depth limits.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, max_depth: int):
|
|
51
|
+
super().__init__(f"Maximum resolution depth exceeded: ({max_depth})")
|
|
52
|
+
self.max_depth = max_depth
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CycleDetectionError(ResolutionError):
|
|
56
|
+
"""
|
|
57
|
+
An error raised when a circular reference is detected.
|
|
58
|
+
|
|
59
|
+
Raised when a circular reference is detected while attempting to resolve
|
|
60
|
+
a string. Ex: Command A references command B, which references command A.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, path: list[str]):
|
|
64
|
+
super().__init__(f"Cycle detected: {' -> '.join(path)}")
|
|
65
|
+
self.path = path
|
cmdbox/resolve/lookup.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from typing import Protocol, Optional
|
|
2
|
+
|
|
3
|
+
from .type_defs import CommandRecord, VariableRecord
|
|
4
|
+
from ..repositories.command_repository import CommandRepository
|
|
5
|
+
from ..repositories.variable_repository import VariableRepository
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResolverLookup(Protocol):
|
|
9
|
+
"""
|
|
10
|
+
Protocol for resolving commands and variables.
|
|
11
|
+
|
|
12
|
+
This protocol defines the interface for looking up command and variable
|
|
13
|
+
definitions based on their respective identifiers. It serves as a contract
|
|
14
|
+
for implementing classes to provide resolution mechanisms for commands and
|
|
15
|
+
variables. Implementers of this protocol must define the behavior for
|
|
16
|
+
retrieving both command and variable records, ensuring consistency and
|
|
17
|
+
reliability across different resolutions.
|
|
18
|
+
|
|
19
|
+
Methods:
|
|
20
|
+
get_command(alias: str) -> Optional[CommandRecord]:
|
|
21
|
+
Retrieves the CommandRecord associated with the given alias, if it
|
|
22
|
+
exists.
|
|
23
|
+
|
|
24
|
+
get_variable(name: str) -> Optional[VariableRecord]:
|
|
25
|
+
Retrieves the VariableRecord associated with the given name, if it
|
|
26
|
+
exists.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get_command(self, alias: str) -> Optional[CommandRecord]:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def get_variable(self, name: str) -> Optional[VariableRecord]:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RepoLookup(ResolverLookup):
|
|
37
|
+
"""
|
|
38
|
+
Provides lookup functionality for commands and variables stored in repositories.
|
|
39
|
+
|
|
40
|
+
This class serves as an adapter for resolving commands and variables from their
|
|
41
|
+
respective repositories. Command and variable records can be retrieved based on
|
|
42
|
+
aliases or names respectively. It is intended to abstract the underlying repository
|
|
43
|
+
interaction by providing an easy interface for lookups.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
cmd_repo (CommandRepository): Repository for storing and retrieving command records.
|
|
47
|
+
var_repo (VariableRepository): Repository for managing and accessing variable records.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, cmd_repo: CommandRepository, var_repo: VariableRepository):
|
|
51
|
+
self._cmd_repo = cmd_repo
|
|
52
|
+
self._var_repo = var_repo
|
|
53
|
+
|
|
54
|
+
def get_command(self, alias: str) -> Optional[CommandRecord]:
|
|
55
|
+
cmd = self._cmd_repo.get_by_alias(alias)
|
|
56
|
+
if cmd is None:
|
|
57
|
+
return None
|
|
58
|
+
return CommandRecord(alias=cmd.alias, template=cmd.template)
|
|
59
|
+
|
|
60
|
+
def get_variable(self, name: str) -> Optional[VariableRecord]:
|
|
61
|
+
var = self._var_repo.get_by_name(name)
|
|
62
|
+
if var is None:
|
|
63
|
+
return None
|
|
64
|
+
return VariableRecord(name=var.name, value=var.value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MemoizedLookup(ResolverLookup):
|
|
68
|
+
"""
|
|
69
|
+
Caches and retrieves command and variable lookups for improved performance.
|
|
70
|
+
|
|
71
|
+
The MemoizedLookup class acts as a wrapper for a ResolverLookup instance,
|
|
72
|
+
adding caching functionality to minimize redundant lookups. Commands and
|
|
73
|
+
variables are cached after their first retrieval, improving performance
|
|
74
|
+
for subsequent requests. Use this class when frequent lookups are
|
|
75
|
+
expected and caching them can significantly enhance speed.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
inner (ResolverLookup): The wrapped ResolverLookup instance used for
|
|
79
|
+
retrieving commands and variables.
|
|
80
|
+
cmd_cache (dict): A dictionary used to cache CommandRecord results
|
|
81
|
+
for quick retrieval based on their aliases.
|
|
82
|
+
var_cache (dict): A dictionary used to cache VariableRecord results
|
|
83
|
+
for quick retrieval based on their names.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, inner: ResolverLookup):
|
|
87
|
+
self._inner = inner
|
|
88
|
+
self._cmd_cache: dict[str, Optional[CommandRecord]] = {}
|
|
89
|
+
self._var_cache: dict[str, Optional[VariableRecord]] = {}
|
|
90
|
+
|
|
91
|
+
def get_command(self, alias: str) -> Optional[CommandRecord]:
|
|
92
|
+
"""
|
|
93
|
+
Retrieves a command by its alias, leveraging the cache for faster access.
|
|
94
|
+
|
|
95
|
+
This method checks if the command associated with the given alias exists
|
|
96
|
+
in the cache. If found, it retrieves the command from the cache. If not,
|
|
97
|
+
it fetches the command from an internal source, stores it in the cache,
|
|
98
|
+
and then returns it.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
alias (str): The alias of the command to retrieve.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Optional[CommandRecord]: The command associated with the alias if
|
|
105
|
+
it exists, otherwise None.
|
|
106
|
+
"""
|
|
107
|
+
if alias in self._cmd_cache:
|
|
108
|
+
return self._cmd_cache[alias]
|
|
109
|
+
cmd = self._inner.get_command(alias)
|
|
110
|
+
self._cmd_cache[alias] = cmd
|
|
111
|
+
return cmd
|
|
112
|
+
|
|
113
|
+
def get_variable(self, name: str) -> Optional[VariableRecord]:
|
|
114
|
+
"""
|
|
115
|
+
Retrieves a variable by its alias, leveraging the cache for faster access.
|
|
116
|
+
|
|
117
|
+
This method checks if the variable associated with the given name exists
|
|
118
|
+
in the cache. If found, it retrieves the variable from the cache. If not,
|
|
119
|
+
it fetches the variable from an internal source, stores it in the cache,
|
|
120
|
+
and then returns it.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name (str): The name of the variable to retrieve.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Optional[VariableRecord]: The variable associated with the alias if
|
|
127
|
+
it exists, otherwise None.
|
|
128
|
+
"""
|
|
129
|
+
if name in self._var_cache:
|
|
130
|
+
return self._var_cache[name]
|
|
131
|
+
var = self._inner.get_variable(name)
|
|
132
|
+
self._var_cache[name] = var
|
|
133
|
+
return var
|
|
134
|
+
|
|
135
|
+
def clear(self) -> None:
|
|
136
|
+
self._cmd_cache.clear()
|
|
137
|
+
self._var_cache.clear()
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .errors import MaxDepthExceeded, UnknownReference, CycleDetectionError
|
|
4
|
+
from .lookup import ResolverLookup
|
|
5
|
+
from .type_defs import ResolveResult, TraceStep, RefKind
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Resolver:
|
|
9
|
+
"""
|
|
10
|
+
Resolves templates by substituting tokens enclosed in angular brackets (<...>) with their
|
|
11
|
+
corresponding values fetched from a predefined lookup. Supports variable and command
|
|
12
|
+
resolution, recursive expansion, and cycle detection.
|
|
13
|
+
|
|
14
|
+
This class is designed to process textual templates with customizable behavior. It provides
|
|
15
|
+
support for tracing resolution steps, enforcing recursion depth limits, and handling both
|
|
16
|
+
commands and variables with a flexible lookup mechanism.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
lookup (ResolverLookup): A lookup mechanism for resolving commands and variables.
|
|
20
|
+
strict (bool): Enforces strict error handling. If True, raises errors for unresolved
|
|
21
|
+
tokens; otherwise, leaves unresolved tokens in their original form.
|
|
22
|
+
max_depth (int): The allowed maximum depth for nested token substitutions. Prevents
|
|
23
|
+
excessively deep or recursive expansions. Defaults to 25.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, lookup: ResolverLookup, *, strict: bool = False, max_depth: int = 25
|
|
28
|
+
):
|
|
29
|
+
self._lookup = lookup
|
|
30
|
+
self._strict = strict
|
|
31
|
+
self._max_depth = max_depth
|
|
32
|
+
|
|
33
|
+
def resolve(
|
|
34
|
+
self,
|
|
35
|
+
template: str,
|
|
36
|
+
root_label: str = "<input>",
|
|
37
|
+
runtime_vars: dict[str, str] | None = None,
|
|
38
|
+
) -> ResolveResult:
|
|
39
|
+
"""
|
|
40
|
+
Resolves a given template string by iterating through its elements and performing
|
|
41
|
+
text and stack manipulations until a final result is obtained. This method employs
|
|
42
|
+
recursive resolution using internal helper functions and produces a structured
|
|
43
|
+
output containing the resolved text and trace steps.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
template (str): The template string to resolve.
|
|
47
|
+
root_label (str): The initial label to use as the root for resolution. Defaults
|
|
48
|
+
to "<input>".
|
|
49
|
+
runtime_vars (dict[str, str] | None): Variables supplied by the user at runtime
|
|
50
|
+
to be used during resolution. Defaults to None.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
ResolveResult: The result of the resolution, including the resolved text
|
|
54
|
+
and the list of trace steps.
|
|
55
|
+
"""
|
|
56
|
+
trace: list[TraceStep] = []
|
|
57
|
+
stack: list[str] = [root_label]
|
|
58
|
+
text = self._resolve_inner(
|
|
59
|
+
template,
|
|
60
|
+
stack=stack,
|
|
61
|
+
depth=0,
|
|
62
|
+
trace=trace,
|
|
63
|
+
runtime_vars=runtime_vars or {},
|
|
64
|
+
)
|
|
65
|
+
return ResolveResult(text=text, trace=trace)
|
|
66
|
+
|
|
67
|
+
def collect_missing_vars(
|
|
68
|
+
self, template: str, runtime_vars: dict[str, str] | None = None
|
|
69
|
+
) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
Collects missing variables from the given template.
|
|
72
|
+
|
|
73
|
+
This function analyzes the provided template and checks for any placeholders
|
|
74
|
+
or variables that are not present in the runtime variables provided. It
|
|
75
|
+
returns a list of all such missing variables that need to be defined or
|
|
76
|
+
replaced in the template.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
template (str): The template string containing placeholder variables
|
|
80
|
+
to be checked.
|
|
81
|
+
runtime_vars (dict[str, str] | None): A dictionary of available
|
|
82
|
+
runtime variables where keys are variable names and values are
|
|
83
|
+
their corresponding values. Defaults to None if not provided.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
list[str]: A list of variable names that are missing in the provided
|
|
87
|
+
runtime variables.
|
|
88
|
+
"""
|
|
89
|
+
missing: list[str] = []
|
|
90
|
+
self._collect_missing_inner(
|
|
91
|
+
template,
|
|
92
|
+
runtime_vars=runtime_vars or {},
|
|
93
|
+
missing=missing,
|
|
94
|
+
seen=set(),
|
|
95
|
+
)
|
|
96
|
+
return missing
|
|
97
|
+
|
|
98
|
+
def _resolve_inner(
|
|
99
|
+
self,
|
|
100
|
+
template: str,
|
|
101
|
+
*,
|
|
102
|
+
stack: list[str],
|
|
103
|
+
depth: int,
|
|
104
|
+
trace: list[TraceStep],
|
|
105
|
+
runtime_vars: dict[str, str] | None = None,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Resolves the given template string with nested tokens, handling escapes and
|
|
109
|
+
recursively processing angle-bracketed tokens until the maximum depth or the
|
|
110
|
+
end of the template is reached.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
template: The template string containing tokens to resolve.
|
|
114
|
+
stack: The stack of currently resolved tokens to avoid circular references.
|
|
115
|
+
depth: The current recursion depth, used to track and prevent exceeding the
|
|
116
|
+
maximum allowed depth.
|
|
117
|
+
trace: A list of TraceStep objects for debugging and understanding the
|
|
118
|
+
resolution flow.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A resolved string with tokens replaced by their corresponding values or
|
|
122
|
+
content.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
MaxDepthExceeded: When the recursion depth exceeds the allowed maximum.
|
|
126
|
+
"""
|
|
127
|
+
if depth > self._max_depth:
|
|
128
|
+
raise MaxDepthExceeded(self._max_depth)
|
|
129
|
+
|
|
130
|
+
out: list[str] = []
|
|
131
|
+
i = 0 # Current index
|
|
132
|
+
t_len = len(template)
|
|
133
|
+
|
|
134
|
+
while i < t_len:
|
|
135
|
+
ch = template[i]
|
|
136
|
+
if ch == "\\":
|
|
137
|
+
if i + 1 < t_len and template[i + 1] in ("\\", "<", ">"):
|
|
138
|
+
out.append(template[i + 1])
|
|
139
|
+
i += 2
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if ch == "<":
|
|
143
|
+
token_inner, raw_token, next_i = self._read_angle_token(template, i)
|
|
144
|
+
if token_inner is None:
|
|
145
|
+
out.append("<")
|
|
146
|
+
i += 1
|
|
147
|
+
continue
|
|
148
|
+
replacement = self._expand_angle_token(
|
|
149
|
+
token_inner,
|
|
150
|
+
raw_token=raw_token,
|
|
151
|
+
stack=stack,
|
|
152
|
+
depth=depth,
|
|
153
|
+
trace=trace,
|
|
154
|
+
runtime_vars=runtime_vars,
|
|
155
|
+
)
|
|
156
|
+
out.append(replacement)
|
|
157
|
+
i = next_i
|
|
158
|
+
continue
|
|
159
|
+
out.append(ch)
|
|
160
|
+
i += 1
|
|
161
|
+
|
|
162
|
+
return "".join(out)
|
|
163
|
+
|
|
164
|
+
def _collect_missing_inner(
|
|
165
|
+
self,
|
|
166
|
+
template: str,
|
|
167
|
+
*,
|
|
168
|
+
runtime_vars: dict[str, str],
|
|
169
|
+
missing: list[str],
|
|
170
|
+
seen: set[str],
|
|
171
|
+
depth: int = 0,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Recursively collects missing variables and commands in the template by analyzing
|
|
175
|
+
placeholders and their references. It ensures that variable and command dependencies
|
|
176
|
+
are resolved to either runtime variables or previously defined variables or commands.
|
|
177
|
+
If a placeholder cannot be resolved within the specified depth, it is added to the
|
|
178
|
+
missing list for further processing.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
template: The template string containing placeholders to analyze.
|
|
182
|
+
runtime_vars: A dictionary mapping variable names to their values. Runtime
|
|
183
|
+
variables are used to satisfy some of the placeholders in the template.
|
|
184
|
+
missing: A list used to collect the names of unresolved variable placeholders.
|
|
185
|
+
seen: A set used to track the names of variables and commands that have already
|
|
186
|
+
been processed, avoiding circular dependencies during recursion.
|
|
187
|
+
depth: The current recursion depth. Defaults to 0. If the recursion depth exceeds
|
|
188
|
+
the allowed maximum (self._max_depth), a MaxDepthExceeded exception is raised.
|
|
189
|
+
"""
|
|
190
|
+
if depth > self._max_depth:
|
|
191
|
+
return
|
|
192
|
+
i = 0
|
|
193
|
+
while i < len(template):
|
|
194
|
+
if template[i] == "\\":
|
|
195
|
+
i += 2
|
|
196
|
+
continue
|
|
197
|
+
if template[i] == "<":
|
|
198
|
+
token_inner, _, next_i = self._read_angle_token(template, i)
|
|
199
|
+
if token_inner:
|
|
200
|
+
kind, key = self._parse_kind_and_key(token_inner)
|
|
201
|
+
if kind == RefKind.VARIABLE:
|
|
202
|
+
if runtime_vars and key in runtime_vars:
|
|
203
|
+
pass # satisfied
|
|
204
|
+
elif self._lookup.get_variable(key) is None:
|
|
205
|
+
if key not in missing:
|
|
206
|
+
missing.append(key)
|
|
207
|
+
else:
|
|
208
|
+
# Recurse into stored variable's value
|
|
209
|
+
rec = self._lookup.get_variable(key)
|
|
210
|
+
if rec and rec.name not in seen:
|
|
211
|
+
seen.add(rec.name)
|
|
212
|
+
self._collect_missing_inner(
|
|
213
|
+
rec.value,
|
|
214
|
+
runtime_vars=runtime_vars,
|
|
215
|
+
missing=missing,
|
|
216
|
+
seen=seen,
|
|
217
|
+
depth=depth + 1,
|
|
218
|
+
)
|
|
219
|
+
else: # COMMAND ref
|
|
220
|
+
rec = self._lookup.get_command(key)
|
|
221
|
+
if rec and rec.alias not in seen:
|
|
222
|
+
seen.add(rec.alias)
|
|
223
|
+
self._collect_missing_inner(
|
|
224
|
+
rec.template,
|
|
225
|
+
runtime_vars=runtime_vars,
|
|
226
|
+
missing=missing,
|
|
227
|
+
seen=seen,
|
|
228
|
+
depth=depth + 1,
|
|
229
|
+
)
|
|
230
|
+
i = next_i
|
|
231
|
+
else:
|
|
232
|
+
i += 1
|
|
233
|
+
|
|
234
|
+
def _read_angle_token(self, s: str, start_i: int) -> tuple[Optional[str], str, int]:
|
|
235
|
+
"""
|
|
236
|
+
Reads a token beginning with "<" until an unescaped ">" is encountered.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
s (str): The string containing the token.
|
|
240
|
+
start_i (int): The index of the first character of the token.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
str: token_inner (with escapes already interpreted), or None if no closing bracket.
|
|
244
|
+
str: raw_token - the original token string, including the angle brackets.
|
|
245
|
+
int: next_i - the index of the first character after the closing bracket.
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
assert s[start_i] == "<"
|
|
249
|
+
i = start_i + 1
|
|
250
|
+
n = len(s)
|
|
251
|
+
|
|
252
|
+
inner_chars: list[str] = []
|
|
253
|
+
|
|
254
|
+
while i < n:
|
|
255
|
+
ch = s[i]
|
|
256
|
+
if ch == "\\":
|
|
257
|
+
if i + 1 < n and s[i + 1] in ("\\", "<", ">"):
|
|
258
|
+
inner_chars.append(s[i + 1])
|
|
259
|
+
i += 2
|
|
260
|
+
continue
|
|
261
|
+
inner_chars.append("\\")
|
|
262
|
+
i += 1
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
if ch == ">":
|
|
266
|
+
raw_token = s[start_i + 1 : i]
|
|
267
|
+
token_inner = "".join(inner_chars).strip()
|
|
268
|
+
return token_inner, raw_token, i + 1
|
|
269
|
+
|
|
270
|
+
inner_chars.append(ch)
|
|
271
|
+
i += 1
|
|
272
|
+
|
|
273
|
+
return None, s[start_i:], n
|
|
274
|
+
|
|
275
|
+
def _expand_angle_token(
|
|
276
|
+
self,
|
|
277
|
+
token_inner: str,
|
|
278
|
+
*,
|
|
279
|
+
raw_token: str,
|
|
280
|
+
stack: list[str],
|
|
281
|
+
depth: int,
|
|
282
|
+
trace: list[TraceStep],
|
|
283
|
+
runtime_vars: dict[str, str] | None = None,
|
|
284
|
+
) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Expands a token enclosed within angular brackets (<...>) into its resolved value
|
|
287
|
+
based on predefined rules such as matching to commands or variables. It uses an
|
|
288
|
+
internal lookup mechanism and checks for cyclic references during the resolution
|
|
289
|
+
process.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
token_inner (str): The content within angular brackets to be resolved.
|
|
293
|
+
raw_token (str): The original token as a fallback if resolution fails.
|
|
294
|
+
stack (list[str]): A stack used for detecting cyclic dependencies during token
|
|
295
|
+
resolution.
|
|
296
|
+
depth (int): The current depth of recursion for token resolution. This is used
|
|
297
|
+
to track and limit nesting levels.
|
|
298
|
+
trace (list[TraceStep]): A list of TraceStep objects that document the steps
|
|
299
|
+
during the resolution of the token.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
str: The expanded and resolved token string.
|
|
303
|
+
"""
|
|
304
|
+
if not token_inner:
|
|
305
|
+
return raw_token
|
|
306
|
+
kind, key = self._parse_kind_and_key(token_inner)
|
|
307
|
+
|
|
308
|
+
if kind == RefKind.VARIABLE:
|
|
309
|
+
|
|
310
|
+
# Runtime vars override DB vars, check for them first
|
|
311
|
+
if runtime_vars and key in runtime_vars:
|
|
312
|
+
value = runtime_vars[key]
|
|
313
|
+
trace.append(
|
|
314
|
+
TraceStep(kind=RefKind.VARIABLE, key=key, expanded_to=value)
|
|
315
|
+
)
|
|
316
|
+
return value
|
|
317
|
+
|
|
318
|
+
rec = self._lookup.get_variable(key)
|
|
319
|
+
if rec is None:
|
|
320
|
+
if self._strict:
|
|
321
|
+
raise UnknownReference("variable", key)
|
|
322
|
+
return raw_token
|
|
323
|
+
|
|
324
|
+
label = f"var:{rec.name}"
|
|
325
|
+
self._check_cycle(label, stack)
|
|
326
|
+
stack.append(label)
|
|
327
|
+
try:
|
|
328
|
+
expanded = self._resolve_inner(
|
|
329
|
+
rec.value, stack=stack, depth=depth + 1, trace=trace
|
|
330
|
+
)
|
|
331
|
+
finally:
|
|
332
|
+
stack.pop()
|
|
333
|
+
|
|
334
|
+
trace.append(
|
|
335
|
+
TraceStep(kind=RefKind.VARIABLE, key=rec.name, expanded_to=expanded)
|
|
336
|
+
)
|
|
337
|
+
return expanded
|
|
338
|
+
|
|
339
|
+
# Fallback to command lookup
|
|
340
|
+
rec = self._lookup.get_command(key)
|
|
341
|
+
if rec is None:
|
|
342
|
+
if self._strict:
|
|
343
|
+
raise UnknownReference("command", key)
|
|
344
|
+
return raw_token
|
|
345
|
+
|
|
346
|
+
label = f"cmd:{rec.alias}"
|
|
347
|
+
self._check_cycle(label, stack)
|
|
348
|
+
stack.append(label)
|
|
349
|
+
try:
|
|
350
|
+
expanded = self._resolve_inner(
|
|
351
|
+
rec.template, stack=stack, depth=depth + 1, trace=trace
|
|
352
|
+
)
|
|
353
|
+
finally:
|
|
354
|
+
stack.pop()
|
|
355
|
+
trace.append(
|
|
356
|
+
TraceStep(kind=RefKind.COMMAND, key=rec.alias, expanded_to=expanded)
|
|
357
|
+
)
|
|
358
|
+
return expanded
|
|
359
|
+
|
|
360
|
+
def _parse_kind_and_key(self, token_inner: str) -> tuple[RefKind, str]:
|
|
361
|
+
"""
|
|
362
|
+
Parses the kind and key from a given token string.
|
|
363
|
+
|
|
364
|
+
This method evaluates the provided token and determines its type, which can be a
|
|
365
|
+
command or variable, based on the prefix present in the token. If no prefix is
|
|
366
|
+
found, it defaults to `RefKind.VARIABLE`. The method ensures that parts of the
|
|
367
|
+
token are appropriately stripped of whitespace during processing.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
token_inner (str): The input token string that may specify a prefix and a key,
|
|
371
|
+
separated by a colon.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
tuple[RefKind, str]: A tuple containing the kind of reference (`RefKind`) and
|
|
375
|
+
its corresponding key as a string.
|
|
376
|
+
"""
|
|
377
|
+
if ":" not in token_inner:
|
|
378
|
+
return RefKind.VARIABLE, token_inner
|
|
379
|
+
prefix, key = (part.strip() for part in token_inner.split(":", 1))
|
|
380
|
+
|
|
381
|
+
if prefix == "cmd":
|
|
382
|
+
return RefKind.COMMAND, key
|
|
383
|
+
if prefix == "var":
|
|
384
|
+
return RefKind.VARIABLE, key
|
|
385
|
+
return RefKind.VARIABLE, token_inner
|
|
386
|
+
|
|
387
|
+
def _check_cycle(self, next_label: str, stack: list[str]) -> None:
|
|
388
|
+
"""
|
|
389
|
+
Checks for a cycle in the provided stack with respect to the `next_label`.
|
|
390
|
+
|
|
391
|
+
This method checks whether the `next_label` appears in the current stack,
|
|
392
|
+
indicating the presence of a cycle. If a cycle is detected, a
|
|
393
|
+
`CycleDetectionError` is raised.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
next_label (str): The label to check within the stack to detect a cycle.
|
|
397
|
+
stack (list[str]): List maintaining the current sequence of elements being
|
|
398
|
+
evaluated.
|
|
399
|
+
"""
|
|
400
|
+
if next_label in stack:
|
|
401
|
+
cycle_start = stack.index(next_label)
|
|
402
|
+
raise CycleDetectionError(stack[cycle_start:] + [next_label])
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class CommandRecord:
|
|
7
|
+
"""
|
|
8
|
+
Represents an immutable record of a command with an alias and a template.
|
|
9
|
+
|
|
10
|
+
This class is designed to store the alias and the corresponding template for a
|
|
11
|
+
specific command in a frozen dataclass format. It serves as a simple
|
|
12
|
+
data container with no additional logic or functionality.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
alias (str): The alias or shorthand used to reference the command.
|
|
16
|
+
template (str): The string template representing the full command.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
alias: str
|
|
20
|
+
template: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class VariableRecord:
|
|
25
|
+
"""
|
|
26
|
+
Represents an immutable record of a variable with an name and a value.
|
|
27
|
+
|
|
28
|
+
This class is designed to store the name and the corresponding value for a
|
|
29
|
+
specific variable in a frozen dataclass format. It serves as a simple
|
|
30
|
+
data container with no additional logic or functionality.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
name (str): The name of the variable.
|
|
34
|
+
value (str): The value associated with the variable.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
value: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RefKind(str, Enum):
|
|
42
|
+
"""
|
|
43
|
+
Enumeration for representing different kinds of references.
|
|
44
|
+
|
|
45
|
+
This class serves as an enumeration to distinguish between various types of
|
|
46
|
+
references such as commands or variables. It is particularly useful in scenarios
|
|
47
|
+
where categorization or identification of reference types is needed for processing
|
|
48
|
+
or decision-making purposes.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
COMMAND (str): Represents a reference type that corresponds to a command.
|
|
52
|
+
VARIABLE (str): Represents a reference type that corresponds to a variable.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
COMMAND = "command"
|
|
56
|
+
VARIABLE = "variable"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class TraceStep:
|
|
61
|
+
"""
|
|
62
|
+
Represents a single step in a trace sequence.
|
|
63
|
+
|
|
64
|
+
This class is used to encapsulate information about a step in a tracing
|
|
65
|
+
process. It is immutable and defined as a dataclass with frozen attributes
|
|
66
|
+
to ensure its contents remain unchanged once initialized.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
kind (RefKind): The type or category of the trace step.
|
|
70
|
+
key (str): The unique identifier or key associated with this step in the trace.
|
|
71
|
+
expanded_to (str): The resulting value or destination that the key is expanded to.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
kind: RefKind
|
|
75
|
+
key: str
|
|
76
|
+
expanded_to: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ResolveResult:
|
|
81
|
+
"""
|
|
82
|
+
Represents the result of a resolution process.
|
|
83
|
+
|
|
84
|
+
This class is designed to encapsulate the details of a resolution operation,
|
|
85
|
+
including the resulting text output and the trace of steps taken during the
|
|
86
|
+
process. It serves as a structured way to manage and access the output and
|
|
87
|
+
execution trace of a resolution operation.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
text (str): The resulting text from the resolution process.
|
|
91
|
+
trace (list[TraceStep]): The list of steps conducted during the resolution
|
|
92
|
+
process, represented as TraceStep objects.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
text: str
|
|
96
|
+
trace: list[TraceStep]
|
|
File without changes
|