wizlib 2.0.15__py3-none-any.whl → 2.0.17__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.
Potentially problematic release.
This version of wizlib might be problematic. Click here for more details.
- wizlib/ui/__init__.py +61 -41
- wizlib/ui/shell_line_editor.py +236 -0
- wizlib/ui/shell_ui.py +5 -2
- wizlib/util.py +8 -0
- {wizlib-2.0.15.dist-info → wizlib-2.0.17.dist-info}/METADATA +1 -1
- {wizlib-2.0.15.dist-info → wizlib-2.0.17.dist-info}/RECORD +7 -5
- {wizlib-2.0.15.dist-info → wizlib-2.0.17.dist-info}/WHEEL +0 -0
wizlib/ui/__init__.py
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
# interfaces (shell, curses, Slack, etc) to drive the same data model without
|
|
3
3
|
# the data model needing specific UI knowledge.
|
|
4
4
|
#
|
|
5
|
-
# Possibly outdated info: The UI keeps a reference to the Handler. The Handler
|
|
6
|
-
# will bring a Command with it, and this is the main root command. In the case
|
|
7
|
-
# of a Shell-type UI, that is the command to be executed. In the case of an
|
|
8
|
-
# interactive UI, it's really just a placeholder; the UI will instantiate other
|
|
9
|
-
# commands as it proceeds. But the Handler won't know about those.
|
|
10
|
-
#
|
|
11
5
|
# Commands might call back to the UI for confirmations or arguments that are
|
|
12
6
|
# previously omitted, using the get_ methods.
|
|
13
7
|
|
|
@@ -45,41 +39,52 @@ from wizlib.class_family import ClassFamily
|
|
|
45
39
|
|
|
46
40
|
@dataclass
|
|
47
41
|
class Choice:
|
|
48
|
-
"""
|
|
42
|
+
"""A single option, equivalent to a single radio button or select option in
|
|
43
|
+
HTML.
|
|
44
|
+
|
|
45
|
+
text - text of the option
|
|
46
|
+
|
|
47
|
+
keys - keystrokes that might indicate this option in a keyboard-driven UI
|
|
48
|
+
|
|
49
|
+
action - what to do when chosen
|
|
50
|
+
"""
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
text: str = ''
|
|
53
|
+
keys: str = ''
|
|
52
54
|
action: object = None
|
|
53
55
|
|
|
54
56
|
@property
|
|
55
57
|
def key(self):
|
|
56
|
-
return self.keys[0]
|
|
57
|
-
|
|
58
|
-
@property
|
|
59
|
-
def word(self):
|
|
60
|
-
return self.words[0]
|
|
58
|
+
return self.keys[0] if self.keys else ''
|
|
61
59
|
|
|
62
60
|
def hit_key(self, key):
|
|
63
61
|
return key in self.keys
|
|
64
62
|
|
|
65
|
-
def
|
|
66
|
-
return
|
|
63
|
+
def hit_text(self, text):
|
|
64
|
+
return self.text.startswith(text)
|
|
67
65
|
|
|
66
|
+
@property
|
|
68
67
|
def value(self):
|
|
69
|
-
return self.
|
|
68
|
+
return self.text if (self.action is None) else self.action
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def key_prompt(self):
|
|
72
|
+
"""Text with keystroke in parens"""
|
|
73
|
+
for key in self.keys:
|
|
74
|
+
if key in self.text:
|
|
75
|
+
pre, it, post = self.text.partition(key)
|
|
76
|
+
return f"{pre}({it}){post}"
|
|
77
|
+
return f"({self.keys[0]}){self.text}"
|
|
70
78
|
|
|
71
|
-
# def call(self, *args, **kwargs):
|
|
72
|
-
# if callable(self.action):
|
|
73
|
-
# return self.action(*args, **kwargs)
|
|
74
79
|
|
|
80
|
+
# TODO: Decide whether the Prompt/Chooser division makes sense
|
|
75
81
|
|
|
76
|
-
@dataclass
|
|
77
82
|
class Prompt:
|
|
78
83
|
"""Tell the user what kind of input to provide"""
|
|
79
84
|
|
|
80
|
-
intro: str =
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
def __init__(self, intro: str = '', default: str = ''):
|
|
86
|
+
self.intro = intro
|
|
87
|
+
self.default = default
|
|
83
88
|
|
|
84
89
|
@property
|
|
85
90
|
def prompt_string(self):
|
|
@@ -90,35 +95,50 @@ class Prompt:
|
|
|
90
95
|
if self.default:
|
|
91
96
|
result.append(f"[{self.default}]")
|
|
92
97
|
for choice in self.choices:
|
|
93
|
-
if choice.
|
|
98
|
+
if choice.text == self.default:
|
|
94
99
|
continue
|
|
95
|
-
|
|
96
|
-
result.append(f"{pre}({it}){post}")
|
|
100
|
+
result.append(choice.key_prompt)
|
|
97
101
|
return " ".join(result) + ": "
|
|
98
102
|
|
|
99
103
|
|
|
100
|
-
@dataclass
|
|
101
104
|
class Chooser(Prompt):
|
|
102
105
|
"""Hold a set of choices and get the result"""
|
|
103
106
|
|
|
107
|
+
def __init__(self, intro: str = '', default: str = '', choices=None):
|
|
108
|
+
super().__init__(intro, default)
|
|
109
|
+
if isinstance(choices, dict):
|
|
110
|
+
self.choices = [Choice(choices[k], k) for k in choices]
|
|
111
|
+
elif isinstance(choices, list):
|
|
112
|
+
self.choices = [(c if isinstance(c, Choice) else Choice(c))
|
|
113
|
+
for c in choices]
|
|
114
|
+
else:
|
|
115
|
+
self.choices = []
|
|
116
|
+
|
|
104
117
|
def add_choice(self, *args, **kwargs):
|
|
105
118
|
choice = Choice(*args, **kwargs)
|
|
106
119
|
self.choices.append(choice)
|
|
107
120
|
|
|
108
121
|
def choice_by_key(self, key):
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
if key == '\n':
|
|
123
|
+
choice = self.default
|
|
124
|
+
else:
|
|
125
|
+
choice = next(
|
|
126
|
+
(o.value for o in self.choices if key in o.keys), None)
|
|
127
|
+
return choice() if callable(choice) else choice
|
|
111
128
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
return self._choose(chosenlist)
|
|
129
|
+
def choices_by_text(self, text):
|
|
130
|
+
return [o.value for o in self.choices if o.hit_text(text)]
|
|
115
131
|
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
# def choice_by_word(self, word):
|
|
133
|
+
# chosenlist = [o.value for o in self.choices if o.hit_word(word)]
|
|
134
|
+
# return self._choose(chosenlist)
|
|
135
|
+
|
|
136
|
+
# def _choose(self, chosenlist):
|
|
137
|
+
# choice = next(iter(chosenlist), None)
|
|
138
|
+
# if callable(choice):
|
|
139
|
+
# return choice()
|
|
140
|
+
# else:
|
|
141
|
+
# return choice
|
|
122
142
|
|
|
123
143
|
|
|
124
144
|
@dataclass
|
|
@@ -131,8 +151,8 @@ class UI(ClassFamily):
|
|
|
131
151
|
# def start(self):
|
|
132
152
|
# pass
|
|
133
153
|
|
|
134
|
-
# # TODO: Make
|
|
135
|
-
# def
|
|
154
|
+
# # TODO: Make get_option_mini an abstract method
|
|
155
|
+
# def get_option_mini(self, chooser: Chooser):
|
|
136
156
|
# """Get an option from a list from the user"""
|
|
137
157
|
# pass
|
|
138
158
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from readchar import readkey
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if (sys.platform == "win32"):
|
|
9
|
+
import ctypes
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
else:
|
|
12
|
+
import termios
|
|
13
|
+
|
|
14
|
+
# https://stackoverflow.com/questions/35526014/how-can-i-get-the-cursors-position-in-an-ansi-terminal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cursorPos(): # pragma: nocover
|
|
18
|
+
if (sys.platform == "win32"):
|
|
19
|
+
OldStdinMode = ctypes.wintypes.DWORD()
|
|
20
|
+
OldStdoutMode = ctypes.wintypes.DWORD()
|
|
21
|
+
kernel32 = ctypes.windll.kernel32
|
|
22
|
+
kernel32.GetConsoleMode(
|
|
23
|
+
kernel32.GetStdHandle(-10), ctypes.byref(OldStdinMode))
|
|
24
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
|
|
25
|
+
kernel32.GetConsoleMode(
|
|
26
|
+
kernel32.GetStdHandle(-11), ctypes.byref(OldStdoutMode))
|
|
27
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
28
|
+
else:
|
|
29
|
+
OldStdinMode = termios.tcgetattr(sys.stdin)
|
|
30
|
+
_ = termios.tcgetattr(sys.stdin)
|
|
31
|
+
_[3] = _[3] & ~(termios.ECHO | termios.ICANON)
|
|
32
|
+
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
|
|
33
|
+
try:
|
|
34
|
+
_ = ""
|
|
35
|
+
sys.stdout.write("\x1b[6n")
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
while not (_ := _ + sys.stdin.read(1)).endswith('R'):
|
|
38
|
+
True
|
|
39
|
+
res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
|
|
40
|
+
finally:
|
|
41
|
+
if (sys.platform == "win32"):
|
|
42
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), OldStdinMode)
|
|
43
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), OldStdoutMode)
|
|
44
|
+
else:
|
|
45
|
+
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, OldStdinMode)
|
|
46
|
+
if (res):
|
|
47
|
+
return (res.group("x"), res.group("y"))
|
|
48
|
+
return (-1, -1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sequence(hexes: str) -> str:
|
|
52
|
+
return bytes.fromhex(hexes).decode()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
LEFT = sequence("1b5b44")
|
|
56
|
+
RIGHT = sequence("1b5b43")
|
|
57
|
+
BACKSPACE = sequence("7f")
|
|
58
|
+
BEGINNING = sequence("01")
|
|
59
|
+
END = sequence("05")
|
|
60
|
+
RETURN = sequence("0a")
|
|
61
|
+
ESC = sequence("1b")
|
|
62
|
+
TAB = sequence("09")
|
|
63
|
+
SHIFT_TAB = sequence("1b5b5a")
|
|
64
|
+
LEFT_WORD = sequence("1b62")
|
|
65
|
+
RIGHT_WORD = sequence("1b66")
|
|
66
|
+
KILL = sequence("0b")
|
|
67
|
+
|
|
68
|
+
RESET = ESC + "[0m"
|
|
69
|
+
FAINT = ESC + "[2m"
|
|
70
|
+
BOLD = ESC + "[1m"
|
|
71
|
+
|
|
72
|
+
WORD_SEPARATORS = ' -_.,'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write(key):
|
|
76
|
+
sys.stderr.write(key)
|
|
77
|
+
sys.stderr.flush()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# "Fill" refers to the lighter text in the editor, to the right of (or instead
|
|
81
|
+
# of) user-typed text.
|
|
82
|
+
#
|
|
83
|
+
# States:
|
|
84
|
+
# - USER: User has typed something, so no fill
|
|
85
|
+
# - TAB: User has hit tab or shift-tab, show tab completion if any
|
|
86
|
+
# - DEFAULT: editor has a default value and user has typed nothing, so show
|
|
87
|
+
# default if any
|
|
88
|
+
# - BLANK: user hit backspace to clear the fill
|
|
89
|
+
|
|
90
|
+
FillState = Enum('FillState', 'USER TAB DEFAULT BLANK')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ShellLineEditor: # pragma: nocover
|
|
94
|
+
|
|
95
|
+
buf = ''
|
|
96
|
+
pos = 0
|
|
97
|
+
index = -1
|
|
98
|
+
|
|
99
|
+
def __init__(self, choices=[], default=''):
|
|
100
|
+
"""Parameters:
|
|
101
|
+
|
|
102
|
+
choices: List of string options for tab completion
|
|
103
|
+
|
|
104
|
+
default: Starting string value, can be accepted by user pressing return
|
|
105
|
+
"""
|
|
106
|
+
self.choices = choices
|
|
107
|
+
self.default = default
|
|
108
|
+
self.fillstate = FillState.DEFAULT
|
|
109
|
+
|
|
110
|
+
def edit(self):
|
|
111
|
+
write(RESET)
|
|
112
|
+
while True:
|
|
113
|
+
self.write_fill()
|
|
114
|
+
key = readkey()
|
|
115
|
+
self.clear_fill()
|
|
116
|
+
if key == RETURN:
|
|
117
|
+
break
|
|
118
|
+
if key.isprintable():
|
|
119
|
+
write(BOLD + key + self.buf[self.pos:] +
|
|
120
|
+
('\b' * (len(self.buf) - self.pos)) + RESET)
|
|
121
|
+
self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
|
|
122
|
+
self.pos += 1
|
|
123
|
+
self.fillstate = FillState.USER
|
|
124
|
+
elif (key in [BACKSPACE, KILL]) and self.has_fill:
|
|
125
|
+
# Backspace clears the fill
|
|
126
|
+
self.fillstate = FillState.BLANK
|
|
127
|
+
elif (key == BACKSPACE) and (self.pos > 0):
|
|
128
|
+
write(BOLD + '\b' + self.buf[self.pos:] + ' ' +
|
|
129
|
+
('\b' * (1 + len(self.buf) - self.pos)) + RESET)
|
|
130
|
+
self.buf = self.buf[:self.pos-1] + self.buf[self.pos:]
|
|
131
|
+
self.pos -= 1
|
|
132
|
+
self.fillstate = FillState.USER if (
|
|
133
|
+
self.pos > 0) else FillState.DEFAULT
|
|
134
|
+
elif key == LEFT and self.pos > 0:
|
|
135
|
+
self.move_left()
|
|
136
|
+
self.fillstate = FillState.USER
|
|
137
|
+
elif key == RIGHT and self.pos < len(self.buf):
|
|
138
|
+
self.move_right()
|
|
139
|
+
self.fillstate = FillState.USER
|
|
140
|
+
elif key == BEGINNING and self.pos > 0:
|
|
141
|
+
self.move_beginning()
|
|
142
|
+
self.fillstate = FillState.USER
|
|
143
|
+
elif key == END and self.has_fill:
|
|
144
|
+
self.accept_fill()
|
|
145
|
+
self.fillstate = FillState.USER
|
|
146
|
+
elif key == END and self.pos < len(self.buf):
|
|
147
|
+
self.move_end_buf()
|
|
148
|
+
self.fillstate = FillState.USER
|
|
149
|
+
elif key == TAB and (choices := self.valid_choices):
|
|
150
|
+
self.index = (self.index + 1) % len(choices)
|
|
151
|
+
self.fillstate = FillState.TAB
|
|
152
|
+
elif key == SHIFT_TAB and self.index > -1:
|
|
153
|
+
self.index = (self.index - 1) % len(self.valid_choices)
|
|
154
|
+
self.fillstate = FillState.TAB
|
|
155
|
+
elif key == LEFT_WORD and self.pos > 0:
|
|
156
|
+
while (self.pos > 0) and self.is_sep(self.pos - 1):
|
|
157
|
+
self.move_left()
|
|
158
|
+
while (self.pos > 0) and not self.is_sep(self.pos - 1):
|
|
159
|
+
self.move_left()
|
|
160
|
+
self.fillstate = FillState.USER
|
|
161
|
+
elif key == RIGHT_WORD and self.pos < len(self.buf):
|
|
162
|
+
while (self.pos < len(self.buf)) and self.is_sep(self.pos):
|
|
163
|
+
self.move_right()
|
|
164
|
+
while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
|
|
165
|
+
self.move_right()
|
|
166
|
+
self.fillstate = FillState.USER
|
|
167
|
+
elif key == KILL:
|
|
168
|
+
chars = len(self.buf) - self.pos
|
|
169
|
+
write(' ' * chars + '\b' * chars)
|
|
170
|
+
self.buf = self.buf[:self.pos]
|
|
171
|
+
self.fillstate = FillState.USER
|
|
172
|
+
else:
|
|
173
|
+
pass
|
|
174
|
+
self.accept_fill()
|
|
175
|
+
write(RETURN)
|
|
176
|
+
return self.buf
|
|
177
|
+
|
|
178
|
+
def is_sep(self, pos):
|
|
179
|
+
return (self.buf[pos] in WORD_SEPARATORS)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def has_fill(self):
|
|
183
|
+
return self.fillstate in [FillState.DEFAULT, FillState.TAB]
|
|
184
|
+
|
|
185
|
+
def accept_fill(self):
|
|
186
|
+
if self.has_fill:
|
|
187
|
+
write(BOLD + self.fill + RESET)
|
|
188
|
+
self.buf += self.fill
|
|
189
|
+
self.pos = len(self.buf)
|
|
190
|
+
|
|
191
|
+
def move_left(self):
|
|
192
|
+
write(LEFT)
|
|
193
|
+
self.pos -= 1
|
|
194
|
+
|
|
195
|
+
def move_right(self):
|
|
196
|
+
write(RIGHT)
|
|
197
|
+
self.pos += 1
|
|
198
|
+
|
|
199
|
+
def move_beginning(self):
|
|
200
|
+
while self.pos > 0:
|
|
201
|
+
self.move_left()
|
|
202
|
+
|
|
203
|
+
def move_end_buf(self):
|
|
204
|
+
while self.pos < len(self.buf):
|
|
205
|
+
self.move_right()
|
|
206
|
+
|
|
207
|
+
def write_fill(self):
|
|
208
|
+
if self.fillstate in [FillState.DEFAULT, FillState.TAB]:
|
|
209
|
+
self.move_end_buf()
|
|
210
|
+
write(FAINT + self.fill + '\b' * len(self.fill) + RESET)
|
|
211
|
+
|
|
212
|
+
def clear_fill(self):
|
|
213
|
+
if self.has_fill:
|
|
214
|
+
self.move_end_buf()
|
|
215
|
+
write(' ' * len(self.fill) + '\b' * len(self.fill))
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def fill(self):
|
|
219
|
+
if self.fillstate == FillState.TAB:
|
|
220
|
+
choice = self.valid_choices[self.index]
|
|
221
|
+
return choice[len(self.last_word):]
|
|
222
|
+
elif self.fillstate == FillState.DEFAULT:
|
|
223
|
+
return self.default
|
|
224
|
+
else:
|
|
225
|
+
return ''
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def last_word(self):
|
|
229
|
+
index = next((c for c in reversed(range(len(self.buf)))
|
|
230
|
+
if self.buf[c] in WORD_SEPARATORS), -1)
|
|
231
|
+
return self.buf[index+1:]
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def valid_choices(self):
|
|
235
|
+
# return [c for c in self.choices if c.startswith(self.buf)]
|
|
236
|
+
return [c for c in self.choices if c.startswith(self.last_word)]
|
wizlib/ui/shell_ui.py
CHANGED
|
@@ -3,6 +3,7 @@ import sys
|
|
|
3
3
|
from readchar import readkey
|
|
4
4
|
from wizlib.rlinput import rlinput
|
|
5
5
|
from wizlib.ui import UI
|
|
6
|
+
from wizlib.ui.shell_line_editor import ShellLineEditor
|
|
6
7
|
|
|
7
8
|
INTERACTIVE = sys.stdin.isatty()
|
|
8
9
|
|
|
@@ -21,7 +22,7 @@ class ShellUI(UI):
|
|
|
21
22
|
if value:
|
|
22
23
|
print(value, file=sys.stderr)
|
|
23
24
|
|
|
24
|
-
def
|
|
25
|
+
def get_option_mini(self, chooser):
|
|
25
26
|
"""Get a choice from the user with a single keystroke"""
|
|
26
27
|
# key = rlinput(chooser.prompt_string)
|
|
27
28
|
print(chooser.prompt_string, end='', file=sys.stderr, flush=True)
|
|
@@ -35,5 +36,7 @@ class ShellUI(UI):
|
|
|
35
36
|
def get_text(self, prompt):
|
|
36
37
|
"""Allow the user to input an arbitrary line of text"""
|
|
37
38
|
sys.stderr.write(prompt)
|
|
38
|
-
|
|
39
|
+
sys.stderr.flush()
|
|
40
|
+
value = ShellLineEditor().edit()
|
|
41
|
+
# value = input()
|
|
39
42
|
return value
|
wizlib/util.py
ADDED
|
@@ -9,9 +9,11 @@ wizlib/input_handler.py,sha256=QFiR9CC5bkW7HofNAKZEq_hL089hbWzyrFGAjAjvozg,624
|
|
|
9
9
|
wizlib/parser.py,sha256=O34azN4ttVfwwAsza0hujxGxDpzc4xUEVAf26DXJS5g,1505
|
|
10
10
|
wizlib/rlinput.py,sha256=l00Pa3rxNeY6LJgz8Aws_rTKoEchw33fuL8yqHF9_-o,1754
|
|
11
11
|
wizlib/super_wrapper.py,sha256=F834ytHqA7zegTD1ezk_uxlF9PLygh84wReuiqcI7BI,272
|
|
12
|
-
wizlib/ui/__init__.py,sha256=
|
|
13
|
-
wizlib/ui/
|
|
12
|
+
wizlib/ui/__init__.py,sha256=55tzM_XphYFqm_jVxQc5vkfPLgD1Z3aqSjDl50GM44A,6139
|
|
13
|
+
wizlib/ui/shell_line_editor.py,sha256=HQNMUijdf-qU2kbzU_iOrK5gT-nEMkxweld2fP8yvPo,7818
|
|
14
|
+
wizlib/ui/shell_ui.py,sha256=phgrFFMZGY9jJB_QwoOXhI6P6VK3W740g4rkVsFbRSc,1234
|
|
14
15
|
wizlib/ui_handler.py,sha256=nvMRuGVCwJ-_H59iZQ72--_tiiqXAsSHP7kYnd43P68,716
|
|
15
|
-
wizlib
|
|
16
|
-
wizlib-2.0.
|
|
17
|
-
wizlib-2.0.
|
|
16
|
+
wizlib/util.py,sha256=6crNn_nI5Ytx5yu4IzcmI0iCfwCWbCN5NOnPcrYKB1c,156
|
|
17
|
+
wizlib-2.0.17.dist-info/METADATA,sha256=TZK-9iDEQKIDY0p3xNCgKA2ZGWAwp1hNuT1F5W2m8MQ,1781
|
|
18
|
+
wizlib-2.0.17.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
19
|
+
wizlib-2.0.17.dist-info/RECORD,,
|
|
File without changes
|