tft-cli 0.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.
- tft/cli/__init__.py +2 -0
- tft/cli/command/__init__.py +2 -0
- tft/cli/command/listing.py +835 -0
- tft/cli/commands.py +2234 -0
- tft/cli/config.py +39 -0
- tft/cli/tool.py +31 -0
- tft/cli/utils.py +377 -0
- tft_cli-0.0.0.dist-info/LICENSE +13 -0
- tft_cli-0.0.0.dist-info/METADATA +23 -0
- tft_cli-0.0.0.dist-info/RECORD +12 -0
- tft_cli-0.0.0.dist-info/WHEEL +4 -0
- tft_cli-0.0.0.dist-info/entry_points.txt +3 -0
tft/cli/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright Contributors to the Testing Farm project.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from dynaconf import LazySettings # type: ignore
|
|
5
|
+
|
|
6
|
+
settings = LazySettings(
|
|
7
|
+
# all environment variables have `TESTING_FARM_` prefix
|
|
8
|
+
ENVVAR_PREFIX_FOR_DYNACONF="TESTING_FARM",
|
|
9
|
+
# defaults
|
|
10
|
+
API_URL="https://api.dev.testing-farm.io/v0.1",
|
|
11
|
+
INTERNAL_API_URL="https://internal.api.dev.testing-farm.io/v0.1",
|
|
12
|
+
API_TOKEN=None,
|
|
13
|
+
# Restart command specific source API configuration (fallback to general settings)
|
|
14
|
+
SOURCE_API_URL=None,
|
|
15
|
+
INTERNAL_SOURCE_API_URL=None,
|
|
16
|
+
SOURCE_API_TOKEN=None,
|
|
17
|
+
# Restart command specific target API configuration (fallback to general settings)
|
|
18
|
+
TARGET_API_URL=None,
|
|
19
|
+
TARGET_API_TOKEN=None,
|
|
20
|
+
ISSUE_TRACKER="https://gitlab.com/testing-farm/general/-/issues/new",
|
|
21
|
+
STATUS_PAGE="https://status.testing-farm.io",
|
|
22
|
+
ONBOARDING_DOCS="https://docs.testing-farm.io/Testing%20Farm/0.1/onboarding.html",
|
|
23
|
+
CONTAINER_SIGN="/.testing-farm-container",
|
|
24
|
+
WATCH_TICK=30,
|
|
25
|
+
DEFAULT_API_TIMEOUT=10,
|
|
26
|
+
DEFAULT_API_RETRIES=7,
|
|
27
|
+
# default reservation duration in minutes
|
|
28
|
+
DEFAULT_RESERVATION_DURATION=30,
|
|
29
|
+
# should lead to delays of 0.5, 1, 2, 4, 8, 16, 32 seconds
|
|
30
|
+
DEFAULT_RETRY_BACKOFF_FACTOR=1,
|
|
31
|
+
# system CA certificates path, default for RHEL variants
|
|
32
|
+
REQUESTS_CA_BUNDLE="/etc/ssl/certs/ca-bundle.crt",
|
|
33
|
+
# Testing Farm sanity test,
|
|
34
|
+
TESTING_FARM_TESTS_GIT_URL="https://gitlab.com/testing-farm/tests",
|
|
35
|
+
TESTING_FARM_SANITY_PLAN="/testing-farm/sanity",
|
|
36
|
+
PUBLIC_IP_CHECKER_URL="https://ipv4.icanhazip.com",
|
|
37
|
+
# number or tries for resolving localhost public IP, useful if the user has multiple IPs
|
|
38
|
+
PUBLIC_IP_RESOLVE_TRIES=1,
|
|
39
|
+
)
|
tft/cli/tool.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright Contributors to the Testing Farm project.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
import tft.cli.commands as commands
|
|
9
|
+
from tft.cli.command.listing import listing
|
|
10
|
+
from tft.cli.config import settings
|
|
11
|
+
|
|
12
|
+
app = typer.Typer()
|
|
13
|
+
|
|
14
|
+
app.command()(commands.cancel)
|
|
15
|
+
app.command(name="list")(listing)
|
|
16
|
+
app.command()(commands.request)
|
|
17
|
+
app.command()(commands.restart)
|
|
18
|
+
app.command()(commands.reserve)
|
|
19
|
+
app.command()(commands.run)
|
|
20
|
+
app.command()(commands.version)
|
|
21
|
+
app.command()(commands.watch)
|
|
22
|
+
app.command()(commands.encrypt)
|
|
23
|
+
|
|
24
|
+
# This command is available only for the container based deployment
|
|
25
|
+
if os.path.exists(settings.CONTAINER_SIGN):
|
|
26
|
+
app.command()(commands.update)
|
|
27
|
+
|
|
28
|
+
# Expose REQUESTS_CA_BUNDLE in the environment for RHEL-like systems
|
|
29
|
+
# This is needed for custom CA certificates to nicely work.
|
|
30
|
+
if "REQUESTS_CA_BUNDLE" not in os.environ and os.path.exists(settings.REQUESTS_CA_BUNDLE):
|
|
31
|
+
os.environ["REQUESTS_CA_BUNDLE"] = settings.REQUESTS_CA_BUNDLE
|
tft/cli/utils.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Copyright Contributors to the Testing Farm project.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import itertools
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Dict, List, NoReturn, Optional, Union
|
|
15
|
+
|
|
16
|
+
import pendulum
|
|
17
|
+
import requests
|
|
18
|
+
import requests.adapters
|
|
19
|
+
import typer
|
|
20
|
+
from click.core import ParameterSource
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from ruamel.yaml import YAML # type: ignore
|
|
23
|
+
from urllib3 import Retry
|
|
24
|
+
|
|
25
|
+
from tft.cli.config import settings
|
|
26
|
+
|
|
27
|
+
console = Console(soft_wrap=True)
|
|
28
|
+
console_stderr = Console(soft_wrap=True, file=sys.stderr)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Age:
|
|
33
|
+
value: int
|
|
34
|
+
unit: str
|
|
35
|
+
|
|
36
|
+
_unit_multiplier = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
37
|
+
_unit_human = {"s": "second", "m": "minute", "h": "hour", "d": "day"}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_string(cls, age_string: str) -> 'Age':
|
|
41
|
+
value, unit = age_string[:-1], age_string[-1]
|
|
42
|
+
if unit not in cls._unit_multiplier:
|
|
43
|
+
raise typer.BadParameter(f"Age must end with {', '.join(cls._unit_multiplier.keys())}")
|
|
44
|
+
|
|
45
|
+
if not value.isdigit():
|
|
46
|
+
raise typer.BadParameter(f"Invalid age value {value}")
|
|
47
|
+
|
|
48
|
+
return cls(value=int(age_string[:-1]), unit=age_string[-1])
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def birth_date(self) -> pendulum.DateTime:
|
|
52
|
+
now = pendulum.now(tz="UTC")
|
|
53
|
+
return now - pendulum.duration(seconds=self.value * self._unit_multiplier[self.unit])
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def human(self) -> str:
|
|
57
|
+
return f"{self.value} {self._unit_human[self.unit]}{'s' if self.value > 1 else ''}"
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def available_units() -> str:
|
|
61
|
+
return "s (seconds), m (minutes), h (hours) or d (days)"
|
|
62
|
+
|
|
63
|
+
def to_string(self, format="%Y-%m-%dT%H:%M:%S") -> str:
|
|
64
|
+
return self.birth_date.strftime(format)
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return f"{self.value}{self.unit}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OutputFormat(str, Enum):
|
|
71
|
+
text = "text"
|
|
72
|
+
json = "json"
|
|
73
|
+
yaml = "yaml"
|
|
74
|
+
table = "table"
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def available_formats():
|
|
78
|
+
return "text, json or table"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def exit_error(error: str) -> NoReturn:
|
|
82
|
+
"""Exit with given error message"""
|
|
83
|
+
console.print(f"⛔ {error}", style="red")
|
|
84
|
+
raise typer.Exit(code=255)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_output_or_exit(command: str, error: str) -> str:
|
|
88
|
+
"""Return local command output or exit with given error message"""
|
|
89
|
+
try:
|
|
90
|
+
output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT)
|
|
91
|
+
|
|
92
|
+
except subprocess.CalledProcessError:
|
|
93
|
+
exit_error(error)
|
|
94
|
+
|
|
95
|
+
return output.rstrip().decode("utf-8")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def artifacts(type: str, artifacts_raw: List[str]) -> List[Dict[str, str]]:
|
|
99
|
+
"""Return artifacts List for given artifact type"""
|
|
100
|
+
artifacts = []
|
|
101
|
+
|
|
102
|
+
for artifact in artifacts_raw:
|
|
103
|
+
if '=' in artifact:
|
|
104
|
+
artifact_dict = options_to_dict('artifact `{}`'.format(artifact), normalize_multistring_option([artifact]))
|
|
105
|
+
else:
|
|
106
|
+
artifact_dict = {'id': artifact}
|
|
107
|
+
|
|
108
|
+
if 'install' in artifact_dict:
|
|
109
|
+
artifact_dict['install'] = normalize_bool_option(artifact_dict['install']) # pyre-ignore[6]
|
|
110
|
+
|
|
111
|
+
artifacts.append({'type': type, **artifact_dict})
|
|
112
|
+
|
|
113
|
+
return artifacts
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def hw_constraints(hardware: List[str]) -> Dict[Any, Any]:
|
|
117
|
+
"""Convert hardware parameters to a dictionary"""
|
|
118
|
+
|
|
119
|
+
constraints: Dict[Any, Any] = {}
|
|
120
|
+
|
|
121
|
+
for raw_constraint in hardware:
|
|
122
|
+
path, value = raw_constraint.split('=', 1)
|
|
123
|
+
|
|
124
|
+
if not path or not value:
|
|
125
|
+
exit_error(f"cannot parse hardware constraint `{raw_constraint}`")
|
|
126
|
+
|
|
127
|
+
path_splitted = path.split('.')
|
|
128
|
+
first_key = path_splitted[0]
|
|
129
|
+
|
|
130
|
+
# Special handling for network and disk as they are lists
|
|
131
|
+
if first_key in ("network", "disk"):
|
|
132
|
+
if first_key not in constraints:
|
|
133
|
+
constraints[first_key] = []
|
|
134
|
+
|
|
135
|
+
if len(path_splitted) > 1:
|
|
136
|
+
new_dict = {}
|
|
137
|
+
current = new_dict
|
|
138
|
+
# Handle all nested levels except the last one
|
|
139
|
+
for key in path_splitted[1:-1]:
|
|
140
|
+
current[key] = {}
|
|
141
|
+
current = current[key]
|
|
142
|
+
# Set the final value
|
|
143
|
+
current[path_splitted[-1]] = value
|
|
144
|
+
constraints[first_key].append(new_dict)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Special handling for CPU flags as they are also a list
|
|
148
|
+
if first_key == 'cpu' and len(path_splitted) == 2 and path_splitted[1] == 'flag':
|
|
149
|
+
second_key = 'flag'
|
|
150
|
+
|
|
151
|
+
if first_key not in constraints:
|
|
152
|
+
constraints[first_key] = {}
|
|
153
|
+
|
|
154
|
+
if second_key not in constraints[first_key]:
|
|
155
|
+
constraints[first_key][second_key] = []
|
|
156
|
+
|
|
157
|
+
constraints[first_key][second_key].append(value)
|
|
158
|
+
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# Walk the path, step by step, and initialize containers along the way. The last step is not
|
|
162
|
+
# a name of another nested container, but actually a name in the last container.
|
|
163
|
+
container: Any = constraints
|
|
164
|
+
|
|
165
|
+
while len(path_splitted) > 1:
|
|
166
|
+
step = path_splitted.pop(0)
|
|
167
|
+
|
|
168
|
+
if step not in container:
|
|
169
|
+
container[step] = {}
|
|
170
|
+
|
|
171
|
+
container = container[step]
|
|
172
|
+
|
|
173
|
+
value_mixed: Union[bool, str] = value
|
|
174
|
+
|
|
175
|
+
if value.lower() in ['true']:
|
|
176
|
+
value_mixed = True
|
|
177
|
+
|
|
178
|
+
elif value.lower() in ['false']:
|
|
179
|
+
value_mixed = False
|
|
180
|
+
|
|
181
|
+
container[path_splitted.pop()] = value_mixed
|
|
182
|
+
|
|
183
|
+
# Only process additional path elements if they exist
|
|
184
|
+
if path_splitted:
|
|
185
|
+
next_path = path_splitted.pop()
|
|
186
|
+
|
|
187
|
+
# handle compatible.distro
|
|
188
|
+
if next_path == 'distro':
|
|
189
|
+
container[next_path] = (
|
|
190
|
+
container[next_path].append(value_mixed) if next_path in container else [value_mixed]
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
container[next_path] = value_mixed
|
|
194
|
+
|
|
195
|
+
return constraints
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def options_from_file(filepath) -> Dict[str, str]:
|
|
199
|
+
"""Read environment variables from a yaml file."""
|
|
200
|
+
|
|
201
|
+
with open(filepath, 'r') as file:
|
|
202
|
+
try:
|
|
203
|
+
yaml = YAML(typ="safe").load(file.read())
|
|
204
|
+
except Exception:
|
|
205
|
+
exit_error(f"Failed to load variables from yaml file {filepath}.")
|
|
206
|
+
|
|
207
|
+
if not yaml: # pyre-ignore[61] # pyre ignores NoReturn in exit_error
|
|
208
|
+
return {}
|
|
209
|
+
|
|
210
|
+
if not isinstance(yaml, dict): # pyre-ignore[61] # pyre ignores NoReturn in exit_error
|
|
211
|
+
exit_error(f"Environment file {filepath} is not a dict.")
|
|
212
|
+
|
|
213
|
+
if any([isinstance(value, (list, dict)) for value in yaml.values()]):
|
|
214
|
+
exit_error(f"Values of environment file {filepath} are not primitive types.")
|
|
215
|
+
|
|
216
|
+
return yaml # pyre-ignore[61] # pyre ignores NoReturn in exit_error
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def options_to_dict(name: str, options: List[str]) -> Dict[str, str]:
|
|
220
|
+
"""Create a dictionary from list of `key=value|@file` options"""
|
|
221
|
+
|
|
222
|
+
options_dict = {}
|
|
223
|
+
|
|
224
|
+
# Turn option list such as
|
|
225
|
+
# `['aaa=bbb "foo foo=bar bar"', 'foo=bar']` into
|
|
226
|
+
# `['aaa=bbb', 'foo foo=bar bar', 'foo=bar']`
|
|
227
|
+
options = list(itertools.chain.from_iterable(shlex.split(option) for option in options))
|
|
228
|
+
|
|
229
|
+
for option in options:
|
|
230
|
+
# Option is `@file`
|
|
231
|
+
if option.startswith('@'):
|
|
232
|
+
if not os.path.isfile(option[1:]):
|
|
233
|
+
exit_error(f"Invalid environment file in option `{option}` specified.")
|
|
234
|
+
options_dict.update(options_from_file(option[1:]))
|
|
235
|
+
|
|
236
|
+
# Option is `key=value`
|
|
237
|
+
else:
|
|
238
|
+
try:
|
|
239
|
+
options_dict.update({option.split("=", 1)[0]: option.split("=", 1)[1]})
|
|
240
|
+
except IndexError:
|
|
241
|
+
exit_error(f"Option `{option}` is invalid, must be defined as `key=value|@file`.")
|
|
242
|
+
|
|
243
|
+
return options_dict
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def uuid_valid(value: str, version: int = 4) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Validates that given `value` is a valid UUID version 4.
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
uuid.UUID(value, version=version)
|
|
252
|
+
return True
|
|
253
|
+
except ValueError:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def extract_uuid(value: str) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Extracts a UUID from a string. If the string is already a valid UUID, returns it.
|
|
260
|
+
If the string contains a UUID, extracts and returns it.
|
|
261
|
+
Raises typer.Exit with error message if no valid UUID is found.
|
|
262
|
+
"""
|
|
263
|
+
# Check if the value is already a valid UUID
|
|
264
|
+
if uuid_valid(value):
|
|
265
|
+
return value
|
|
266
|
+
|
|
267
|
+
# UUID pattern for extracting UUIDs from strings
|
|
268
|
+
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}')
|
|
269
|
+
|
|
270
|
+
# Try to extract UUID from string
|
|
271
|
+
uuid_match = uuid_pattern.search(value)
|
|
272
|
+
if uuid_match:
|
|
273
|
+
return uuid_match.group()
|
|
274
|
+
|
|
275
|
+
exit_error(f"Could not find a valid Testing Farm request id in '{value}'.")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TimeoutHTTPAdapter(requests.adapters.HTTPAdapter):
|
|
279
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
280
|
+
self.timeout = kwargs.pop('timeout', settings.DEFAULT_API_TIMEOUT)
|
|
281
|
+
|
|
282
|
+
super().__init__(*args, **kwargs)
|
|
283
|
+
|
|
284
|
+
def send(self, request: requests.PreparedRequest, **kwargs: Any) -> requests.Response: # type: ignore[override]
|
|
285
|
+
kwargs.setdefault('timeout', self.timeout)
|
|
286
|
+
|
|
287
|
+
return super().send(request, **kwargs)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def install_http_retries(
|
|
291
|
+
session: requests.Session,
|
|
292
|
+
timeout: int = settings.DEFAULT_API_TIMEOUT,
|
|
293
|
+
retries: int = settings.DEFAULT_API_RETRIES,
|
|
294
|
+
retry_backoff_factor: float = settings.DEFAULT_RETRY_BACKOFF_FACTOR,
|
|
295
|
+
status_forcelist_extend: Optional[List[int]] = None,
|
|
296
|
+
) -> None:
|
|
297
|
+
# urllib3 1.26.0 deprecated method_whitelist, and 2.0.0 removed it:
|
|
298
|
+
# - https://github.com/urllib3/urllib3/commit/382ab32f23795c44faae83b4e8b18a16fb605a0a
|
|
299
|
+
# - https://github.com/urllib3/urllib3/commit/c67c0949e9c91c7621ea718a7f297ecac7c3b79e
|
|
300
|
+
if hasattr(Retry, "DEFAULT_ALLOWED_METHODS"):
|
|
301
|
+
allowed_retry_parameter = "allowed_methods"
|
|
302
|
+
else:
|
|
303
|
+
allowed_retry_parameter = "method_whitelist"
|
|
304
|
+
|
|
305
|
+
status_forcelist_extend = status_forcelist_extend or []
|
|
306
|
+
|
|
307
|
+
from typing import Any, Dict
|
|
308
|
+
|
|
309
|
+
params: Dict[str, Any] = {
|
|
310
|
+
"total": retries,
|
|
311
|
+
"status_forcelist": [
|
|
312
|
+
429, # Too Many Requests
|
|
313
|
+
500, # Internal Server Error
|
|
314
|
+
502, # Bad Gateway
|
|
315
|
+
503, # Service Unavailable
|
|
316
|
+
504, # Gateway Timeout
|
|
317
|
+
]
|
|
318
|
+
+ status_forcelist_extend,
|
|
319
|
+
"backoff_factor": retry_backoff_factor,
|
|
320
|
+
}
|
|
321
|
+
params[allowed_retry_parameter] = ['HEAD', 'GET', 'POST', 'DELETE', 'PUT']
|
|
322
|
+
retry_strategy = Retry(**params)
|
|
323
|
+
|
|
324
|
+
timeout_adapter = TimeoutHTTPAdapter(timeout=timeout, max_retries=retry_strategy)
|
|
325
|
+
|
|
326
|
+
session.mount('https://', timeout_adapter)
|
|
327
|
+
session.mount('http://', timeout_adapter)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def normalize_multistring_option(options: List[str], separator: str = ',') -> List[str]:
|
|
331
|
+
return sum([[option.strip() for option in item.split(separator)] for item in options], [])
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def normalize_bool_option(option_value: Union[str, bool]) -> bool:
|
|
335
|
+
if str(option_value).strip().lower() in ('yes', 'true', '1', 'y', 'on'):
|
|
336
|
+
return True
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def read_glob_paths(glob_paths: List[str]) -> str:
|
|
341
|
+
paths = [path for glob_path in glob_paths for path in glob.glob(os.path.expanduser(glob_path))]
|
|
342
|
+
|
|
343
|
+
contents: List[str] = []
|
|
344
|
+
|
|
345
|
+
for path in paths:
|
|
346
|
+
if not os.path.isfile(path) or not os.access(path, os.R_OK):
|
|
347
|
+
exit_error(f"Error reading '{path}'.")
|
|
348
|
+
with open(path, 'r') as file:
|
|
349
|
+
contents.append(file.read())
|
|
350
|
+
|
|
351
|
+
return ''.join(contents)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def check_unexpected_arguments(context: typer.Context, *args: str) -> Union[None, NoReturn]:
|
|
355
|
+
for argument in args:
|
|
356
|
+
if context.get_parameter_source(argument) == ParameterSource.COMMANDLINE:
|
|
357
|
+
exit_error(
|
|
358
|
+
f"Unexpected argument '{context.params.get(argument)}'. "
|
|
359
|
+
"Please make sure you are passing the parameters correctly."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def validate_age(value: str) -> Age:
|
|
364
|
+
if value.endswith("m"):
|
|
365
|
+
return Age(int(value[:-1]), "m")
|
|
366
|
+
elif value.endswith("d"):
|
|
367
|
+
return Age(int(value[:-1]), "d")
|
|
368
|
+
else:
|
|
369
|
+
raise ValueError("Age must end with 'm' for months or 'd' for days.")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def authorization_headers(api_key: str) -> Dict[str, str]:
|
|
373
|
+
"""
|
|
374
|
+
Return a dict with headers for a request to Testing Farm API.
|
|
375
|
+
Used for authentication.
|
|
376
|
+
"""
|
|
377
|
+
return {'Authorization': f'Bearer {api_key}'}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright Red Hat, Inc.
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tft-cli
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Testing Farm CLI tool
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Miroslav Vadkerti
|
|
7
|
+
Author-email: mvadkert@redhat.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Requires-Dist: click (>=8.0.4,<9.0.0)
|
|
15
|
+
Requires-Dist: colorama (>=0.4.4,<0.5.0)
|
|
16
|
+
Requires-Dist: dynaconf (>=3.1.2,<4.0.0)
|
|
17
|
+
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
|
18
|
+
Requires-Dist: requests (>=2.27.1,<3.0.0)
|
|
19
|
+
Requires-Dist: rich (>=12)
|
|
20
|
+
Requires-Dist: ruamel-yaml (>=0.18.5,<0.19.0)
|
|
21
|
+
Requires-Dist: setuptools
|
|
22
|
+
Requires-Dist: shellingham (>=1.3.0,<2.0.0)
|
|
23
|
+
Requires-Dist: typer (>=0.11)
|
|
@@ -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=e3i8zEqDXyMc4Zc-e94A9A2WeXwbb5K9z9aRC3n0nmU,31241
|
|
4
|
+
tft/cli/commands.py,sha256=2Kp6zickQf0sltX4Gp3JVloV1ReW7RZPTdKqaWReFWk,85138
|
|
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=eLoldFsYPxTEuVj2zV2gePiIjKtZVYzsVo1fzrU0STA,12369
|
|
8
|
+
tft_cli-0.0.0.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
9
|
+
tft_cli-0.0.0.dist-info/METADATA,sha256=xRRgGqlWPVBfk9fXsMokmBRjZ8atoJkHWgvFMfxYmEc,829
|
|
10
|
+
tft_cli-0.0.0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
11
|
+
tft_cli-0.0.0.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
12
|
+
tft_cli-0.0.0.dist-info/RECORD,,
|