chromatic-python 0.1.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.
chromatic/demo.py ADDED
@@ -0,0 +1,417 @@
1
+ import functools
2
+ import math
3
+ import os
4
+ import sys
5
+ import time
6
+ from os import PathLike
7
+ from pathlib import PurePath
8
+ from types import FunctionType
9
+ from typing import Callable
10
+
11
+
12
+ def escher_dragon_ascii():
13
+ """Displays the image-to-ASCII transform of 'Dragon' by M.C. Escher."""
14
+ from chromatic.ascii import ascii2img, img2ascii
15
+ from chromatic.data import UserFont, escher
16
+
17
+ input_img = escher()
18
+ font = UserFont.IBM_VGA_437_8X16
19
+ char_set = r" ._-~+<vX♦'^Vx>|πΦ0Ω#$║╫"
20
+
21
+ ascii_str = img2ascii(
22
+ input_img,
23
+ font,
24
+ factor=240,
25
+ char_set=char_set,
26
+ sort_glyphs=True)
27
+
28
+ ascii_img = ascii2img(
29
+ ascii_str,
30
+ font,
31
+ font_size=16,
32
+ fg='white',
33
+ bg='black')
34
+
35
+ ascii_img.show()
36
+
37
+
38
+ def escher_dragon_256color():
39
+ """Displays the image-to-ANSI transform of 'Dragon' by M.C. Escher in 8-bit color."""
40
+ from chromatic.ascii import ansi2img, img2ansi
41
+ from chromatic.data import UserFont, escher
42
+
43
+ input_img = escher()
44
+ font = UserFont.IBM_VGA_437_8X16
45
+
46
+ ansi_array = img2ansi(
47
+ input_img,
48
+ font,
49
+ factor=240,
50
+ ansi_type='8b',
51
+ equalize=True)
52
+
53
+ ansi_img = ansi2img(
54
+ ansi_array,
55
+ font,
56
+ font_size=16)
57
+
58
+ ansi_img.show()
59
+
60
+
61
+ def butterfly_16color():
62
+ """Displays image-to-ANSI transform of 'Spider Lily & Papilio xuthus' in 4-bit color.
63
+
64
+ Good ol' C-x M-c M-butterfly...
65
+ """
66
+ from chromatic.color import ansicolor4Bit
67
+ from chromatic.ascii import ansi2img, img2ansi
68
+ from chromatic.data import UserFont, butterfly
69
+
70
+ input_img = butterfly()
71
+
72
+ font = UserFont.IBM_VGA_437_8X16
73
+
74
+ char_set = r"'·,•-_→+<>ⁿ*%⌂7√Iï∞πbz£9yîU{}1αHSw♥æ?GX╕╒éà⌡MF╝╩ΘûǃQ½☻Ŷ┤▄╪║▒█"
75
+
76
+ ansi_array = img2ansi(
77
+ input_img,
78
+ font,
79
+ factor=200,
80
+ char_set=char_set,
81
+ ansi_type=ansicolor4Bit)
82
+
83
+ ansi_img = ansi2img(
84
+ ansi_array,
85
+ font,
86
+ font_size=16)
87
+
88
+ ansi_img.show()
89
+
90
+
91
+ def butterfly_truecolor():
92
+ """Displays the image-to-ANSI transform of 'Spider Lily & Papilio xuthus' in 24-bit color."""
93
+ from chromatic.ascii import ansi2img, img2ansi
94
+ from chromatic.data import UserFont, butterfly
95
+
96
+ input_img = butterfly()
97
+
98
+ font = UserFont.IBM_VGA_437_8X16
99
+
100
+ ansi_array = img2ansi(
101
+ input_img,
102
+ font,
103
+ factor=200,
104
+ ansi_type='24b',
105
+ equalize='white_point')
106
+
107
+ ansi_img = ansi2img(
108
+ ansi_array,
109
+ font,
110
+ font_size=16)
111
+
112
+ ansi_img.show()
113
+
114
+
115
+ def butterfly_randcolor():
116
+ from chromatic.ascii import ansi2img, img2ansi
117
+ from chromatic.color import randcolor, rgb2hsv, hsv2rgb, Color
118
+ from chromatic.data import UserFont, butterfly
119
+
120
+ input_img = butterfly()
121
+
122
+ font = UserFont.IBM_VGA_437_8X16
123
+
124
+ ansi_array = img2ansi(
125
+ input_img,
126
+ font,
127
+ factor=200,
128
+ ansi_type='8b',
129
+ equalize='white_point')
130
+
131
+ for row in range(len(ansi_array)):
132
+ for idx, cs in enumerate(ansi_array[row]):
133
+ if (fg := cs.fg) is not None:
134
+ _, _, v = rgb2hsv(fg.rgb)
135
+ h, s, _ = rgb2hsv(randcolor().rgb)
136
+ ansi_array[row][idx] = cs.recolor(fg=Color.from_rgb(hsv2rgb((h, s, v))))
137
+
138
+ ansi_img = ansi2img(
139
+ ansi_array,
140
+ font,
141
+ font_size=16)
142
+
143
+ ansi_img.show()
144
+
145
+
146
+ def goblin_virus_truecolor():
147
+ """`G-O-B-L-I-N VIRUS <https://imgur.com/n0Mng2P>`__"""
148
+ from chromatic.ascii import ansi2img, img2ansi
149
+ from chromatic.data import UserFont, goblin_virus
150
+
151
+ input_img = goblin_virus()
152
+
153
+ font = UserFont.IBM_VGA_437_8X16
154
+
155
+ char_set = r' .-|_⌐¬^:()═+<>v≥≤«*»x└┘π╛╘┴┐┌┬╧╚╙X╒╜╨#0╓╝╩╤╥│╔┤├╞╗╦┼╪║╟╠╫╣╬░▒▓█▄▌▐▀'
156
+
157
+ ansi_array = img2ansi(
158
+ input_img,
159
+ font,
160
+ factor=200,
161
+ char_set=char_set,
162
+ ansi_type='24b',
163
+ equalize=False)
164
+
165
+ ansi_img = ansi2img(
166
+ ansi_array,
167
+ font,
168
+ font_size=16)
169
+
170
+ ansi_img.show()
171
+
172
+
173
+ def named_colors():
174
+ from chromatic.color.palette import display_named_colors, ColorNamespace
175
+ from chromatic.color.colorconv import rgb2hsv, rgb2lab
176
+
177
+ print(f"{'.'.join([ColorNamespace.__module__, ColorNamespace.__name__])}:")
178
+ named = display_named_colors()
179
+ whites = [0]
180
+ for idx, n in enumerate(named):
181
+ hsv = rgb2hsv(n.fg.rgb)
182
+ if all(
183
+ map(
184
+ lambda i, x: math.isclose(hsv[i], x, abs_tol=.16),
185
+ (-1, 1), (1, 0))):
186
+ if idx - whites[-1] < 4:
187
+ whites.pop()
188
+ whites.append(idx)
189
+ whites.append(-1)
190
+ buffer = []
191
+ for start, stop in zip(whites, whites[1:]):
192
+ xs = sorted(
193
+ named[start + 1 if start else None:stop + 1 if ~stop else None],
194
+ key=lambda x: rgb2lab(x.fg.rgb))
195
+ buffer.append(xs)
196
+ for ln in buffer:
197
+ print(' | '.join(ln))
198
+
199
+
200
+ def color_table():
201
+ """Print foreground / background combinations in each ANSI format.
202
+
203
+ A handful of stylistic SGR parameters are displayed as well.
204
+ """
205
+ from chromatic.color import (
206
+ ColorStr,
207
+ SgrParameter,
208
+ ansicolor24Bit,
209
+ ansicolor4Bit,
210
+ ansicolor8Bit
211
+ )
212
+ from chromatic.color.palette import ColorNamespace
213
+
214
+ color_ns = ColorNamespace()
215
+ ansi_types = [ansicolor4Bit, ansicolor8Bit, ansicolor24Bit]
216
+ colors = [
217
+ color_ns.BLACK,
218
+ color_ns.WHITE,
219
+ color_ns.RED,
220
+ color_ns.ORANGE,
221
+ color_ns.YELLOW,
222
+ color_ns.GREEN,
223
+ color_ns.BLUE,
224
+ color_ns.INDIGO,
225
+ color_ns.PURPLE]
226
+ colors_dict = {v.name.title(): v for v in colors}
227
+ spacing = max(map(len, colors_dict)) + 1
228
+ fg_colors = [ColorStr(
229
+ f"{c.name.title(): ^{spacing}}",
230
+ color_spec=dict(fg=c),
231
+ ansi_type=ansicolor24Bit)
232
+ for c in colors]
233
+ bg_colors = [
234
+ ColorStr().recolor(bg=None)
235
+ ] + [c.recolor(fg=None, bg=c.fg) for c in fg_colors]
236
+ pad = spacing - 1
237
+ print(
238
+ '|'.join(
239
+ [f"{'4bit': ^{pad}}",
240
+ f"{'8bit': ^{pad}}",
241
+ f"{'24bit': >{pad}}"]))
242
+ for row in fg_colors:
243
+ for col in bg_colors:
244
+ for typ in ansi_types:
245
+ print(row.as_ansi_type(typ).recolor(bg=col.bg), end='\x1b[0m')
246
+ print()
247
+ print('\nstyles:')
248
+ print()
249
+ style_params = [
250
+ SgrParameter.BOLD,
251
+ SgrParameter.ITALICS,
252
+ SgrParameter.CROSSED_OUT,
253
+ SgrParameter.ENCIRCLED,
254
+ SgrParameter.SINGLE_UNDERLINE,
255
+ SgrParameter.DOUBLE_UNDERLINE,
256
+ SgrParameter.NEGATIVE]
257
+ for style in style_params:
258
+ print(
259
+ ColorStr('.'.join([SgrParameter.__qualname__, style.name]))
260
+ .update_sgr(style),
261
+ end='\x1b[0m' + (' ' * 4))
262
+ print()
263
+
264
+
265
+ def glyph_comparisons(__output_dir: str | PathLike[str] = None):
266
+ from skimage.metrics import mean_squared_error
267
+ from numpy import ndarray
268
+ from chromatic.ascii import cp437_printable
269
+ from chromatic import get_glyph_masks
270
+ from chromatic.data import UserFont
271
+ from random import choices as get_random
272
+
273
+ def _find_best_matches(glyph_masks1: dict[str, ndarray],
274
+ glyph_masks2: dict[str, ndarray]) -> dict[str, str]:
275
+ best_matches = {}
276
+ for char1, mask1 in glyph_masks1.items():
277
+ best_char = None
278
+ best_score = float('inf')
279
+ for char2, mask2 in glyph_masks2.items():
280
+ score = mean_squared_error(mask1, mask2)
281
+ if score < best_score:
282
+ best_score = score
283
+ best_char = char2
284
+ best_matches[char1] = best_char
285
+ return best_matches
286
+
287
+ if __output_dir and not os.path.isdir(__output_dir):
288
+ raise NotADirectoryError(
289
+ __output_dir)
290
+ user_fonts = [pair := (UserFont.IBM_VGA_437_8X16, UserFont.CONSOLAS), pair[::-1]]
291
+ trans_table = str.maketrans({']': None, '0': ' ', '[': ' '})
292
+ char_set = cp437_printable()
293
+ separator = '#' * 100
294
+ for font1, font2 in user_fonts:
295
+ glyph_masks_1 = get_glyph_masks(font1, char_set, dist_transform=True)
296
+ glyph_masks_2 = get_glyph_masks(font2, char_set, dist_transform=True)
297
+ best_matches_ = _find_best_matches(glyph_masks_1, glyph_masks_2)
298
+ txt = ''.join(
299
+ '->'.center(32, ' ').join(['{}'] * 2).format(
300
+ f"{font1.name}"
301
+ f"[{input_char!r}, {input_char.encode('unicode_escape').decode()!r}]",
302
+ f"{font2.name}"
303
+ f"[{matched_char!r}, {matched_char.encode('unicode_escape').decode()!r}]")
304
+ .center(100, ' ')
305
+ + '\n\n'
306
+ + '\n'.join(
307
+ ''.join
308
+ (z).translate(trans_table)
309
+ for z in zip(
310
+ f'{glyph_masks_1[input_char].astype(int)}\n'.splitlines(),
311
+ f'{glyph_masks_2[matched_char].astype(int)}\n'.splitlines()[1:]))
312
+ + separator.join(['\n'] * 2) for input_char, matched_char in best_matches_.items())
313
+ if __output_dir is not None:
314
+ fname = (PurePath(__output_dir) /
315
+ f"{'_to_'.join(font.name.lower() for font in (font1, font2))}.txt")
316
+ with open(fname, 'w', encoding='utf-8') as f:
317
+ f.write(txt)
318
+ else:
319
+ for glyph in get_random(txt.split(separator), k=len(char_set) // 2):
320
+ print(separator + glyph)
321
+
322
+
323
+ class _time_wrapper[** P, R]:
324
+
325
+ def __init__(
326
+ self,
327
+ func: Callable[P, R] | FunctionType | type = None
328
+ ):
329
+ self.func = func
330
+ if self.func is not None:
331
+ functools.update_wrapper(self, self.func)
332
+
333
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
334
+ if self.func is not None:
335
+ start = time.perf_counter()
336
+ result = self.func(*args, **kwargs)
337
+ stop = time.perf_counter()
338
+ print(f"Total execution time: {self._delta(start, stop)}")
339
+ return result
340
+ else:
341
+ self.func = args[0]
342
+ functools.update_wrapper(self, self.func)
343
+ return self
344
+
345
+ @staticmethod
346
+ def _delta(start: float, stop: float) -> str:
347
+ delta = stop - start
348
+ mag, fmt = min(
349
+ [(1, 's'), (1e-3, 'ms'), (1e-6, 'μs'), (1e-9, 'ns'), (1e-12, 'ps')],
350
+ key=lambda x: abs(math.log10(x[0]) - math.log10(delta)))
351
+ delta *= 1 / mag
352
+ return f"{delta:.3f} {fmt}"
353
+
354
+
355
+ def main():
356
+ demo_globals = dict(globals())
357
+ demo_globals.pop('main')
358
+ from inspect import getargs
359
+
360
+ global_func_enum = dict(
361
+ enumerate(
362
+ sorted(
363
+ k for k, v in demo_globals.items() if
364
+ isinstance(v, FunctionType) and not v.__name__.startswith('_'))))
365
+ safe_funcs = {-1: exit}
366
+ choices = [f'[{x[0]}]: {x[1].name}' for x in safe_funcs.items()]
367
+ names = []
368
+ for k, v in global_func_enum.items():
369
+ if not any(getargs(demo_globals[v].__code__)):
370
+ if safe_funcs.get(k - 1) is None:
371
+ k_val = list(safe_funcs).pop() + 1
372
+ else:
373
+ k_val = k
374
+ safe_funcs[k_val] = globals()[v]
375
+ choices.append(f"[{k_val}]: {v}")
376
+ names.append(v)
377
+
378
+ def _check_user_input(user_key: str):
379
+ if user_key.strip('-').isdigit():
380
+ if (k := int(user_key)) in safe_funcs:
381
+ return k
382
+ if (s := user_key.strip().replace(' ', '_').casefold()) in names:
383
+ return next(i for i, v in enumerate(names) if v == s)
384
+ return
385
+
386
+ selection = None
387
+ if len(sys.argv) > 1:
388
+ key = sys.argv[1]
389
+ if key.casefold() == '-h'.casefold():
390
+ xs = ['Run one of the following demo functions:',
391
+ *('{}:\n\t{}'.format(
392
+ n, '\n'.join(
393
+ filter(None, (globals()[n].__doc__ or '...').splitlines())).strip())
394
+ for n in names)]
395
+ print(f'\n{'\n\n'.join(xs)}')
396
+ exit()
397
+ selection = _check_user_input(key)
398
+ if selection is None:
399
+ print('\n'.join(choices))
400
+ while selection not in safe_funcs:
401
+ try:
402
+ selection = _check_user_input(input(f"Select a demo function:\t"))
403
+ except ValueError:
404
+ pass
405
+ except KeyboardInterrupt:
406
+ exit()
407
+ try:
408
+ if selection == -1:
409
+ exit()
410
+ print(f"Running {names[selection]!r}...\n")
411
+ except KeyError:
412
+ pass
413
+ _time_wrapper(safe_funcs[selection])()
414
+
415
+
416
+ if __name__ == '__main__':
417
+ main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 crypt0lith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: chromatic-python
3
+ Version: 0.1.0
4
+ Summary: ANSI art image processing and colored terminal text
5
+ Author: crypt0lith
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 crypt0lith
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Keywords: ansi,ascii,art,font,image,terminal,parser
28
+ Classifier: Programming Language :: Python :: 3.12
29
+ Requires-Python: >=3.12
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: fonttools~=4.51.0
33
+ Requires-Dist: networkx~=3.4.2
34
+ Requires-Dist: numpy~=2.1.1
35
+ Requires-Dist: opencv-python~=4.10.0.84
36
+ Requires-Dist: pillow~=10.4.0
37
+ Requires-Dist: scikit-image~=0.25.0rc1
38
+ Requires-Dist: scikit-learn~=1.5.2
39
+ Requires-Dist: scipy~=1.14.1
@@ -0,0 +1,26 @@
1
+ chromatic/__init__.py,sha256=0wwCYq16TA-FUJTMt9KxcaA09owmh5yrtfNXCB1fg3s,589
2
+ chromatic/_typing.py,sha256=6AYJx2dsRhLOtOwy3TTLSYLcBdCGaqKwR46Z41u9bbY,6085
3
+ chromatic/demo.py,sha256=Vi-TFpw1ARnZRbfaCca1bT4-FbtrjXejTOspycr8eU8,13151
4
+ chromatic/ascii/__init__.py,sha256=Ic3bqfqr0uAJd6vB4kToDookvZVr8Nrkyy3speiH5oM,164
5
+ chromatic/ascii/_array.py,sha256=1vR_LsGvQrAUmwIBBeFfmyEKciI7chdV_0kr2Q-gAdw,36197
6
+ chromatic/ascii/_curses.py,sha256=9zUeBrylcMXD-R0n5KBVqafAecd6lrQX1Yxjp-BrN98,3719
7
+ chromatic/ascii/_glyph_proc.py,sha256=rqtx6zLxwqqHa1YzuBhL-zhr3LptaHZBnL6ow9W3eI0,2840
8
+ chromatic/color/__init__.py,sha256=8dnpWqbmEciFgJrQpUC0gV9M5D1bKIF7d9Sl4mj0x7g,185
9
+ chromatic/color/colorconv.py,sha256=Ie6P_MUIXk6KcxyIJMw7HuGZWg9sBZVzgGYX4XRMDfA,8581
10
+ chromatic/color/core.py,sha256=K3JpWxsenRqd2IoNRnJRSxK6cpSpGV_M8wVqaQxJfmI,58488
11
+ chromatic/color/core.pyi,sha256=0bREo8eV5LNiA-a014w0C1yML7F3MG3_Vj_XcC6bYAs,11310
12
+ chromatic/color/palette.py,sha256=RTEj-YO20ZlVP2D9lhlge2v-ef5ENUyxHubAo0Ji5Og,23450
13
+ chromatic/color/palette.pyi,sha256=cj9Lmgw6MVCDY8Ew7wc9CvyWP6G4ZqRZnXJgOizBAO8,8780
14
+ chromatic/data/__init__.py,sha256=TCSacSbkvVExjufkNxarEYCSpKYnybm-gljfA_eumG4,6581
15
+ chromatic/data/__init__.pyi,sha256=AZGa46n1Hck3xHN91Mmm7Qibg--CYweKJMMFPHH6yY8,556
16
+ chromatic/data/fonts/IBM_VGA_437_8x16.ttf,sha256=qMdn-pJWJNKNmHnDoDqGIE94vOTezaCiBv0VK92QbJQ,50124
17
+ chromatic/data/fonts/consolas.ttf,sha256=xubOgRn91H7GpUSaCOLSrX9B6gMUOq4ZMGjtn6WOrrw,459180
18
+ chromatic/data/images/butterfly.jpg,sha256=rnSfMVotSgpTNRPQ9CzYpDp8S8lkCAJ5ZySRrvyDe7I,448399
19
+ chromatic/data/images/escher.png,sha256=FvAC-WOkUVIqHBxasfgCJbH1CN1IwLE-JyMiATOEKFc,132667
20
+ chromatic/data/images/goblin_virus.png,sha256=ygs19t4ZeZYsbiRFXjRyEz8OtgC9Zyl8owX0O-5n40o,8849
21
+ chromatic/data/images/hotdog.jpg,sha256=b0vnoQCarhq-gFu3df512qPTvauvF1Oa0oXVO9msEFc,60823
22
+ chromatic_python-0.1.0.dist-info/LICENSE,sha256=e7GjzmO7L12JDai3XRWHD8PgZvFmzNceEztB6d-9kuA,1086
23
+ chromatic_python-0.1.0.dist-info/METADATA,sha256=VhHfYHh_7GjWE67GYyCQOOuny1tvlgyup780MAC72e8,1869
24
+ chromatic_python-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
25
+ chromatic_python-0.1.0.dist-info/top_level.txt,sha256=wjzxcxfjO8I4u22BS8wL67Bq64c59kbZCqQ--Fc_Mqw,10
26
+ chromatic_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ chromatic