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,91 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from peewee import IntegrityError
|
|
4
|
+
|
|
5
|
+
from .base_repository import BaseRepository
|
|
6
|
+
from .errors import (
|
|
7
|
+
NameConflictError,
|
|
8
|
+
UnknownNameError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
UpdateError,
|
|
11
|
+
UnknownTagError,
|
|
12
|
+
)
|
|
13
|
+
from .validators import TagValidator
|
|
14
|
+
from cmdbox.models import Tag
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TagRepository(BaseRepository[Tag]):
|
|
18
|
+
model = Tag
|
|
19
|
+
|
|
20
|
+
def __init__(self, validator: TagValidator | None = None):
|
|
21
|
+
self.validator = validator or TagValidator()
|
|
22
|
+
|
|
23
|
+
def create(self, name: str, description: str | None = None) -> Tag:
|
|
24
|
+
name = name.strip() if name else None
|
|
25
|
+
self.validator.validate_create(name=name, description=description)
|
|
26
|
+
try:
|
|
27
|
+
return Tag.create(name=name, description=description)
|
|
28
|
+
except IntegrityError as exc:
|
|
29
|
+
if name is not None and self._is_unique_name_violation(exc):
|
|
30
|
+
raise NameConflictError(name=name) from exc
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
def get_by_name(self, name: str) -> Tag | None:
|
|
34
|
+
name = name.lower()
|
|
35
|
+
tag = Tag.get_or_none(Tag.name == name)
|
|
36
|
+
if tag is None:
|
|
37
|
+
raise UnknownNameError(name=name)
|
|
38
|
+
return tag
|
|
39
|
+
|
|
40
|
+
def get_by_id(self, tag_id: int) -> Tag | None:
|
|
41
|
+
tag = Tag.get_or_none(Tag.id == tag_id)
|
|
42
|
+
if tag is None:
|
|
43
|
+
raise UnknownTagError(str(tag_id))
|
|
44
|
+
return tag
|
|
45
|
+
|
|
46
|
+
def update(self, tag: Tag, **fields) -> Tag | None:
|
|
47
|
+
if not tag:
|
|
48
|
+
raise UpdateError("Tag not found.")
|
|
49
|
+
if not fields:
|
|
50
|
+
raise UpdateError("No fields provided for update.")
|
|
51
|
+
|
|
52
|
+
if "name" in fields and fields.get("name") is not None:
|
|
53
|
+
fields["name"] = fields.get("name").strip()
|
|
54
|
+
|
|
55
|
+
self.validator.validate_update(
|
|
56
|
+
name=fields.get("name", tag.name),
|
|
57
|
+
description=fields.get("description", tag.description),
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
for key, value in fields.items():
|
|
61
|
+
if not hasattr(tag, key):
|
|
62
|
+
raise ValidationError(f"Invalid field: {key}")
|
|
63
|
+
if value is not None:
|
|
64
|
+
setattr(tag, key, value)
|
|
65
|
+
tag.save()
|
|
66
|
+
return tag
|
|
67
|
+
except IntegrityError as exc:
|
|
68
|
+
name = fields.get("name", "")
|
|
69
|
+
if name is not None and self._is_unique_name_violation(exc):
|
|
70
|
+
raise NameConflictError(name=name) from exc
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
def list_all(
|
|
74
|
+
self, order_by: str | Sequence[str] = "name", limit: int = 25
|
|
75
|
+
) -> list[Tag]:
|
|
76
|
+
ordering = self._resolve_ordering(order_by)
|
|
77
|
+
return list(Tag.select().order_by(*ordering).limit(limit))
|
|
78
|
+
|
|
79
|
+
def search(
|
|
80
|
+
self,
|
|
81
|
+
query: str,
|
|
82
|
+
fields: str | Sequence[str] | None = ("name", "description"),
|
|
83
|
+
limit: int = 25,
|
|
84
|
+
) -> list[Tag]:
|
|
85
|
+
return self._search(query, "name", fields, limit=limit)
|
|
86
|
+
|
|
87
|
+
def delete(self, tag: Tag) -> bool:
|
|
88
|
+
if not tag:
|
|
89
|
+
return False
|
|
90
|
+
tag.delete_instance()
|
|
91
|
+
return True
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .errors import ValidationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class CommandValidatorConfig:
|
|
8
|
+
"""Configuration for command validation rules."""
|
|
9
|
+
|
|
10
|
+
reserved_aliases: frozenset[str] = frozenset(
|
|
11
|
+
{
|
|
12
|
+
"help",
|
|
13
|
+
"init",
|
|
14
|
+
"list",
|
|
15
|
+
"ls",
|
|
16
|
+
"add",
|
|
17
|
+
"rm",
|
|
18
|
+
"delete",
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
max_alias_length: int = 100
|
|
22
|
+
max_description_length: int = 1000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CommandValidator:
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: CommandValidatorConfig | None = None):
|
|
28
|
+
self.config = config or CommandValidatorConfig()
|
|
29
|
+
|
|
30
|
+
def validate_create(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
alias: str,
|
|
34
|
+
template: str,
|
|
35
|
+
description: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Validates the parameters for creating a Command. Ensures that the provided alias,
|
|
39
|
+
template, and description meet the required conditions and do not contain
|
|
40
|
+
self-referencing patterns.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
alias (str): The alias to validate.
|
|
44
|
+
template (str): The template to validate.
|
|
45
|
+
description (str | None): The optional description to validate.
|
|
46
|
+
"""
|
|
47
|
+
self.validate_alias(alias)
|
|
48
|
+
self.validate_template(template)
|
|
49
|
+
self.validate_description(description)
|
|
50
|
+
self.validate_no_self_reference(alias, template)
|
|
51
|
+
|
|
52
|
+
def validate_update(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
alias: str | None = None,
|
|
56
|
+
template: str | None = None,
|
|
57
|
+
description: str | None = None,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Validates various update parameters, ensuring they meet the required conditions
|
|
61
|
+
and constraints. This method can validate a combination of alias, template,
|
|
62
|
+
and description, applying specific checks for each provided parameter.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
alias (str | None): An optional alias to validate. If provided, it will
|
|
66
|
+
be checked using the `self.validate_alias` method.
|
|
67
|
+
template (str | None): An optional template to validate. If provided, it
|
|
68
|
+
will be checked using the `self.validate_template` method.
|
|
69
|
+
description (str | None): An optional description to validate. If
|
|
70
|
+
provided, it will be checked using the `self.validate_description`
|
|
71
|
+
method.
|
|
72
|
+
"""
|
|
73
|
+
if alias is not None:
|
|
74
|
+
self.validate_alias(alias)
|
|
75
|
+
if template is not None:
|
|
76
|
+
self.validate_template(template)
|
|
77
|
+
if description is not None:
|
|
78
|
+
self.validate_description(description)
|
|
79
|
+
if alias is not None and template is not None:
|
|
80
|
+
self.validate_no_self_reference(alias, template)
|
|
81
|
+
|
|
82
|
+
def validate_alias(self, alias: str) -> None:
|
|
83
|
+
if not alias:
|
|
84
|
+
raise ValidationError("Alias cannot be empty.")
|
|
85
|
+
|
|
86
|
+
stripped = alias.strip()
|
|
87
|
+
if not stripped:
|
|
88
|
+
raise ValidationError("Alias cannot contain only whitespace.")
|
|
89
|
+
|
|
90
|
+
if " " in stripped:
|
|
91
|
+
raise ValidationError("Alias cannot contain spaces.")
|
|
92
|
+
|
|
93
|
+
if stripped in self.config.reserved_aliases:
|
|
94
|
+
raise ValidationError(f"Alias '{stripped}' is reserved.")
|
|
95
|
+
|
|
96
|
+
if len(stripped) > self.config.max_alias_length:
|
|
97
|
+
raise ValidationError(
|
|
98
|
+
f"Alias '{stripped}' is too long. Maximum length is {self.config.max_alias_length}."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def validate_template(self, template: str) -> None:
|
|
102
|
+
if not template:
|
|
103
|
+
raise ValidationError("Template cannot be empty.")
|
|
104
|
+
|
|
105
|
+
stripped = template.strip()
|
|
106
|
+
if not stripped:
|
|
107
|
+
raise ValidationError("Template cannot contain only whitespace.")
|
|
108
|
+
|
|
109
|
+
def validate_description(self, description: str | None) -> None:
|
|
110
|
+
if not description:
|
|
111
|
+
return
|
|
112
|
+
if len(description) > self.config.max_description_length:
|
|
113
|
+
raise ValidationError(
|
|
114
|
+
f"Description is too long. Maximum length is {self.config.max_description_length}."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def validate_no_self_reference(self, alias: str, template: str) -> None:
|
|
118
|
+
self_reference = f"<{alias}>"
|
|
119
|
+
if self_reference in template:
|
|
120
|
+
raise ValidationError(
|
|
121
|
+
f"Template cannot contain self-reference: {self_reference}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class VariableValidatorConfig:
|
|
127
|
+
"""Configuration for variable validation rules."""
|
|
128
|
+
|
|
129
|
+
reserved_names: frozenset[str] = frozenset(
|
|
130
|
+
{
|
|
131
|
+
"help",
|
|
132
|
+
"init",
|
|
133
|
+
"list",
|
|
134
|
+
"ls",
|
|
135
|
+
"add",
|
|
136
|
+
"rm",
|
|
137
|
+
"delete",
|
|
138
|
+
# Reserved because they are options to the run command and can conflict when dynamically called
|
|
139
|
+
"preview",
|
|
140
|
+
"cwd",
|
|
141
|
+
"env",
|
|
142
|
+
"capture",
|
|
143
|
+
"shell",
|
|
144
|
+
"emit",
|
|
145
|
+
"verbose",
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
max_name_length: int = 100
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class VariableValidator:
|
|
152
|
+
|
|
153
|
+
def __init__(self, config: VariableValidatorConfig | None = None):
|
|
154
|
+
self.config = config or VariableValidatorConfig()
|
|
155
|
+
|
|
156
|
+
def validate_create(self, name: str, value: str) -> None:
|
|
157
|
+
self.validate_name(name)
|
|
158
|
+
self.validate_value(value)
|
|
159
|
+
|
|
160
|
+
def validate_update(self, name: str | None, value: str | None) -> None:
|
|
161
|
+
if name is not None:
|
|
162
|
+
self.validate_name(name)
|
|
163
|
+
if value is not None:
|
|
164
|
+
self.validate_value(value)
|
|
165
|
+
|
|
166
|
+
def validate_name(self, name: str) -> None:
|
|
167
|
+
if not name:
|
|
168
|
+
raise ValidationError("Variable name cannot be empty.")
|
|
169
|
+
|
|
170
|
+
stripped = name.strip()
|
|
171
|
+
if not stripped:
|
|
172
|
+
raise ValidationError("Variable name cannot contain only whitespace.")
|
|
173
|
+
|
|
174
|
+
if " " in stripped:
|
|
175
|
+
raise ValidationError("Variable name cannot contain spaces.")
|
|
176
|
+
|
|
177
|
+
if stripped in self.config.reserved_names:
|
|
178
|
+
raise ValidationError(f"Variable name '{stripped}' is reserved.")
|
|
179
|
+
|
|
180
|
+
if len(stripped) > self.config.max_name_length:
|
|
181
|
+
raise ValidationError(
|
|
182
|
+
f"Variable name '{stripped}' is too long. Maximum length is {self.config.max_name_length}."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def validate_value(self, value: str) -> None:
|
|
186
|
+
if value is None:
|
|
187
|
+
raise ValidationError("Variable value cannot be None.")
|
|
188
|
+
|
|
189
|
+
def validate_no_self_reference(self, name: str, value: str) -> None:
|
|
190
|
+
self_reference = f"<{name}>"
|
|
191
|
+
if self_reference in value:
|
|
192
|
+
raise ValidationError(
|
|
193
|
+
f"Variable value cannot contain self-reference: {self_reference}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class TagValidatorConfig:
|
|
199
|
+
"""Configuration for variable validation rules."""
|
|
200
|
+
|
|
201
|
+
reserved_names: frozenset[str] = frozenset(
|
|
202
|
+
{
|
|
203
|
+
"help",
|
|
204
|
+
"init",
|
|
205
|
+
"list",
|
|
206
|
+
"ls",
|
|
207
|
+
"add",
|
|
208
|
+
"rm",
|
|
209
|
+
"delete",
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
max_name_length: int = 100
|
|
213
|
+
max_description_length: int = 1000
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TagValidator:
|
|
217
|
+
|
|
218
|
+
def __init__(self, config: TagValidatorConfig | None = None):
|
|
219
|
+
self.config = config or TagValidatorConfig()
|
|
220
|
+
|
|
221
|
+
def validate_create(self, name: str, description: str | None) -> None:
|
|
222
|
+
self.validate_name(name)
|
|
223
|
+
self.validate_description(description)
|
|
224
|
+
|
|
225
|
+
def validate_update(self, name: str | None, description: str | None) -> None:
|
|
226
|
+
if name is not None:
|
|
227
|
+
self.validate_name(name)
|
|
228
|
+
if description is not None:
|
|
229
|
+
self.validate_description(description)
|
|
230
|
+
|
|
231
|
+
def validate_name(self, name: str) -> None:
|
|
232
|
+
if not name:
|
|
233
|
+
raise ValidationError("Variable name cannot be empty.")
|
|
234
|
+
|
|
235
|
+
stripped = name.strip()
|
|
236
|
+
if not stripped:
|
|
237
|
+
raise ValidationError("Variable name cannot contain only whitespace.")
|
|
238
|
+
|
|
239
|
+
if " " in stripped:
|
|
240
|
+
raise ValidationError("Variable name cannot contain spaces.")
|
|
241
|
+
|
|
242
|
+
if stripped in self.config.reserved_names:
|
|
243
|
+
raise ValidationError(f"Variable name '{stripped}' is reserved.")
|
|
244
|
+
|
|
245
|
+
if len(stripped) > self.config.max_name_length:
|
|
246
|
+
raise ValidationError(
|
|
247
|
+
f"Variable name '{stripped}' is too long. Maximum length is {self.config.max_name_length}."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def validate_description(self, description: str | None) -> None:
|
|
251
|
+
if not description:
|
|
252
|
+
return
|
|
253
|
+
if len(description) > self.config.max_description_length:
|
|
254
|
+
raise ValidationError(
|
|
255
|
+
f"Description is too long. Maximum length is {self.config.max_description_length}."
|
|
256
|
+
)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from peewee import IntegrityError
|
|
4
|
+
|
|
5
|
+
from .base_repository import BaseRepository
|
|
6
|
+
from .errors import (
|
|
7
|
+
ValidationError,
|
|
8
|
+
NameConflictError,
|
|
9
|
+
UnknownTagError,
|
|
10
|
+
TagAttachError,
|
|
11
|
+
TagDetachError,
|
|
12
|
+
UpdateError,
|
|
13
|
+
UnknownVariableError,
|
|
14
|
+
)
|
|
15
|
+
from .validators import VariableValidator
|
|
16
|
+
from .results import TagAttachResult, TagDetachResult
|
|
17
|
+
from cmdbox.database import db
|
|
18
|
+
from cmdbox.models import Variable, Tag, VariableTag
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VariableRepository(BaseRepository[Variable]):
|
|
22
|
+
model = Variable
|
|
23
|
+
|
|
24
|
+
def __init__(self, validator: VariableValidator | None = None):
|
|
25
|
+
self.validator = validator or VariableValidator()
|
|
26
|
+
|
|
27
|
+
def create(self, name: str, value: str) -> Variable:
|
|
28
|
+
"""
|
|
29
|
+
Validates and creates a new Variable object based on provided input parameters.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name (str): Unique identifier for the variable to be created.
|
|
33
|
+
value (str): The value that will be subbed for the name when executing.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Variable: The created Variable object.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
NameConflictError: If a variable with the provided name already exists
|
|
40
|
+
and causes a unique constraint violation during creation.
|
|
41
|
+
IntegrityError: If any database integrity issue occurs during the creation.
|
|
42
|
+
"""
|
|
43
|
+
name = name.strip() if name else None
|
|
44
|
+
self.validator.validate_create(name=name, value=value)
|
|
45
|
+
try:
|
|
46
|
+
return Variable.create(name=name, value=value)
|
|
47
|
+
except IntegrityError as exc:
|
|
48
|
+
if name is not None and self._is_unique_name_violation(exc):
|
|
49
|
+
raise NameConflictError(name=name) from exc
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def get_by_name(self, name: str) -> Variable | None:
|
|
53
|
+
"""
|
|
54
|
+
Retrieves a variable instance by its name.
|
|
55
|
+
|
|
56
|
+
Converts the provided name to lowercase, searches for a variable with the
|
|
57
|
+
matching name in the database, and retrieves it. This function returns None
|
|
58
|
+
if no variable with the specified name is found.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name (str): The name of the variable to retrieve.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Variable | None: The variable object if found, otherwise None.
|
|
65
|
+
"""
|
|
66
|
+
name = name.lower()
|
|
67
|
+
return Variable.get_or_none(Variable.name == name)
|
|
68
|
+
|
|
69
|
+
def get_by_id(self, var_id: int) -> Variable:
|
|
70
|
+
var = Variable.get_or_none(Variable.id == var_id)
|
|
71
|
+
if var is None:
|
|
72
|
+
raise UnknownVariableError(var_id=var_id)
|
|
73
|
+
return var
|
|
74
|
+
|
|
75
|
+
def update(self, variable: Variable, **fields) -> Variable:
|
|
76
|
+
"""
|
|
77
|
+
Updates an existing variable based on the provided name and fields.
|
|
78
|
+
|
|
79
|
+
This method retrieves a variable by its name and updates its fields with the
|
|
80
|
+
provided values. It validates the updates, ensuring field integrity and
|
|
81
|
+
uniqueness. If the variable doesn't exist or an update violates constraints,
|
|
82
|
+
appropriate actions are taken.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
variable (Variable): The variable to update.
|
|
86
|
+
**fields: Arbitrary keyword arguments representing the fields to update.
|
|
87
|
+
Supported fields include 'name' and 'value'.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Variable: The updated variable object if successful, or None if the
|
|
91
|
+
variable is not found.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValidationError: If any of the provided fields are invalid or do not exist
|
|
95
|
+
on a variable.
|
|
96
|
+
NameConflictError: If the new name conflicts with an existing variable's
|
|
97
|
+
name.
|
|
98
|
+
IntegrityError: If there is a general integrity constraint violation during
|
|
99
|
+
the update process.
|
|
100
|
+
"""
|
|
101
|
+
if not variable:
|
|
102
|
+
raise UpdateError("No variable provided for update.")
|
|
103
|
+
if not fields:
|
|
104
|
+
raise UpdateError("No fields provided for update.")
|
|
105
|
+
|
|
106
|
+
if "name" in fields and fields.get("name") is not None:
|
|
107
|
+
fields["name"] = fields.get("name").strip()
|
|
108
|
+
|
|
109
|
+
self.validator.validate_update(
|
|
110
|
+
name=fields.get("name", variable.name),
|
|
111
|
+
value=fields.get("value", variable.value),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
for key, value in fields.items():
|
|
116
|
+
if not hasattr(variable, key):
|
|
117
|
+
raise ValidationError(f"Invalid field: {key}")
|
|
118
|
+
if value is not None:
|
|
119
|
+
setattr(variable, key, value)
|
|
120
|
+
variable.save()
|
|
121
|
+
return variable
|
|
122
|
+
except IntegrityError as exc:
|
|
123
|
+
name = fields.get("name", "")
|
|
124
|
+
if name is not None and self._is_unique_name_violation(exc):
|
|
125
|
+
raise NameConflictError(name=name) from exc
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
def add_tags(self, variable: Variable, tags: Sequence[Tag]) -> TagAttachResult:
|
|
129
|
+
"""
|
|
130
|
+
Attach tags to a variable identified by its name.
|
|
131
|
+
|
|
132
|
+
This function associates tags with a specified variable. If the tags already
|
|
133
|
+
exist for the variable, they are added to an existing list. Otherwise, new tags
|
|
134
|
+
are created and linked to the variable. If no tags are provided, it returns
|
|
135
|
+
immediately with empty results. In case of a database integrity issue, an
|
|
136
|
+
appropriate error is raised.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
variable (Variable): The variable to which the tags are to be attached.
|
|
140
|
+
tags (Sequence[Tag]): A collection of tags to be attached to the
|
|
141
|
+
variable.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
TagAttachResult: An object containing lists of newly added tags and
|
|
145
|
+
tags that already existed.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
TagAttachError: If there is an issue attaching the tags to the variable,
|
|
149
|
+
typically due to database integrity constraints.
|
|
150
|
+
"""
|
|
151
|
+
if not tags:
|
|
152
|
+
return TagAttachResult(added=[], existing=[])
|
|
153
|
+
try:
|
|
154
|
+
with db.atomic():
|
|
155
|
+
added = []
|
|
156
|
+
existing = []
|
|
157
|
+
for tag in tags:
|
|
158
|
+
var_tag, created = VariableTag.get_or_create(
|
|
159
|
+
variable=variable, tag=tag
|
|
160
|
+
)
|
|
161
|
+
if created:
|
|
162
|
+
added.append(tag.name)
|
|
163
|
+
else:
|
|
164
|
+
existing.append(tag.name)
|
|
165
|
+
return TagAttachResult(added=added, existing=existing)
|
|
166
|
+
except UnknownTagError:
|
|
167
|
+
raise
|
|
168
|
+
except IntegrityError as exc:
|
|
169
|
+
raise TagAttachError("Could not attach tags to variable.") from exc
|
|
170
|
+
|
|
171
|
+
def remove_tags(self, variable: Variable, tags: Sequence[Tag]) -> TagDetachResult:
|
|
172
|
+
"""
|
|
173
|
+
Removes tags from a variable identified by the provided name. The method first validates
|
|
174
|
+
the tags to ensure they exist in the database, then attempts to remove the associations
|
|
175
|
+
between the variable and the respective tags. If a tag is not attached to the variable, it
|
|
176
|
+
is recorded in the `not_attached` list, while successfully removed tags are recorded in
|
|
177
|
+
the `removed` list. Any errors during detachment raise a `TagDetachError`.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
variable (Variable): The variable from which the tags will be detached.
|
|
181
|
+
tags (Sequence[Tag]): A list of tags to be detached from the variable.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
TagDetachResult: Object that contains two lists:
|
|
185
|
+
- `removed`: A list of successfully detached tags.
|
|
186
|
+
- `not_attached`: A list of tags that were not associated with the variable.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
TagDetachError: If the detachment process encounters an issue, such as database
|
|
190
|
+
integrity errors.
|
|
191
|
+
"""
|
|
192
|
+
if not variable and not tags:
|
|
193
|
+
return TagDetachResult(removed=[], not_attached=[])
|
|
194
|
+
removed = []
|
|
195
|
+
not_attached = []
|
|
196
|
+
try:
|
|
197
|
+
with db.atomic():
|
|
198
|
+
for tag in tags:
|
|
199
|
+
deleted = (
|
|
200
|
+
VariableTag.delete()
|
|
201
|
+
.where(
|
|
202
|
+
(VariableTag.variable == variable)
|
|
203
|
+
& (VariableTag.tag == tag)
|
|
204
|
+
)
|
|
205
|
+
.execute()
|
|
206
|
+
)
|
|
207
|
+
if deleted:
|
|
208
|
+
removed.append(tag.name)
|
|
209
|
+
else:
|
|
210
|
+
not_attached.append(tag.name)
|
|
211
|
+
except IntegrityError as exc:
|
|
212
|
+
raise TagDetachError("Could not detach tags from variable.") from exc
|
|
213
|
+
except AttributeError:
|
|
214
|
+
raise TagDetachError("Invalid tag provided.")
|
|
215
|
+
return TagDetachResult(removed=removed, not_attached=not_attached)
|
|
216
|
+
|
|
217
|
+
def list_all(
|
|
218
|
+
self, order_by: str | Sequence[str] = "name", limit: int = 25
|
|
219
|
+
) -> list[Variable]:
|
|
220
|
+
"""
|
|
221
|
+
Lists all variables from the database, optionally ordered by specified fields.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
order_by (str | Sequence[str]): A string or sequence of strings indicating the field(s)
|
|
225
|
+
by which the variables should be ordered. Defaults to "name".
|
|
226
|
+
limit (int): The maximum number of variables to return. Defaults to 25.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List[Variable]: A list of Variable objects retrieved from the database,
|
|
230
|
+
sorted based on the specified ordering criteria.
|
|
231
|
+
"""
|
|
232
|
+
ordering = self._resolve_ordering(order_by)
|
|
233
|
+
return list(Variable.select().order_by(*ordering).limit(limit))
|
|
234
|
+
|
|
235
|
+
def list_by_tag(
|
|
236
|
+
self,
|
|
237
|
+
tags: Sequence[Tag],
|
|
238
|
+
order_by: str | Sequence[str] = "name",
|
|
239
|
+
limit: int = 25,
|
|
240
|
+
) -> list[Variable]:
|
|
241
|
+
"""
|
|
242
|
+
Fetches a list of variables filtered by specified tags and ordered by specific
|
|
243
|
+
criteria.
|
|
244
|
+
|
|
245
|
+
This method retrieves a list of `Variable` objects associated with the tags
|
|
246
|
+
provided in the `tags` argument. The results are optionally ordered based on the
|
|
247
|
+
`order_by` field and limited by the `limit` argument.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
tags (Sequence[Tag]): A list of Tag objects to filter variables by.
|
|
251
|
+
order_by (str | Sequence[str]): Criteria to order the resulting variable
|
|
252
|
+
list. Defaults to "name".
|
|
253
|
+
limit (int): The maximum number of variables to return. Defaults to 25.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
list[Variable]: A list of `Variable` objects matching the tags and ordered as
|
|
257
|
+
requested.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
UnknownTagError: If one or more provided tags do not exist in the database.
|
|
261
|
+
"""
|
|
262
|
+
ordering = self._resolve_ordering(order_by)
|
|
263
|
+
return list(
|
|
264
|
+
Variable.select()
|
|
265
|
+
.join(VariableTag)
|
|
266
|
+
.where(VariableTag.tag << tags)
|
|
267
|
+
.order_by(*ordering)
|
|
268
|
+
.distinct()
|
|
269
|
+
.limit(limit)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def search(
|
|
273
|
+
self,
|
|
274
|
+
query: str,
|
|
275
|
+
fields: str | Sequence[str] | None = ("name", "value"),
|
|
276
|
+
limit: int = 25,
|
|
277
|
+
) -> list[Variable]:
|
|
278
|
+
"""
|
|
279
|
+
Searches for variables matching the given query across specified fields.
|
|
280
|
+
|
|
281
|
+
This function performs a case-insensitive search for the query in the specified
|
|
282
|
+
fields of the Variable model. It calculates a relevance score for each match
|
|
283
|
+
based on the occurrence count and position of the query within the fields and
|
|
284
|
+
sorts the results by relevance.
|
|
285
|
+
Args:
|
|
286
|
+
query (str): The search query to match in the specified fields.
|
|
287
|
+
fields (str | Sequence[str] | None): The fields to search within. By
|
|
288
|
+
default, searches within "name". Can be a single field name as a string
|
|
289
|
+
or a sequence of field names.
|
|
290
|
+
limit (int): The maximum number of results to return. Defaults to 25.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
list[Variable]: A list of Variable objects matching the search query, sorted
|
|
294
|
+
by relevance.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: If any provided field does not exist on the Variable model.
|
|
298
|
+
"""
|
|
299
|
+
return self._search(
|
|
300
|
+
query, secondary_ordering="name", fields=fields, limit=limit
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def delete(self, variable: Variable) -> bool:
|
|
304
|
+
"""
|
|
305
|
+
Deletes the variable with the specified name.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
variable (Variable): The variable to delete.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
bool: True if the variable was deleted, False otherwise.
|
|
312
|
+
"""
|
|
313
|
+
if not variable:
|
|
314
|
+
return False
|
|
315
|
+
variable.delete_instance()
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def _is_unique_variable_tag_violation(self, exc: IntegrityError) -> bool:
|
|
319
|
+
msg = str(exc)
|
|
320
|
+
return (
|
|
321
|
+
"UNIQUE constraint failed" in msg
|
|
322
|
+
and "variabletag.variable_id" in msg
|
|
323
|
+
and "variabletag.tag_id" in msg
|
|
324
|
+
)
|
|
File without changes
|