knotctl 0.1.0__py3-none-any.whl → 0.1.1__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.
- knotctl/__init__.py +138 -82
- {knotctl-0.1.0.dist-info → knotctl-0.1.1.dist-info}/METADATA +26 -3
- knotctl-0.1.1.dist-info/RECORD +6 -0
- knotctl-0.1.0.dist-info/RECORD +0 -6
- {knotctl-0.1.0.dist-info → knotctl-0.1.1.dist-info}/LICENSE +0 -0
- {knotctl-0.1.0.dist-info → knotctl-0.1.1.dist-info}/WHEEL +0 -0
- {knotctl-0.1.0.dist-info → knotctl-0.1.1.dist-info}/entry_points.txt +0 -0
knotctl/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
import urllib.parse
|
|
9
9
|
from os import environ, mkdir
|
|
10
|
-
from os.path import isdir, isfile,
|
|
10
|
+
from os.path import isdir, isfile, join
|
|
11
11
|
from typing import Union
|
|
12
12
|
from urllib.parse import urlparse
|
|
13
13
|
|
|
@@ -36,6 +36,9 @@ def error(description: str, error: str) -> list[dict]:
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def get_config(config_filename: str):
|
|
39
|
+
if not isfile(config_filename):
|
|
40
|
+
print("You need to configure knotctl before proceeding")
|
|
41
|
+
run_config(config_filename)
|
|
39
42
|
with open(config_filename, "r") as fh:
|
|
40
43
|
return yaml.safe_load(fh.read())
|
|
41
44
|
|
|
@@ -46,7 +49,8 @@ def nested_out(input, tabs="") -> str:
|
|
|
46
49
|
string += "{}\n".format(input)
|
|
47
50
|
elif isinstance(input, dict):
|
|
48
51
|
for key, value in input.items():
|
|
49
|
-
string += "{}{}: {}".format(tabs, key,
|
|
52
|
+
string += "{}{}: {}".format(tabs, key,
|
|
53
|
+
nested_out(value, tabs + " "))
|
|
50
54
|
elif isinstance(input, list):
|
|
51
55
|
for entry in input:
|
|
52
56
|
string += "{}\n{}".format(tabs, nested_out(entry, tabs + " "))
|
|
@@ -70,11 +74,9 @@ def run_add(url: str, jsonout: bool, headers: dict):
|
|
|
70
74
|
out = response.json()
|
|
71
75
|
if isinstance(out, list):
|
|
72
76
|
for record in out:
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
and record["rtype"] == parsed["rtype"]
|
|
77
|
-
):
|
|
77
|
+
if (record["data"] == parsed["data"]
|
|
78
|
+
and record["name"] == parsed["name"]
|
|
79
|
+
and record["rtype"] == parsed["rtype"]):
|
|
78
80
|
output(record, jsonout)
|
|
79
81
|
break
|
|
80
82
|
else:
|
|
@@ -94,29 +96,28 @@ def run_log(url: str, jsonout: bool, headers: dict):
|
|
|
94
96
|
line = lines[index]
|
|
95
97
|
index += 1
|
|
96
98
|
cur_has_timestamp = line.startswith("[")
|
|
97
|
-
next_has_timestamp = index < len(
|
|
98
|
-
"["
|
|
99
|
-
)
|
|
99
|
+
next_has_timestamp = index < len(
|
|
100
|
+
lines) and lines[index].startswith("[")
|
|
100
101
|
# Simple case, just one line with timestamp
|
|
101
102
|
if cur_has_timestamp and next_has_timestamp:
|
|
102
|
-
timestamp = line.split(
|
|
103
|
-
text = line.split(
|
|
104
|
-
out.append({
|
|
103
|
+
timestamp = line.split("]")[0].split("[")[1]
|
|
104
|
+
text = line.split("]")[1].lstrip(":").strip()
|
|
105
|
+
out.append({"timestamp": timestamp, "text": text})
|
|
105
106
|
text = ""
|
|
106
107
|
timestamp = ""
|
|
107
108
|
# Start of multiline
|
|
108
109
|
elif cur_has_timestamp:
|
|
109
|
-
timestamp = line.split(
|
|
110
|
-
text = line.split(
|
|
110
|
+
timestamp = line.split("]")[0].split("[")[1]
|
|
111
|
+
text = line.split("]")[1].lstrip(":").strip()
|
|
111
112
|
# End of multiline
|
|
112
113
|
elif next_has_timestamp:
|
|
113
|
-
text += f
|
|
114
|
-
out.append({
|
|
114
|
+
text += f"\n{line.strip()}"
|
|
115
|
+
out.append({"timestamp": timestamp, "text": text})
|
|
115
116
|
text = ""
|
|
116
117
|
timestamp = ""
|
|
117
118
|
# Middle of multiline
|
|
118
119
|
else:
|
|
119
|
-
text += f
|
|
120
|
+
text += f"\n{line.strip()}"
|
|
120
121
|
|
|
121
122
|
else:
|
|
122
123
|
out = string
|
|
@@ -144,7 +145,7 @@ def run_config(
|
|
|
144
145
|
if current:
|
|
145
146
|
if os.path.islink(config_filename):
|
|
146
147
|
actual_path = os.readlink(config_filename)
|
|
147
|
-
print(actual_path.split(
|
|
148
|
+
print(actual_path.split("-")[-1])
|
|
148
149
|
else:
|
|
149
150
|
print("none")
|
|
150
151
|
return
|
|
@@ -171,8 +172,7 @@ def run_config(
|
|
|
171
172
|
error(
|
|
172
173
|
"Can not configure without {}".format(need),
|
|
173
174
|
"No {}".format(need),
|
|
174
|
-
)
|
|
175
|
-
)
|
|
175
|
+
))
|
|
176
176
|
sys.exit(1)
|
|
177
177
|
config[need] = input("Enter {}: ".format(need))
|
|
178
178
|
|
|
@@ -196,7 +196,10 @@ def run_delete(url: str, jsonout: bool, headers: dict):
|
|
|
196
196
|
output(reply, jsonout)
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
def run_list(url: str,
|
|
199
|
+
def run_list(url: str,
|
|
200
|
+
jsonout: bool,
|
|
201
|
+
headers: dict,
|
|
202
|
+
ret=False) -> Union[None, str]:
|
|
200
203
|
response = requests.get(url, headers=headers)
|
|
201
204
|
string = response.json()
|
|
202
205
|
if ret:
|
|
@@ -210,6 +213,22 @@ def run_update(url: str, jsonout: bool, headers: dict):
|
|
|
210
213
|
output(response.json(), jsonout)
|
|
211
214
|
|
|
212
215
|
|
|
216
|
+
def run_zone(url: str,
|
|
217
|
+
jsonout: bool,
|
|
218
|
+
headers: dict,
|
|
219
|
+
ret=False) -> Union[None, str]:
|
|
220
|
+
response = requests.get(url, headers=headers)
|
|
221
|
+
zones = response.json()
|
|
222
|
+
for zone in zones:
|
|
223
|
+
del zone["records"]
|
|
224
|
+
string = zones
|
|
225
|
+
|
|
226
|
+
if ret:
|
|
227
|
+
return string
|
|
228
|
+
else:
|
|
229
|
+
output(string, jsonout)
|
|
230
|
+
|
|
231
|
+
|
|
213
232
|
# Set up the url
|
|
214
233
|
def setup_url(
|
|
215
234
|
baseurl: str,
|
|
@@ -248,16 +267,14 @@ def setup_url(
|
|
|
248
267
|
error(
|
|
249
268
|
"ttl only makes sense with rtype, name and zone",
|
|
250
269
|
"Missing parameter",
|
|
251
|
-
)
|
|
252
|
-
)
|
|
270
|
+
))
|
|
253
271
|
sys.exit(1)
|
|
254
272
|
if rtype and (not name or not zone):
|
|
255
273
|
output(
|
|
256
274
|
error(
|
|
257
275
|
"rtype only makes sense with name and zone",
|
|
258
276
|
"Missing parameter",
|
|
259
|
-
)
|
|
260
|
-
)
|
|
277
|
+
))
|
|
261
278
|
sys.exit(1)
|
|
262
279
|
if name and not zone:
|
|
263
280
|
output(error("name only makes sense with a zone", "Missing parameter"))
|
|
@@ -297,8 +314,7 @@ def split_url(url: str) -> dict:
|
|
|
297
314
|
}
|
|
298
315
|
|
|
299
316
|
|
|
300
|
-
|
|
301
|
-
def main() -> int:
|
|
317
|
+
def get_parser() -> dict:
|
|
302
318
|
description = """Manage DNS records with knot dns rest api:
|
|
303
319
|
* https://gitlab.nic.cz/knot/knot-dns-rest"""
|
|
304
320
|
|
|
@@ -366,18 +382,22 @@ def main() -> int:
|
|
|
366
382
|
subparsers.add_parser("auditlog", description=auditlog_description)
|
|
367
383
|
|
|
368
384
|
changelog_description = "View the changelog of a zone."
|
|
369
|
-
changelogcmd = subparsers.add_parser("changelog",
|
|
385
|
+
changelogcmd = subparsers.add_parser("changelog",
|
|
386
|
+
description=changelog_description)
|
|
370
387
|
changelogcmd.add_argument("-z", "--zone", required=True)
|
|
371
388
|
|
|
372
389
|
complete_description = "Generate shell completion script."
|
|
373
|
-
completecmd = subparsers.add_parser("completion",
|
|
390
|
+
completecmd = subparsers.add_parser("completion",
|
|
391
|
+
description=complete_description)
|
|
374
392
|
completecmd.add_argument("-s", "--shell")
|
|
375
393
|
|
|
376
394
|
config_description = "Configure access to knot-dns-rest-api."
|
|
377
395
|
configcmd = subparsers.add_parser("config", description=config_description)
|
|
378
396
|
configcmd.add_argument("-b", "--baseurl")
|
|
379
397
|
configcmd.add_argument("-c", "--context")
|
|
380
|
-
configcmd.add_argument("-C",
|
|
398
|
+
configcmd.add_argument("-C",
|
|
399
|
+
"--current",
|
|
400
|
+
action=argparse.BooleanOptionalAction)
|
|
381
401
|
configcmd.add_argument("-p", "--password")
|
|
382
402
|
configcmd.add_argument("-u", "--username")
|
|
383
403
|
|
|
@@ -388,22 +408,23 @@ def main() -> int:
|
|
|
388
408
|
deletecmd.add_argument("-r", "--rtype")
|
|
389
409
|
deletecmd.add_argument("-z", "--zone", required=True)
|
|
390
410
|
|
|
391
|
-
list_description = "List records
|
|
411
|
+
list_description = "List records."
|
|
392
412
|
listcmd = subparsers.add_parser("list", description=list_description)
|
|
393
413
|
listcmd.add_argument("-d", "--data")
|
|
394
414
|
listcmd.add_argument("-n", "--name")
|
|
395
415
|
listcmd.add_argument("-r", "--rtype")
|
|
396
|
-
listcmd.add_argument("-z", "--zone", required=
|
|
416
|
+
listcmd.add_argument("-z", "--zone", required=False)
|
|
417
|
+
|
|
418
|
+
user_description = "View user information."
|
|
419
|
+
usercmd = subparsers.add_parser("user", description=user_description)
|
|
420
|
+
usercmd.add_argument("-u", "--username", default=None)
|
|
397
421
|
|
|
398
422
|
update_description = (
|
|
399
|
-
"Update a record in the zone. The record must exist in the zone.\n"
|
|
400
|
-
)
|
|
423
|
+
"Update a record in the zone. The record must exist in the zone.\n")
|
|
401
424
|
update_description += (
|
|
402
|
-
"In this case --data, --name, --rtype and --ttl switches are used\n"
|
|
403
|
-
)
|
|
425
|
+
"In this case --data, --name, --rtype and --ttl switches are used\n")
|
|
404
426
|
update_description += (
|
|
405
|
-
"for searching for the appropriate record, while the --argument\n"
|
|
406
|
-
)
|
|
427
|
+
"for searching for the appropriate record, while the --argument\n")
|
|
407
428
|
update_description += "switches are used for updating the record."
|
|
408
429
|
update_epilog = """Available arguments are:
|
|
409
430
|
data: New record data.
|
|
@@ -421,7 +442,8 @@ def main() -> int:
|
|
|
421
442
|
"--argument",
|
|
422
443
|
action="append",
|
|
423
444
|
metavar="KEY=VALUE",
|
|
424
|
-
help="Specify key - value pairs to be updated: name=dns1.example.com.
|
|
445
|
+
help="Specify key - value pairs to be updated: name=dns1.example.com."
|
|
446
|
+
+ " or data=127.0.0.1 for example. --argument can be repeated",
|
|
425
447
|
required=True,
|
|
426
448
|
)
|
|
427
449
|
updatecmd.add_argument("-d", "--data", required=True)
|
|
@@ -430,7 +452,68 @@ def main() -> int:
|
|
|
430
452
|
updatecmd.add_argument("-t", "--ttl")
|
|
431
453
|
updatecmd.add_argument("-z", "--zone", required=True)
|
|
432
454
|
|
|
455
|
+
zone_description = "View zones."
|
|
456
|
+
subparsers.add_parser("zone", description=zone_description)
|
|
457
|
+
|
|
433
458
|
argcomplete.autocomplete(parser)
|
|
459
|
+
|
|
460
|
+
return parser
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def get_token(config) -> str:
|
|
464
|
+
# Authenticate
|
|
465
|
+
baseurl = config["baseurl"]
|
|
466
|
+
username = config["username"]
|
|
467
|
+
password = config["password"]
|
|
468
|
+
basic = HTTPBasicAuth(username, password)
|
|
469
|
+
response = requests.get(baseurl + "/user/login", auth=basic)
|
|
470
|
+
token = ""
|
|
471
|
+
try:
|
|
472
|
+
token = response.json()["token"]
|
|
473
|
+
except KeyError:
|
|
474
|
+
output(response.json())
|
|
475
|
+
except requests.exceptions.JSONDecodeError:
|
|
476
|
+
output(
|
|
477
|
+
error("Could not decode api response as JSON", "Could not decode"))
|
|
478
|
+
return token
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def run(url, args, headers, baseurl, parser, username):
|
|
482
|
+
try:
|
|
483
|
+
if args.command == "add":
|
|
484
|
+
run_add(url, args.json, headers)
|
|
485
|
+
elif args.command == "delete":
|
|
486
|
+
run_delete(url, args.json, headers)
|
|
487
|
+
elif args.command == "list":
|
|
488
|
+
run_list(url, args.json, headers)
|
|
489
|
+
elif args.command == "update":
|
|
490
|
+
run_update(url, args.json, headers)
|
|
491
|
+
elif args.command == "user":
|
|
492
|
+
url = baseurl + f"/user/info/{username}"
|
|
493
|
+
run_list(url, args.json, headers)
|
|
494
|
+
elif args.command == "auditlog":
|
|
495
|
+
url = baseurl + "/user/auditlog"
|
|
496
|
+
run_log(url, args.json, headers)
|
|
497
|
+
elif args.command == "changelog":
|
|
498
|
+
url = baseurl + f"/zones/changelog/{args.zone.rstrip('.')}"
|
|
499
|
+
run_log(url, args.json, headers)
|
|
500
|
+
elif args.command == "zone":
|
|
501
|
+
url = baseurl + "/zones"
|
|
502
|
+
run_zone(url, args.json, headers)
|
|
503
|
+
else:
|
|
504
|
+
parser.print_help(sys.stderr)
|
|
505
|
+
return 2
|
|
506
|
+
except requests.exceptions.RequestException as e:
|
|
507
|
+
output(error(e, "Could not connect to server"))
|
|
508
|
+
except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
|
|
509
|
+
output(
|
|
510
|
+
error("Could not decode api response as JSON", "Could not decode"))
|
|
511
|
+
return 0
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Entry point to program
|
|
515
|
+
def main() -> int:
|
|
516
|
+
parser = get_parser()
|
|
434
517
|
args = parser.parse_args()
|
|
435
518
|
if args.command == "completion":
|
|
436
519
|
run_complete(args.shell)
|
|
@@ -445,35 +528,27 @@ def main() -> int:
|
|
|
445
528
|
|
|
446
529
|
if args.command == "config":
|
|
447
530
|
run_config(
|
|
448
|
-
config_filename,
|
|
531
|
+
config_filename,
|
|
532
|
+
args.context,
|
|
533
|
+
args.baseurl,
|
|
534
|
+
args.username,
|
|
535
|
+
args.password,
|
|
536
|
+
args.current,
|
|
449
537
|
)
|
|
450
538
|
return 0
|
|
451
539
|
|
|
452
|
-
if not isfile(config_filename):
|
|
453
|
-
print("You need to configure knotctl before proceeding")
|
|
454
|
-
run_config(config_filename)
|
|
455
|
-
|
|
456
540
|
config = get_config(config_filename)
|
|
457
541
|
baseurl = config["baseurl"]
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
# Authenticate
|
|
462
|
-
basic = HTTPBasicAuth(username, password)
|
|
463
|
-
response = requests.get(baseurl + "/user/login", auth=basic)
|
|
464
|
-
try:
|
|
465
|
-
token = response.json()["token"]
|
|
466
|
-
except KeyError:
|
|
467
|
-
output(response.json())
|
|
468
|
-
return 1
|
|
469
|
-
except requests.exceptions.JSONDecodeError:
|
|
470
|
-
output(error("Could not decode api response as JSON", "Could not decode"))
|
|
542
|
+
token = get_token(config)
|
|
543
|
+
if token == "":
|
|
544
|
+
print("Could not get token, exiting")
|
|
471
545
|
return 1
|
|
472
546
|
headers = {"Authorization": "Bearer {}".format(token)}
|
|
473
547
|
|
|
474
548
|
# Route based on command
|
|
475
549
|
url = ""
|
|
476
550
|
ttl = None
|
|
551
|
+
user = config["username"]
|
|
477
552
|
if "ttl" in args:
|
|
478
553
|
ttl = args.ttl
|
|
479
554
|
if args.command != "update":
|
|
@@ -486,7 +561,10 @@ def main() -> int:
|
|
|
486
561
|
soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
|
|
487
562
|
soa_json = run_list(soa_url, True, headers, ret=True)
|
|
488
563
|
ttl = soa_json[0]["ttl"]
|
|
489
|
-
if args.command
|
|
564
|
+
if args.command == "user":
|
|
565
|
+
if args.username:
|
|
566
|
+
user = args.username
|
|
567
|
+
if args.command in ["auditlog", "changelog", "user", "zone"]:
|
|
490
568
|
pass
|
|
491
569
|
else:
|
|
492
570
|
try:
|
|
@@ -503,29 +581,7 @@ def main() -> int:
|
|
|
503
581
|
parser.print_help(sys.stderr)
|
|
504
582
|
return 1
|
|
505
583
|
|
|
506
|
-
|
|
507
|
-
if args.command == "add":
|
|
508
|
-
run_add(url, args.json, headers)
|
|
509
|
-
elif args.command == "delete":
|
|
510
|
-
run_delete(url, args.json, headers)
|
|
511
|
-
elif args.command == "list":
|
|
512
|
-
run_list(url, args.json, headers)
|
|
513
|
-
elif args.command == "update":
|
|
514
|
-
run_update(url, args.json, headers)
|
|
515
|
-
elif args.command == "auditlog":
|
|
516
|
-
url = baseurl + "/user/auditlog"
|
|
517
|
-
run_log(url, args.json, headers)
|
|
518
|
-
elif args.command == "changelog":
|
|
519
|
-
url = baseurl + f"/zones/changelog/{args.zone.rstrip('.')}"
|
|
520
|
-
run_log(url, args.json, headers)
|
|
521
|
-
else:
|
|
522
|
-
parser.print_help(sys.stderr)
|
|
523
|
-
return 2
|
|
524
|
-
except requests.exceptions.RequestException as e:
|
|
525
|
-
output(error(e, "Could not connect to server"))
|
|
526
|
-
except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
|
|
527
|
-
output(error("Could not decode api response as JSON", "Could not decode"))
|
|
528
|
-
return 0
|
|
584
|
+
return run(url, args, headers, baseurl, parser, user)
|
|
529
585
|
|
|
530
586
|
|
|
531
587
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: knotctl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A CLI for knotapi.
|
|
5
5
|
Author-email: Micke Nordin <hej@mic.ke>
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -208,9 +208,9 @@ options:
|
|
|
208
208
|
### LIST
|
|
209
209
|
|
|
210
210
|
```
|
|
211
|
-
usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] -z ZONE
|
|
211
|
+
usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] [-z ZONE]
|
|
212
212
|
|
|
213
|
-
List records
|
|
213
|
+
List records.
|
|
214
214
|
|
|
215
215
|
options:
|
|
216
216
|
-h, --help show this help message and exit
|
|
@@ -249,3 +249,26 @@ Available arguments are:
|
|
|
249
249
|
ttl: New record time to live (TTL).
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
### USER
|
|
253
|
+
```
|
|
254
|
+
usage: knotctl user [-h] [-u USERNAME]
|
|
255
|
+
|
|
256
|
+
View user information.
|
|
257
|
+
|
|
258
|
+
options:
|
|
259
|
+
-h, --help show this help message and exit
|
|
260
|
+
-u USERNAME, --username USERNAME
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### ZONE
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
usage: knotctl zone
|
|
267
|
+
|
|
268
|
+
List zones.
|
|
269
|
+
|
|
270
|
+
options:
|
|
271
|
+
-h, --help show this help message and exit
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
knotctl/__init__.py,sha256=Z2qrtQAzH-V6uPCCaPGwbV2QXhzCO49T5SHGyoFanVI,20018
|
|
2
|
+
knotctl-0.1.1.dist-info/entry_points.txt,sha256=oGFZaAfsfqC-KbiCm04W6DTBFCq2f5F_p3KMEgNoY4s,40
|
|
3
|
+
knotctl-0.1.1.dist-info/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
|
|
4
|
+
knotctl-0.1.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
|
5
|
+
knotctl-0.1.1.dist-info/METADATA,sha256=S1ap70TUQ3ClrEbyz_gylivlIMq1y4QmpPWWOONeoaw,7528
|
|
6
|
+
knotctl-0.1.1.dist-info/RECORD,,
|
knotctl-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
knotctl/__init__.py,sha256=kqE3iZEX2pxMSfXTSDyJUCayRanSpHLgdd4EqVR1kV0,18422
|
|
2
|
-
knotctl-0.1.0.dist-info/entry_points.txt,sha256=oGFZaAfsfqC-KbiCm04W6DTBFCq2f5F_p3KMEgNoY4s,40
|
|
3
|
-
knotctl-0.1.0.dist-info/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
|
|
4
|
-
knotctl-0.1.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
|
5
|
-
knotctl-0.1.0.dist-info/METADATA,sha256=yqUFEkuHQ_GgpuV2TJox78ujwdKagC0dYEtrCUGbwBk,7236
|
|
6
|
-
knotctl-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|