xulbux 1.8.2__py3-none-any.whl → 1.8.3__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 xulbux might be problematic. Click here for more details.
- xulbux/__init__.py +1 -1
- xulbux/cli/help.py +2 -2
- xulbux/console.py +180 -49
- xulbux/data.py +9 -6
- xulbux/format_codes.py +11 -5
- xulbux/json.py +1 -1
- xulbux/path.py +1 -1
- xulbux/system.py +36 -11
- {xulbux-1.8.2.dist-info → xulbux-1.8.3.dist-info}/METADATA +1 -1
- xulbux-1.8.3.dist-info/RECORD +20 -0
- xulbux-1.8.2.dist-info/RECORD +0 -20
- {xulbux-1.8.2.dist-info → xulbux-1.8.3.dist-info}/WHEEL +0 -0
- {xulbux-1.8.2.dist-info → xulbux-1.8.3.dist-info}/entry_points.txt +0 -0
- {xulbux-1.8.2.dist-info → xulbux-1.8.3.dist-info}/top_level.txt +0 -0
xulbux/__init__.py
CHANGED
xulbux/cli/help.py
CHANGED
|
@@ -20,8 +20,8 @@ def get_latest_version() -> Optional[str]:
|
|
|
20
20
|
|
|
21
21
|
def is_latest_version() -> Optional[bool]:
|
|
22
22
|
try:
|
|
23
|
-
latest
|
|
24
|
-
|
|
23
|
+
if (latest := get_latest_version()) in ("", None):
|
|
24
|
+
return None
|
|
25
25
|
latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
|
|
26
26
|
installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
|
|
27
27
|
return latest_v_parts <= installed_v_parts
|
xulbux/console.py
CHANGED
|
@@ -10,7 +10,7 @@ from .format_codes import FormatCodes, _COMPILED as _FC_COMPILED
|
|
|
10
10
|
from .string import String
|
|
11
11
|
from .color import Color, Rgba, Hexa
|
|
12
12
|
|
|
13
|
-
from typing import Generator, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, cast
|
|
13
|
+
from typing import Generator, TypedDict, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, overload, cast, Protocol
|
|
14
14
|
from prompt_toolkit.key_binding import KeyPressEvent, KeyBindings
|
|
15
15
|
from prompt_toolkit.validation import ValidationError, Validator
|
|
16
16
|
from prompt_toolkit.styles import Style
|
|
@@ -27,6 +27,10 @@ import io as _io
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
_COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
30
|
+
"hr": _re.compile(r"(?i)\{hr\}"),
|
|
31
|
+
"hr_no_nl": _re.compile(r"(?i)(?<!\n){hr}(?!\n)"),
|
|
32
|
+
"hr_r_nl": _re.compile(r"(?i)(?<!\n){hr}(?=\n)"),
|
|
33
|
+
"hr_l_nl": _re.compile(r"(?i)(?<=\n){hr}(?!\n)"),
|
|
30
34
|
"label": _re.compile(r"(?i)\{(?:label|l)\}"),
|
|
31
35
|
"bar": _re.compile(r"(?i)\{(?:bar|b)\}"),
|
|
32
36
|
"current": _re.compile(r"(?i)\{(?:current|c)\}"),
|
|
@@ -69,6 +73,11 @@ class _ConsoleUser:
|
|
|
69
73
|
return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
|
|
70
74
|
|
|
71
75
|
|
|
76
|
+
class _ArgConfigWithDefault(TypedDict):
|
|
77
|
+
flags: list[str] | tuple[str, ...]
|
|
78
|
+
default: Any
|
|
79
|
+
|
|
80
|
+
|
|
72
81
|
class ArgResult:
|
|
73
82
|
"""Represents the result of a parsed command-line argument and contains the following attributes:
|
|
74
83
|
- `exists` -⠀if the argument was found or not
|
|
@@ -146,8 +155,10 @@ class Console:
|
|
|
146
155
|
|
|
147
156
|
@staticmethod
|
|
148
157
|
def get_args(
|
|
149
|
-
find_args: Mapping[
|
|
150
|
-
|
|
158
|
+
find_args: Mapping[
|
|
159
|
+
str,
|
|
160
|
+
list[str] | tuple[str, ...] | _ArgConfigWithDefault | Literal["before", "after"],
|
|
161
|
+
],
|
|
151
162
|
allow_spaces: bool = False
|
|
152
163
|
) -> Args:
|
|
153
164
|
"""Will search for the specified arguments in the command line
|
|
@@ -241,8 +252,11 @@ class Console:
|
|
|
241
252
|
elif isinstance(config, dict):
|
|
242
253
|
if "flags" not in config:
|
|
243
254
|
raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
if "default" not in config:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'default' key. Use a simple list/tuple if no default value is needed."
|
|
258
|
+
)
|
|
259
|
+
flags, default_value = config["flags"], config["default"]
|
|
246
260
|
if not isinstance(flags, (list, tuple)):
|
|
247
261
|
raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
|
|
248
262
|
results[alias] = {"exists": False, "value": default_value}
|
|
@@ -269,14 +283,14 @@ class Console:
|
|
|
269
283
|
if first_flag_pos is None:
|
|
270
284
|
first_flag_pos = i
|
|
271
285
|
# CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
|
|
272
|
-
flag_has_value = (i + 1 < args_len and
|
|
286
|
+
flag_has_value = (i + 1 < args_len and args[i + 1] not in arg_lookup)
|
|
273
287
|
if flag_has_value:
|
|
274
288
|
if not allow_spaces:
|
|
275
289
|
last_flag_with_value_pos = i + 1
|
|
276
290
|
else:
|
|
277
291
|
# FIND THE END OF THE MULTI-WORD VALUE
|
|
278
292
|
j = i + 1
|
|
279
|
-
while j < args_len and
|
|
293
|
+
while j < args_len and args[j] not in arg_lookup:
|
|
280
294
|
j += 1
|
|
281
295
|
last_flag_with_value_pos = j - 1
|
|
282
296
|
|
|
@@ -286,7 +300,7 @@ class Console:
|
|
|
286
300
|
before_args = []
|
|
287
301
|
end_pos = first_flag_pos if first_flag_pos is not None else args_len
|
|
288
302
|
for i in range(end_pos):
|
|
289
|
-
if
|
|
303
|
+
if args[i] not in arg_lookup:
|
|
290
304
|
before_args.append(String.to_type(args[i]))
|
|
291
305
|
if before_args:
|
|
292
306
|
results[alias]["value"] = before_args
|
|
@@ -300,7 +314,7 @@ class Console:
|
|
|
300
314
|
if alias:
|
|
301
315
|
results[alias]["exists"] = True
|
|
302
316
|
value_found_after_flag = False
|
|
303
|
-
if i + 1 < args_len and
|
|
317
|
+
if i + 1 < args_len and args[i + 1] not in arg_lookup:
|
|
304
318
|
if not allow_spaces:
|
|
305
319
|
results[alias]["value"] = String.to_type(args[i + 1])
|
|
306
320
|
i += 1
|
|
@@ -308,7 +322,7 @@ class Console:
|
|
|
308
322
|
else:
|
|
309
323
|
value_parts = []
|
|
310
324
|
j = i + 1
|
|
311
|
-
while j < args_len and
|
|
325
|
+
while j < args_len and args[j] not in arg_lookup:
|
|
312
326
|
value_parts.append(args[j])
|
|
313
327
|
j += 1
|
|
314
328
|
if value_parts:
|
|
@@ -316,7 +330,7 @@ class Console:
|
|
|
316
330
|
i = j - 1
|
|
317
331
|
value_found_after_flag = True
|
|
318
332
|
if not value_found_after_flag:
|
|
319
|
-
results[alias]["value"] =
|
|
333
|
+
results[alias]["value"] = None
|
|
320
334
|
i += 1
|
|
321
335
|
|
|
322
336
|
# COLLECT "after" POSITIONAL ARGUMENTS
|
|
@@ -335,7 +349,7 @@ class Console:
|
|
|
335
349
|
start_pos = last_flag_pos + 1
|
|
336
350
|
|
|
337
351
|
for i in range(start_pos, args_len):
|
|
338
|
-
if
|
|
352
|
+
if args[i] not in arg_lookup:
|
|
339
353
|
after_args.append(String.to_type(args[i]))
|
|
340
354
|
|
|
341
355
|
if after_args:
|
|
@@ -346,9 +360,9 @@ class Console:
|
|
|
346
360
|
|
|
347
361
|
@staticmethod
|
|
348
362
|
def pause_exit(
|
|
363
|
+
prompt: object = "",
|
|
349
364
|
pause: bool = True,
|
|
350
365
|
exit: bool = False,
|
|
351
|
-
prompt: object = "",
|
|
352
366
|
exit_code: int = 0,
|
|
353
367
|
reset_ansi: bool = False,
|
|
354
368
|
) -> None:
|
|
@@ -473,13 +487,15 @@ class Console:
|
|
|
473
487
|
default_color: Optional[Rgba | Hexa] = None,
|
|
474
488
|
pause: bool = False,
|
|
475
489
|
exit: bool = False,
|
|
490
|
+
exit_code: int = 0,
|
|
491
|
+
reset_ansi: bool = True,
|
|
476
492
|
) -> None:
|
|
477
493
|
"""A preset for `log()`: `DEBUG` log message with the options to pause
|
|
478
494
|
at the message and exit the program after the message was printed.
|
|
479
495
|
If `active` is false, no debug message will be printed."""
|
|
480
496
|
if active:
|
|
481
497
|
Console.log("DEBUG", prompt, format_linebreaks, start, end, COLOR.YELLOW, default_color)
|
|
482
|
-
Console.pause_exit(pause, exit)
|
|
498
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
483
499
|
|
|
484
500
|
@staticmethod
|
|
485
501
|
def info(
|
|
@@ -490,11 +506,13 @@ class Console:
|
|
|
490
506
|
default_color: Optional[Rgba | Hexa] = None,
|
|
491
507
|
pause: bool = False,
|
|
492
508
|
exit: bool = False,
|
|
509
|
+
exit_code: int = 0,
|
|
510
|
+
reset_ansi: bool = True,
|
|
493
511
|
) -> None:
|
|
494
512
|
"""A preset for `log()`: `INFO` log message with the options to pause
|
|
495
513
|
at the message and exit the program after the message was printed."""
|
|
496
514
|
Console.log("INFO", prompt, format_linebreaks, start, end, COLOR.BLUE, default_color)
|
|
497
|
-
Console.pause_exit(pause, exit)
|
|
515
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
498
516
|
|
|
499
517
|
@staticmethod
|
|
500
518
|
def done(
|
|
@@ -505,11 +523,13 @@ class Console:
|
|
|
505
523
|
default_color: Optional[Rgba | Hexa] = None,
|
|
506
524
|
pause: bool = False,
|
|
507
525
|
exit: bool = False,
|
|
526
|
+
exit_code: int = 0,
|
|
527
|
+
reset_ansi: bool = True,
|
|
508
528
|
) -> None:
|
|
509
529
|
"""A preset for `log()`: `DONE` log message with the options to pause
|
|
510
530
|
at the message and exit the program after the message was printed."""
|
|
511
531
|
Console.log("DONE", prompt, format_linebreaks, start, end, COLOR.TEAL, default_color)
|
|
512
|
-
Console.pause_exit(pause, exit)
|
|
532
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
513
533
|
|
|
514
534
|
@staticmethod
|
|
515
535
|
def warn(
|
|
@@ -520,11 +540,13 @@ class Console:
|
|
|
520
540
|
default_color: Optional[Rgba | Hexa] = None,
|
|
521
541
|
pause: bool = False,
|
|
522
542
|
exit: bool = False,
|
|
543
|
+
exit_code: int = 1,
|
|
544
|
+
reset_ansi: bool = True,
|
|
523
545
|
) -> None:
|
|
524
546
|
"""A preset for `log()`: `WARN` log message with the options to pause
|
|
525
547
|
at the message and exit the program after the message was printed."""
|
|
526
548
|
Console.log("WARN", prompt, format_linebreaks, start, end, COLOR.ORANGE, default_color)
|
|
527
|
-
Console.pause_exit(pause, exit)
|
|
549
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
528
550
|
|
|
529
551
|
@staticmethod
|
|
530
552
|
def fail(
|
|
@@ -535,12 +557,13 @@ class Console:
|
|
|
535
557
|
default_color: Optional[Rgba | Hexa] = None,
|
|
536
558
|
pause: bool = False,
|
|
537
559
|
exit: bool = True,
|
|
560
|
+
exit_code: int = 1,
|
|
538
561
|
reset_ansi: bool = True,
|
|
539
562
|
) -> None:
|
|
540
563
|
"""A preset for `log()`: `FAIL` log message with the options to pause
|
|
541
564
|
at the message and exit the program after the message was printed."""
|
|
542
565
|
Console.log("FAIL", prompt, format_linebreaks, start, end, COLOR.RED, default_color)
|
|
543
|
-
Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
|
|
566
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
544
567
|
|
|
545
568
|
@staticmethod
|
|
546
569
|
def exit(
|
|
@@ -551,12 +574,13 @@ class Console:
|
|
|
551
574
|
default_color: Optional[Rgba | Hexa] = None,
|
|
552
575
|
pause: bool = False,
|
|
553
576
|
exit: bool = True,
|
|
577
|
+
exit_code: int = 0,
|
|
554
578
|
reset_ansi: bool = True,
|
|
555
579
|
) -> None:
|
|
556
580
|
"""A preset for `log()`: `EXIT` log message with the options to pause
|
|
557
581
|
at the message and exit the program after the message was printed."""
|
|
558
582
|
Console.log("EXIT", prompt, format_linebreaks, start, end, COLOR.MAGENTA, default_color)
|
|
559
|
-
Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
|
|
583
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
560
584
|
|
|
561
585
|
@staticmethod
|
|
562
586
|
def log_box_filled(
|
|
@@ -611,7 +635,7 @@ class Console:
|
|
|
611
635
|
w_padding: int = 1,
|
|
612
636
|
w_full: bool = False,
|
|
613
637
|
indent: int = 0,
|
|
614
|
-
_border_chars: Optional[tuple[str, str, str, str, str, str, str, str]] = None,
|
|
638
|
+
_border_chars: Optional[tuple[str, str, str, str, str, str, str, str, str, str, str]] = None,
|
|
615
639
|
) -> None:
|
|
616
640
|
"""Will print a bordered box, containing a formatted log message:
|
|
617
641
|
- `*values` -⠀the box content (each value is on a new line)
|
|
@@ -624,15 +648,17 @@ class Console:
|
|
|
624
648
|
- `w_full` -⠀whether to make the box be the full console width or not
|
|
625
649
|
- `indent` -⠀the indentation of the box (in chars)
|
|
626
650
|
- `_border_chars` -⠀define your own border characters set (overwrites `border_type`)\n
|
|
627
|
-
|
|
651
|
+
---------------------------------------------------------------------------------------------
|
|
652
|
+
You can insert horizontal rules to split the box content by using `{hr}` in the `*values`.\n
|
|
653
|
+
---------------------------------------------------------------------------------------------
|
|
628
654
|
The box content can be formatted with special formatting codes. For more detailed
|
|
629
655
|
information about formatting codes, see `format_codes` module documentation.\n
|
|
630
|
-
|
|
656
|
+
---------------------------------------------------------------------------------------------
|
|
631
657
|
The `border_type` can be one of the following:
|
|
632
|
-
- `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│')`
|
|
633
|
-
- `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│')`
|
|
634
|
-
- `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃')`
|
|
635
|
-
- `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║')`\n
|
|
658
|
+
- `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤')`
|
|
659
|
+
- `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤')`
|
|
660
|
+
- `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫')`
|
|
661
|
+
- `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣')`\n
|
|
636
662
|
The order of the characters is always:
|
|
637
663
|
1. top-left corner
|
|
638
664
|
2. top border
|
|
@@ -641,27 +667,31 @@ class Console:
|
|
|
641
667
|
5. bottom-right corner
|
|
642
668
|
6. bottom border
|
|
643
669
|
7. bottom-left corner
|
|
644
|
-
8. left border
|
|
670
|
+
8. left border
|
|
671
|
+
9. left horizontal rule connector
|
|
672
|
+
10. horizontal rule
|
|
673
|
+
11. right horizontal rule connector"""
|
|
645
674
|
borders = {
|
|
646
|
-
"standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│'),
|
|
647
|
-
"rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│'),
|
|
648
|
-
"strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃'),
|
|
649
|
-
"double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║'),
|
|
675
|
+
"standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤'),
|
|
676
|
+
"rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤'),
|
|
677
|
+
"strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫'),
|
|
678
|
+
"double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣'),
|
|
650
679
|
}
|
|
651
680
|
border_chars = borders.get(border_type, borders["standard"]) if _border_chars is None else _border_chars
|
|
652
|
-
lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color)
|
|
681
|
+
lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color, has_rules=True)
|
|
653
682
|
pad_w_full = (Console.w - (max_line_len + (2 * w_padding)) - (len(border_chars[1] * 2))) if w_full else 0
|
|
654
683
|
if border_style is not None and Color.is_valid(border_style):
|
|
655
684
|
border_style = Color.to_hexa(border_style)
|
|
656
685
|
spaces_l = " " * indent
|
|
657
686
|
border_l = f"[{border_style}]{border_chars[7]}[*]"
|
|
658
687
|
border_r = f"[{border_style}]{border_chars[3]}[_]"
|
|
688
|
+
border_t = f"{spaces_l}[{border_style}]{border_chars[0]}{border_chars[1] * (Console.w - (len(border_chars[1] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[2]}[_]"
|
|
689
|
+
border_b = f"{spaces_l}[{border_style}]{border_chars[6]}{border_chars[5] * (Console.w - (len(border_chars[5] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[4]}[_]"
|
|
690
|
+
h_rule = f"{spaces_l}[{border_style}]{border_chars[8]}{border_chars[9] * (Console.w - (len(border_chars[9] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[10]}[_]"
|
|
659
691
|
lines = [
|
|
660
|
-
f"{spaces_l}{border_l}{' ' * w_padding}{line}[_]" + " " *
|
|
692
|
+
h_rule if _COMPILED["hr"].match(line) else f"{spaces_l}{border_l}{' ' * w_padding}{line}[_]" + " " *
|
|
661
693
|
((w_padding + max_line_len - len(unfmt)) + pad_w_full) + border_r for line, unfmt in zip(lines, unfmt_lines)
|
|
662
694
|
]
|
|
663
|
-
border_t = f"{spaces_l}[{border_style}]{border_chars[0]}{border_chars[1] * (Console.w - (len(border_chars[1] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[2]}[_]"
|
|
664
|
-
border_b = f"{spaces_l}[{border_style}]{border_chars[6]}{border_chars[5] * (Console.w - (len(border_chars[1] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[4]}[_]"
|
|
665
695
|
FormatCodes.print(
|
|
666
696
|
f"{start}{border_t}[_]\n" + "\n".join(lines) + f"\n{border_b}[_]",
|
|
667
697
|
default_color=default_color,
|
|
@@ -673,9 +703,42 @@ class Console:
|
|
|
673
703
|
def __prepare_log_box(
|
|
674
704
|
values: tuple[object, ...],
|
|
675
705
|
default_color: Optional[Rgba | Hexa] = None,
|
|
706
|
+
has_rules: bool = False,
|
|
676
707
|
) -> tuple[list[str], list[tuple[str, tuple[tuple[int, str], ...]]], int]:
|
|
677
708
|
"""Prepares the log box content and returns it along with the max line length."""
|
|
678
|
-
|
|
709
|
+
if has_rules:
|
|
710
|
+
lines = []
|
|
711
|
+
for val in values:
|
|
712
|
+
val_str, result_parts, current_pos = str(val), [], 0
|
|
713
|
+
for match in _COMPILED["hr"].finditer(val_str):
|
|
714
|
+
start, end = match.span()
|
|
715
|
+
should_split_before = start > 0 and val_str[start - 1] != '\n'
|
|
716
|
+
should_split_after = end < len(val_str) and val_str[end] != '\n'
|
|
717
|
+
|
|
718
|
+
if should_split_before:
|
|
719
|
+
if start > current_pos:
|
|
720
|
+
result_parts.append(val_str[current_pos:start])
|
|
721
|
+
if should_split_after:
|
|
722
|
+
result_parts.append(match.group())
|
|
723
|
+
current_pos = end
|
|
724
|
+
else:
|
|
725
|
+
current_pos = start
|
|
726
|
+
else:
|
|
727
|
+
if should_split_after:
|
|
728
|
+
result_parts.append(val_str[current_pos:end])
|
|
729
|
+
current_pos = end
|
|
730
|
+
|
|
731
|
+
if current_pos < len(val_str):
|
|
732
|
+
result_parts.append(val_str[current_pos:])
|
|
733
|
+
|
|
734
|
+
if not result_parts:
|
|
735
|
+
result_parts.append(val_str)
|
|
736
|
+
|
|
737
|
+
for part in result_parts:
|
|
738
|
+
lines.extend(part.splitlines())
|
|
739
|
+
else:
|
|
740
|
+
lines = [line for val in values for line in str(val).splitlines()]
|
|
741
|
+
|
|
679
742
|
unfmt_lines = [FormatCodes.remove_formatting(line, default_color) for line in lines]
|
|
680
743
|
max_line_len = max(len(line) for line in unfmt_lines)
|
|
681
744
|
return lines, cast(list[tuple[str, tuple[tuple[int, str], ...]]], unfmt_lines), max_line_len
|
|
@@ -755,7 +818,7 @@ class Console:
|
|
|
755
818
|
mask_char: Optional[str] = None,
|
|
756
819
|
min_len: Optional[int] = None,
|
|
757
820
|
max_len: Optional[int] = None,
|
|
758
|
-
allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
|
|
821
|
+
allowed_chars: str = CHARS.ALL, # type: ignore[assignment]
|
|
759
822
|
allow_paste: bool = True,
|
|
760
823
|
validator: Optional[Callable[[str], Optional[str]]] = None,
|
|
761
824
|
default_val: Optional[T] = None,
|
|
@@ -825,7 +888,8 @@ class Console:
|
|
|
825
888
|
|
|
826
889
|
def process_insert_text(text: str) -> tuple[str, set[str]]:
|
|
827
890
|
removed_chars = set()
|
|
828
|
-
if not text:
|
|
891
|
+
if not text:
|
|
892
|
+
return "", removed_chars
|
|
829
893
|
processed_text = "".join(c for c in text if ord(c) >= 32)
|
|
830
894
|
if allowed_chars != CHARS.ALL:
|
|
831
895
|
filtered_text = ""
|
|
@@ -846,8 +910,8 @@ class Console:
|
|
|
846
910
|
def insert_text_event(event: KeyPressEvent) -> None:
|
|
847
911
|
nonlocal result_text, filtered_chars
|
|
848
912
|
try:
|
|
849
|
-
insert_text
|
|
850
|
-
|
|
913
|
+
if not (insert_text := event.data):
|
|
914
|
+
return
|
|
851
915
|
buffer = event.app.current_buffer
|
|
852
916
|
cursor_pos = buffer.cursor_position
|
|
853
917
|
insert_text, filtered_chars = process_insert_text(insert_text)
|
|
@@ -928,7 +992,8 @@ class Console:
|
|
|
928
992
|
FormatCodes.print(end, end="")
|
|
929
993
|
|
|
930
994
|
if result_text in ("", None):
|
|
931
|
-
if has_default:
|
|
995
|
+
if has_default:
|
|
996
|
+
return default_val
|
|
932
997
|
result_text = ""
|
|
933
998
|
|
|
934
999
|
if output_type == str:
|
|
@@ -937,10 +1002,30 @@ class Console:
|
|
|
937
1002
|
try:
|
|
938
1003
|
return output_type(result_text) # type: ignore[call-arg]
|
|
939
1004
|
except (ValueError, TypeError):
|
|
940
|
-
if has_default:
|
|
1005
|
+
if has_default:
|
|
1006
|
+
return default_val
|
|
941
1007
|
raise
|
|
942
1008
|
|
|
943
1009
|
|
|
1010
|
+
class _ProgressUpdater(Protocol):
|
|
1011
|
+
"""Protocol for progress update function with proper type hints."""
|
|
1012
|
+
|
|
1013
|
+
@overload
|
|
1014
|
+
def __call__(self, current: int) -> None:
|
|
1015
|
+
"""Update the current progress value."""
|
|
1016
|
+
...
|
|
1017
|
+
|
|
1018
|
+
@overload
|
|
1019
|
+
def __call__(self, current: int, label: str) -> None:
|
|
1020
|
+
"""Update both current progress value and label."""
|
|
1021
|
+
...
|
|
1022
|
+
|
|
1023
|
+
@overload
|
|
1024
|
+
def __call__(self, *, label: str) -> None:
|
|
1025
|
+
"""Update the progress label only (keyword-only)."""
|
|
1026
|
+
...
|
|
1027
|
+
|
|
1028
|
+
|
|
944
1029
|
class ProgressBar:
|
|
945
1030
|
"""A console progress bar with smooth transitions and customizable appearance.\n
|
|
946
1031
|
-------------------------------------------------------------------------------------------------
|
|
@@ -1067,23 +1152,69 @@ class ProgressBar:
|
|
|
1067
1152
|
self._stop_intercepting()
|
|
1068
1153
|
|
|
1069
1154
|
@contextmanager
|
|
1070
|
-
def progress_context(self, total: int, label: Optional[str] = None) -> Generator[
|
|
1155
|
+
def progress_context(self, total: int, label: Optional[str] = None) -> Generator[_ProgressUpdater, None, None]:
|
|
1071
1156
|
"""Context manager for automatic cleanup. Returns a function to update progress.\n
|
|
1072
|
-
|
|
1157
|
+
----------------------------------------------------------------------------------------------------
|
|
1073
1158
|
- `total` -⠀the total value representing 100% progress (must be greater than `0`)
|
|
1074
1159
|
- `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder
|
|
1075
|
-
|
|
1160
|
+
----------------------------------------------------------------------------------------------------
|
|
1161
|
+
The returned callable accepts keyword arguments. At least one of these parameters must be provided:
|
|
1162
|
+
- `current` -⠀update the current progress value
|
|
1163
|
+
- `label` -⠀update the progress label\n
|
|
1164
|
+
|
|
1076
1165
|
Example usage:
|
|
1077
1166
|
```python
|
|
1078
|
-
with ProgressBar().progress_context(500, "Loading") as update_progress:
|
|
1079
|
-
|
|
1167
|
+
with ProgressBar().progress_context(500, "Loading...") as update_progress:
|
|
1168
|
+
update_progress(0) # Show empty bar at start
|
|
1169
|
+
|
|
1170
|
+
for i in range(400):
|
|
1080
1171
|
# Do some work...
|
|
1081
1172
|
update_progress(i) # Update progress
|
|
1173
|
+
|
|
1174
|
+
update_progress(label="Finalizing...") # Update label
|
|
1175
|
+
|
|
1176
|
+
for i in range(400, 500):
|
|
1177
|
+
# Do some work...
|
|
1178
|
+
update_progress(i, f"Finalizing ({i})") # Update both
|
|
1082
1179
|
```"""
|
|
1180
|
+
current_progress = 0
|
|
1181
|
+
current_label = label
|
|
1182
|
+
|
|
1083
1183
|
try:
|
|
1084
1184
|
|
|
1085
|
-
def update_progress(
|
|
1086
|
-
|
|
1185
|
+
def update_progress(*args, **kwargs) -> None: # TYPE HINTS DEFINED IN '_ProgressUpdater' PROTOCOL
|
|
1186
|
+
"""Update the progress bar's current value and/or label."""
|
|
1187
|
+
nonlocal current_progress, current_label
|
|
1188
|
+
current = label = None
|
|
1189
|
+
|
|
1190
|
+
if len(args) > 2:
|
|
1191
|
+
raise TypeError(f"update_progress() takes at most 2 positional arguments ({len(args)} given)")
|
|
1192
|
+
elif len(args) >= 1:
|
|
1193
|
+
current = args[0]
|
|
1194
|
+
if len(args) >= 2:
|
|
1195
|
+
label = args[1]
|
|
1196
|
+
|
|
1197
|
+
if "current" in kwargs:
|
|
1198
|
+
if current is not None:
|
|
1199
|
+
raise TypeError("update_progress() got multiple values for argument 'current'")
|
|
1200
|
+
current = kwargs["current"]
|
|
1201
|
+
if "label" in kwargs:
|
|
1202
|
+
if label is not None:
|
|
1203
|
+
raise TypeError("update_progress() got multiple values for argument 'label'")
|
|
1204
|
+
label = kwargs["label"]
|
|
1205
|
+
|
|
1206
|
+
if unexpected := set(kwargs.keys()) - {"current", "label"}:
|
|
1207
|
+
raise TypeError(f"update_progress() got unexpected keyword argument(s): {', '.join(unexpected)}")
|
|
1208
|
+
|
|
1209
|
+
if current is None and label is None:
|
|
1210
|
+
raise TypeError("At least one of 'current' or 'label' must be provided")
|
|
1211
|
+
|
|
1212
|
+
if current is not None:
|
|
1213
|
+
current_progress = current
|
|
1214
|
+
if label is not None:
|
|
1215
|
+
current_label = label
|
|
1216
|
+
|
|
1217
|
+
self.show_progress(current_progress, total, current_label)
|
|
1087
1218
|
|
|
1088
1219
|
yield update_progress
|
|
1089
1220
|
except Exception:
|
xulbux/data.py
CHANGED
|
@@ -24,11 +24,14 @@ class Data:
|
|
|
24
24
|
except UnicodeDecodeError:
|
|
25
25
|
pass
|
|
26
26
|
return {key: _base64.b64encode(data).decode("utf-8"), "encoding": "base64"}
|
|
27
|
-
raise TypeError("Unsupported data type")
|
|
27
|
+
raise TypeError(f"Unsupported data type '{type(data)}'")
|
|
28
28
|
|
|
29
29
|
@staticmethod
|
|
30
30
|
def deserialize_bytes(obj: dict[str, str]) -> bytes | bytearray:
|
|
31
|
-
"""
|
|
31
|
+
"""Tries to converts a JSON-compatible bytes/bytearray format (dictionary) back to its original type.\n
|
|
32
|
+
--------------------------------------------------------------------------------------------------------
|
|
33
|
+
If the serialized object was created with `Data.serialize_bytes()`, it will work.
|
|
34
|
+
If it fails to decode the data, it will raise a `ValueError`."""
|
|
32
35
|
for key in ("bytes", "bytearray"):
|
|
33
36
|
if key in obj and "encoding" in obj:
|
|
34
37
|
if obj["encoding"] == "utf-8":
|
|
@@ -36,9 +39,9 @@ class Data:
|
|
|
36
39
|
elif obj["encoding"] == "base64":
|
|
37
40
|
data = _base64.b64decode(obj[key].encode("utf-8"))
|
|
38
41
|
else:
|
|
39
|
-
raise ValueError("Unknown encoding method")
|
|
42
|
+
raise ValueError(f"Unknown encoding method '{obj['encoding']}'")
|
|
40
43
|
return bytearray(data) if key == "bytearray" else data
|
|
41
|
-
raise ValueError("Invalid serialized data")
|
|
44
|
+
raise ValueError(f"Invalid serialized data: {obj}")
|
|
42
45
|
|
|
43
46
|
@staticmethod
|
|
44
47
|
def chars_count(data: DataStructure) -> int:
|
|
@@ -387,7 +390,7 @@ class Data:
|
|
|
387
390
|
|
|
388
391
|
valid_entries = [(path_id, new_val) for path_id, new_val in update_values.items()]
|
|
389
392
|
if not valid_entries:
|
|
390
|
-
raise ValueError(f"No valid update_values found in dictionary: {update_values}")
|
|
393
|
+
raise ValueError(f"No valid 'update_values' found in dictionary: {update_values}")
|
|
391
394
|
for path_id, new_val in valid_entries:
|
|
392
395
|
path = Data.__sep_path_id(path_id)
|
|
393
396
|
data = update_nested(data, path, new_val)
|
|
@@ -581,7 +584,7 @@ class Data:
|
|
|
581
584
|
@staticmethod
|
|
582
585
|
def __sep_path_id(path_id: str) -> list[int]:
|
|
583
586
|
if path_id.count(">") != 1:
|
|
584
|
-
raise ValueError(f"Invalid path ID
|
|
587
|
+
raise ValueError(f"Invalid path ID '{path_id}'")
|
|
585
588
|
id_part_len = int(path_id.split(">")[0])
|
|
586
589
|
path_ids_str = path_id.split(">")[1]
|
|
587
590
|
return [int(path_ids_str[i:i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
|
xulbux/format_codes.py
CHANGED
|
@@ -166,6 +166,7 @@ from typing import Optional, cast
|
|
|
166
166
|
import ctypes as _ctypes
|
|
167
167
|
import regex as _rx
|
|
168
168
|
import sys as _sys
|
|
169
|
+
import os as _os
|
|
169
170
|
import re as _re
|
|
170
171
|
|
|
171
172
|
|
|
@@ -419,11 +420,16 @@ class FormatCodes:
|
|
|
419
420
|
global _CONSOLE_ANSI_CONFIGURED
|
|
420
421
|
if not _CONSOLE_ANSI_CONFIGURED:
|
|
421
422
|
_sys.stdout.flush()
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
423
|
+
if _os.name == "nt":
|
|
424
|
+
try:
|
|
425
|
+
# ENABLE VT100 MODE ON WINDOWS TO BE ABLE TO USE ANSI CODES
|
|
426
|
+
kernel32 = _ctypes.windll.kernel32
|
|
427
|
+
h = kernel32.GetStdHandle(-11)
|
|
428
|
+
mode = _ctypes.c_ulong()
|
|
429
|
+
kernel32.GetConsoleMode(h, _ctypes.byref(mode))
|
|
430
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
427
433
|
_CONSOLE_ANSI_CONFIGURED = True
|
|
428
434
|
|
|
429
435
|
@staticmethod
|
xulbux/json.py
CHANGED
|
@@ -134,7 +134,7 @@ class Json:
|
|
|
134
134
|
current[idx] = [] if next_key.isdigit() else {}
|
|
135
135
|
current = current[idx]
|
|
136
136
|
else:
|
|
137
|
-
raise TypeError(f"Cannot navigate through {type(current).__name__}")
|
|
137
|
+
raise TypeError(f"Cannot navigate through '{type(current).__name__}'")
|
|
138
138
|
return data_obj
|
|
139
139
|
|
|
140
140
|
update = {}
|
xulbux/path.py
CHANGED
|
@@ -46,7 +46,7 @@ class Path:
|
|
|
46
46
|
raise_error: bool = False,
|
|
47
47
|
use_closest_match: bool = False,
|
|
48
48
|
) -> Optional[str]:
|
|
49
|
-
"""Tries to
|
|
49
|
+
"""Tries to resolve and extend a relative path to an absolute path.\n
|
|
50
50
|
--------------------------------------------------------------------------------
|
|
51
51
|
If the `rel_path` couldn't be located in predefined directories, it will be
|
|
52
52
|
searched in the `search_in` directory/s. If the `rel_path` is still not found,
|
xulbux/system.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from .format_codes import FormatCodes
|
|
2
|
+
from .console import Console
|
|
3
|
+
|
|
1
4
|
from typing import Optional
|
|
2
5
|
import subprocess as _subprocess
|
|
3
6
|
import platform as _platform
|
|
@@ -67,11 +70,25 @@ class System:
|
|
|
67
70
|
raise NotImplementedError(f"Restart not implemented for `{system}`")
|
|
68
71
|
|
|
69
72
|
@staticmethod
|
|
70
|
-
def check_libs(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
def check_libs(
|
|
74
|
+
lib_names: list[str],
|
|
75
|
+
install_missing: bool = False,
|
|
76
|
+
missing_libs_msgs: tuple[str, str] = (
|
|
77
|
+
"The following required libraries are missing:",
|
|
78
|
+
"Do you want to install them now?",
|
|
79
|
+
),
|
|
80
|
+
confirm_install: bool = True,
|
|
81
|
+
) -> Optional[list[str]]:
|
|
82
|
+
"""Checks if the given list of libraries are installed and optionally installs missing libraries.\n
|
|
83
|
+
------------------------------------------------------------------------------------------------------------
|
|
84
|
+
- `lib_names` -⠀a list of library names to check
|
|
85
|
+
- `install_missing` -⠀whether to directly missing libraries will be installed automatically using pip
|
|
86
|
+
- `missing_libs_msgs` -⠀two messages: the first one is displayed when missing libraries are found,
|
|
87
|
+
the second one is the confirmation message before installing missing libraries
|
|
88
|
+
- `confirm_install` -⠀whether the user will be asked for confirmation before installing missing libraries\n
|
|
89
|
+
------------------------------------------------------------------------------------------------------------
|
|
90
|
+
If some libraries are missing or they could not be installed, their names will be returned as a list.
|
|
91
|
+
If all libraries are installed (or were installed successfully), `None` will be returned."""
|
|
75
92
|
missing = []
|
|
76
93
|
for lib in lib_names:
|
|
77
94
|
try:
|
|
@@ -83,14 +100,22 @@ class System:
|
|
|
83
100
|
elif not install_missing:
|
|
84
101
|
return missing
|
|
85
102
|
if confirm_install:
|
|
86
|
-
print("
|
|
103
|
+
FormatCodes.print(f"[b]({missing_libs_msgs[0]})")
|
|
87
104
|
for lib in missing:
|
|
88
|
-
print(f"
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
FormatCodes.print(f" [dim](•) [i]{lib}[_i]")
|
|
106
|
+
print()
|
|
107
|
+
if not Console.confirm(missing_libs_msgs[1], end="\n"):
|
|
108
|
+
return missing
|
|
91
109
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
for lib in missing:
|
|
111
|
+
try:
|
|
112
|
+
_subprocess.check_call([_sys.executable, "-m", "pip", "install", lib])
|
|
113
|
+
missing.remove(lib)
|
|
114
|
+
except _subprocess.CalledProcessError:
|
|
115
|
+
pass
|
|
116
|
+
if len(missing) == 0:
|
|
117
|
+
return None
|
|
118
|
+
return missing
|
|
94
119
|
except _subprocess.CalledProcessError:
|
|
95
120
|
return missing
|
|
96
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xulbux
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.3
|
|
4
4
|
Summary: A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler.
|
|
5
5
|
Author-email: XulbuX <xulbux.real@gmail.com>
|
|
6
6
|
Maintainer-email: XulbuX <xulbux.real@gmail.com>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
xulbux/__init__.py,sha256=qGusxHHAnHPl-qVU9Rj1_WaOOlNCf_cRbyCUTF0wvBM,935
|
|
2
|
+
xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
|
|
3
|
+
xulbux/color.py,sha256=tTNH30Wf2QNmyopKz4LADdels_TDzDMCptCtm76KkNo,50375
|
|
4
|
+
xulbux/console.py,sha256=5Z7pHp6hNXSHpN5YVQnGWhw5USZR9AJ7xKjN98ssuLE,63288
|
|
5
|
+
xulbux/data.py,sha256=U9T3lLEzas3A5V8Z0jPKnTy8WPhBmmF2bit8J4VkeB8,31219
|
|
6
|
+
xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
|
|
7
|
+
xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
|
|
8
|
+
xulbux/format_codes.py,sha256=kT1vn8_aaOelzkhy2NYcCJPX_n_LHFRVaXbeDLA1Ir8,25020
|
|
9
|
+
xulbux/json.py,sha256=Lo8vraQ3c-BoKFNYbXCdcJndh8tSsP4CMoYoEKarrmc,7450
|
|
10
|
+
xulbux/path.py,sha256=_Xm4k5aOk2CGZ_ErMbnJPzonF03L8pq8nT8Wy44l8Qo,7674
|
|
11
|
+
xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
|
|
12
|
+
xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
|
|
13
|
+
xulbux/system.py,sha256=CAV-mpTGEJ-0VWVRnTqWqhIsqS1HrIlM3LZOcTz9YBU,7793
|
|
14
|
+
xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
|
|
15
|
+
xulbux/cli/help.py,sha256=iKSEm1zEmng-BpfEa9xYvnIsTnJbZ_sRvMk4NtavwGY,4403
|
|
16
|
+
xulbux-1.8.3.dist-info/METADATA,sha256=C67KKJKYblIESkTwM3Gyh9jlFtWQ4ToRUaY8wORdhAM,11042
|
|
17
|
+
xulbux-1.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
xulbux-1.8.3.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
|
|
19
|
+
xulbux-1.8.3.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
|
|
20
|
+
xulbux-1.8.3.dist-info/RECORD,,
|
xulbux-1.8.2.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
xulbux/__init__.py,sha256=NnqeQz_8_t4bkghp5qN3vQEm_Dff6AJBghJsTUVUaks,935
|
|
2
|
-
xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
|
|
3
|
-
xulbux/color.py,sha256=tTNH30Wf2QNmyopKz4LADdels_TDzDMCptCtm76KkNo,50375
|
|
4
|
-
xulbux/console.py,sha256=b9B1lQzAl5OEhmIlCbTyCyu_WRZwMVOuI-v1GfGtxzo,57308
|
|
5
|
-
xulbux/data.py,sha256=hB9JxrSC7_6kelJ_TwOaapNrlig-jiyNZS3YoiJX8F8,30884
|
|
6
|
-
xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
|
|
7
|
-
xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
|
|
8
|
-
xulbux/format_codes.py,sha256=UwJOH3JJebylYd-MclDvKOG72fhuku63JzZU_eUzMtA,24764
|
|
9
|
-
xulbux/json.py,sha256=Ei5FdCjfM0FrrAEBmuuTcexl7mUY4eirXr-QPct2OS0,7448
|
|
10
|
-
xulbux/path.py,sha256=lLAEVZrW0TAwCewlONFVQcQ_8tVn9LTJZVOZpeGvE5s,7673
|
|
11
|
-
xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
|
|
12
|
-
xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
|
|
13
|
-
xulbux/system.py,sha256=uCjQhqlfOoLkEBdjWn5cLCEnaI4Ac3o9cwRKev07eWI,6612
|
|
14
|
-
xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
|
|
15
|
-
xulbux/cli/help.py,sha256=wlDTjFhyWQucywMGDmrYIjYn1TV__TwMlYzeSCkQ__U,4403
|
|
16
|
-
xulbux-1.8.2.dist-info/METADATA,sha256=h7DU1OI_NtZNXoq6pFPP1dQhYnDlcbdEbaSsgYajXdc,11042
|
|
17
|
-
xulbux-1.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
-
xulbux-1.8.2.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
|
|
19
|
-
xulbux-1.8.2.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
|
|
20
|
-
xulbux-1.8.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|