tft-cli 0.0.25__py3-none-any.whl → 0.0.28__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.
- tft/cli/command/__init__.py +2 -0
- tft/cli/command/listing.py +836 -0
- tft/cli/commands.py +162 -74
- tft/cli/config.py +12 -3
- tft/cli/tool.py +2 -0
- tft/cli/utils.py +149 -5
- {tft_cli-0.0.25.dist-info → tft_cli-0.0.28.dist-info}/METADATA +2 -1
- tft_cli-0.0.28.dist-info/RECORD +12 -0
- tft_cli-0.0.25.dist-info/RECORD +0 -10
- {tft_cli-0.0.25.dist-info → tft_cli-0.0.28.dist-info}/LICENSE +0 -0
- {tft_cli-0.0.25.dist-info → tft_cli-0.0.28.dist-info}/WHEEL +0 -0
- {tft_cli-0.0.25.dist-info → tft_cli-0.0.28.dist-info}/entry_points.txt +0 -0
tft/cli/utils.py
CHANGED
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
import glob
|
|
5
5
|
import itertools
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import shlex
|
|
9
|
+
import shutil
|
|
8
10
|
import subprocess
|
|
9
11
|
import sys
|
|
12
|
+
import tempfile
|
|
10
13
|
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
11
16
|
from typing import Any, Dict, List, NoReturn, Optional, Union
|
|
12
17
|
|
|
18
|
+
import pendulum
|
|
13
19
|
import requests
|
|
14
20
|
import requests.adapters
|
|
15
21
|
import typer
|
|
16
|
-
from click.core import ParameterSource
|
|
22
|
+
from click.core import ParameterSource
|
|
17
23
|
from rich.console import Console
|
|
18
|
-
from ruamel.yaml import YAML
|
|
24
|
+
from ruamel.yaml import YAML # type: ignore
|
|
19
25
|
from urllib3 import Retry
|
|
20
26
|
|
|
21
27
|
from tft.cli.config import settings
|
|
@@ -24,6 +30,56 @@ console = Console(soft_wrap=True)
|
|
|
24
30
|
console_stderr = Console(soft_wrap=True, file=sys.stderr)
|
|
25
31
|
|
|
26
32
|
|
|
33
|
+
@dataclass
|
|
34
|
+
class Age:
|
|
35
|
+
value: int
|
|
36
|
+
unit: str
|
|
37
|
+
|
|
38
|
+
_unit_multiplier = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
39
|
+
_unit_human = {"s": "second", "m": "minute", "h": "hour", "d": "day"}
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_string(cls, age_string: str) -> 'Age':
|
|
43
|
+
value, unit = age_string[:-1], age_string[-1]
|
|
44
|
+
if unit not in cls._unit_multiplier:
|
|
45
|
+
raise typer.BadParameter(f"Age must end with {', '.join(cls._unit_multiplier.keys())}")
|
|
46
|
+
|
|
47
|
+
if not value.isdigit():
|
|
48
|
+
raise typer.BadParameter(f"Invalid age value {value}")
|
|
49
|
+
|
|
50
|
+
return cls(value=int(age_string[:-1]), unit=age_string[-1])
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def birth_date(self) -> pendulum.DateTime:
|
|
54
|
+
now = pendulum.now(tz="UTC")
|
|
55
|
+
return now - pendulum.duration(seconds=self.value * self._unit_multiplier[self.unit])
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def human(self) -> str:
|
|
59
|
+
return f"{self.value} {self._unit_human[self.unit]}{'s' if self.value > 1 else ''}"
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def available_units() -> str:
|
|
63
|
+
return "s (seconds), m (minutes), h (hours) or d (days)"
|
|
64
|
+
|
|
65
|
+
def to_string(self, format="%Y-%m-%dT%H:%M:%S") -> str:
|
|
66
|
+
return self.birth_date.strftime(format)
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return f"{self.value}{self.unit}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OutputFormat(str, Enum):
|
|
73
|
+
text = "text"
|
|
74
|
+
json = "json"
|
|
75
|
+
yaml = "yaml"
|
|
76
|
+
table = "table"
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def available_formats():
|
|
80
|
+
return "text, json or table"
|
|
81
|
+
|
|
82
|
+
|
|
27
83
|
def exit_error(error: str) -> NoReturn:
|
|
28
84
|
"""Exit with given error message"""
|
|
29
85
|
console.print(f"⛔ {error}", style="red")
|
|
@@ -125,6 +181,19 @@ def hw_constraints(hardware: List[str]) -> Dict[Any, Any]:
|
|
|
125
181
|
value_mixed = False
|
|
126
182
|
|
|
127
183
|
container[path_splitted.pop()] = value_mixed
|
|
184
|
+
|
|
185
|
+
# Only process additional path elements if they exist
|
|
186
|
+
if path_splitted:
|
|
187
|
+
next_path = path_splitted.pop()
|
|
188
|
+
|
|
189
|
+
# handle compatible.distro
|
|
190
|
+
if next_path == 'distro':
|
|
191
|
+
container[next_path] = (
|
|
192
|
+
container[next_path].append(value_mixed) if next_path in container else [value_mixed]
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
container[next_path] = value_mixed
|
|
196
|
+
|
|
128
197
|
return constraints
|
|
129
198
|
|
|
130
199
|
|
|
@@ -187,6 +256,27 @@ def uuid_valid(value: str, version: int = 4) -> bool:
|
|
|
187
256
|
return False
|
|
188
257
|
|
|
189
258
|
|
|
259
|
+
def extract_uuid(value: str) -> str:
|
|
260
|
+
"""
|
|
261
|
+
Extracts a UUID from a string. If the string is already a valid UUID, returns it.
|
|
262
|
+
If the string contains a UUID, extracts and returns it.
|
|
263
|
+
Raises typer.Exit with error message if no valid UUID is found.
|
|
264
|
+
"""
|
|
265
|
+
# Check if the value is already a valid UUID
|
|
266
|
+
if uuid_valid(value):
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
# UUID pattern for extracting UUIDs from strings
|
|
270
|
+
uuid_pattern = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
|
|
271
|
+
|
|
272
|
+
# Try to extract UUID from string
|
|
273
|
+
uuid_match = uuid_pattern.search(value)
|
|
274
|
+
if uuid_match:
|
|
275
|
+
return uuid_match.group()
|
|
276
|
+
|
|
277
|
+
exit_error(f"Could not find a valid Testing Farm request id in '{value}'.")
|
|
278
|
+
|
|
279
|
+
|
|
190
280
|
class TimeoutHTTPAdapter(requests.adapters.HTTPAdapter):
|
|
191
281
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
192
282
|
self.timeout = kwargs.pop('timeout', settings.DEFAULT_API_TIMEOUT)
|
|
@@ -216,7 +306,9 @@ def install_http_retries(
|
|
|
216
306
|
|
|
217
307
|
status_forcelist_extend = status_forcelist_extend or []
|
|
218
308
|
|
|
219
|
-
|
|
309
|
+
from typing import Any, Dict
|
|
310
|
+
|
|
311
|
+
params: Dict[str, Any] = {
|
|
220
312
|
"total": retries,
|
|
221
313
|
"status_forcelist": [
|
|
222
314
|
429, # Too Many Requests
|
|
@@ -226,9 +318,9 @@ def install_http_retries(
|
|
|
226
318
|
504, # Gateway Timeout
|
|
227
319
|
]
|
|
228
320
|
+ status_forcelist_extend,
|
|
229
|
-
allowed_retry_parameter: ['HEAD', 'GET', 'POST', 'DELETE', 'PUT'],
|
|
230
321
|
"backoff_factor": retry_backoff_factor,
|
|
231
322
|
}
|
|
323
|
+
params[allowed_retry_parameter] = ['HEAD', 'GET', 'POST', 'DELETE', 'PUT']
|
|
232
324
|
retry_strategy = Retry(**params)
|
|
233
325
|
|
|
234
326
|
timeout_adapter = TimeoutHTTPAdapter(timeout=timeout, max_retries=retry_strategy)
|
|
@@ -263,8 +355,60 @@ def read_glob_paths(glob_paths: List[str]) -> str:
|
|
|
263
355
|
|
|
264
356
|
def check_unexpected_arguments(context: typer.Context, *args: str) -> Union[None, NoReturn]:
|
|
265
357
|
for argument in args:
|
|
266
|
-
if context.get_parameter_source(argument) == ParameterSource.COMMANDLINE:
|
|
358
|
+
if context.get_parameter_source(argument) == ParameterSource.COMMANDLINE:
|
|
267
359
|
exit_error(
|
|
268
360
|
f"Unexpected argument '{context.params.get(argument)}'. "
|
|
269
361
|
"Please make sure you are passing the parameters correctly."
|
|
270
362
|
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def validate_age(value: str) -> Age:
|
|
366
|
+
if value.endswith("m"):
|
|
367
|
+
return Age(int(value[:-1]), "m")
|
|
368
|
+
elif value.endswith("d"):
|
|
369
|
+
return Age(int(value[:-1]), "d")
|
|
370
|
+
else:
|
|
371
|
+
raise ValueError("Age must end with 'm' for months or 'd' for days.")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def authorization_headers(api_key: str) -> Dict[str, str]:
|
|
375
|
+
"""
|
|
376
|
+
Return a dict with headers for a request to Testing Farm API.
|
|
377
|
+
Used for authentication.
|
|
378
|
+
"""
|
|
379
|
+
return {'Authorization': f'Bearer {api_key}'}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def edit_with_editor(data: Any, description: Optional[str]) -> Any:
|
|
383
|
+
"""
|
|
384
|
+
Open data in an editor for user modification and return it back.
|
|
385
|
+
If description specified, print it as a user message together with the used editor.
|
|
386
|
+
"""
|
|
387
|
+
# Get the editor from environment variable, fallback to sensible defaults
|
|
388
|
+
editor = os.environ.get('EDITOR')
|
|
389
|
+
if not editor:
|
|
390
|
+
# Try common editors in order of preference
|
|
391
|
+
for candidate in ['vim', 'vi', 'nano', 'emacs']:
|
|
392
|
+
if shutil.which(candidate):
|
|
393
|
+
editor = candidate
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
if not editor:
|
|
397
|
+
exit_error("No editor found. Please set the 'EDITOR' environment variable.")
|
|
398
|
+
|
|
399
|
+
# Create a temporary file with the JSON content
|
|
400
|
+
with tempfile.NamedTemporaryFile(mode='w') as temp_file:
|
|
401
|
+
temp_file.write(data)
|
|
402
|
+
temp_file.flush()
|
|
403
|
+
|
|
404
|
+
# Open the editor
|
|
405
|
+
if description:
|
|
406
|
+
console.print(f"✏️ {description}, editor '{editor}'")
|
|
407
|
+
result = subprocess.run([editor, temp_file.name])
|
|
408
|
+
|
|
409
|
+
if result.returncode != 0:
|
|
410
|
+
exit_error(f"Editor '{editor}' exited with non-zero status: {result.returncode}")
|
|
411
|
+
|
|
412
|
+
# Read the modified content
|
|
413
|
+
with open(temp_file.name, 'r') as modified_file:
|
|
414
|
+
return modified_file.read()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tft-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.28
|
|
4
4
|
Summary: Testing Farm CLI tool
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Miroslav Vadkerti
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Requires-Dist: click (>=8.0.4,<9.0.0)
|
|
15
15
|
Requires-Dist: colorama (>=0.4.4,<0.5.0)
|
|
16
16
|
Requires-Dist: dynaconf (>=3.1.2,<4.0.0)
|
|
17
|
+
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
|
17
18
|
Requires-Dist: requests (>=2.27.1,<3.0.0)
|
|
18
19
|
Requires-Dist: rich (>=12)
|
|
19
20
|
Requires-Dist: ruamel-yaml (>=0.18.5,<0.19.0)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
+
tft/cli/command/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
3
|
+
tft/cli/command/listing.py,sha256=gEkoeKHOcEiBt2TyLzSAongOKHKi7BgEO7GQ0AAa0Ss,31276
|
|
4
|
+
tft/cli/commands.py,sha256=aZ_P7B9z4IorT6ksQH0ciCY6KxvL5q5NJdWuHNv9fHE,85890
|
|
5
|
+
tft/cli/config.py,sha256=9JWjEwBiS4_Eq1B53lxxCIceERJ7AeryUyqxj9fm9c4,1714
|
|
6
|
+
tft/cli/tool.py,sha256=AaJ7RZwWEoP7WA_f2NbmcZSolNETkTWvVAt9pgt9j-o,975
|
|
7
|
+
tft/cli/utils.py,sha256=j8B30qpieBUieaiVo_3h5bRqoKnGeGKxR9eJ7GZcNiM,13702
|
|
8
|
+
tft_cli-0.0.28.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
9
|
+
tft_cli-0.0.28.dist-info/METADATA,sha256=MybsQ9HApc32-yILZ14mMu8YcWCSlhtH0gioJoMI8ZM,830
|
|
10
|
+
tft_cli-0.0.28.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
11
|
+
tft_cli-0.0.28.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
12
|
+
tft_cli-0.0.28.dist-info/RECORD,,
|
tft_cli-0.0.25.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
-
tft/cli/commands.py,sha256=5fcoNlJ_K1r6j99qoWKh9hJM2nJ3xY1Tp6vL4wcdQxk,82322
|
|
3
|
-
tft/cli/config.py,sha256=JiVLrgM4REWdljA9DjAuH4fNR1ekE7Crv2oM6vBPt9Q,1254
|
|
4
|
-
tft/cli/tool.py,sha256=nuz57u3yE4fKgdMgNtAFM7PCTcF6PSNFBQbCYDzxBOw,897
|
|
5
|
-
tft/cli/utils.py,sha256=t3ZSnviGxVbBQ4u4ltwQwRgN647BvuyL1QrSlQAAT1Q,9136
|
|
6
|
-
tft_cli-0.0.25.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
7
|
-
tft_cli-0.0.25.dist-info/METADATA,sha256=tIo9HRQmpNjq58ru63CRTun0owhYGucYQDFuWhB_fFE,789
|
|
8
|
-
tft_cli-0.0.25.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
9
|
-
tft_cli-0.0.25.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
10
|
-
tft_cli-0.0.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|