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,391 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from peewee import IntegrityError
|
|
6
|
+
|
|
7
|
+
from .base_repository import BaseRepository
|
|
8
|
+
from .validators import CommandValidator
|
|
9
|
+
from .errors import (
|
|
10
|
+
ValidationError,
|
|
11
|
+
AliasConflictError,
|
|
12
|
+
UnknownAliasError,
|
|
13
|
+
TagAttachError,
|
|
14
|
+
TagDetachError,
|
|
15
|
+
UpdateError,
|
|
16
|
+
UnknownCommandError,
|
|
17
|
+
)
|
|
18
|
+
from .results import TagAttachResult, TagDetachResult
|
|
19
|
+
from cmdbox.database import db
|
|
20
|
+
from cmdbox.models import Command, Tag, CommandTag
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommandRepository(BaseRepository[Command]):
|
|
24
|
+
|
|
25
|
+
model = Command
|
|
26
|
+
|
|
27
|
+
def __init__(self, validator: CommandValidator | None = None):
|
|
28
|
+
self.validator = validator or CommandValidator()
|
|
29
|
+
|
|
30
|
+
def create(
|
|
31
|
+
self,
|
|
32
|
+
alias: str,
|
|
33
|
+
template: str,
|
|
34
|
+
description: str | None = None,
|
|
35
|
+
cwd: str | None = None,
|
|
36
|
+
shell: str | None = None,
|
|
37
|
+
env: dict[str, str] | None = None,
|
|
38
|
+
timeout: int | None = None,
|
|
39
|
+
) -> Command:
|
|
40
|
+
"""
|
|
41
|
+
Validates and creates a new Command object based on provided input parameters.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
alias (str): Unique identifier for the command to be created.
|
|
45
|
+
template (str): Template string associated with the command.
|
|
46
|
+
description (str | None): Optional description of the command.
|
|
47
|
+
cwd (str | None): Current working directory for command execution.
|
|
48
|
+
shell (str | None): Shell to use for command execution.
|
|
49
|
+
env (dict[str, str] | None): Environment variables to set for command execution.
|
|
50
|
+
timeout (int | None): Number of seconds before the process is killed.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Command: The created Command object.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
AliasConflictError: If the provided alias already exists and causes a
|
|
57
|
+
unique constraint violation during creation.
|
|
58
|
+
IntegrityError: If any database integrity issue occurs during the creation.
|
|
59
|
+
"""
|
|
60
|
+
alias = alias.strip() if alias else None
|
|
61
|
+
self.validator.validate_create(
|
|
62
|
+
alias=alias, template=template, description=description
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
return Command.create(
|
|
66
|
+
alias=alias,
|
|
67
|
+
template=template,
|
|
68
|
+
description=description,
|
|
69
|
+
cwd=cwd,
|
|
70
|
+
shell=shell,
|
|
71
|
+
env=json.dumps(env) if env else None,
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
except IntegrityError as exc:
|
|
75
|
+
if alias is not None and self._is_unique_alias_violation(exc):
|
|
76
|
+
raise AliasConflictError(alias=alias) from exc
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
def get_by_alias(self, alias: str) -> Command:
|
|
80
|
+
"""
|
|
81
|
+
Retrieves a Command instance by its alias.
|
|
82
|
+
|
|
83
|
+
This method searches for a Command instance using the given alias. The alias
|
|
84
|
+
is converted to lowercase before searching. If no matching Command instance
|
|
85
|
+
is found, an UnknownAliasError is raised.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
alias (str): The alias of the Command being searched for.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Command: The Command instance that matches the provided alias.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
UnknownAliasError: If no Command is found with the provided alias.
|
|
95
|
+
"""
|
|
96
|
+
alias = alias.lower()
|
|
97
|
+
cmd = Command.get_or_none(Command.alias == alias)
|
|
98
|
+
if cmd is None:
|
|
99
|
+
raise UnknownAliasError(alias=alias)
|
|
100
|
+
return cmd
|
|
101
|
+
|
|
102
|
+
def get_by_id(self, cmd_id: int) -> Command:
|
|
103
|
+
"""
|
|
104
|
+
Retrieves a command by its ID.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
cmd_id (int): The ID of the command to retrieve.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Command: The Command instance with the specified ID.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
UnknownCommandError: If no Command is found with the provided ID.
|
|
114
|
+
"""
|
|
115
|
+
cmd = Command.get_or_none(Command.id == cmd_id)
|
|
116
|
+
if cmd is None:
|
|
117
|
+
raise UnknownCommandError(cmd_id=cmd_id)
|
|
118
|
+
return cmd
|
|
119
|
+
|
|
120
|
+
def update(self, command: Command, **fields) -> Command:
|
|
121
|
+
"""
|
|
122
|
+
Updates an existing command based on the provided alias and fields.
|
|
123
|
+
|
|
124
|
+
This method retrieves a command by its alias and updates its fields with the
|
|
125
|
+
provided values. It validates the updates, ensuring field integrity and
|
|
126
|
+
uniqueness. If the command doesn't exist or an update violates constraints,
|
|
127
|
+
appropriate actions are taken.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
command (Command): The command to update.
|
|
131
|
+
**fields: Arbitrary keyword arguments representing the fields to update.
|
|
132
|
+
Supported fields include 'alias', 'template', and 'description'.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Command: The updated command object if successful.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValidationError: If any of the provided fields are invalid or do not exist
|
|
139
|
+
on the command.
|
|
140
|
+
AliasConflictError: If the new alias conflicts with an existing command's
|
|
141
|
+
alias.
|
|
142
|
+
IntegrityError: If there is a general integrity constraint violation during
|
|
143
|
+
the update process.
|
|
144
|
+
"""
|
|
145
|
+
if not command:
|
|
146
|
+
raise UpdateError("No command provided for update.")
|
|
147
|
+
if not fields:
|
|
148
|
+
raise UpdateError("No fields provided for update.")
|
|
149
|
+
|
|
150
|
+
# Strip whitespace from alias field if alias field is supplied
|
|
151
|
+
if "alias" in fields and fields.get("alias") is not None:
|
|
152
|
+
fields["alias"] = fields.get("alias").strip()
|
|
153
|
+
|
|
154
|
+
self.validator.validate_update(
|
|
155
|
+
alias=fields.get("alias", command.alias),
|
|
156
|
+
template=fields.get("template", command.template),
|
|
157
|
+
description=fields.get("description", None),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if "env" in fields:
|
|
161
|
+
fields["env"] = json.dumps(fields.get("env")) if fields.get("env") else None
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
for key, value in fields.items():
|
|
165
|
+
if not hasattr(command, key):
|
|
166
|
+
raise ValidationError(f"Invalid field: {key}")
|
|
167
|
+
if value is not None:
|
|
168
|
+
setattr(command, key, value)
|
|
169
|
+
command.save()
|
|
170
|
+
return command
|
|
171
|
+
except IntegrityError as exc:
|
|
172
|
+
alias = fields.get("alias", "")
|
|
173
|
+
if alias is not None and self._is_unique_alias_violation(exc):
|
|
174
|
+
raise AliasConflictError(alias=alias) from exc
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
def record_use(self, command_id: int) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Records the use of a specific command by updating its usage count and the timestamp of
|
|
180
|
+
its last use in the database.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
command_id (int): The unique identifier of the command to update.
|
|
184
|
+
"""
|
|
185
|
+
Command.update(
|
|
186
|
+
used=Command.used + 1,
|
|
187
|
+
last_used=datetime.now(),
|
|
188
|
+
).where(Command.id == command_id).execute()
|
|
189
|
+
|
|
190
|
+
def add_tags(self, command: Command, tags: Sequence[Tag]) -> TagAttachResult:
|
|
191
|
+
"""
|
|
192
|
+
Attach tags to a command identified by its alias.
|
|
193
|
+
|
|
194
|
+
This function associates tags with a specified command. If the tags already
|
|
195
|
+
exist for the command, they are added to an existing list. Otherwise, new tags
|
|
196
|
+
are created and linked to the command. If no tags are provided, it returns
|
|
197
|
+
immediately with empty results. In case of a database integrity issue, an
|
|
198
|
+
appropriate error is raised.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
command (Command): The command to which the tags are to
|
|
202
|
+
be attached.
|
|
203
|
+
tags (Sequence[Tag]): A collection of tags to be attached to the
|
|
204
|
+
command.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
TagAttachResult: An object containing lists of newly added tags and
|
|
208
|
+
tags that already existed.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
TagAttachError: If there is an issue attaching the tags to the command,
|
|
212
|
+
typically due to database integrity constraints.
|
|
213
|
+
"""
|
|
214
|
+
if not tags:
|
|
215
|
+
return TagAttachResult(added=[], existing=[])
|
|
216
|
+
try:
|
|
217
|
+
with db.atomic():
|
|
218
|
+
added = []
|
|
219
|
+
existing = []
|
|
220
|
+
for tag in tags:
|
|
221
|
+
cmd_tag, created = CommandTag.get_or_create(
|
|
222
|
+
command=command, tag=tag
|
|
223
|
+
)
|
|
224
|
+
if created:
|
|
225
|
+
added.append(tag.name)
|
|
226
|
+
else:
|
|
227
|
+
existing.append(tag.name)
|
|
228
|
+
return TagAttachResult(added=added, existing=existing)
|
|
229
|
+
except IntegrityError as exc:
|
|
230
|
+
raise TagAttachError("Could not attach tags to command.") from exc
|
|
231
|
+
|
|
232
|
+
def remove_tags(self, command: Command, tags: Sequence[Tag]) -> TagDetachResult:
|
|
233
|
+
"""
|
|
234
|
+
Removes tags from a command identified by the provided alias. The method first validates
|
|
235
|
+
the tags to ensure they exist in the database, then attempts to remove the associations
|
|
236
|
+
between the command and the respective tags. If a tag is not attached to the command, it
|
|
237
|
+
is recorded in the `not_attached` list, while successfully removed tags are recorded in
|
|
238
|
+
the `removed` list. Any errors during detachment raise a `TagDetachError`.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
command (Command): The command from which the tags will be detached.
|
|
242
|
+
tags (Sequence[Tag]): A list of tags to be detached from the command.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
TagDetachResult: Object that contains two lists:
|
|
246
|
+
- `removed`: A list of successfully detached tags.
|
|
247
|
+
- `not_attached`: A list of tags that were not associated with the command.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
TagDetachError: If the detachment process encounters an issue, such as database
|
|
251
|
+
integrity errors.
|
|
252
|
+
"""
|
|
253
|
+
if not tags:
|
|
254
|
+
return TagDetachResult(removed=[], not_attached=[])
|
|
255
|
+
removed = []
|
|
256
|
+
not_attached = []
|
|
257
|
+
try:
|
|
258
|
+
with db.atomic():
|
|
259
|
+
for tag in tags:
|
|
260
|
+
deleted = (
|
|
261
|
+
CommandTag.delete()
|
|
262
|
+
.where(
|
|
263
|
+
(CommandTag.command == command) & (CommandTag.tag == tag)
|
|
264
|
+
)
|
|
265
|
+
.execute()
|
|
266
|
+
)
|
|
267
|
+
if deleted:
|
|
268
|
+
removed.append(tag.name)
|
|
269
|
+
else:
|
|
270
|
+
not_attached.append(tag.name)
|
|
271
|
+
except IntegrityError as exc:
|
|
272
|
+
raise TagDetachError("Could not detach tags from command.") from exc
|
|
273
|
+
except AttributeError:
|
|
274
|
+
raise TagDetachError("Invalid tag provided.")
|
|
275
|
+
return TagDetachResult(removed=removed, not_attached=not_attached)
|
|
276
|
+
|
|
277
|
+
def list_all(
|
|
278
|
+
self, order_by: str | Sequence[str] = "alias", limit: int = 25
|
|
279
|
+
) -> list[Command]:
|
|
280
|
+
"""
|
|
281
|
+
Lists all Command objects from the database, optionally ordered by specified fields.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
order_by (str | Sequence[str]): A string or sequence of strings indicating the field(s)
|
|
285
|
+
by which the Command objects should be ordered. Defaults to "name".
|
|
286
|
+
limit (int): The maximum number of Command objects to return. Defaults to 25.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
list[Command]: A list of Command objects retrieved from the database,
|
|
290
|
+
sorted based on the specified ordering criteria.
|
|
291
|
+
"""
|
|
292
|
+
ordering = self._resolve_ordering(order_by)
|
|
293
|
+
return list(Command.select().order_by(*ordering).limit(limit))
|
|
294
|
+
|
|
295
|
+
def list_by_tag(
|
|
296
|
+
self,
|
|
297
|
+
tags: Sequence[Tag],
|
|
298
|
+
order_by: str | Sequence[str] = "alias",
|
|
299
|
+
limit: int = 25,
|
|
300
|
+
) -> list[Command]:
|
|
301
|
+
"""
|
|
302
|
+
Fetches a list of commands filtered by specified tags and ordered by specific
|
|
303
|
+
criteria.
|
|
304
|
+
|
|
305
|
+
This method retrieves a list of `Command` objects associated with the tags
|
|
306
|
+
provided in the `tags` parameter. The results can be customized through
|
|
307
|
+
ordering and limited in number.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
tags (Sequence[Tag]): A sequence of tag objects used to filter the commands.
|
|
311
|
+
Only commands associated with these tags will be retrieved.
|
|
312
|
+
order_by (str | Sequence[str], optional): Specifies the criteria to order
|
|
313
|
+
the commands. Defaults to "alias".
|
|
314
|
+
limit (int, optional): The maximum number of commands to retrieve. Defaults
|
|
315
|
+
to 25.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
list[Command]: A list of `Command` objects that meet the specified filters
|
|
319
|
+
and ordering criteria.
|
|
320
|
+
"""
|
|
321
|
+
if not tags:
|
|
322
|
+
return []
|
|
323
|
+
commands = (
|
|
324
|
+
Command.select().join(CommandTag).where(CommandTag.tag << tags).distinct()
|
|
325
|
+
)
|
|
326
|
+
ordering = self._resolve_ordering(order_by)
|
|
327
|
+
return list(commands.order_by(*ordering).limit(limit))
|
|
328
|
+
|
|
329
|
+
def search(
|
|
330
|
+
self,
|
|
331
|
+
query: str,
|
|
332
|
+
fields: str | Sequence[str] | None = ("alias", "template", "description"),
|
|
333
|
+
limit: int = 25,
|
|
334
|
+
) -> list[Command]:
|
|
335
|
+
"""
|
|
336
|
+
Searches for commands matching the given query across specified fields.
|
|
337
|
+
|
|
338
|
+
This function performs a case-insensitive search for the query in the specified
|
|
339
|
+
fields of the Command model. It calculates a relevance score for each match
|
|
340
|
+
based on the occurrence count and position of the query within the fields and
|
|
341
|
+
sorts the results by relevance.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
query (str): The search query to match in the specified fields.
|
|
345
|
+
fields (str | Sequence[str] | None): The fields to search within. By
|
|
346
|
+
default, searches within "name" and "description". Can be a single field
|
|
347
|
+
name as a string or a sequence of field names.
|
|
348
|
+
limit (int): The maximum number of results to return. Defaults to 25.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
list[Command]: A list of Command objects matching the search query, sorted
|
|
352
|
+
by relevance.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ValueError: If any provided field does not exist on the Command model.
|
|
356
|
+
"""
|
|
357
|
+
return self._search(query, "alias", fields, limit)
|
|
358
|
+
|
|
359
|
+
def delete(self, command: Command) -> bool:
|
|
360
|
+
"""
|
|
361
|
+
Deletes the command with the specified alias.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
command (Command): The command to delete.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
bool: True if the command was deleted, False otherwise.
|
|
368
|
+
"""
|
|
369
|
+
if not command:
|
|
370
|
+
return False
|
|
371
|
+
command.delete_instance()
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
def _is_unique_alias_violation(self, exc: IntegrityError) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Checks if the exception indicates a unique constraint violation.
|
|
377
|
+
|
|
378
|
+
This method analyzes the provided IntegrityError to determine if the
|
|
379
|
+
error was caused by a UNIQUE constraint violation on a field of the
|
|
380
|
+
database table associated with the model.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
exc (IntegrityError): The IntegrityError exception to be checked.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
bool: True if the exception indicates a unique constraint violation;
|
|
387
|
+
otherwise, False.
|
|
388
|
+
"""
|
|
389
|
+
msg = str(exc)
|
|
390
|
+
table = self.model._meta.table_name
|
|
391
|
+
return "UNIQUE constraint failed" in msg and f"{table}.alias" in msg
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from cmdbox.exceptions import CmdboxError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnknownCommandError(CmdboxError):
|
|
5
|
+
|
|
6
|
+
def __init__(self, cmd_id: int) -> None:
|
|
7
|
+
super().__init__(f"Command with ID '{cmd_id}' not found.")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnknownAliasError(CmdboxError):
|
|
11
|
+
"""
|
|
12
|
+
Exception raised when an unknown alias is encountered.
|
|
13
|
+
|
|
14
|
+
This error is raised when a requested alias is not found within the
|
|
15
|
+
storage system.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
alias (str): The alias that was not found.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, alias: str) -> None:
|
|
22
|
+
super().__init__(f"Alias '{alias}' not found.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AliasConflictError(CmdboxError):
|
|
26
|
+
"""
|
|
27
|
+
Indicates a conflict caused by an already existing alias.
|
|
28
|
+
|
|
29
|
+
This exception is raised when attempting to create or use an alias that
|
|
30
|
+
already exists.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
alias (str): The alias string that caused the conflict.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, alias: str) -> None:
|
|
37
|
+
super().__init__(f"Alias '{alias}' already exists.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UnknownVariableError(CmdboxError):
|
|
41
|
+
|
|
42
|
+
def __init__(self, var_id: int) -> None:
|
|
43
|
+
super().__init__(f"Variable with ID '{var_id}' not found.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UnknownNameError(CmdboxError):
|
|
47
|
+
"""
|
|
48
|
+
Exception raised for accessing an unknown variable.
|
|
49
|
+
|
|
50
|
+
This exception is raised when a requested variable is not found in
|
|
51
|
+
the storage system.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
variable (str): The name of the variable that was not found.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, name: str) -> None:
|
|
58
|
+
super().__init__(f"Variable name '{name}' not found.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NameConflictError(CmdboxError):
|
|
62
|
+
"""
|
|
63
|
+
Exception caused by an already existing variable.
|
|
64
|
+
|
|
65
|
+
This exception is raised when attempting to create or use a variable that
|
|
66
|
+
already exists.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
name (str): The name of the variable that caused the conflict.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, name: str) -> None:
|
|
73
|
+
super().__init__(f"Name '{name}' already exists.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class UpdateError(CmdboxError):
|
|
77
|
+
|
|
78
|
+
def __init__(self, message: str | None = None) -> None:
|
|
79
|
+
super().__init__(f"Failed to update: {message}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UnknownTagError(CmdboxError):
|
|
83
|
+
|
|
84
|
+
def __init__(self, tag_name: str) -> None:
|
|
85
|
+
super().__init__(f"Tag '{tag_name}' not found.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TagAttachError(CmdboxError):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TagDetachError(CmdboxError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ValidationError(CmdboxError):
|
|
97
|
+
"""
|
|
98
|
+
Represents an error related to invalid data.
|
|
99
|
+
|
|
100
|
+
This class is used to handle exceptions that occur due to invalid data.
|
|
101
|
+
Invalid data is data that technically can be stored in the database, but
|
|
102
|
+
is against the use principles allowed by the application.
|
|
103
|
+
ex: Creating a command with an empty alias or template.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class UnknownHistoryEntryError(CmdboxError):
|
|
110
|
+
|
|
111
|
+
def __init__(self, ref: str):
|
|
112
|
+
super().__init__(f"No history entry found matching '{ref}'.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AmbiguousHistoryIdEntryError(CmdboxError):
|
|
116
|
+
|
|
117
|
+
def __init__(self, prefix: str):
|
|
118
|
+
super().__init__(
|
|
119
|
+
f"ID prefix '{prefix}' matches multiple history entries. Use more ID characters."
|
|
120
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from cmdbox.models import CommandHistory
|
|
6
|
+
from cmdbox.repositories.errors import (
|
|
7
|
+
UnknownHistoryEntryError,
|
|
8
|
+
AmbiguousHistoryIdEntryError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HistoryRepository:
|
|
13
|
+
|
|
14
|
+
def record(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
alias: str,
|
|
18
|
+
template: str,
|
|
19
|
+
resolved: str,
|
|
20
|
+
variables_used: dict[str, str] | None,
|
|
21
|
+
exit_code: int | None,
|
|
22
|
+
limit: int | None,
|
|
23
|
+
) -> CommandHistory:
|
|
24
|
+
"""
|
|
25
|
+
Records a command execution in the history and applies retention rules
|
|
26
|
+
to maintain the history within a specified limit.
|
|
27
|
+
|
|
28
|
+
This function creates a new entry in the command history by storing
|
|
29
|
+
details about the executed command, including its alias, the template
|
|
30
|
+
used, resolved result, variables involved, and its exit code. It then
|
|
31
|
+
applies retention policies for the alias based on the provided limit.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
alias (str): The alias of the command being recorded.
|
|
35
|
+
template (str): The template of the command executed.
|
|
36
|
+
resolved (str): The resolved command after variable substitution.
|
|
37
|
+
variables_used (dict[str, str] | None): The variables used in the
|
|
38
|
+
command execution. Defaults to None if no variables were used.
|
|
39
|
+
exit_code (int | None): The exit code of the command. Defaults to
|
|
40
|
+
None if the command has no exit code.
|
|
41
|
+
limit (int | None): The retention limit for the history of the
|
|
42
|
+
specified alias. If None, no limit is applied.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
CommandHistory: The newly created command history entry.
|
|
46
|
+
"""
|
|
47
|
+
entry = CommandHistory.create(
|
|
48
|
+
id=uuid.uuid4().hex,
|
|
49
|
+
alias=alias,
|
|
50
|
+
template=template,
|
|
51
|
+
resolved=resolved,
|
|
52
|
+
variables_used=json.dumps(variables_used) if variables_used else None,
|
|
53
|
+
exit_code=exit_code,
|
|
54
|
+
ran_at=datetime.now(),
|
|
55
|
+
)
|
|
56
|
+
self._apply_retention(alias, limit)
|
|
57
|
+
return entry
|
|
58
|
+
|
|
59
|
+
def get_by_id(self, ref: str) -> CommandHistory:
|
|
60
|
+
"""
|
|
61
|
+
Retrieves a `CommandHistory` entry by its ID or a prefix of the ID.
|
|
62
|
+
|
|
63
|
+
This method fetches an entry from the `CommandHistory` database table using the provided
|
|
64
|
+
ID or ID prefix. It ensures that one and only one match exists for the given reference.
|
|
65
|
+
If no match is found, or if multiple matches are found, specific exceptions are raised.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
ref (str): The ID or prefix of the command history entry to retrieve.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
CommandHistory: The command history entry corresponding to the provided reference.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
UnknownHistoryEntryError: If no command history entry matches the given reference.
|
|
75
|
+
AmbiguousHistoryIdEntryError: If multiple command history entries match the given
|
|
76
|
+
prefix, making the reference ambiguous.
|
|
77
|
+
"""
|
|
78
|
+
matches = list(
|
|
79
|
+
CommandHistory.select().where(CommandHistory.id.startswith(ref)).limit(2)
|
|
80
|
+
)
|
|
81
|
+
if not matches:
|
|
82
|
+
raise UnknownHistoryEntryError(ref=ref)
|
|
83
|
+
if len(matches) > 1:
|
|
84
|
+
raise AmbiguousHistoryIdEntryError(prefix=ref)
|
|
85
|
+
return matches[0]
|
|
86
|
+
|
|
87
|
+
def get_recent(
|
|
88
|
+
self, alias: str | None = None, limit: int = 25
|
|
89
|
+
) -> list[CommandHistory]:
|
|
90
|
+
"""
|
|
91
|
+
Fetches the most recent command history records, optionally filtered by alias and limited
|
|
92
|
+
to a specified number of entries.
|
|
93
|
+
|
|
94
|
+
This method queries the CommandHistory records in descending order of their `ran_at`
|
|
95
|
+
timestamps, which represent the time the commands were executed. If the `alias` parameter
|
|
96
|
+
is provided, the results are filtered to include only those records where the alias matches
|
|
97
|
+
the provided value. The number of returned records can be limited by the `limit` parameter.
|
|
98
|
+
If `limit` is 0 or negative, no results will be retrieved.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
alias: Optional. The alias name to filter the command history records.
|
|
102
|
+
limit: The maximum number of command history records to return. Defaults
|
|
103
|
+
to 25. If set to 0 or a negative number, no records will be retrieved.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
list[CommandHistory]: A list of `CommandHistory` objects representing the
|
|
107
|
+
queried command history records.
|
|
108
|
+
"""
|
|
109
|
+
query = CommandHistory.select().order_by(CommandHistory.ran_at.desc())
|
|
110
|
+
if alias:
|
|
111
|
+
query = query.where(CommandHistory.alias == alias)
|
|
112
|
+
if limit > 0:
|
|
113
|
+
query = query.limit(limit)
|
|
114
|
+
return list(query)
|
|
115
|
+
|
|
116
|
+
def delete_by_id(self, history_id: str) -> bool:
|
|
117
|
+
entry = self.get_by_id(history_id)
|
|
118
|
+
entry.delete_instance()
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def clear(self, alias: str | None = None) -> int:
|
|
122
|
+
query = CommandHistory.delete()
|
|
123
|
+
if alias:
|
|
124
|
+
query = query.where(CommandHistory.alias == alias)
|
|
125
|
+
return query.execute()
|
|
126
|
+
|
|
127
|
+
def _apply_retention(self, alias: str, limit: int | None) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Applies retention logic by limiting the number of recent command history
|
|
130
|
+
entries for the specified alias. Deletes older command history entries
|
|
131
|
+
exceeding the given limit.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
alias (str): The alias to filter command history entries.
|
|
135
|
+
limit (int | None): The maximum number of recent entries to retain. If
|
|
136
|
+
the limit is less than or equal to zero, no action is performed.
|
|
137
|
+
"""
|
|
138
|
+
if limit is None or limit <= 0:
|
|
139
|
+
return
|
|
140
|
+
keep_ids = [
|
|
141
|
+
row.id
|
|
142
|
+
for row in CommandHistory.select(CommandHistory.id)
|
|
143
|
+
.where(CommandHistory.alias == alias)
|
|
144
|
+
.order_by(CommandHistory.ran_at.desc())
|
|
145
|
+
.limit(limit)
|
|
146
|
+
]
|
|
147
|
+
if len(keep_ids) >= limit:
|
|
148
|
+
(
|
|
149
|
+
CommandHistory.delete()
|
|
150
|
+
.where(
|
|
151
|
+
(CommandHistory.alias == alias)
|
|
152
|
+
& (CommandHistory.id.not_in(keep_ids))
|
|
153
|
+
)
|
|
154
|
+
.execute()
|
|
155
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class TagAttachResult:
|
|
7
|
+
"""
|
|
8
|
+
Represents the result of attaching tags.
|
|
9
|
+
|
|
10
|
+
This class holds information about successfully added tags and tags
|
|
11
|
+
that were already present. It helps in distinguishing between newly
|
|
12
|
+
added tags and pre-existing tags when managing tag assignments.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
added (List[str]): List of tags that were newly added.
|
|
16
|
+
existing (List[str]): List of tags that were already present.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
added: List[str]
|
|
20
|
+
existing: List[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TagDetachResult:
|
|
25
|
+
"""
|
|
26
|
+
Represents the result of detaching tags.
|
|
27
|
+
|
|
28
|
+
This class holds information about tags that were successfully removed
|
|
29
|
+
and tags that were not attached in the first place to be removed.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
removed (List[str]): List of tags that were successfully removed.
|
|
33
|
+
not_attached (List[str]): List of tags that were not attached in the first place.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
removed: List[str]
|
|
37
|
+
not_attached: List[str]
|