tft-cli 0.0.20__py3-none-any.whl → 0.0.22__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/commands.py +171 -32
- tft/cli/config.py +1 -0
- tft/cli/utils.py +8 -0
- {tft_cli-0.0.20.dist-info → tft_cli-0.0.22.dist-info}/METADATA +2 -1
- tft_cli-0.0.22.dist-info/RECORD +10 -0
- tft_cli-0.0.20.dist-info/RECORD +0 -10
- {tft_cli-0.0.20.dist-info → tft_cli-0.0.22.dist-info}/LICENSE +0 -0
- {tft_cli-0.0.20.dist-info → tft_cli-0.0.22.dist-info}/WHEEL +0 -0
- {tft_cli-0.0.20.dist-info → tft_cli-0.0.22.dist-info}/entry_points.txt +0 -0
tft/cli/commands.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import base64
|
|
5
|
+
import ipaddress
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
@@ -60,6 +61,9 @@ RESERVE_REF = os.getenv("TESTING_FARM_RESERVE_REF", "main")
|
|
|
60
61
|
|
|
61
62
|
DEFAULT_PIPELINE_TIMEOUT = 60 * 12
|
|
62
63
|
|
|
64
|
+
# Won't be validating CIDR and 65535 max port range with regex here, not worth it
|
|
65
|
+
SECURITY_GROUP_RULE_FORMAT = re.compile(r"(tcp|ip|icmp|udp|-1|[0-255]):(.*):(\d{1,5}-\d{1,5}|\d{1,5}|-1)")
|
|
66
|
+
|
|
63
67
|
|
|
64
68
|
class WatchFormat(str, Enum):
|
|
65
69
|
text = 'text'
|
|
@@ -134,6 +138,24 @@ OPTION_PIPELINE_TYPE: Optional[PipelineType] = typer.Option(None, help="Force a
|
|
|
134
138
|
OPTION_POST_INSTALL_SCRIPT: Optional[str] = typer.Option(
|
|
135
139
|
None, help="Post-install script to run right after the guest boots for the first time."
|
|
136
140
|
)
|
|
141
|
+
OPTION_SECURITY_GROUP_RULE_INGRESS: Optional[List[str]] = typer.Option(
|
|
142
|
+
None,
|
|
143
|
+
help=(
|
|
144
|
+
"Additional ingress security group rules to be passed to guest in "
|
|
145
|
+
"PROTOCOL:CIDR:PORT format. Multiple rules can be specified as comma separated, "
|
|
146
|
+
"eg. `tcp:109.81.42.42/32:22,142.0.42.0/24:22`. "
|
|
147
|
+
"Supported by AWS only atm."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
OPTION_SECURITY_GROUP_RULE_EGRESS: Optional[List[str]] = typer.Option(
|
|
151
|
+
None,
|
|
152
|
+
help=(
|
|
153
|
+
"Additional egress security group rules to be passed to guest in "
|
|
154
|
+
"PROTOCOL:CIDR:PORT format. Multiple rules can be specified as comma separated, "
|
|
155
|
+
"eg. `tcp:109.81.42.42/32:22,142.0.42.0/24:22`. "
|
|
156
|
+
"Supported by AWS only atm."
|
|
157
|
+
),
|
|
158
|
+
)
|
|
137
159
|
OPTION_KICKSTART: Optional[List[str]] = typer.Option(
|
|
138
160
|
None,
|
|
139
161
|
metavar="key=value|@file",
|
|
@@ -212,6 +234,58 @@ OPTION_PARALLEL_LIMIT: Optional[int] = typer.Option(
|
|
|
212
234
|
"Red Hat Ranch."
|
|
213
235
|
),
|
|
214
236
|
)
|
|
237
|
+
OPTION_TAGS = typer.Option(
|
|
238
|
+
None,
|
|
239
|
+
"-t",
|
|
240
|
+
"--tag",
|
|
241
|
+
metavar="key=value|@file",
|
|
242
|
+
help="Tag cloud resources with given value. The @ prefix marks a yaml file to load.",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# NOTE(ivasilev) Largely borrowed from artemis-cli
|
|
247
|
+
def _parse_security_group_rules(ingress_rules: List[str], egress_rules: List[str]) -> Dict[str, Any]:
|
|
248
|
+
"""
|
|
249
|
+
Returns a dictionary with ingress/egress rules in TFT request friendly format
|
|
250
|
+
"""
|
|
251
|
+
security_group_rules = {}
|
|
252
|
+
|
|
253
|
+
def _add_secgroup_rules(sg_type: str, sg_data: List[str]) -> None:
|
|
254
|
+
security_group_rules[sg_type] = []
|
|
255
|
+
|
|
256
|
+
for sg_rule in normalize_multistring_option(sg_data):
|
|
257
|
+
matches = re.match(SECURITY_GROUP_RULE_FORMAT, sg_rule)
|
|
258
|
+
if not matches:
|
|
259
|
+
exit_error(f"Bad format of security group rule '{sg_rule}', should be PROTOCOL:CIDR:PORT") # noqa: E231
|
|
260
|
+
|
|
261
|
+
protocol, cidr, port = matches[1], matches[2], matches[3]
|
|
262
|
+
|
|
263
|
+
# Let's validate cidr
|
|
264
|
+
try:
|
|
265
|
+
# This way a single ip address will be converted to a valid ip/32 cidr.
|
|
266
|
+
cidr = str(ipaddress.ip_network(cidr))
|
|
267
|
+
except ValueError as err:
|
|
268
|
+
exit_error(f'CIDR {cidr} has incorrect format: {err}')
|
|
269
|
+
|
|
270
|
+
# Artemis expectes port_min/port_max, -1 has to be convered to a proper range 0-65535
|
|
271
|
+
port_min = 0 if port == '-1' else int(port.split('-')[0])
|
|
272
|
+
port_max = 65535 if port == '-1' else int(port.split('-')[-1])
|
|
273
|
+
|
|
274
|
+
# Add rule for Artemis API
|
|
275
|
+
security_group_rules[sg_type].append(
|
|
276
|
+
{
|
|
277
|
+
'type': sg_type.split('_')[-1],
|
|
278
|
+
'protocol': protocol,
|
|
279
|
+
'cidr': cidr,
|
|
280
|
+
'port_min': port_min,
|
|
281
|
+
'port_max': port_max,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
_add_secgroup_rules('security_group_rules_ingress', ingress_rules)
|
|
286
|
+
_add_secgroup_rules('security_group_rules_egress', egress_rules)
|
|
287
|
+
|
|
288
|
+
return security_group_rules
|
|
215
289
|
|
|
216
290
|
|
|
217
291
|
def _parse_xunit(xunit: str):
|
|
@@ -441,7 +515,12 @@ def watch(
|
|
|
441
515
|
raise typer.Exit(code=1)
|
|
442
516
|
|
|
443
517
|
elif state == "error":
|
|
444
|
-
|
|
518
|
+
msg = (
|
|
519
|
+
request['result'].get('summary')
|
|
520
|
+
if request['result']
|
|
521
|
+
else '\n'.join(note['message'] for note in request['notes'])
|
|
522
|
+
)
|
|
523
|
+
_console_print(f"📛 pipeline error\n{msg}", style="red")
|
|
445
524
|
_print_summary_table(request_summary, format)
|
|
446
525
|
raise typer.Exit(code=2)
|
|
447
526
|
|
|
@@ -521,13 +600,7 @@ def request(
|
|
|
521
600
|
repository: List[str] = OPTION_REPOSITORY,
|
|
522
601
|
repository_file: List[str] = OPTION_REPOSITORY_FILE,
|
|
523
602
|
sanity: bool = typer.Option(False, help="Run Testing Farm sanity test.", rich_help_panel=RESERVE_PANEL_GENERAL),
|
|
524
|
-
tags: Optional[List[str]] =
|
|
525
|
-
None,
|
|
526
|
-
"-t",
|
|
527
|
-
"--tag",
|
|
528
|
-
metavar="key=value|@file",
|
|
529
|
-
help="Tag cloud resources with given value. The @ prefix marks a yaml file to load.",
|
|
530
|
-
),
|
|
603
|
+
tags: Optional[List[str]] = OPTION_TAGS,
|
|
531
604
|
watchdog_dispatch_delay: Optional[int] = typer.Option(
|
|
532
605
|
None,
|
|
533
606
|
help="How long (seconds) before the guest \"is-alive\" watchdog is dispatched. Note that this is implemented only in Artemis service.", # noqa
|
|
@@ -539,6 +612,8 @@ def request(
|
|
|
539
612
|
dry_run: bool = OPTION_DRY_RUN,
|
|
540
613
|
pipeline_type: Optional[PipelineType] = OPTION_PIPELINE_TYPE,
|
|
541
614
|
post_install_script: Optional[str] = OPTION_POST_INSTALL_SCRIPT,
|
|
615
|
+
security_group_rule_ingress: Optional[List[str]] = OPTION_SECURITY_GROUP_RULE_INGRESS,
|
|
616
|
+
security_group_rule_egress: Optional[List[str]] = OPTION_SECURITY_GROUP_RULE_EGRESS,
|
|
542
617
|
user_webpage: Optional[str] = typer.Option(
|
|
543
618
|
None, help="URL to the user's webpage. The link will be shown in the results viewer."
|
|
544
619
|
),
|
|
@@ -707,7 +782,17 @@ def request(
|
|
|
707
782
|
|
|
708
783
|
environments.append(environment)
|
|
709
784
|
|
|
710
|
-
if
|
|
785
|
+
if any(
|
|
786
|
+
provisioning_detail
|
|
787
|
+
for provisioning_detail in [
|
|
788
|
+
tags,
|
|
789
|
+
watchdog_dispatch_delay,
|
|
790
|
+
watchdog_period_delay,
|
|
791
|
+
post_install_script,
|
|
792
|
+
security_group_rule_ingress,
|
|
793
|
+
security_group_rule_egress,
|
|
794
|
+
]
|
|
795
|
+
):
|
|
711
796
|
if "settings" not in environments[0]:
|
|
712
797
|
environments[0]["settings"] = {}
|
|
713
798
|
|
|
@@ -726,6 +811,10 @@ def request(
|
|
|
726
811
|
if post_install_script:
|
|
727
812
|
environments[0]["settings"]["provisioning"]["post_install_script"] = post_install_script
|
|
728
813
|
|
|
814
|
+
if security_group_rule_ingress or security_group_rule_egress:
|
|
815
|
+
rules = _parse_security_group_rules(security_group_rule_ingress or [], security_group_rule_egress or [])
|
|
816
|
+
environments[0]["settings"]["provisioning"].update(rules)
|
|
817
|
+
|
|
729
818
|
# create final request
|
|
730
819
|
request = TestingFarmRequestV1
|
|
731
820
|
request["api_key"] = api_token
|
|
@@ -747,6 +836,7 @@ def request(
|
|
|
747
836
|
|
|
748
837
|
# worker image
|
|
749
838
|
if worker_image:
|
|
839
|
+
console.print(f"👷 Forcing worker image [blue]{worker_image}[/blue]")
|
|
750
840
|
request["settings"]["worker"] = {"image": worker_image}
|
|
751
841
|
|
|
752
842
|
if not user_webpage and (user_webpage_name or user_webpage_icon):
|
|
@@ -810,6 +900,7 @@ def restart(
|
|
|
810
900
|
git_ref: Optional[str] = typer.Option(None, help="Force GIT ref or branch to test."),
|
|
811
901
|
git_merge_sha: Optional[str] = typer.Option(None, help="Force GIT ref or branch into which --ref will be merged."),
|
|
812
902
|
hardware: List[str] = OPTION_HARDWARE,
|
|
903
|
+
tags: Optional[List[str]] = OPTION_TAGS,
|
|
813
904
|
tmt_plan_name: Optional[str] = OPTION_TMT_PLAN_NAME,
|
|
814
905
|
tmt_plan_filter: Optional[str] = OPTION_TMT_PLAN_FILTER,
|
|
815
906
|
tmt_test_name: Optional[str] = OPTION_TMT_TEST_NAME,
|
|
@@ -853,6 +944,17 @@ def restart(
|
|
|
853
944
|
if response.status_code == 401:
|
|
854
945
|
exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
|
|
855
946
|
|
|
947
|
+
# The API token is valid, but it doesn't own the request
|
|
948
|
+
if response.status_code == 403:
|
|
949
|
+
console.print(
|
|
950
|
+
"⚠️ [yellow] You are not the owner of this request. Any secrets associated with the request will not be included on the restart.[/yellow]" # noqa: E501
|
|
951
|
+
)
|
|
952
|
+
# Construct URL to the internal API
|
|
953
|
+
get_url = urllib.parse.urljoin(str(api_url), f"v0.1/requests/{_request_id}")
|
|
954
|
+
|
|
955
|
+
# Get the request details
|
|
956
|
+
response = session.get(get_url)
|
|
957
|
+
|
|
856
958
|
if response.status_code != 200:
|
|
857
959
|
exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
|
|
858
960
|
|
|
@@ -933,7 +1035,7 @@ def restart(
|
|
|
933
1035
|
|
|
934
1036
|
# worker image
|
|
935
1037
|
if worker_image:
|
|
936
|
-
console.print(f"👷
|
|
1038
|
+
console.print(f"👷 Forcing worker image [blue]{worker_image}[/blue]")
|
|
937
1039
|
request["settings"] = request["settings"] if request.get("settings") else {}
|
|
938
1040
|
request["settings"]["worker"] = {"image": worker_image}
|
|
939
1041
|
# it is required to have also pipeline key set, otherwise API will fail
|
|
@@ -954,6 +1056,16 @@ def restart(
|
|
|
954
1056
|
if parallel_limit:
|
|
955
1057
|
request["settings"]["pipeline"]["parallel-limit"] = parallel_limit
|
|
956
1058
|
|
|
1059
|
+
if tags:
|
|
1060
|
+
for environment in request["environments"]:
|
|
1061
|
+
if "settings" not in environment or not environment["settings"]:
|
|
1062
|
+
environment["settings"] = {}
|
|
1063
|
+
|
|
1064
|
+
if 'provisioning' not in environment["settings"]:
|
|
1065
|
+
environment["settings"]["provisioning"] = {}
|
|
1066
|
+
|
|
1067
|
+
environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
|
|
1068
|
+
|
|
957
1069
|
# dry run
|
|
958
1070
|
if dry_run:
|
|
959
1071
|
console.print("🔍 Dry run, showing POST json only", style="bright_yellow")
|
|
@@ -1163,6 +1275,7 @@ def reserve(
|
|
|
1163
1275
|
rich_help_panel=RESERVE_PANEL_ENVIRONMENT,
|
|
1164
1276
|
),
|
|
1165
1277
|
hardware: List[str] = OPTION_HARDWARE,
|
|
1278
|
+
tags: Optional[List[str]] = OPTION_TAGS,
|
|
1166
1279
|
kickstart: Optional[List[str]] = OPTION_KICKSTART,
|
|
1167
1280
|
pool: Optional[str] = OPTION_POOL,
|
|
1168
1281
|
fedora_koji_build: List[str] = OPTION_FEDORA_KOJI_BUILD,
|
|
@@ -1180,6 +1293,12 @@ def reserve(
|
|
|
1180
1293
|
autoconnect: bool = typer.Option(
|
|
1181
1294
|
True, help="Automatically connect to the guest via SSH.", rich_help_panel=RESERVE_PANEL_GENERAL
|
|
1182
1295
|
),
|
|
1296
|
+
worker_image: Optional[str] = OPTION_WORKER_IMAGE,
|
|
1297
|
+
security_group_rule_ingress: Optional[List[str]] = OPTION_SECURITY_GROUP_RULE_INGRESS,
|
|
1298
|
+
security_group_rule_egress: Optional[List[str]] = OPTION_SECURITY_GROUP_RULE_EGRESS,
|
|
1299
|
+
skip_workstation_access: bool = typer.Option(
|
|
1300
|
+
False, help="Do not allow ingress traffic from this workstation's ip to the reserved machine"
|
|
1301
|
+
),
|
|
1183
1302
|
):
|
|
1184
1303
|
"""
|
|
1185
1304
|
Reserve a system in Testing Farm.
|
|
@@ -1230,12 +1349,22 @@ def reserve(
|
|
|
1230
1349
|
environment["pool"] = pool
|
|
1231
1350
|
environment["artifacts"] = []
|
|
1232
1351
|
|
|
1233
|
-
if
|
|
1352
|
+
if "settings" not in environment:
|
|
1353
|
+
environment["settings"] = {}
|
|
1354
|
+
|
|
1355
|
+
if post_install_script or security_group_rule_ingress or security_group_rule_egress or tags:
|
|
1234
1356
|
if "settings" not in environment:
|
|
1235
1357
|
environment["settings"] = {}
|
|
1236
1358
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1359
|
+
if "provisioning" not in environment["settings"]:
|
|
1360
|
+
environment["settings"]["provisioning"] = {}
|
|
1361
|
+
|
|
1362
|
+
if "tags" not in environment["settings"]["provisioning"]:
|
|
1363
|
+
environment["settings"]["provisioning"]["tags"] = {}
|
|
1364
|
+
|
|
1365
|
+
# reserve command is for interacting with the guest, and so non-spot instances
|
|
1366
|
+
# would be nicer for the user than them getting shocked when they loose their work.
|
|
1367
|
+
environment["settings"]["provisioning"]["tags"]["ArtemisUseSpot"] = "false"
|
|
1239
1368
|
|
|
1240
1369
|
if compose:
|
|
1241
1370
|
environment["os"] = {"compose": compose}
|
|
@@ -1243,6 +1372,9 @@ def reserve(
|
|
|
1243
1372
|
if hardware:
|
|
1244
1373
|
environment["hardware"] = hw_constraints(hardware)
|
|
1245
1374
|
|
|
1375
|
+
if tags:
|
|
1376
|
+
environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
|
|
1377
|
+
|
|
1246
1378
|
if kickstart:
|
|
1247
1379
|
environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
|
|
1248
1380
|
|
|
@@ -1264,6 +1396,24 @@ def reserve(
|
|
|
1264
1396
|
if post_install_script:
|
|
1265
1397
|
environment["settings"]["provisioning"]["post_install_script"] = post_install_script
|
|
1266
1398
|
|
|
1399
|
+
if not skip_workstation_access or security_group_rule_ingress or security_group_rule_egress:
|
|
1400
|
+
ingress_rules = security_group_rule_ingress or []
|
|
1401
|
+
if not skip_workstation_access:
|
|
1402
|
+
try:
|
|
1403
|
+
get_ip = requests.get(settings.PUBLIC_IP_CHECKER_URL)
|
|
1404
|
+
except requests.exceptions.RequestException as err:
|
|
1405
|
+
exit_error(f"Could not get workstation ip to form a security group rule: {err}")
|
|
1406
|
+
|
|
1407
|
+
if get_ip.ok:
|
|
1408
|
+
ip = get_ip.text.strip()
|
|
1409
|
+
ingress_rules.append(f'-1:{ip}:-1') # noqa: E231
|
|
1410
|
+
|
|
1411
|
+
else:
|
|
1412
|
+
exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
|
|
1413
|
+
|
|
1414
|
+
rules = _parse_security_group_rules(ingress_rules, security_group_rule_egress or [])
|
|
1415
|
+
environment["settings"]["provisioning"].update(rules)
|
|
1416
|
+
|
|
1267
1417
|
console.print(f"🕗 Reserved for [blue]{str(reservation_duration)}[/blue] minutes")
|
|
1268
1418
|
environment["variables"] = {"TF_RESERVATION_DURATION": str(reservation_duration)}
|
|
1269
1419
|
|
|
@@ -1283,6 +1433,12 @@ def reserve(
|
|
|
1283
1433
|
request["api_key"] = settings.API_TOKEN
|
|
1284
1434
|
request["test"]["fmf"] = test
|
|
1285
1435
|
|
|
1436
|
+
# worker image
|
|
1437
|
+
if worker_image:
|
|
1438
|
+
console.print(f"👷 Forcing worker image [blue]{worker_image}[/blue]")
|
|
1439
|
+
request["settings"] = request["settings"] if request.get("settings") else {}
|
|
1440
|
+
request["settings"]["worker"] = {"image": worker_image}
|
|
1441
|
+
|
|
1286
1442
|
request["environments"] = [environment]
|
|
1287
1443
|
|
|
1288
1444
|
# in case the reservation duration is more than the pipeline timeout, adjust also the pipeline timeout
|
|
@@ -1438,27 +1594,10 @@ def reserve(
|
|
|
1438
1594
|
|
|
1439
1595
|
time.sleep(1)
|
|
1440
1596
|
|
|
1441
|
-
|
|
1442
|
-
response = session.get(sshproxy_url)
|
|
1443
|
-
|
|
1444
|
-
content = response.json()
|
|
1445
|
-
|
|
1446
|
-
ssh_private_key = ""
|
|
1447
|
-
if content.get('ssh_private_key_base_64'):
|
|
1448
|
-
ssh_private_key = base64.b64decode(content['ssh_private_key_base_64']).decode()
|
|
1449
|
-
|
|
1450
|
-
ssh_proxy_option = f" -J {content['ssh_proxy']}" if content.get('ssh_proxy') else ""
|
|
1451
|
-
|
|
1452
|
-
if ssh_private_key:
|
|
1453
|
-
console.print("🔑 [blue]Adding SSH proxy key[/blue]")
|
|
1454
|
-
subprocess.run(["ssh-add", "-"], input=ssh_private_key.encode())
|
|
1455
|
-
|
|
1456
|
-
console.print(f"🌎 ssh{ssh_proxy_option} root@{guest}")
|
|
1597
|
+
console.print(f"🌎 ssh root@{guest}")
|
|
1457
1598
|
|
|
1458
1599
|
if autoconnect:
|
|
1459
|
-
os.system(
|
|
1460
|
-
f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null{ssh_proxy_option} root@{guest}" # noqa: E501
|
|
1461
|
-
)
|
|
1600
|
+
os.system(f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guest}") # noqa: E501
|
|
1462
1601
|
|
|
1463
1602
|
|
|
1464
1603
|
def update():
|
tft/cli/config.py
CHANGED
tft/cli/utils.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import itertools
|
|
5
6
|
import os
|
|
7
|
+
import shlex
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
import uuid
|
|
@@ -119,6 +121,12 @@ def options_to_dict(name: str, options: List[str]) -> Dict[str, str]:
|
|
|
119
121
|
"""Create a dictionary from list of `key=value|@file` options"""
|
|
120
122
|
|
|
121
123
|
options_dict = {}
|
|
124
|
+
|
|
125
|
+
# Turn option list such as
|
|
126
|
+
# `['aaa=bbb "foo foo=bar bar"', 'foo=bar']` into
|
|
127
|
+
# `['aaa=bbb', 'foo foo=bar bar', 'foo=bar']`
|
|
128
|
+
options = list(itertools.chain.from_iterable(shlex.split(option) for option in options))
|
|
129
|
+
|
|
122
130
|
for option in options:
|
|
123
131
|
# Option is `@file`
|
|
124
132
|
if option.startswith('@'):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tft-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.22
|
|
4
4
|
Summary: Testing Farm CLI tool
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Miroslav Vadkerti
|
|
@@ -15,6 +15,7 @@ Requires-Dist: click (>=8.0.4,<8.1.0)
|
|
|
15
15
|
Requires-Dist: colorama (>=0.4.4,<0.5.0)
|
|
16
16
|
Requires-Dist: dynaconf (>=3.1.7,<4.0.0)
|
|
17
17
|
Requires-Dist: requests (>=2.27.1,<3.0.0)
|
|
18
|
+
Requires-Dist: rich (>=12,<13)
|
|
18
19
|
Requires-Dist: ruamel-yaml (>=0.18.6,<0.19.0)
|
|
19
20
|
Requires-Dist: setuptools
|
|
20
21
|
Requires-Dist: typer[all] (>=0.7.0,<0.8.0)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
+
tft/cli/commands.py,sha256=mRIeB9JsvRHhiAGJUxXLFMTKuvEt2AGN8AhVujBQcZo,62824
|
|
3
|
+
tft/cli/config.py,sha256=CQrqucLMNlRrjK_y5WY6vLbAYA5TcnHG_kw8CgJ09OA,1165
|
|
4
|
+
tft/cli/tool.py,sha256=wFcVxe1NRGW8stputOZlKMasZHjpysas7f0sgpEzipQ,865
|
|
5
|
+
tft/cli/utils.py,sha256=iWQnjA5rYhwMaz8cP_zmfJ5HihTbaKE3XzZk0N4nfcE,7664
|
|
6
|
+
tft_cli-0.0.22.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
7
|
+
tft_cli-0.0.22.dist-info/METADATA,sha256=1FC__NfxUWFgQUr-8R1glWqIKi3kBvsSpZrM4rEBUEM,762
|
|
8
|
+
tft_cli-0.0.22.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
9
|
+
tft_cli-0.0.22.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
10
|
+
tft_cli-0.0.22.dist-info/RECORD,,
|
tft_cli-0.0.20.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
|
|
2
|
-
tft/cli/commands.py,sha256=nGzFCuKTFrwRAJwhm7Fd-uPmTqh5ONEBoa-2ubH9Fpg,56699
|
|
3
|
-
tft/cli/config.py,sha256=lJ9TtsBAdcNDbh4xZd0x1b48V7IsGl3t7kALmNjCqNs,1115
|
|
4
|
-
tft/cli/tool.py,sha256=wFcVxe1NRGW8stputOZlKMasZHjpysas7f0sgpEzipQ,865
|
|
5
|
-
tft/cli/utils.py,sha256=9s7zY_k1MYYPTF4Gr2AMH2DMcySUCIgXbF3LjYa7bzY,7404
|
|
6
|
-
tft_cli-0.0.20.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
|
|
7
|
-
tft_cli-0.0.20.dist-info/METADATA,sha256=dVw-eLgGMMRxiN055vL_nIX59bDxInpZ_XSRsbtLjOI,731
|
|
8
|
-
tft_cli-0.0.20.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
9
|
-
tft_cli-0.0.20.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
|
|
10
|
-
tft_cli-0.0.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|