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,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]