xulbux 1.8.1__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 +17 -2
- xulbux/cli/help.py +30 -2
- xulbux/color.py +12 -0
- xulbux/console.py +475 -45
- xulbux/data.py +9 -6
- xulbux/format_codes.py +15 -8
- xulbux/json.py +1 -1
- xulbux/path.py +1 -1
- xulbux/system.py +37 -12
- {xulbux-1.8.1.dist-info → xulbux-1.8.3.dist-info}/METADATA +2 -2
- xulbux-1.8.3.dist-info/RECORD +20 -0
- xulbux-1.8.1.dist-info/RECORD +0 -20
- {xulbux-1.8.1.dist-info → xulbux-1.8.3.dist-info}/WHEEL +0 -0
- {xulbux-1.8.1.dist-info → xulbux-1.8.3.dist-info}/entry_points.txt +0 -0
- {xulbux-1.8.1.dist-info → xulbux-1.8.3.dist-info}/top_level.txt +0 -0
xulbux/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "1.8.
|
|
1
|
+
__version__ = "1.8.3"
|
|
2
2
|
|
|
3
3
|
__author__ = "XulbuX"
|
|
4
4
|
__email__ = "xulbux.real@gmail.com"
|
|
@@ -7,11 +7,26 @@ __copyright__ = "Copyright (c) 2024 XulbuX"
|
|
|
7
7
|
__url__ = "https://github.com/XulbuX/PythonLibraryXulbuX"
|
|
8
8
|
__description__ = "A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler."
|
|
9
9
|
|
|
10
|
-
__all__ = [
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Code",
|
|
12
|
+
"Color",
|
|
13
|
+
"Console",
|
|
14
|
+
"Data",
|
|
15
|
+
"EnvPath",
|
|
16
|
+
"File",
|
|
17
|
+
"FormatCodes",
|
|
18
|
+
"Json",
|
|
19
|
+
"Path",
|
|
20
|
+
"ProgressBar",
|
|
21
|
+
"Regex",
|
|
22
|
+
"String",
|
|
23
|
+
"System",
|
|
24
|
+
]
|
|
11
25
|
|
|
12
26
|
from .code import Code
|
|
13
27
|
from .color import Color
|
|
14
28
|
from .console import Console
|
|
29
|
+
from .console import ProgressBar
|
|
15
30
|
from .data import Data
|
|
16
31
|
from .env_path import EnvPath
|
|
17
32
|
from .file import File
|
xulbux/cli/help.py
CHANGED
|
@@ -3,22 +3,50 @@ from ..base.consts import COLOR
|
|
|
3
3
|
from ..format_codes import FormatCodes
|
|
4
4
|
from ..console import Console
|
|
5
5
|
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import urllib.request as _request
|
|
9
|
+
import json as _json
|
|
6
10
|
|
|
11
|
+
|
|
12
|
+
def get_latest_version() -> Optional[str]:
|
|
13
|
+
with _request.urlopen(URL) as response:
|
|
14
|
+
if response.status == 200:
|
|
15
|
+
data = _json.load(response)
|
|
16
|
+
return data["info"]["version"]
|
|
17
|
+
else:
|
|
18
|
+
raise HTTPError(URL, response.status, "Failed to fetch latest version info", response.headers, None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_latest_version() -> Optional[bool]:
|
|
22
|
+
try:
|
|
23
|
+
if (latest := get_latest_version()) in ("", None):
|
|
24
|
+
return None
|
|
25
|
+
latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
|
|
26
|
+
installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
|
|
27
|
+
return latest_v_parts <= installed_v_parts
|
|
28
|
+
except Exception:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
URL = "https://pypi.org/pypi/xulbux/json"
|
|
33
|
+
IS_LATEST_VERSION = is_latest_version()
|
|
7
34
|
CLR = {
|
|
8
35
|
"class": COLOR.TANGERINE,
|
|
36
|
+
"code_border": COLOR.GRAY,
|
|
9
37
|
"const": COLOR.RED,
|
|
10
38
|
"func": COLOR.CYAN,
|
|
11
39
|
"import": COLOR.NEON_GREEN,
|
|
12
40
|
"lib": COLOR.ORANGE,
|
|
41
|
+
"notice": COLOR.YELLOW,
|
|
13
42
|
"punctuators": COLOR.DARK_GRAY,
|
|
14
|
-
"code_border": COLOR.GRAY,
|
|
15
43
|
}
|
|
16
44
|
HELP = FormatCodes.to_ansi(
|
|
17
45
|
rf""" [_|b|#7075FF] __ __
|
|
18
46
|
[b|#7075FF] _ __ __ __/ / / /_ __ ___ __
|
|
19
47
|
[b|#7075FF] | |/ // / / / / / __ \/ / / | |/ /
|
|
20
48
|
[b|#7075FF] > , </ /_/ / /_/ /_/ / /_/ /> , <
|
|
21
|
-
[b|#7075FF]/_/|_|\____/\__/\____/\____//_/|_| [*|BG:{COLOR.GRAY}|#000] v[b]{__version__} [*]
|
|
49
|
+
[b|#7075FF]/_/|_|\____/\__/\____/\____//_/|_| [*|BG:{COLOR.GRAY}|#000] v[b]{__version__} [*|dim|{CLR['notice']}]({'' if IS_LATEST_VERSION else ' (newer available)'})[*]
|
|
22
50
|
|
|
23
51
|
[i|{COLOR.CORAL}]A TON OF COOL FUNCTIONS, YOU NEED![*]
|
|
24
52
|
|
xulbux/color.py
CHANGED
|
@@ -92,9 +92,13 @@ class rgba:
|
|
|
92
92
|
|
|
93
93
|
def __init__(self, r: int, g: int, b: int, a: Optional[float] = None, _validate: bool = True):
|
|
94
94
|
self.r: int
|
|
95
|
+
"""The red channel (`0`–`255`)"""
|
|
95
96
|
self.g: int
|
|
97
|
+
"""The green channel (`0`–`255`)"""
|
|
96
98
|
self.b: int
|
|
99
|
+
"""The blue channel (`0`–`255`)"""
|
|
97
100
|
self.a: Optional[float]
|
|
101
|
+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
|
|
98
102
|
if not _validate:
|
|
99
103
|
self.r, self.g, self.b, self.a = r, g, b, a
|
|
100
104
|
return
|
|
@@ -291,9 +295,13 @@ class hsla:
|
|
|
291
295
|
|
|
292
296
|
def __init__(self, h: int, s: int, l: int, a: Optional[float] = None, _validate: bool = True):
|
|
293
297
|
self.h: int
|
|
298
|
+
"""The hue channel (`0`–`360`)"""
|
|
294
299
|
self.s: int
|
|
300
|
+
"""The saturation channel (`0`–`100`)"""
|
|
295
301
|
self.l: int
|
|
302
|
+
"""The lightness channel (`0`–`100`)"""
|
|
296
303
|
self.a: Optional[float]
|
|
304
|
+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
|
|
297
305
|
if not _validate:
|
|
298
306
|
self.h, self.s, self.l, self.a = h, s, l, a
|
|
299
307
|
return
|
|
@@ -496,9 +504,13 @@ class hexa:
|
|
|
496
504
|
_a: Optional[float] = None,
|
|
497
505
|
):
|
|
498
506
|
self.r: int
|
|
507
|
+
"""The red channel (`0`–`255`)"""
|
|
499
508
|
self.g: int
|
|
509
|
+
"""The green channel (`0`–`255`)"""
|
|
500
510
|
self.b: int
|
|
511
|
+
"""The blue channel (`0`–`255`)"""
|
|
501
512
|
self.a: Optional[float]
|
|
513
|
+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
|
|
502
514
|
if all(x is not None for x in (_r, _g, _b)):
|
|
503
515
|
self.r, self.g, self.b, self.a = cast(int, _r), cast(int, _g), cast(int, _b), _a
|
|
504
516
|
return
|
xulbux/console.py
CHANGED
|
@@ -5,15 +5,16 @@ You can also use special formatting codes directly inside the log message to cha
|
|
|
5
5
|
For more detailed information about formatting codes, see the the `format_codes` module documentation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .base.consts import COLOR, CHARS
|
|
9
|
-
from .format_codes import FormatCodes, _COMPILED
|
|
8
|
+
from .base.consts import COLOR, CHARS, ANSI
|
|
9
|
+
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 Callable, Optional, Literal, Mapping, 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
|
|
17
|
+
from contextlib import contextmanager
|
|
17
18
|
from prompt_toolkit.keys import Keys
|
|
18
19
|
import prompt_toolkit as _pt
|
|
19
20
|
import keyboard as _keyboard
|
|
@@ -21,6 +22,21 @@ import getpass as _getpass
|
|
|
21
22
|
import shutil as _shutil
|
|
22
23
|
import sys as _sys
|
|
23
24
|
import os as _os
|
|
25
|
+
import re as _re
|
|
26
|
+
import io as _io
|
|
27
|
+
|
|
28
|
+
|
|
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)"),
|
|
34
|
+
"label": _re.compile(r"(?i)\{(?:label|l)\}"),
|
|
35
|
+
"bar": _re.compile(r"(?i)\{(?:bar|b)\}"),
|
|
36
|
+
"current": _re.compile(r"(?i)\{(?:current|c)\}"),
|
|
37
|
+
"total": _re.compile(r"(?i)\{(?:total|t)\}"),
|
|
38
|
+
"percentage": _re.compile(r"(?i)\{(?:percentage|percent|p)\}"),
|
|
39
|
+
}
|
|
24
40
|
|
|
25
41
|
|
|
26
42
|
class _ConsoleWidth:
|
|
@@ -57,6 +73,11 @@ class _ConsoleUser:
|
|
|
57
73
|
return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
|
|
58
74
|
|
|
59
75
|
|
|
76
|
+
class _ArgConfigWithDefault(TypedDict):
|
|
77
|
+
flags: list[str] | tuple[str, ...]
|
|
78
|
+
default: Any
|
|
79
|
+
|
|
80
|
+
|
|
60
81
|
class ArgResult:
|
|
61
82
|
"""Represents the result of a parsed command-line argument and contains the following attributes:
|
|
62
83
|
- `exists` -⠀if the argument was found or not
|
|
@@ -66,7 +87,9 @@ class ArgResult:
|
|
|
66
87
|
|
|
67
88
|
def __init__(self, exists: bool, value: Any | list[Any]):
|
|
68
89
|
self.exists: bool = exists
|
|
90
|
+
"""Whether the argument was found or not."""
|
|
69
91
|
self.value: Any = value
|
|
92
|
+
"""The value given with the found argument."""
|
|
70
93
|
|
|
71
94
|
def __bool__(self):
|
|
72
95
|
return self.exists
|
|
@@ -132,8 +155,10 @@ class Console:
|
|
|
132
155
|
|
|
133
156
|
@staticmethod
|
|
134
157
|
def get_args(
|
|
135
|
-
find_args: Mapping[
|
|
136
|
-
|
|
158
|
+
find_args: Mapping[
|
|
159
|
+
str,
|
|
160
|
+
list[str] | tuple[str, ...] | _ArgConfigWithDefault | Literal["before", "after"],
|
|
161
|
+
],
|
|
137
162
|
allow_spaces: bool = False
|
|
138
163
|
) -> Args:
|
|
139
164
|
"""Will search for the specified arguments in the command line
|
|
@@ -227,8 +252,11 @@ class Console:
|
|
|
227
252
|
elif isinstance(config, dict):
|
|
228
253
|
if "flags" not in config:
|
|
229
254
|
raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
|
|
230
|
-
|
|
231
|
-
|
|
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"]
|
|
232
260
|
if not isinstance(flags, (list, tuple)):
|
|
233
261
|
raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
|
|
234
262
|
results[alias] = {"exists": False, "value": default_value}
|
|
@@ -255,14 +283,14 @@ class Console:
|
|
|
255
283
|
if first_flag_pos is None:
|
|
256
284
|
first_flag_pos = i
|
|
257
285
|
# CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
|
|
258
|
-
flag_has_value = (i + 1 < args_len and
|
|
286
|
+
flag_has_value = (i + 1 < args_len and args[i + 1] not in arg_lookup)
|
|
259
287
|
if flag_has_value:
|
|
260
288
|
if not allow_spaces:
|
|
261
289
|
last_flag_with_value_pos = i + 1
|
|
262
290
|
else:
|
|
263
291
|
# FIND THE END OF THE MULTI-WORD VALUE
|
|
264
292
|
j = i + 1
|
|
265
|
-
while j < args_len and
|
|
293
|
+
while j < args_len and args[j] not in arg_lookup:
|
|
266
294
|
j += 1
|
|
267
295
|
last_flag_with_value_pos = j - 1
|
|
268
296
|
|
|
@@ -272,7 +300,7 @@ class Console:
|
|
|
272
300
|
before_args = []
|
|
273
301
|
end_pos = first_flag_pos if first_flag_pos is not None else args_len
|
|
274
302
|
for i in range(end_pos):
|
|
275
|
-
if
|
|
303
|
+
if args[i] not in arg_lookup:
|
|
276
304
|
before_args.append(String.to_type(args[i]))
|
|
277
305
|
if before_args:
|
|
278
306
|
results[alias]["value"] = before_args
|
|
@@ -286,7 +314,7 @@ class Console:
|
|
|
286
314
|
if alias:
|
|
287
315
|
results[alias]["exists"] = True
|
|
288
316
|
value_found_after_flag = False
|
|
289
|
-
if i + 1 < args_len and
|
|
317
|
+
if i + 1 < args_len and args[i + 1] not in arg_lookup:
|
|
290
318
|
if not allow_spaces:
|
|
291
319
|
results[alias]["value"] = String.to_type(args[i + 1])
|
|
292
320
|
i += 1
|
|
@@ -294,7 +322,7 @@ class Console:
|
|
|
294
322
|
else:
|
|
295
323
|
value_parts = []
|
|
296
324
|
j = i + 1
|
|
297
|
-
while j < args_len and
|
|
325
|
+
while j < args_len and args[j] not in arg_lookup:
|
|
298
326
|
value_parts.append(args[j])
|
|
299
327
|
j += 1
|
|
300
328
|
if value_parts:
|
|
@@ -302,7 +330,7 @@ class Console:
|
|
|
302
330
|
i = j - 1
|
|
303
331
|
value_found_after_flag = True
|
|
304
332
|
if not value_found_after_flag:
|
|
305
|
-
results[alias]["value"] =
|
|
333
|
+
results[alias]["value"] = None
|
|
306
334
|
i += 1
|
|
307
335
|
|
|
308
336
|
# COLLECT "after" POSITIONAL ARGUMENTS
|
|
@@ -321,7 +349,7 @@ class Console:
|
|
|
321
349
|
start_pos = last_flag_pos + 1
|
|
322
350
|
|
|
323
351
|
for i in range(start_pos, args_len):
|
|
324
|
-
if
|
|
352
|
+
if args[i] not in arg_lookup:
|
|
325
353
|
after_args.append(String.to_type(args[i]))
|
|
326
354
|
|
|
327
355
|
if after_args:
|
|
@@ -332,9 +360,9 @@ class Console:
|
|
|
332
360
|
|
|
333
361
|
@staticmethod
|
|
334
362
|
def pause_exit(
|
|
363
|
+
prompt: object = "",
|
|
335
364
|
pause: bool = True,
|
|
336
365
|
exit: bool = False,
|
|
337
|
-
prompt: object = "",
|
|
338
366
|
exit_code: int = 0,
|
|
339
367
|
reset_ansi: bool = False,
|
|
340
368
|
) -> None:
|
|
@@ -459,13 +487,15 @@ class Console:
|
|
|
459
487
|
default_color: Optional[Rgba | Hexa] = None,
|
|
460
488
|
pause: bool = False,
|
|
461
489
|
exit: bool = False,
|
|
490
|
+
exit_code: int = 0,
|
|
491
|
+
reset_ansi: bool = True,
|
|
462
492
|
) -> None:
|
|
463
493
|
"""A preset for `log()`: `DEBUG` log message with the options to pause
|
|
464
494
|
at the message and exit the program after the message was printed.
|
|
465
495
|
If `active` is false, no debug message will be printed."""
|
|
466
496
|
if active:
|
|
467
497
|
Console.log("DEBUG", prompt, format_linebreaks, start, end, COLOR.YELLOW, default_color)
|
|
468
|
-
Console.pause_exit(pause, exit)
|
|
498
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
469
499
|
|
|
470
500
|
@staticmethod
|
|
471
501
|
def info(
|
|
@@ -476,11 +506,13 @@ class Console:
|
|
|
476
506
|
default_color: Optional[Rgba | Hexa] = None,
|
|
477
507
|
pause: bool = False,
|
|
478
508
|
exit: bool = False,
|
|
509
|
+
exit_code: int = 0,
|
|
510
|
+
reset_ansi: bool = True,
|
|
479
511
|
) -> None:
|
|
480
512
|
"""A preset for `log()`: `INFO` log message with the options to pause
|
|
481
513
|
at the message and exit the program after the message was printed."""
|
|
482
514
|
Console.log("INFO", prompt, format_linebreaks, start, end, COLOR.BLUE, default_color)
|
|
483
|
-
Console.pause_exit(pause, exit)
|
|
515
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
484
516
|
|
|
485
517
|
@staticmethod
|
|
486
518
|
def done(
|
|
@@ -491,11 +523,13 @@ class Console:
|
|
|
491
523
|
default_color: Optional[Rgba | Hexa] = None,
|
|
492
524
|
pause: bool = False,
|
|
493
525
|
exit: bool = False,
|
|
526
|
+
exit_code: int = 0,
|
|
527
|
+
reset_ansi: bool = True,
|
|
494
528
|
) -> None:
|
|
495
529
|
"""A preset for `log()`: `DONE` log message with the options to pause
|
|
496
530
|
at the message and exit the program after the message was printed."""
|
|
497
531
|
Console.log("DONE", prompt, format_linebreaks, start, end, COLOR.TEAL, default_color)
|
|
498
|
-
Console.pause_exit(pause, exit)
|
|
532
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
499
533
|
|
|
500
534
|
@staticmethod
|
|
501
535
|
def warn(
|
|
@@ -506,11 +540,13 @@ class Console:
|
|
|
506
540
|
default_color: Optional[Rgba | Hexa] = None,
|
|
507
541
|
pause: bool = False,
|
|
508
542
|
exit: bool = False,
|
|
543
|
+
exit_code: int = 1,
|
|
544
|
+
reset_ansi: bool = True,
|
|
509
545
|
) -> None:
|
|
510
546
|
"""A preset for `log()`: `WARN` log message with the options to pause
|
|
511
547
|
at the message and exit the program after the message was printed."""
|
|
512
548
|
Console.log("WARN", prompt, format_linebreaks, start, end, COLOR.ORANGE, default_color)
|
|
513
|
-
Console.pause_exit(pause, exit)
|
|
549
|
+
Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
|
|
514
550
|
|
|
515
551
|
@staticmethod
|
|
516
552
|
def fail(
|
|
@@ -521,12 +557,13 @@ class Console:
|
|
|
521
557
|
default_color: Optional[Rgba | Hexa] = None,
|
|
522
558
|
pause: bool = False,
|
|
523
559
|
exit: bool = True,
|
|
560
|
+
exit_code: int = 1,
|
|
524
561
|
reset_ansi: bool = True,
|
|
525
562
|
) -> None:
|
|
526
563
|
"""A preset for `log()`: `FAIL` log message with the options to pause
|
|
527
564
|
at the message and exit the program after the message was printed."""
|
|
528
565
|
Console.log("FAIL", prompt, format_linebreaks, start, end, COLOR.RED, default_color)
|
|
529
|
-
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)
|
|
530
567
|
|
|
531
568
|
@staticmethod
|
|
532
569
|
def exit(
|
|
@@ -537,12 +574,13 @@ class Console:
|
|
|
537
574
|
default_color: Optional[Rgba | Hexa] = None,
|
|
538
575
|
pause: bool = False,
|
|
539
576
|
exit: bool = True,
|
|
577
|
+
exit_code: int = 0,
|
|
540
578
|
reset_ansi: bool = True,
|
|
541
579
|
) -> None:
|
|
542
580
|
"""A preset for `log()`: `EXIT` log message with the options to pause
|
|
543
581
|
at the message and exit the program after the message was printed."""
|
|
544
582
|
Console.log("EXIT", prompt, format_linebreaks, start, end, COLOR.MAGENTA, default_color)
|
|
545
|
-
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)
|
|
546
584
|
|
|
547
585
|
@staticmethod
|
|
548
586
|
def log_box_filled(
|
|
@@ -574,7 +612,7 @@ class Console:
|
|
|
574
612
|
spaces_l = " " * indent
|
|
575
613
|
lines = [
|
|
576
614
|
f"{spaces_l}[bg:{box_bg_color}]{' ' * w_padding}"
|
|
577
|
-
+
|
|
615
|
+
+ _FC_COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", line) +
|
|
578
616
|
(" " * ((w_padding + max_line_len - len(unfmt)) + pad_w_full)) + "[*]" for line, unfmt in zip(lines, unfmt_lines)
|
|
579
617
|
]
|
|
580
618
|
pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
|
|
@@ -597,7 +635,7 @@ class Console:
|
|
|
597
635
|
w_padding: int = 1,
|
|
598
636
|
w_full: bool = False,
|
|
599
637
|
indent: int = 0,
|
|
600
|
-
_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,
|
|
601
639
|
) -> None:
|
|
602
640
|
"""Will print a bordered box, containing a formatted log message:
|
|
603
641
|
- `*values` -⠀the box content (each value is on a new line)
|
|
@@ -610,15 +648,17 @@ class Console:
|
|
|
610
648
|
- `w_full` -⠀whether to make the box be the full console width or not
|
|
611
649
|
- `indent` -⠀the indentation of the box (in chars)
|
|
612
650
|
- `_border_chars` -⠀define your own border characters set (overwrites `border_type`)\n
|
|
613
|
-
|
|
651
|
+
---------------------------------------------------------------------------------------------
|
|
652
|
+
You can insert horizontal rules to split the box content by using `{hr}` in the `*values`.\n
|
|
653
|
+
---------------------------------------------------------------------------------------------
|
|
614
654
|
The box content can be formatted with special formatting codes. For more detailed
|
|
615
655
|
information about formatting codes, see `format_codes` module documentation.\n
|
|
616
|
-
|
|
656
|
+
---------------------------------------------------------------------------------------------
|
|
617
657
|
The `border_type` can be one of the following:
|
|
618
|
-
- `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│')`
|
|
619
|
-
- `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│')`
|
|
620
|
-
- `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃')`
|
|
621
|
-
- `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║')`\n
|
|
658
|
+
- `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤')`
|
|
659
|
+
- `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤')`
|
|
660
|
+
- `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫')`
|
|
661
|
+
- `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣')`\n
|
|
622
662
|
The order of the characters is always:
|
|
623
663
|
1. top-left corner
|
|
624
664
|
2. top border
|
|
@@ -627,27 +667,31 @@ class Console:
|
|
|
627
667
|
5. bottom-right corner
|
|
628
668
|
6. bottom border
|
|
629
669
|
7. bottom-left corner
|
|
630
|
-
8. left border
|
|
670
|
+
8. left border
|
|
671
|
+
9. left horizontal rule connector
|
|
672
|
+
10. horizontal rule
|
|
673
|
+
11. right horizontal rule connector"""
|
|
631
674
|
borders = {
|
|
632
|
-
"standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│'),
|
|
633
|
-
"rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│'),
|
|
634
|
-
"strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃'),
|
|
635
|
-
"double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║'),
|
|
675
|
+
"standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤'),
|
|
676
|
+
"rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤'),
|
|
677
|
+
"strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫'),
|
|
678
|
+
"double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣'),
|
|
636
679
|
}
|
|
637
680
|
border_chars = borders.get(border_type, borders["standard"]) if _border_chars is None else _border_chars
|
|
638
|
-
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)
|
|
639
682
|
pad_w_full = (Console.w - (max_line_len + (2 * w_padding)) - (len(border_chars[1] * 2))) if w_full else 0
|
|
640
683
|
if border_style is not None and Color.is_valid(border_style):
|
|
641
684
|
border_style = Color.to_hexa(border_style)
|
|
642
685
|
spaces_l = " " * indent
|
|
643
686
|
border_l = f"[{border_style}]{border_chars[7]}[*]"
|
|
644
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]}[_]"
|
|
645
691
|
lines = [
|
|
646
|
-
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}[_]" + " " *
|
|
647
693
|
((w_padding + max_line_len - len(unfmt)) + pad_w_full) + border_r for line, unfmt in zip(lines, unfmt_lines)
|
|
648
694
|
]
|
|
649
|
-
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]}[_]"
|
|
650
|
-
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]}[_]"
|
|
651
695
|
FormatCodes.print(
|
|
652
696
|
f"{start}{border_t}[_]\n" + "\n".join(lines) + f"\n{border_b}[_]",
|
|
653
697
|
default_color=default_color,
|
|
@@ -659,9 +703,42 @@ class Console:
|
|
|
659
703
|
def __prepare_log_box(
|
|
660
704
|
values: tuple[object, ...],
|
|
661
705
|
default_color: Optional[Rgba | Hexa] = None,
|
|
706
|
+
has_rules: bool = False,
|
|
662
707
|
) -> tuple[list[str], list[tuple[str, tuple[tuple[int, str], ...]]], int]:
|
|
663
708
|
"""Prepares the log box content and returns it along with the max line length."""
|
|
664
|
-
|
|
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
|
+
|
|
665
742
|
unfmt_lines = [FormatCodes.remove_formatting(line, default_color) for line in lines]
|
|
666
743
|
max_line_len = max(len(line) for line in unfmt_lines)
|
|
667
744
|
return lines, cast(list[tuple[str, tuple[tuple[int, str], ...]]], unfmt_lines), max_line_len
|
|
@@ -729,6 +806,8 @@ class Console:
|
|
|
729
806
|
FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
|
|
730
807
|
return input_string
|
|
731
808
|
|
|
809
|
+
T = TypeVar("T")
|
|
810
|
+
|
|
732
811
|
@staticmethod
|
|
733
812
|
def input(
|
|
734
813
|
prompt: object = "",
|
|
@@ -739,10 +818,12 @@ class Console:
|
|
|
739
818
|
mask_char: Optional[str] = None,
|
|
740
819
|
min_len: Optional[int] = None,
|
|
741
820
|
max_len: Optional[int] = None,
|
|
742
|
-
allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
|
|
821
|
+
allowed_chars: str = CHARS.ALL, # type: ignore[assignment]
|
|
743
822
|
allow_paste: bool = True,
|
|
744
823
|
validator: Optional[Callable[[str], Optional[str]]] = None,
|
|
745
|
-
|
|
824
|
+
default_val: Optional[T] = None,
|
|
825
|
+
output_type: type[T] = str, # type: ignore[assignment]
|
|
826
|
+
) -> T:
|
|
746
827
|
"""Acts like a standard Python `input()` a bunch of cool extra features.\n
|
|
747
828
|
------------------------------------------------------------------------------------
|
|
748
829
|
- `prompt` -⠀the input prompt
|
|
@@ -758,12 +839,15 @@ class Console:
|
|
|
758
839
|
- `allow_paste` -⠀whether to allow pasting text into the input or not
|
|
759
840
|
- `validator` -⠀a function that takes the input string and returns a string error
|
|
760
841
|
message if invalid, or nothing if valid
|
|
842
|
+
- `default_val` -⠀the default value to return if the input is empty
|
|
843
|
+
- `output_type` -⠀the type (class) to convert the input to before returning it\n
|
|
761
844
|
------------------------------------------------------------------------------------
|
|
762
845
|
The input prompt can be formatted with special formatting codes. For more detailed
|
|
763
846
|
information about formatting codes, see the `format_codes` module documentation."""
|
|
764
847
|
result_text = ""
|
|
765
848
|
tried_pasting = False
|
|
766
849
|
filtered_chars = set()
|
|
850
|
+
has_default = default_val is not None
|
|
767
851
|
|
|
768
852
|
class InputValidator(Validator):
|
|
769
853
|
|
|
@@ -804,7 +888,8 @@ class Console:
|
|
|
804
888
|
|
|
805
889
|
def process_insert_text(text: str) -> tuple[str, set[str]]:
|
|
806
890
|
removed_chars = set()
|
|
807
|
-
if not text:
|
|
891
|
+
if not text:
|
|
892
|
+
return "", removed_chars
|
|
808
893
|
processed_text = "".join(c for c in text if ord(c) >= 32)
|
|
809
894
|
if allowed_chars != CHARS.ALL:
|
|
810
895
|
filtered_text = ""
|
|
@@ -825,8 +910,8 @@ class Console:
|
|
|
825
910
|
def insert_text_event(event: KeyPressEvent) -> None:
|
|
826
911
|
nonlocal result_text, filtered_chars
|
|
827
912
|
try:
|
|
828
|
-
insert_text
|
|
829
|
-
|
|
913
|
+
if not (insert_text := event.data):
|
|
914
|
+
return
|
|
830
915
|
buffer = event.app.current_buffer
|
|
831
916
|
cursor_pos = buffer.cursor_position
|
|
832
917
|
insert_text, filtered_chars = process_insert_text(insert_text)
|
|
@@ -905,4 +990,349 @@ class Console:
|
|
|
905
990
|
FormatCodes.print(start, end="")
|
|
906
991
|
session.prompt()
|
|
907
992
|
FormatCodes.print(end, end="")
|
|
908
|
-
|
|
993
|
+
|
|
994
|
+
if result_text in ("", None):
|
|
995
|
+
if has_default:
|
|
996
|
+
return default_val
|
|
997
|
+
result_text = ""
|
|
998
|
+
|
|
999
|
+
if output_type == str:
|
|
1000
|
+
return result_text # type: ignore[return-value]
|
|
1001
|
+
else:
|
|
1002
|
+
try:
|
|
1003
|
+
return output_type(result_text) # type: ignore[call-arg]
|
|
1004
|
+
except (ValueError, TypeError):
|
|
1005
|
+
if has_default:
|
|
1006
|
+
return default_val
|
|
1007
|
+
raise
|
|
1008
|
+
|
|
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
|
+
|
|
1029
|
+
class ProgressBar:
|
|
1030
|
+
"""A console progress bar with smooth transitions and customizable appearance.\n
|
|
1031
|
+
-------------------------------------------------------------------------------------------------
|
|
1032
|
+
- `min_width` -⠀the min width of the progress bar in chars
|
|
1033
|
+
- `max_width` -⠀the max width of the progress bar in chars
|
|
1034
|
+
- `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
|
|
1035
|
+
* `{label}` `{l}`
|
|
1036
|
+
* `{bar}` `{b}`
|
|
1037
|
+
* `{current}` `{c}`
|
|
1038
|
+
* `{total}` `{t}`
|
|
1039
|
+
* `{percentage}` `{percent}` `{p}`
|
|
1040
|
+
- `limited_bar_format` -⠀a simplified format string used when the console width is too small
|
|
1041
|
+
- `chars` -⠀a tuple of characters ordered from full to empty progress<br>
|
|
1042
|
+
The first character represents completely filled sections, intermediate
|
|
1043
|
+
characters create smooth transitions, and the last character represents
|
|
1044
|
+
empty sections. Default is a set of Unicode block characters.
|
|
1045
|
+
--------------------------------------------------------------------------------------------------
|
|
1046
|
+
The bar format (also limited) can additionally be formatted with special formatting codes. For
|
|
1047
|
+
more detailed information about formatting codes, see the `format_codes` module documentation."""
|
|
1048
|
+
|
|
1049
|
+
def __init__(
|
|
1050
|
+
self,
|
|
1051
|
+
min_width: int = 10,
|
|
1052
|
+
max_width: int = 50,
|
|
1053
|
+
bar_format: str = "{l} |{b}| [b]({c})/{t} [dim](([i]({p}%)))",
|
|
1054
|
+
limited_bar_format: str = "|{b}|",
|
|
1055
|
+
chars: tuple[str, ...] = ("█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", " "),
|
|
1056
|
+
):
|
|
1057
|
+
self.active: bool = False
|
|
1058
|
+
"""Whether the progress bar is currently active (intercepting stdout) or not."""
|
|
1059
|
+
self.min_width: int
|
|
1060
|
+
"""The min width of the progress bar in chars."""
|
|
1061
|
+
self.max_width: int
|
|
1062
|
+
"""The max width of the progress bar in chars."""
|
|
1063
|
+
self.bar_format: str
|
|
1064
|
+
"""The format string used to render the progress bar."""
|
|
1065
|
+
self.limited_bar_format: str
|
|
1066
|
+
"""The simplified format string used when the console width is too small."""
|
|
1067
|
+
self.chars: tuple[str, ...]
|
|
1068
|
+
"""A tuple of characters ordered from full to empty progress."""
|
|
1069
|
+
|
|
1070
|
+
self.set_width(min_width, max_width)
|
|
1071
|
+
self.set_bar_format(bar_format, limited_bar_format)
|
|
1072
|
+
self.set_chars(chars)
|
|
1073
|
+
|
|
1074
|
+
self._buffer: list[str] = []
|
|
1075
|
+
self._original_stdout: Optional[TextIO] = None
|
|
1076
|
+
self._current_progress_str: str = ""
|
|
1077
|
+
self._last_line_len: int = 0
|
|
1078
|
+
|
|
1079
|
+
def set_width(self, min_width: Optional[int] = None, max_width: Optional[int] = None) -> None:
|
|
1080
|
+
"""Set the width of the progress bar.\n
|
|
1081
|
+
--------------------------------------------------------------
|
|
1082
|
+
- `min_width` -⠀the min width of the progress bar in chars
|
|
1083
|
+
- `max_width` -⠀the max width of the progress bar in chars"""
|
|
1084
|
+
if min_width is not None:
|
|
1085
|
+
if min_width < 1:
|
|
1086
|
+
raise ValueError("Minimum width must be at least 1.")
|
|
1087
|
+
self.min_width = max(1, min_width)
|
|
1088
|
+
if max_width is not None:
|
|
1089
|
+
if max_width < 1:
|
|
1090
|
+
raise ValueError("Maximum width must be at least 1.")
|
|
1091
|
+
self.max_width = max(self.min_width, max_width)
|
|
1092
|
+
|
|
1093
|
+
def set_bar_format(self, bar_format: Optional[str] = None, limited_bar_format: Optional[str] = None) -> None:
|
|
1094
|
+
"""Set the format string used to render the progress bar.\n
|
|
1095
|
+
--------------------------------------------------------------------------------------------------
|
|
1096
|
+
- `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
|
|
1097
|
+
* `{label}` `{l}`
|
|
1098
|
+
* `{bar}` `{b}`
|
|
1099
|
+
* `{current}` `{c}`
|
|
1100
|
+
* `{total}` `{t}`
|
|
1101
|
+
* `{percentage}` `{percent}` `{p}`
|
|
1102
|
+
- `limited_bar_format` -⠀a simplified format string used when the console width is too small
|
|
1103
|
+
--------------------------------------------------------------------------------------------------
|
|
1104
|
+
The bar format (also limited) can additionally be formatted with special formatting codes. For
|
|
1105
|
+
more detailed information about formatting codes, see the `format_codes` module documentation."""
|
|
1106
|
+
if bar_format is not None:
|
|
1107
|
+
if not _COMPILED["bar"].search(bar_format):
|
|
1108
|
+
raise ValueError("'bar_format' must contain the '{bar}' or '{b}' placeholder.")
|
|
1109
|
+
self.bar_format = bar_format
|
|
1110
|
+
if limited_bar_format is not None:
|
|
1111
|
+
if not _COMPILED["bar"].search(limited_bar_format):
|
|
1112
|
+
raise ValueError("'limited_bar_format' must contain the '{bar}' or '{b}' placeholder.")
|
|
1113
|
+
self.limited_bar_format = limited_bar_format
|
|
1114
|
+
|
|
1115
|
+
def set_chars(self, chars: tuple[str, ...]) -> None:
|
|
1116
|
+
"""Set the characters used to render the progress bar.\n
|
|
1117
|
+
--------------------------------------------------------------------------
|
|
1118
|
+
- `chars` -⠀a tuple of characters ordered from full to empty progress<br>
|
|
1119
|
+
The first character represents completely filled sections, intermediate
|
|
1120
|
+
characters create smooth transitions, and the last character represents
|
|
1121
|
+
empty sections. If None, uses default Unicode block characters."""
|
|
1122
|
+
if len(chars) < 2:
|
|
1123
|
+
raise ValueError("'chars' must contain at least two characters (full and empty).")
|
|
1124
|
+
if not all(len(c) == 1 for c in chars if isinstance(c, str)):
|
|
1125
|
+
raise ValueError("All 'chars' items must be single-character strings.")
|
|
1126
|
+
self.chars = chars
|
|
1127
|
+
|
|
1128
|
+
def show_progress(self, current: int, total: int, label: Optional[str] = None) -> None:
|
|
1129
|
+
"""Show or update the progress bar.\n
|
|
1130
|
+
-------------------------------------------------------------------------------------------
|
|
1131
|
+
- `current` -⠀the current progress value (below `0` or greater than `total` hides the bar)
|
|
1132
|
+
- `total` -⠀the total value representing 100% progress (must be greater than `0`)
|
|
1133
|
+
- `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder"""
|
|
1134
|
+
if total <= 0:
|
|
1135
|
+
raise ValueError("Total must be greater than 0.")
|
|
1136
|
+
|
|
1137
|
+
try:
|
|
1138
|
+
if not self.active:
|
|
1139
|
+
self._start_intercepting()
|
|
1140
|
+
self._flush_buffer()
|
|
1141
|
+
self._draw_progress_bar(current, total, label or "")
|
|
1142
|
+
if current < 0 or current > total:
|
|
1143
|
+
self.hide_progress()
|
|
1144
|
+
except Exception:
|
|
1145
|
+
self._emergency_cleanup()
|
|
1146
|
+
raise
|
|
1147
|
+
|
|
1148
|
+
def hide_progress(self) -> None:
|
|
1149
|
+
"""Hide the progress bar and restore normal console output."""
|
|
1150
|
+
if self.active:
|
|
1151
|
+
self._clear_progress_line()
|
|
1152
|
+
self._stop_intercepting()
|
|
1153
|
+
|
|
1154
|
+
@contextmanager
|
|
1155
|
+
def progress_context(self, total: int, label: Optional[str] = None) -> Generator[_ProgressUpdater, None, None]:
|
|
1156
|
+
"""Context manager for automatic cleanup. Returns a function to update progress.\n
|
|
1157
|
+
----------------------------------------------------------------------------------------------------
|
|
1158
|
+
- `total` -⠀the total value representing 100% progress (must be greater than `0`)
|
|
1159
|
+
- `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder
|
|
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
|
+
|
|
1165
|
+
Example usage:
|
|
1166
|
+
```python
|
|
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):
|
|
1171
|
+
# Do some work...
|
|
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
|
|
1179
|
+
```"""
|
|
1180
|
+
current_progress = 0
|
|
1181
|
+
current_label = label
|
|
1182
|
+
|
|
1183
|
+
try:
|
|
1184
|
+
|
|
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)
|
|
1218
|
+
|
|
1219
|
+
yield update_progress
|
|
1220
|
+
except Exception:
|
|
1221
|
+
self._emergency_cleanup()
|
|
1222
|
+
raise
|
|
1223
|
+
finally:
|
|
1224
|
+
self.hide_progress()
|
|
1225
|
+
|
|
1226
|
+
def _start_intercepting(self) -> None:
|
|
1227
|
+
self.active = True
|
|
1228
|
+
self._original_stdout = _sys.stdout
|
|
1229
|
+
_sys.stdout = _InterceptedOutput(self)
|
|
1230
|
+
|
|
1231
|
+
def _stop_intercepting(self) -> None:
|
|
1232
|
+
if self._original_stdout:
|
|
1233
|
+
_sys.stdout = self._original_stdout
|
|
1234
|
+
self._original_stdout = None
|
|
1235
|
+
self.active = False
|
|
1236
|
+
self._buffer.clear()
|
|
1237
|
+
self._last_line_len = 0
|
|
1238
|
+
self._current_progress_str = ""
|
|
1239
|
+
|
|
1240
|
+
def _emergency_cleanup(self) -> None:
|
|
1241
|
+
"""Emergency cleanup to restore stdout in case of exceptions."""
|
|
1242
|
+
try:
|
|
1243
|
+
self._stop_intercepting()
|
|
1244
|
+
except Exception:
|
|
1245
|
+
pass
|
|
1246
|
+
|
|
1247
|
+
def _flush_buffer(self) -> None:
|
|
1248
|
+
if self._buffer and self._original_stdout:
|
|
1249
|
+
self._clear_progress_line()
|
|
1250
|
+
for content in self._buffer:
|
|
1251
|
+
self._original_stdout.write(content)
|
|
1252
|
+
self._original_stdout.flush()
|
|
1253
|
+
self._buffer.clear()
|
|
1254
|
+
|
|
1255
|
+
def _draw_progress_bar(self, current: int, total: int, label: Optional[str] = None) -> None:
|
|
1256
|
+
if total <= 0 or not self._original_stdout:
|
|
1257
|
+
return
|
|
1258
|
+
percentage = min(100, (current / total) * 100)
|
|
1259
|
+
formatted, bar_width = self._get_formatted_info_and_bar_width(self.bar_format, current, total, percentage, label)
|
|
1260
|
+
if bar_width < self.min_width:
|
|
1261
|
+
formatted, bar_width = self._get_formatted_info_and_bar_width(
|
|
1262
|
+
self.limited_bar_format, current, total, percentage, label
|
|
1263
|
+
)
|
|
1264
|
+
bar = self._create_bar(current, total, max(1, bar_width)) + "[*]"
|
|
1265
|
+
progress_text = _COMPILED["bar"].sub(FormatCodes.to_ansi(bar), formatted)
|
|
1266
|
+
self._current_progress_str = progress_text
|
|
1267
|
+
self._last_line_len = len(progress_text)
|
|
1268
|
+
self._original_stdout.write(f"\r{progress_text}")
|
|
1269
|
+
self._original_stdout.flush()
|
|
1270
|
+
|
|
1271
|
+
def _get_formatted_info_and_bar_width(
|
|
1272
|
+
self,
|
|
1273
|
+
bar_format: str,
|
|
1274
|
+
current: int,
|
|
1275
|
+
total: int,
|
|
1276
|
+
percentage: float,
|
|
1277
|
+
label: Optional[str] = None,
|
|
1278
|
+
) -> tuple[str, int]:
|
|
1279
|
+
formatted = _COMPILED["label"].sub(label or "", bar_format)
|
|
1280
|
+
formatted = _COMPILED["current"].sub(str(current), formatted)
|
|
1281
|
+
formatted = _COMPILED["total"].sub(str(total), formatted)
|
|
1282
|
+
formatted = _COMPILED["percentage"].sub(f"{percentage:.1f}", formatted)
|
|
1283
|
+
formatted = FormatCodes.to_ansi(formatted)
|
|
1284
|
+
bar_space = Console.w - len(FormatCodes.remove_ansi(_COMPILED["bar"].sub("", formatted)))
|
|
1285
|
+
bar_width = min(bar_space, self.max_width) if bar_space > 0 else 0
|
|
1286
|
+
return formatted, bar_width
|
|
1287
|
+
|
|
1288
|
+
def _create_bar(self, current: int, total: int, bar_width: int) -> str:
|
|
1289
|
+
progress = current / total if total > 0 else 0
|
|
1290
|
+
bar = []
|
|
1291
|
+
|
|
1292
|
+
for i in range(bar_width):
|
|
1293
|
+
pos_progress = (i + 1) / bar_width
|
|
1294
|
+
if progress >= pos_progress:
|
|
1295
|
+
bar.append(self.chars[0])
|
|
1296
|
+
elif progress >= pos_progress - (1 / bar_width):
|
|
1297
|
+
remainder = (progress - (pos_progress - (1 / bar_width))) * bar_width
|
|
1298
|
+
char_idx = len(self.chars) - 1 - min(int(remainder * len(self.chars)), len(self.chars) - 1)
|
|
1299
|
+
bar.append(self.chars[char_idx])
|
|
1300
|
+
else:
|
|
1301
|
+
bar.append(self.chars[-1])
|
|
1302
|
+
return "".join(bar)
|
|
1303
|
+
|
|
1304
|
+
def _clear_progress_line(self) -> None:
|
|
1305
|
+
if self._last_line_len > 0 and self._original_stdout:
|
|
1306
|
+
self._original_stdout.write(f"{ANSI.CHAR}[2K\r")
|
|
1307
|
+
self._original_stdout.flush()
|
|
1308
|
+
|
|
1309
|
+
def _redraw_progress_bar(self) -> None:
|
|
1310
|
+
if self._current_progress_str and self._original_stdout:
|
|
1311
|
+
self._original_stdout.write(f"{self._current_progress_str}")
|
|
1312
|
+
self._original_stdout.flush()
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
class _InterceptedOutput(_io.StringIO):
|
|
1316
|
+
"""Custom StringIO that captures output and stores it in the progress bar buffer."""
|
|
1317
|
+
|
|
1318
|
+
def __init__(self, progress_bar: ProgressBar):
|
|
1319
|
+
super().__init__()
|
|
1320
|
+
self.progress_bar = progress_bar
|
|
1321
|
+
|
|
1322
|
+
def write(self, content: str) -> int:
|
|
1323
|
+
try:
|
|
1324
|
+
if content and content != "\r":
|
|
1325
|
+
self.progress_bar._buffer.append(content)
|
|
1326
|
+
return len(content)
|
|
1327
|
+
except Exception:
|
|
1328
|
+
self.progress_bar._emergency_cleanup()
|
|
1329
|
+
raise
|
|
1330
|
+
|
|
1331
|
+
def flush(self) -> None:
|
|
1332
|
+
try:
|
|
1333
|
+
if self.progress_bar.active and self.progress_bar._buffer:
|
|
1334
|
+
self.progress_bar._flush_buffer()
|
|
1335
|
+
self.progress_bar._redraw_progress_bar()
|
|
1336
|
+
except Exception:
|
|
1337
|
+
self.progress_bar._emergency_cleanup()
|
|
1338
|
+
raise
|
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
|
|
|
@@ -190,7 +191,7 @@ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
|
190
191
|
"ansi_seq": _re.compile(ANSI.CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
|
|
191
192
|
"formatting": _rx.compile(
|
|
192
193
|
Regex.brackets("[", "]", is_group=True, ignore_in_strings=False)
|
|
193
|
-
+ r"(
|
|
194
|
+
+ r"(?:([/\\]?)"
|
|
194
195
|
+ Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False)
|
|
195
196
|
+ r")?"
|
|
196
197
|
),
|
|
@@ -317,6 +318,7 @@ class FormatCodes:
|
|
|
317
318
|
]
|
|
318
319
|
if auto_reset_txt and not auto_reset_escaped:
|
|
319
320
|
reset_keys = []
|
|
321
|
+
default_color_resets = ("_bg", "default") if use_default else ("_bg", "_c")
|
|
320
322
|
for k in format_keys:
|
|
321
323
|
k_lower = k.lower()
|
|
322
324
|
k_set = set(k_lower.split(":"))
|
|
@@ -324,7 +326,7 @@ class FormatCodes:
|
|
|
324
326
|
if k_set & _PREFIX["BR"]:
|
|
325
327
|
for i in range(len(k)):
|
|
326
328
|
if is_valid_color(k[i:]):
|
|
327
|
-
reset_keys.extend(
|
|
329
|
+
reset_keys.extend(default_color_resets)
|
|
328
330
|
break
|
|
329
331
|
else:
|
|
330
332
|
for i in range(len(k)):
|
|
@@ -334,7 +336,7 @@ class FormatCodes:
|
|
|
334
336
|
elif is_valid_color(k) or any(
|
|
335
337
|
k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon):])
|
|
336
338
|
for prefix in _PREFIX["BR"]):
|
|
337
|
-
reset_keys.append(
|
|
339
|
+
reset_keys.append(default_color_resets[1])
|
|
338
340
|
else:
|
|
339
341
|
reset_keys.append(f"_{k}")
|
|
340
342
|
ansi_resets = [
|
|
@@ -418,11 +420,16 @@ class FormatCodes:
|
|
|
418
420
|
global _CONSOLE_ANSI_CONFIGURED
|
|
419
421
|
if not _CONSOLE_ANSI_CONFIGURED:
|
|
420
422
|
_sys.stdout.flush()
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
426
433
|
_CONSOLE_ANSI_CONFIGURED = True
|
|
427
434
|
|
|
428
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
|
|
@@ -12,7 +15,7 @@ class _IsElevated:
|
|
|
12
15
|
def __get__(self, obj, owner=None):
|
|
13
16
|
try:
|
|
14
17
|
if _os.name == "nt":
|
|
15
|
-
return _ctypes.windll.shell32.IsUserAnAdmin() != 0
|
|
18
|
+
return _ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
|
|
16
19
|
elif _os.name == "posix":
|
|
17
20
|
return _os.geteuid() == 0 # type: ignore[attr-defined]
|
|
18
21
|
except Exception:
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xulbux
|
|
3
|
-
Version: 1.8.
|
|
4
|
-
Summary: A Python library which includes lots of helpful classes, types and functions aiming to make common programming tasks simpler.
|
|
3
|
+
Version: 1.8.3
|
|
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>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -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.1.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
xulbux/__init__.py,sha256=FkJeare6GgT7xau1OcZVKoLk3uUOULFWdP2DYzOCW58,817
|
|
2
|
-
xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
|
|
3
|
-
xulbux/color.py,sha256=XCB05xBSEz6-dpX9yVkMB2zTSdTH01MfUazJEthL_zM,49741
|
|
4
|
-
xulbux/console.py,sha256=U9t4Fd_ZGrdhTf8KMKJJqw12aijpur3b9J3rlEgntW4,43164
|
|
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=94VyxnUZI2dS1glteNs7izKYrTI_W2pukFGF5f9k72E,24720
|
|
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=Y5x719Ocwi93VJ6DVE8NR_Js7FQ6QT5wmtjX9FAsw9U,6582
|
|
14
|
-
xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
|
|
15
|
-
xulbux/cli/help.py,sha256=QtyAqHEC_0p3qvLXdUBlpvjV9khPy70-po7CQOz1Fag,3314
|
|
16
|
-
xulbux-1.8.1.dist-info/METADATA,sha256=Z3S8mdtlmI85R3M3Ks38oUUnx4vzBn91_N3CJabSUIo,11041
|
|
17
|
-
xulbux-1.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
-
xulbux-1.8.1.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
|
|
19
|
-
xulbux-1.8.1.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
|
|
20
|
-
xulbux-1.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|