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.
Files changed (112) hide show
  1. cmdbox/__init__.py +0 -0
  2. cmdbox/cli/__init__.py +0 -0
  3. cmdbox/cli/app.py +125 -0
  4. cmdbox/cli/commands/__init__.py +0 -0
  5. cmdbox/cli/commands/alias_fallback.py +102 -0
  6. cmdbox/cli/commands/command_crud.py +429 -0
  7. cmdbox/cli/commands/command_run.py +255 -0
  8. cmdbox/cli/commands/history.py +109 -0
  9. cmdbox/cli/commands/init.py +54 -0
  10. cmdbox/cli/commands/settings.py +62 -0
  11. cmdbox/cli/commands/tag_crud.py +277 -0
  12. cmdbox/cli/commands/variable_crud.py +349 -0
  13. cmdbox/cli/common/__init__.py +0 -0
  14. cmdbox/cli/common/errors.py +58 -0
  15. cmdbox/cli/common/update_fields.py +88 -0
  16. cmdbox/cli/completions/__init__.py +0 -0
  17. cmdbox/cli/completions/commands.py +26 -0
  18. cmdbox/cli/completions/fields.py +31 -0
  19. cmdbox/cli/completions/tags.py +24 -0
  20. cmdbox/cli/completions/variables.py +26 -0
  21. cmdbox/cli/handlers/__init__.py +0 -0
  22. cmdbox/cli/handlers/command_handlers.py +357 -0
  23. cmdbox/cli/handlers/common_handlers.py +15 -0
  24. cmdbox/cli/handlers/history_handlers.py +94 -0
  25. cmdbox/cli/handlers/init_handler.py +127 -0
  26. cmdbox/cli/handlers/run_handler.py +178 -0
  27. cmdbox/cli/handlers/settings_handler.py +59 -0
  28. cmdbox/cli/handlers/tag_handlers.py +220 -0
  29. cmdbox/cli/handlers/variable_handlers.py +272 -0
  30. cmdbox/cli/prompts/__init__.py +0 -0
  31. cmdbox/cli/prompts/completers.py +161 -0
  32. cmdbox/cli/prompts/prompts.py +108 -0
  33. cmdbox/cli/prompts/validators.py +46 -0
  34. cmdbox/cli/ui/__init__.py +0 -0
  35. cmdbox/cli/ui/console.py +31 -0
  36. cmdbox/cli/ui/editor.py +141 -0
  37. cmdbox/cli/ui/presenters/__init__.py +0 -0
  38. cmdbox/cli/ui/presenters/app_presenter.py +8 -0
  39. cmdbox/cli/ui/presenters/command_presenter.py +168 -0
  40. cmdbox/cli/ui/presenters/history_presenter.py +83 -0
  41. cmdbox/cli/ui/presenters/init_instructions.py +52 -0
  42. cmdbox/cli/ui/presenters/init_presenter.py +57 -0
  43. cmdbox/cli/ui/presenters/result_presenter.py +144 -0
  44. cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
  45. cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
  46. cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
  47. cmdbox/cli/ui/primitives.py +410 -0
  48. cmdbox/cli/ui/theme.py +43 -0
  49. cmdbox/cli/ui/theme_builder.py +49 -0
  50. cmdbox/common/__init__.py +0 -0
  51. cmdbox/common/io.py +34 -0
  52. cmdbox/container.py +156 -0
  53. cmdbox/core/__init__.py +0 -0
  54. cmdbox/core/fields.py +48 -0
  55. cmdbox/core/paths.py +52 -0
  56. cmdbox/database.py +65 -0
  57. cmdbox/exceptions.py +10 -0
  58. cmdbox/init/__init__.py +0 -0
  59. cmdbox/init/detect.py +82 -0
  60. cmdbox/init/integrations/bash.sh +10 -0
  61. cmdbox/init/integrations/cmd.bat +14 -0
  62. cmdbox/init/integrations/fish.fish +11 -0
  63. cmdbox/init/integrations/powershell.ps1 +14 -0
  64. cmdbox/init/integrations/zsh.sh +10 -0
  65. cmdbox/init/io.py +68 -0
  66. cmdbox/init/specs.py +54 -0
  67. cmdbox/logging_setup/__init__.py +0 -0
  68. cmdbox/logging_setup/log_config.py +123 -0
  69. cmdbox/logging_setup/log_decorators.py +40 -0
  70. cmdbox/logging_setup/log_handlers.py +94 -0
  71. cmdbox/migrations/__init__.py +1 -0
  72. cmdbox/migrations/errors.py +10 -0
  73. cmdbox/migrations/runner.py +127 -0
  74. cmdbox/migrations/versions/__init__.py +0 -0
  75. cmdbox/models.py +165 -0
  76. cmdbox/repositories/__init__.py +0 -0
  77. cmdbox/repositories/base_repository.py +181 -0
  78. cmdbox/repositories/command_repository.py +391 -0
  79. cmdbox/repositories/errors.py +120 -0
  80. cmdbox/repositories/history_repository.py +155 -0
  81. cmdbox/repositories/results.py +37 -0
  82. cmdbox/repositories/tag_repository.py +91 -0
  83. cmdbox/repositories/validators.py +256 -0
  84. cmdbox/repositories/variable_repository.py +324 -0
  85. cmdbox/resolve/__init__.py +0 -0
  86. cmdbox/resolve/errors.py +65 -0
  87. cmdbox/resolve/lookup.py +137 -0
  88. cmdbox/resolve/resolver.py +402 -0
  89. cmdbox/resolve/type_defs.py +96 -0
  90. cmdbox/runtime/__init__.py +0 -0
  91. cmdbox/runtime/executor.py +454 -0
  92. cmdbox/runtime/results.py +25 -0
  93. cmdbox/runtime/shell.py +90 -0
  94. cmdbox/services/__init__.py +0 -0
  95. cmdbox/services/command_services.py +261 -0
  96. cmdbox/services/errors.py +37 -0
  97. cmdbox/services/field_selection.py +162 -0
  98. cmdbox/services/history_service.py +68 -0
  99. cmdbox/services/run_service.py +204 -0
  100. cmdbox/services/tag_services.py +134 -0
  101. cmdbox/services/variable_services.py +224 -0
  102. cmdbox/settings/__init__.py +0 -0
  103. cmdbox/settings/models.py +129 -0
  104. cmdbox/settings/settings_repository.py +36 -0
  105. cmdbox/settings/settings_service.py +144 -0
  106. cmdbox/version.py +1 -0
  107. cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
  108. cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
  109. cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
  110. cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
  111. cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  112. cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -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
@@ -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