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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from cmdbox.models import Tag, Command
|
|
4
|
+
from cmdbox.repositories.command_repository import CommandRepository
|
|
5
|
+
from cmdbox.database import db
|
|
6
|
+
from cmdbox.repositories.results import TagAttachResult, TagDetachResult
|
|
7
|
+
from cmdbox.repositories.tag_repository import TagRepository
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandServices:
|
|
11
|
+
"""
|
|
12
|
+
Provides services for managing commands and their associated tags.
|
|
13
|
+
|
|
14
|
+
The `CommandServices` class encapsulates the logic for working with commands and
|
|
15
|
+
tags, offering methods to create, update, delete, and retrieve command records
|
|
16
|
+
and their tag associations. It also supports additional functionality such as
|
|
17
|
+
searching, tagging, and listing commands with filtering and sorting options.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
command_repository (CommandRepository): Repository for managing command records.
|
|
21
|
+
tag_repository (TagRepository): Repository for managing tag-related operations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, command_repository: CommandRepository, tag_repository: TagRepository
|
|
26
|
+
):
|
|
27
|
+
self._repo = command_repository
|
|
28
|
+
self._tag_repo = tag_repository
|
|
29
|
+
|
|
30
|
+
def create_command(
|
|
31
|
+
self,
|
|
32
|
+
alias: str,
|
|
33
|
+
template: str,
|
|
34
|
+
description: str | None = None,
|
|
35
|
+
tags: list[str] | None = None,
|
|
36
|
+
cwd: str | None = None,
|
|
37
|
+
shell: str | None = None,
|
|
38
|
+
env: dict[str, str] | None = None,
|
|
39
|
+
timeout: int | None = None,
|
|
40
|
+
) -> Command:
|
|
41
|
+
"""
|
|
42
|
+
Creates a new command by storing the provided alias, template, and optional description
|
|
43
|
+
and tags into the database. If tags are provided, they are associated with the created
|
|
44
|
+
command.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
alias (str): The alias of the command to be created.
|
|
48
|
+
template (str): The template associated with the command.
|
|
49
|
+
description (str | None): An optional description for the command. Defaults to None.
|
|
50
|
+
tags (list[str] | None): An optional list of tags to associate with the command.
|
|
51
|
+
Defaults to None.
|
|
52
|
+
cwd (str | None): Optional working directory to run the command from. Defaults to None.
|
|
53
|
+
shell (str | None): Optional shell to use when running the command. Defaults to None.
|
|
54
|
+
env (dict[str, str] | None): Optional environment variables to set when running
|
|
55
|
+
the command. Defaults to None.
|
|
56
|
+
timeout (int | None): Optional maximum number of seconds before the process is
|
|
57
|
+
killed. Defaults to None.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Command: The created command record.
|
|
61
|
+
"""
|
|
62
|
+
with db.atomic():
|
|
63
|
+
tags = self._get_tags(tags)
|
|
64
|
+
cmd = self._repo.create(
|
|
65
|
+
alias=alias,
|
|
66
|
+
template=template,
|
|
67
|
+
description=description,
|
|
68
|
+
cwd=cwd,
|
|
69
|
+
shell=shell,
|
|
70
|
+
env=env,
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
)
|
|
73
|
+
if tags:
|
|
74
|
+
self._repo.add_tags(cmd, tags)
|
|
75
|
+
|
|
76
|
+
command = self._repo.get_by_id(cmd.id)
|
|
77
|
+
|
|
78
|
+
return command
|
|
79
|
+
|
|
80
|
+
def update_command(self, alias: str, **fields) -> Command:
|
|
81
|
+
"""
|
|
82
|
+
Updates an existing command by its alias with new field values.
|
|
83
|
+
|
|
84
|
+
Retrieves the command corresponding to the given alias and updates it
|
|
85
|
+
with the provided fields. The update is performed using the repository.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
alias (str): The alias of the command to update.
|
|
89
|
+
**fields: Arbitrary field values to update on the command.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Command: The updated command object.
|
|
93
|
+
"""
|
|
94
|
+
cmd = self._repo.get_by_alias(alias)
|
|
95
|
+
return self._repo.update(cmd, **fields)
|
|
96
|
+
|
|
97
|
+
def delete_command(self, alias_: str) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Deletes a command by its alias.
|
|
100
|
+
|
|
101
|
+
This method removes a command record from the repository that matches the
|
|
102
|
+
provided alias.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
alias_: The alias of the command to delete.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
bool: True if the command was deleted successfully, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
cmd = self._repo.get_by_alias(alias_)
|
|
111
|
+
return self._repo.delete(cmd)
|
|
112
|
+
|
|
113
|
+
def add_tags(self, alias: str, tags: list[str]) -> TagAttachResult:
|
|
114
|
+
"""
|
|
115
|
+
Adds specified tags to an existing alias.
|
|
116
|
+
|
|
117
|
+
The method retrieves a command object associated with the given alias and
|
|
118
|
+
applies the provided tags to the command.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
alias (str): The alias identifying the command to which tags are to be
|
|
122
|
+
attached.
|
|
123
|
+
tags (list[str]): A list of tags to attach to the command.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
TagAttachResult: The result of the tag attachment operation, indicating
|
|
127
|
+
whether the tags were successfully attached to the command.
|
|
128
|
+
"""
|
|
129
|
+
cmd = self._repo.get_by_alias(alias)
|
|
130
|
+
tags = self._get_tags(tags)
|
|
131
|
+
return self._repo.add_tags(cmd, tags)
|
|
132
|
+
|
|
133
|
+
def remove_tags(self, alias: str, tags: list[str]) -> TagDetachResult:
|
|
134
|
+
"""
|
|
135
|
+
Removes specific tags associated with a command alias.
|
|
136
|
+
|
|
137
|
+
This method retrieves a command by its alias and detaches the specified tags
|
|
138
|
+
from it. The updated tag associations will be handled by the repository.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
alias: The unique identifier for the command to update.
|
|
142
|
+
tags: A list of tags to be removed from the specified command.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
TagDetachResult: An object representing the result of tag detachment.
|
|
146
|
+
"""
|
|
147
|
+
cmd = self._repo.get_by_alias(alias)
|
|
148
|
+
tags = self._get_tags(tags)
|
|
149
|
+
return self._repo.remove_tags(cmd, tags)
|
|
150
|
+
|
|
151
|
+
def get_command(self, alias: str) -> Command:
|
|
152
|
+
"""
|
|
153
|
+
Retrieves a command by its alias.
|
|
154
|
+
|
|
155
|
+
This method fetches a command object associated with the given alias from the
|
|
156
|
+
repository.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
alias (str): The alias of the command to retrieve.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Command: The command object associated with the given alias.
|
|
163
|
+
"""
|
|
164
|
+
cmd = self._repo.get_by_alias(alias)
|
|
165
|
+
return cmd
|
|
166
|
+
|
|
167
|
+
def get_command_by_id(self, cmd_id: int) -> Command:
|
|
168
|
+
"""
|
|
169
|
+
Retrieves a command by its unique identifier from the repository.
|
|
170
|
+
|
|
171
|
+
This method accesses the repository to fetch a command based on the provided
|
|
172
|
+
command ID. It assumes that the command ID corresponds to a valid identifier
|
|
173
|
+
within the repository.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
cmd_id (int): The unique identifier of the command to be retrieved.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The command object associated with the given command ID, or None if no
|
|
180
|
+
matching command is found.
|
|
181
|
+
"""
|
|
182
|
+
cmd = self._repo.get_by_id(cmd_id)
|
|
183
|
+
return cmd
|
|
184
|
+
|
|
185
|
+
def list_commands(
|
|
186
|
+
self,
|
|
187
|
+
order_by: str | Sequence[str] = "alias",
|
|
188
|
+
tags: Sequence[str] | None = None,
|
|
189
|
+
limit: int = 25,
|
|
190
|
+
) -> list[Command]:
|
|
191
|
+
"""
|
|
192
|
+
Lists commands, optionally filtered by tags, with sorting and limit options.
|
|
193
|
+
|
|
194
|
+
This function fetches a list of commands, either filtered by specific tags
|
|
195
|
+
or returning all available commands. The results can be sorted and limited
|
|
196
|
+
based on the provided arguments.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
order_by (str | Sequence[str]): Specifies the field(s) to sort the results by.
|
|
200
|
+
Default is "alias".
|
|
201
|
+
tags (Sequence[str], optional): A list of tags to filter the commands. If
|
|
202
|
+
provided, only commands matching the tags will be included.
|
|
203
|
+
limit (int, optional): The maximum number of commands to return. Default is 25.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
list[Command]: A list of commands matching the provided filters and sorted
|
|
207
|
+
according to the specified criteria.
|
|
208
|
+
"""
|
|
209
|
+
if tags:
|
|
210
|
+
tags = self._get_tags(tags)
|
|
211
|
+
return self._repo.list_by_tag(tags, order_by, limit)
|
|
212
|
+
return self._repo.list_all(order_by, limit)
|
|
213
|
+
|
|
214
|
+
def search(
|
|
215
|
+
self,
|
|
216
|
+
query: str,
|
|
217
|
+
fields: str | Sequence[str] | None = None,
|
|
218
|
+
limit: int = 25,
|
|
219
|
+
) -> list[Command]:
|
|
220
|
+
"""
|
|
221
|
+
Searches for commands that match the given query across specified fields.
|
|
222
|
+
|
|
223
|
+
This method allows you to perform a search within the data repository for commands
|
|
224
|
+
that match the provided query string in the specified fields. It returns a list of
|
|
225
|
+
commands that satisfy the search criteria.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
query (str): The search term used for matching against the repository.
|
|
229
|
+
fields (str | Sequence[str] | None): The fields to perform the search within. Defaults
|
|
230
|
+
to ("alias", "template", "description"). If None, no specific fields are targeted.
|
|
231
|
+
limit (int): The maximum number of results to return. Defaults to 25.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
list[Command]: A list of Command objects that match the search query.
|
|
235
|
+
"""
|
|
236
|
+
if not fields:
|
|
237
|
+
fields = ("alias", "template", "description")
|
|
238
|
+
return self._repo.search(query, fields=fields, limit=limit)
|
|
239
|
+
|
|
240
|
+
def _get_tags(self, tags: Sequence[str] | None) -> list[Tag]:
|
|
241
|
+
"""
|
|
242
|
+
Fetches a list of Tag objects based on the provided tag names.
|
|
243
|
+
|
|
244
|
+
This method takes a list of tag names as input and retrieves the corresponding
|
|
245
|
+
Tag objects from the tag repository. The resulting list of Tag objects is then
|
|
246
|
+
returned.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
tags (list[str] | None): A list of string representing the names of the tags to
|
|
250
|
+
retrieve.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list[Tag]: A list of Tag objects corresponding to the specified tag names.
|
|
254
|
+
"""
|
|
255
|
+
if tags is None:
|
|
256
|
+
return []
|
|
257
|
+
ret_tags: list[Tag] = []
|
|
258
|
+
for name in tags:
|
|
259
|
+
tag = self._tag_repo.get_by_name(name)
|
|
260
|
+
ret_tags.append(tag)
|
|
261
|
+
return ret_tags
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from cmdbox.exceptions import CmdboxError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FieldSelectionError(CmdboxError):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnknownFieldError(FieldSelectionError):
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self, unknown: str, allowed: Sequence[str], context: str | None = None
|
|
14
|
+
):
|
|
15
|
+
self.unknown = unknown
|
|
16
|
+
self.allowed = allowed
|
|
17
|
+
self.context = context
|
|
18
|
+
msg = f'Unknown field "{unknown}".'
|
|
19
|
+
if context:
|
|
20
|
+
msg += f" ({context})"
|
|
21
|
+
msg += f" Allowed fields: {', '.join(allowed)}"
|
|
22
|
+
super().__init__(msg)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EmptyFieldSelectionError(FieldSelectionError):
|
|
26
|
+
|
|
27
|
+
def __init__(self, context: str | None = None):
|
|
28
|
+
msg = "No fields specified"
|
|
29
|
+
if context:
|
|
30
|
+
msg += f" ({context})"
|
|
31
|
+
super().__init__(msg)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HistoryIndexError(CmdboxError):
|
|
35
|
+
|
|
36
|
+
def __init__(self, index: int):
|
|
37
|
+
super().__init__(f"No history entry at index {index}.")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from typing import Mapping, Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from cmdbox.services.errors import EmptyFieldSelectionError, UnknownFieldError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class FieldSelectionResolver:
|
|
9
|
+
"""
|
|
10
|
+
Resolves and validates field selections based on allowed fields, aliases, and additional rules.
|
|
11
|
+
|
|
12
|
+
This class is designed to handle validation and resolution of a set of field selections provided
|
|
13
|
+
by the user. It allows for enforcement of constraints like allowed fields, support for field aliases,
|
|
14
|
+
removal of duplicates, and handling special tokens like "all". You can use it to process raw field
|
|
15
|
+
selections into a sanitized and verified list of valid field names.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
allowed_fields (list[str]): A list of fields that are explicitly allowed. Any field not
|
|
19
|
+
present in this list will result in a validation error.
|
|
20
|
+
allow_duplicates (bool): A flag indicating whether duplicate fields are permitted in the
|
|
21
|
+
final resolved list. Defaults to False.
|
|
22
|
+
all_token (str): A special token used to indicate that all allowed fields should be selected.
|
|
23
|
+
Defaults to "all".
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
allowed_fields: list[str]
|
|
27
|
+
allow_duplicates: bool = False
|
|
28
|
+
all_token: str = "all"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def _allowed_fields(self):
|
|
32
|
+
"""
|
|
33
|
+
Property method that retrieves a list of allowed fields in lowercase.
|
|
34
|
+
|
|
35
|
+
This method iterates through the fields defined in the `allowed_fields`
|
|
36
|
+
attribute, converts each field to lowercase, and returns the updated list.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
list: A list of strings representing the fields converted to lowercase.
|
|
40
|
+
"""
|
|
41
|
+
return [x.lower() for x in self.allowed_fields]
|
|
42
|
+
|
|
43
|
+
def resolve(
|
|
44
|
+
self,
|
|
45
|
+
raw: Sequence[str] | None,
|
|
46
|
+
*,
|
|
47
|
+
default_fields: Sequence[str] | None = None,
|
|
48
|
+
aliases: Mapping[str, str] | None = None,
|
|
49
|
+
context: str | None = None,
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""
|
|
52
|
+
Resolves and validates a list of field names based on the provided raw input, default
|
|
53
|
+
fields, and context. This method processes the field input, checks for special tokens,
|
|
54
|
+
and ensures the fields comply with the allowed fields defined in the current instance.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
raw (Sequence[str] | None): A sequence of raw field names provided for resolution.
|
|
58
|
+
If None, the `default_fields` argument or allowed fields will be used as a
|
|
59
|
+
fallback.
|
|
60
|
+
default_fields (Sequence[str] | None, optional): A sequence of default field names
|
|
61
|
+
to use if `raw` is None. If omitted or None, all allowed fields are used.
|
|
62
|
+
aliases: (Mapping[str, str] | None, optional): A dictionary mapping field aliases to
|
|
63
|
+
a corresponding field name. If provided, aliases will be applied to the raw input.
|
|
64
|
+
context (str | None, optional): An optional string providing additional contextual
|
|
65
|
+
information for validation or error handling.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
list[str]: A list of validated and resolved field names, taking into account allowed
|
|
69
|
+
fields, special tokens, and fallback mechanisms.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
EmptyFieldSelectionError: If `raw` contains no valid field tokens or is equivalent
|
|
73
|
+
to an empty selection within the specified context.
|
|
74
|
+
"""
|
|
75
|
+
if not self._allowed_fields:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
if raw is None:
|
|
79
|
+
fields = (
|
|
80
|
+
list(default_fields)
|
|
81
|
+
if default_fields is not None
|
|
82
|
+
else self._allowed_fields
|
|
83
|
+
)
|
|
84
|
+
return self.validate(fields, context)
|
|
85
|
+
|
|
86
|
+
raw = [x.lower() for x in raw]
|
|
87
|
+
tokens = [self.apply_alias(x, aliases) for x in raw if x.strip()]
|
|
88
|
+
|
|
89
|
+
if not tokens:
|
|
90
|
+
raise EmptyFieldSelectionError(context=context)
|
|
91
|
+
|
|
92
|
+
if any(x.lower() == self.all_token.lower() for x in tokens):
|
|
93
|
+
return self._allowed_fields
|
|
94
|
+
|
|
95
|
+
return self.validate(tokens, context)
|
|
96
|
+
|
|
97
|
+
def validate(self, tokens: Sequence[str], context: str | None) -> list[str]:
|
|
98
|
+
"""
|
|
99
|
+
Validates a sequence of tokens based on allowed fields and duplicate rules.
|
|
100
|
+
|
|
101
|
+
This method processes the provided tokens to ensure they are within the set of
|
|
102
|
+
allowed fields. Optionally, it filters out duplicate tokens based on the
|
|
103
|
+
`allow_duplicates` rule. If any token does not match the allowed fields, an
|
|
104
|
+
`UnknownFieldError` is raised.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
tokens (Sequence[str]): A sequence of tokens to validate.
|
|
108
|
+
context (str | None): An optional context string providing additional
|
|
109
|
+
information for debug or error messages.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
list[str]: A list of validated tokens, preserving the original order and
|
|
113
|
+
optionally excluding duplicates.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
UnknownFieldError: If a token is not in the allowed fields or violates
|
|
117
|
+
validation rules.
|
|
118
|
+
"""
|
|
119
|
+
out: list[str] = []
|
|
120
|
+
seen: set[str] = set()
|
|
121
|
+
|
|
122
|
+
for token in tokens:
|
|
123
|
+
key = token.lower()
|
|
124
|
+
|
|
125
|
+
if key not in self._allowed_fields:
|
|
126
|
+
raise UnknownFieldError(token, self._allowed_fields, context=context)
|
|
127
|
+
|
|
128
|
+
if not self.allow_duplicates and key in seen:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
seen.add(key)
|
|
132
|
+
out.append(token)
|
|
133
|
+
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
def apply_alias(self, token: str, aliases: Mapping[str, str] | None) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Processes a token and applies alias transformations if a matching alias is found.
|
|
139
|
+
|
|
140
|
+
This method checks if a given token matches any of the keys in the provided alias
|
|
141
|
+
mappings. If a match is found, the token is replaced with the associated value
|
|
142
|
+
from the mapping. If no match is found or no aliases are provided, the original
|
|
143
|
+
token is returned unmodified.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
token (str): The token to be processed and potentially transformed.
|
|
147
|
+
aliases (Mapping[str, str] | None): A dictionary where keys represent alias
|
|
148
|
+
tokens, and values are the replacement tokens. If None is provided, no
|
|
149
|
+
alias transformations will be applied.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The transformed token if a matching alias was found, or the original
|
|
153
|
+
token otherwise.
|
|
154
|
+
"""
|
|
155
|
+
if not aliases:
|
|
156
|
+
return token
|
|
157
|
+
|
|
158
|
+
for alias, target in aliases.items():
|
|
159
|
+
if alias.lower() == token.lower():
|
|
160
|
+
return target
|
|
161
|
+
|
|
162
|
+
return token
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from cmdbox.models import CommandHistory
|
|
5
|
+
from cmdbox.repositories.history_repository import HistoryRepository
|
|
6
|
+
from cmdbox.services.errors import HistoryIndexError
|
|
7
|
+
from cmdbox.settings.models import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HistoryService:
|
|
11
|
+
|
|
12
|
+
def __init__(self, repo: HistoryRepository, get_settings: Callable[[], Settings]):
|
|
13
|
+
self._repo = repo
|
|
14
|
+
self._get_settings = get_settings
|
|
15
|
+
|
|
16
|
+
def get_recent(
|
|
17
|
+
self,
|
|
18
|
+
alias: str | None = None,
|
|
19
|
+
limit: int = 20,
|
|
20
|
+
) -> list[CommandHistory]:
|
|
21
|
+
return self._repo.get_recent(alias, limit)
|
|
22
|
+
|
|
23
|
+
def get_by_ref(self, ref: str, alias: str | None = None) -> CommandHistory:
|
|
24
|
+
if ref.isdigit():
|
|
25
|
+
return self._get_by_index(int(ref), alias=alias)
|
|
26
|
+
return self._repo.get_by_id(ref)
|
|
27
|
+
|
|
28
|
+
def get_variables(self, entry: CommandHistory) -> dict[str, str] | None:
|
|
29
|
+
if entry.variables_used is None:
|
|
30
|
+
return None
|
|
31
|
+
return json.loads(entry.variables_used)
|
|
32
|
+
|
|
33
|
+
def delete_by_ref(self, ref: str) -> bool:
|
|
34
|
+
entry = self.get_by_ref(ref)
|
|
35
|
+
return self._repo.delete_by_id(entry.id)
|
|
36
|
+
|
|
37
|
+
def clear(self, alias: str | None = None) -> int:
|
|
38
|
+
return self._repo.clear(alias=alias)
|
|
39
|
+
|
|
40
|
+
def _get_by_index(self, index: int, alias: str | None = None) -> CommandHistory:
|
|
41
|
+
"""
|
|
42
|
+
Fetches a specific command history entry by its index.
|
|
43
|
+
|
|
44
|
+
This method retrieves a command history entry from the repository based on
|
|
45
|
+
the passed index. If the index is out of range or invalid, a
|
|
46
|
+
HistoryIndexError is raised. The method optionally allows limiting the
|
|
47
|
+
retrieval to a specific alias.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
index (int): The one-based index of the command history entry to
|
|
51
|
+
retrieve. Must be greater than 0.
|
|
52
|
+
alias (str | None): Optional alias to filter the history entries. If
|
|
53
|
+
None, no alias filter is applied.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
CommandHistory: The command history entry corresponding to the provided
|
|
57
|
+
index.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
HistoryIndexError: If the provided index is less than 1 or if the index
|
|
61
|
+
exceeds the number of available entries.
|
|
62
|
+
"""
|
|
63
|
+
if index < 1:
|
|
64
|
+
raise HistoryIndexError(index=index)
|
|
65
|
+
entries = self._repo.get_recent(alias=alias, limit=index)
|
|
66
|
+
if index > len(entries):
|
|
67
|
+
raise HistoryIndexError(index=index)
|
|
68
|
+
return entries[index - 1]
|