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/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 # pyre-ignore[21]
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
- params = {
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: # pyre-ignore[16]
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.25
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,,
@@ -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,,