xulbux 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xulbux might be problematic. Click here for more details.
- xulbux/__init__.py +57 -0
- xulbux/_cli_.py +53 -0
- xulbux/_consts_.py +145 -0
- xulbux/xx_code.py +105 -0
- xulbux/xx_color.py +955 -0
- xulbux/xx_console.py +378 -0
- xulbux/xx_data.py +531 -0
- xulbux/xx_env_path.py +113 -0
- xulbux/xx_file.py +65 -0
- xulbux/xx_format_codes.py +305 -0
- xulbux/xx_json.py +106 -0
- xulbux/xx_path.py +107 -0
- xulbux/xx_regex.py +156 -0
- xulbux/xx_string.py +159 -0
- xulbux/xx_system.py +85 -0
- xulbux-1.6.1.dist-info/LICENSE +21 -0
- xulbux-1.6.1.dist-info/METADATA +110 -0
- xulbux-1.6.1.dist-info/RECORD +21 -0
- xulbux-1.6.1.dist-info/WHEEL +5 -0
- xulbux-1.6.1.dist-info/entry_points.txt +3 -0
- xulbux-1.6.1.dist-info/top_level.txt +1 -0
xulbux/xx_file.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from .xx_string import String
|
|
2
|
+
from .xx_path import Path
|
|
3
|
+
|
|
4
|
+
import os as _os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class File:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def rename_extension(
|
|
11
|
+
file: str,
|
|
12
|
+
new_extension: str,
|
|
13
|
+
camel_case_filename: bool = False,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Rename the extension of a file.\n
|
|
16
|
+
--------------------------------------------------------------------------
|
|
17
|
+
If the `camel_case_filename` parameter is true, the filename will be made
|
|
18
|
+
CamelCase in addition to changing the files extension."""
|
|
19
|
+
directory, filename_with_ext = _os.path.split(file)
|
|
20
|
+
filename = filename_with_ext.split(".")[0]
|
|
21
|
+
if camel_case_filename:
|
|
22
|
+
filename = String.to_camel_case(filename)
|
|
23
|
+
return _os.path.join(directory, f"{filename}{new_extension}")
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def create(
|
|
27
|
+
file: str,
|
|
28
|
+
content: str = "",
|
|
29
|
+
force: bool = False,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Create a file with ot without content.\n
|
|
32
|
+
------------------------------------------------------------------------
|
|
33
|
+
The function will throw a `FileExistsError` if the file already exists.
|
|
34
|
+
To always overwrite the file, set the `force` parameter to `True`."""
|
|
35
|
+
if _os.path.exists(file) and not force:
|
|
36
|
+
with open(file, "r", encoding="utf-8") as existing_file:
|
|
37
|
+
existing_content = existing_file.read()
|
|
38
|
+
if existing_content == content:
|
|
39
|
+
raise FileExistsError("Already created this file. (nothing changed)")
|
|
40
|
+
raise FileExistsError("File already exists.")
|
|
41
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
42
|
+
f.write(content)
|
|
43
|
+
full_path = _os.path.abspath(file)
|
|
44
|
+
return full_path
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def extend_or_make_path(
|
|
48
|
+
file: str,
|
|
49
|
+
search_in: str | list[str] = None,
|
|
50
|
+
prefer_base_dir: bool = True,
|
|
51
|
+
correct_paths: bool = False,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Tries to find the file and extend the path to be absolute and if the file was not found:\n
|
|
54
|
+
Generate the absolute path to the file in the CWD or the running program's base-directory.\n
|
|
55
|
+
----------------------------------------------------------------------------------------------
|
|
56
|
+
If the `file` is not found in the above directories, it will be searched in the `search_in`
|
|
57
|
+
directory/directories. If the file is still not found, it will return the path to the file in
|
|
58
|
+
the base-dir per default or to the file in the CWD if `prefer_base_dir` is set to `False`.\n
|
|
59
|
+
----------------------------------------------------------------------------------------------
|
|
60
|
+
If `correct_paths` is true, it is possible to have typos in the `search_in` path/s and it
|
|
61
|
+
will still find the file if it is under one of those paths."""
|
|
62
|
+
try:
|
|
63
|
+
return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
return _os.path.join(Path.get(base_dir=True), file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions to be able to use special (easy) formatting codes directly inside some message (string).\n
|
|
3
|
+
These codes, when used within following functions, will change the look of log within the console:
|
|
4
|
+
- `FormatCodes.print()` (print a special format-codes containing string)
|
|
5
|
+
- `FormatCodes.input()` (input with a special format-codes containing prompt)
|
|
6
|
+
- `FormatCodes.to_ansi()` (transform all special format-codes into ANSI codes in a string)\n
|
|
7
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
How to change the text format and color?\n
|
|
9
|
+
Example string with formatting codes:
|
|
10
|
+
```string
|
|
11
|
+
[bold]This is bold text, [#F08]which is pink now [black|BG:#FF0088] and now it changed`
|
|
12
|
+
to black with a pink background. [_]And this is the boring text, where everything is reset.
|
|
13
|
+
```
|
|
14
|
+
⇾ Instead of writing the formats all separate `[…][…][…]` you can join them like this `[…|…|…]`\n
|
|
15
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
16
|
+
You can also automatically reset a certain format, behind text like shown in the following example:
|
|
17
|
+
```string
|
|
18
|
+
This is normal text [b](which is bold now) but now it was automatically reset to normal.
|
|
19
|
+
```
|
|
20
|
+
This will only reset formats, that have a reset listed below. Colors and BG-colors won't be reset.\n
|
|
21
|
+
This is what will happen, if you use it with a color-format:
|
|
22
|
+
```string
|
|
23
|
+
[cyan]This is cyan text [b](which is bold now.) Now it's not bold any more but still cyan.
|
|
24
|
+
```
|
|
25
|
+
If you want to ignore the `()` brackets you can put a `\\` or `/` between:
|
|
26
|
+
```string
|
|
27
|
+
[cyan]This is cyan text [b]/(which is bold now.) And now it is still bold and cyan.
|
|
28
|
+
```
|
|
29
|
+
⇾ To see these examples in action, you can put them into the `FormatCodes.print()` function.\n
|
|
30
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
31
|
+
All possible formatting codes:
|
|
32
|
+
- HEX colors: `[#F08]` or `[#FF0088]` (with or without leading #)
|
|
33
|
+
- RGB colors: `[rgb(255, 0, 136)]`
|
|
34
|
+
- bright colors: `[bright:#F08]`
|
|
35
|
+
- background colors: `[BG:#F08]`
|
|
36
|
+
- standard cmd colors:
|
|
37
|
+
- `[black]`
|
|
38
|
+
- `[red]`
|
|
39
|
+
- `[green]`
|
|
40
|
+
- `[yellow]`
|
|
41
|
+
- `[blue]`
|
|
42
|
+
- `[magenta]`
|
|
43
|
+
- `[cyan]`
|
|
44
|
+
- `[white]`
|
|
45
|
+
- bright cmd colors: `[bright:black]` or `[br:black]`, `[bright:red]` or `[br:red]`, ...
|
|
46
|
+
- background cmd colors: `[BG:black]`, `[BG:red]`, ...
|
|
47
|
+
- bright background cmd colors: `[BG:bright:black]` or `[BG:br:black]`, `[BG:bright:red]` or `[BG:br:red]`, ...\n
|
|
48
|
+
⇾ The order of `BG:` and `bright:` or `br:` does not matter.
|
|
49
|
+
- text formats:
|
|
50
|
+
- `[bold]` or `[b]`
|
|
51
|
+
- `[dim]`
|
|
52
|
+
- `[italic]` or `[i]`
|
|
53
|
+
- `[underline]` or `[u]`
|
|
54
|
+
- `[inverse]`, `[invert]` or `[in]`
|
|
55
|
+
- `[hidden]`, `[hide]` or `[h]`
|
|
56
|
+
- `[strikethrough]` or `[s]`
|
|
57
|
+
- `[double-underline]` or `[du]`
|
|
58
|
+
- specific reset: `[_bold]` or `[_b]`, `[_dim]`, ... or `[_color]` or `[_c]`, `[_background]` or `[_bg]`
|
|
59
|
+
- total reset: `[_]` (only if no `default_color` is set, otherwise see ↓ )
|
|
60
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
61
|
+
Special formatting when param `default_color` is set to a color:
|
|
62
|
+
- `[*]` will reset everything, just like `[_]`, but the text-color will remain in `default_color`
|
|
63
|
+
- `[*color]` will reset the text-color, just like `[_color]`, but then also make it `default_color`
|
|
64
|
+
- `[default]` will just color the text in `default_color`,
|
|
65
|
+
- `[BG:default]` will color the background in `default_color`\n
|
|
66
|
+
Unlike the standard cmd colors, the default color can be changed by using the following modifiers:
|
|
67
|
+
- `[l]` will lighten the `default_color` text by `brightness_steps`%
|
|
68
|
+
- `[ll]` will lighten the `default_color` text by `2 × brightness_steps`%
|
|
69
|
+
- `[lll]` will lighten the `default_color` text by `3 × brightness_steps`%
|
|
70
|
+
- ... etc. Same thing for darkening:
|
|
71
|
+
- `[d]` will darken the `default_color` text by `brightness_steps`%
|
|
72
|
+
- `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
|
|
73
|
+
- `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
|
|
74
|
+
- ... etc.\n
|
|
75
|
+
Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
from ._consts_ import ANSI
|
|
79
|
+
from .xx_string import String
|
|
80
|
+
from .xx_regex import Regex
|
|
81
|
+
from .xx_color import *
|
|
82
|
+
|
|
83
|
+
from functools import lru_cache
|
|
84
|
+
import ctypes as _ctypes
|
|
85
|
+
import regex as _rx
|
|
86
|
+
import sys as _sys
|
|
87
|
+
import re as _re
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
COMPILED = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
91
|
+
"*": _re.compile(r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]"),
|
|
92
|
+
"*color": _re.compile(r"\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]"),
|
|
93
|
+
"format": _rx.compile(
|
|
94
|
+
Regex.brackets("[", "]", is_group=True) + r"(?:\s*([/\\]?)\s*" + Regex.brackets("(", ")", is_group=True) + r")?"
|
|
95
|
+
),
|
|
96
|
+
"bg?_default": _re.compile(r"(?i)((?:BG\s*:)?)\s*default"),
|
|
97
|
+
"bg_default": _re.compile(r"(?i)BG\s*:\s*default"),
|
|
98
|
+
"modifier": _re.compile(
|
|
99
|
+
rf'(?i)((?:BG\s*:)?)\s*({"|".join([f"{_re.escape(m)}+" for m in ANSI.modifier["lighten"] + ANSI.modifier["darken"]])})$'
|
|
100
|
+
),
|
|
101
|
+
"rgb": _re.compile(r"(?i)^\s*(BG\s*:)?\s*(?:rgb|rgba)?\s*\(?\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)?\s*$"),
|
|
102
|
+
"hex": _re.compile(r"(?i)^\s*(BG\s*:)?\s*(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})\s*$"),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FormatCodes:
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def print(
|
|
110
|
+
*values: object,
|
|
111
|
+
default_color: hexa | rgba = None,
|
|
112
|
+
brightness_steps: int = 20,
|
|
113
|
+
sep: str = " ",
|
|
114
|
+
end: str = "\n",
|
|
115
|
+
flush: bool = True,
|
|
116
|
+
) -> None:
|
|
117
|
+
FormatCodes.__config_console()
|
|
118
|
+
_sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)) + end, default_color, brightness_steps))
|
|
119
|
+
if flush:
|
|
120
|
+
_sys.stdout.flush()
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def input(
|
|
124
|
+
prompt: object = "",
|
|
125
|
+
default_color: hexa | rgba = None,
|
|
126
|
+
brightness_steps: int = 20,
|
|
127
|
+
) -> str:
|
|
128
|
+
FormatCodes.__config_console()
|
|
129
|
+
return input(FormatCodes.to_ansi(prompt, default_color, brightness_steps))
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def to_ansi(
|
|
133
|
+
string: str, default_color: hexa | rgba = None, brightness_steps: int = 20, _default_start: bool = True
|
|
134
|
+
) -> str:
|
|
135
|
+
result, bg_kwd, color_pref = string, {"bg"}, {"br", "bright"}
|
|
136
|
+
|
|
137
|
+
if Color.is_valid_rgba(default_color, False):
|
|
138
|
+
use_default = True
|
|
139
|
+
elif Color.is_valid_hexa(default_color, False):
|
|
140
|
+
use_default, default_color = True, Color.to_rgba(default_color)
|
|
141
|
+
else:
|
|
142
|
+
use_default = False
|
|
143
|
+
if use_default:
|
|
144
|
+
string = COMPILED["*"].sub(r"[\1_|default\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
|
|
145
|
+
string = COMPILED["*color"].sub(r"[\1default\2]", string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
|
|
146
|
+
|
|
147
|
+
def is_valid_color(color: str) -> bool:
|
|
148
|
+
return color in ANSI.color_map or Color.is_valid_rgba(color) or Color.is_valid_hexa(color)
|
|
149
|
+
|
|
150
|
+
def replace_keys(match: _re.Match) -> str:
|
|
151
|
+
formats = match.group(1)
|
|
152
|
+
escaped = match.group(2)
|
|
153
|
+
auto_reset_txt = match.group(3)
|
|
154
|
+
if auto_reset_txt and auto_reset_txt.count("[") > 0 and auto_reset_txt.count("]") > 0:
|
|
155
|
+
auto_reset_txt = FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)
|
|
156
|
+
if not formats:
|
|
157
|
+
return match.group(0)
|
|
158
|
+
if formats.count("[") > 0 and formats.count("]") > 0:
|
|
159
|
+
formats = FormatCodes.to_ansi(formats, default_color, brightness_steps, False)
|
|
160
|
+
format_keys = [k.strip() for k in formats.split("|") if k.strip()]
|
|
161
|
+
ansi_formats = [
|
|
162
|
+
r if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)) != k else f"[{k}]"
|
|
163
|
+
for k in format_keys
|
|
164
|
+
]
|
|
165
|
+
if auto_reset_txt and not escaped:
|
|
166
|
+
reset_keys = []
|
|
167
|
+
for k in format_keys:
|
|
168
|
+
k_lower = k.lower()
|
|
169
|
+
k_parts = k_lower.split(":")
|
|
170
|
+
k_set = set(k_parts)
|
|
171
|
+
if bg_kwd & k_set and len(k_parts) <= 3:
|
|
172
|
+
if k_set & color_pref:
|
|
173
|
+
for i in range(len(k)):
|
|
174
|
+
if is_valid_color(k[i:]):
|
|
175
|
+
reset_keys.extend(["_bg", "_color"])
|
|
176
|
+
break
|
|
177
|
+
else:
|
|
178
|
+
for i in range(len(k)):
|
|
179
|
+
if is_valid_color(k[i:]):
|
|
180
|
+
reset_keys.append("_bg")
|
|
181
|
+
break
|
|
182
|
+
elif is_valid_color(k) or any(
|
|
183
|
+
k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon) :])
|
|
184
|
+
for prefix in color_pref
|
|
185
|
+
):
|
|
186
|
+
reset_keys.append("_color")
|
|
187
|
+
else:
|
|
188
|
+
reset_keys.append(f"_{k}")
|
|
189
|
+
ansi_resets = [
|
|
190
|
+
r
|
|
191
|
+
for k in reset_keys
|
|
192
|
+
if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(
|
|
193
|
+
f"{ANSI.char}{ANSI.start}"
|
|
194
|
+
)
|
|
195
|
+
]
|
|
196
|
+
else:
|
|
197
|
+
ansi_resets = []
|
|
198
|
+
if not (len(ansi_formats) == 1 and ansi_formats[0].count(f"{ANSI.char}{ANSI.start}") >= 1) and not all(
|
|
199
|
+
f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats
|
|
200
|
+
):
|
|
201
|
+
return match.group(0)
|
|
202
|
+
return (
|
|
203
|
+
"".join(ansi_formats)
|
|
204
|
+
+ (
|
|
205
|
+
f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
|
|
206
|
+
if escaped and auto_reset_txt
|
|
207
|
+
else auto_reset_txt if auto_reset_txt else ""
|
|
208
|
+
)
|
|
209
|
+
+ ("" if escaped else "".join(ansi_resets))
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
result = "\n".join(COMPILED["format"].sub(replace_keys, line) for line in string.split("\n"))
|
|
213
|
+
return (FormatCodes.__get_default_ansi(default_color) if _default_start else "") + result if use_default else result
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def escape_ansi(ansi_string: str, escaped_char: str = ANSI.char_esc) -> str:
|
|
217
|
+
"""Makes the string printable with the ANSI formats visible."""
|
|
218
|
+
return ansi_string.replace(ANSI.char, escaped_char)
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
@lru_cache(maxsize=64)
|
|
222
|
+
def __config_console() -> None:
|
|
223
|
+
_sys.stdout.flush()
|
|
224
|
+
kernel32 = _ctypes.windll.kernel32
|
|
225
|
+
h = kernel32.GetStdHandle(-11)
|
|
226
|
+
mode = _ctypes.c_ulong()
|
|
227
|
+
kernel32.GetConsoleMode(h, _ctypes.byref(mode))
|
|
228
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def __get_default_ansi(
|
|
232
|
+
default_color: tuple,
|
|
233
|
+
format_key: str = None,
|
|
234
|
+
brightness_steps: int = None,
|
|
235
|
+
_modifiers: tuple[str, str] = (ANSI.modifier["lighten"], ANSI.modifier["darken"]),
|
|
236
|
+
) -> str | None:
|
|
237
|
+
if not brightness_steps or (format_key and COMPILED["bg?_default"].search(format_key)):
|
|
238
|
+
return (ANSI.seq_bg_color if format_key and COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
|
|
239
|
+
*default_color[:3]
|
|
240
|
+
)
|
|
241
|
+
if not (format_key in _modifiers[0] or format_key in _modifiers[1]):
|
|
242
|
+
return None
|
|
243
|
+
match = COMPILED["modifier"].match(format_key)
|
|
244
|
+
if not match:
|
|
245
|
+
return None
|
|
246
|
+
is_bg, modifiers = match.groups()
|
|
247
|
+
adjust = 0
|
|
248
|
+
for mod in _modifiers[0] + _modifiers[1]:
|
|
249
|
+
adjust = String.single_char_repeats(modifiers, mod)
|
|
250
|
+
if adjust and adjust > 0:
|
|
251
|
+
modifiers = mod
|
|
252
|
+
break
|
|
253
|
+
if adjust == 0:
|
|
254
|
+
return None
|
|
255
|
+
elif modifiers in _modifiers[0]:
|
|
256
|
+
new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust)
|
|
257
|
+
elif modifiers in _modifiers[1]:
|
|
258
|
+
new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust)
|
|
259
|
+
return (ANSI.seq_bg_color if is_bg else ANSI.seq_color).format(*new_rgb[:3])
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def __get_replacement(format_key: str, default_color: rgba = None, brightness_steps: int = 20) -> str:
|
|
263
|
+
"""Gives you the corresponding ANSI code for the given format key.
|
|
264
|
+
If `default_color` is not `None`, the text color will be `default_color` if all formats
|
|
265
|
+
are reset or you can get lighter or darker version of `default_color` (also as BG)"""
|
|
266
|
+
use_default = default_color and Color.is_valid_rgba(default_color, False)
|
|
267
|
+
_format_key, format_key = format_key, ( # NORMALIZE THE FORMAT KEY (+ SAVE ORIGINAL)
|
|
268
|
+
"bg:" if "bg" in (parts := format_key.replace(" ", "").lower().split(":")) else ""
|
|
269
|
+
) + ("bright:" if any(x in parts for x in ("bright", "br")) else "") + ":".join(
|
|
270
|
+
p for p in parts if p not in ("bg", "bright", "br")
|
|
271
|
+
)
|
|
272
|
+
if use_default:
|
|
273
|
+
if new_default_color := FormatCodes.__get_default_ansi(default_color, format_key, brightness_steps):
|
|
274
|
+
return new_default_color
|
|
275
|
+
for map_key in ANSI.codes_map:
|
|
276
|
+
if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
|
|
277
|
+
return ANSI.seq().format(
|
|
278
|
+
next(
|
|
279
|
+
(
|
|
280
|
+
v
|
|
281
|
+
for k, v in ANSI.codes_map.items()
|
|
282
|
+
if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
283
|
+
),
|
|
284
|
+
None,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
rgb_match = _re.match(COMPILED["rgb"], format_key)
|
|
288
|
+
hex_match = _re.match(COMPILED["hex"], format_key)
|
|
289
|
+
try:
|
|
290
|
+
if rgb_match:
|
|
291
|
+
is_bg = rgb_match.group(1)
|
|
292
|
+
r, g, b = map(int, rgb_match.groups()[1:])
|
|
293
|
+
if Color.is_valid_rgba((r, g, b)):
|
|
294
|
+
return ANSI.seq_bg_color.format(r, g, b) if is_bg else ANSI.seq_color.format(r, g, b)
|
|
295
|
+
elif hex_match:
|
|
296
|
+
is_bg = hex_match.group(1)
|
|
297
|
+
rgb = Color.to_rgba(hex_match.group(2))
|
|
298
|
+
return (
|
|
299
|
+
ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2])
|
|
300
|
+
if is_bg
|
|
301
|
+
else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
|
|
302
|
+
)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
return _format_key
|
xulbux/xx_json.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from .xx_data import Data
|
|
2
|
+
from .xx_file import File
|
|
3
|
+
|
|
4
|
+
import json as _json
|
|
5
|
+
import os as _os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Json:
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def read(
|
|
12
|
+
json_file: str,
|
|
13
|
+
comment_start: str = ">>",
|
|
14
|
+
comment_end: str = "<<",
|
|
15
|
+
return_original: bool = False,
|
|
16
|
+
) -> dict | tuple[dict, dict]:
|
|
17
|
+
"""Read JSON files, ignoring comments.\n
|
|
18
|
+
-------------------------------------------------------------------------
|
|
19
|
+
If only `comment_start` is found at the beginning of an item,
|
|
20
|
+
the whole item is counted as a comment and therefore ignored.
|
|
21
|
+
If `comment_start` and `comment_end` are found inside an item,
|
|
22
|
+
the the section from `comment_start` to `comment_end` is ignored.
|
|
23
|
+
If `return_original` is set to `True`, the original JSON is returned
|
|
24
|
+
additionally. (returns: `[processed_json, original_json]`)"""
|
|
25
|
+
if not json_file.endswith(".json"):
|
|
26
|
+
json_file += ".json"
|
|
27
|
+
file_path = File.extend_or_make_path(json_file, prefer_base_dir=True)
|
|
28
|
+
with open(file_path, "r") as f:
|
|
29
|
+
content = f.read()
|
|
30
|
+
try:
|
|
31
|
+
data = _json.loads(content)
|
|
32
|
+
except _json.JSONDecodeError as e:
|
|
33
|
+
raise ValueError(f"Error parsing JSON in '{file_path}': {str(e)}")
|
|
34
|
+
processed_data = Data.remove_comments(data, comment_start, comment_end)
|
|
35
|
+
if not processed_data:
|
|
36
|
+
raise ValueError(f"The JSON file '{file_path}' is empty or contains only comments.")
|
|
37
|
+
return (processed_data, data) if return_original else processed_data
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def create(
|
|
41
|
+
content: dict,
|
|
42
|
+
new_file: str = "config",
|
|
43
|
+
indent: int = 2,
|
|
44
|
+
compactness: int = 1,
|
|
45
|
+
force: bool = False,
|
|
46
|
+
) -> str:
|
|
47
|
+
if not new_file.endswith(".json"):
|
|
48
|
+
new_file += ".json"
|
|
49
|
+
file_path = File.extend_or_make_path(new_file, prefer_base_dir=True)
|
|
50
|
+
if _os.path.exists(file_path) and not force:
|
|
51
|
+
with open(file_path, "r", encoding="utf-8") as existing_f:
|
|
52
|
+
existing_content = _json.load(existing_f)
|
|
53
|
+
if existing_content == content:
|
|
54
|
+
raise FileExistsError("Already created this file. (nothing changed)")
|
|
55
|
+
raise FileExistsError("File already exists.")
|
|
56
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
57
|
+
f.write(Data.to_str(content, indent, compactness, as_json=True))
|
|
58
|
+
full_path = _os.path.abspath(file_path)
|
|
59
|
+
return full_path
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def update(
|
|
63
|
+
json_file: str,
|
|
64
|
+
update_values: str | list[str],
|
|
65
|
+
comment_start: str = ">>",
|
|
66
|
+
comment_end: str = "<<",
|
|
67
|
+
sep: tuple[str, str] = ("->", "::"),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Function to easily update single/multiple values inside JSON files.\n
|
|
70
|
+
------------------------------------------------------------------------------------------------------
|
|
71
|
+
The param `json_file` is the path to the JSON file or just the name of the JSON file to be updated.\n
|
|
72
|
+
------------------------------------------------------------------------------------------------------
|
|
73
|
+
The param `update_values` is a sort of path (or a list of paths) to the value/s to be updated, with
|
|
74
|
+
the new value at the end of the path.\n
|
|
75
|
+
In this example:
|
|
76
|
+
```python
|
|
77
|
+
{
|
|
78
|
+
'healthy': {
|
|
79
|
+
'fruit': ['apples', 'bananas', 'oranges'],
|
|
80
|
+
'vegetables': ['carrots', 'broccoli', 'celery']
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
... if you want to change the value of `'apples'` to `'strawberries'`, `update_values` would be
|
|
85
|
+
`healthy->fruit->apples::strawberries` or if you don't know that the value to update is `apples` you
|
|
86
|
+
can also use the position of the value, so `healthy->fruit->0::strawberries`.\n
|
|
87
|
+
⇾ If the path from `update_values` doesn't exist, it will be created.\n
|
|
88
|
+
------------------------------------------------------------------------------------------------------
|
|
89
|
+
If only `comment_start` is found at the beginning of an item, the whole item is counted as a comment
|
|
90
|
+
and therefore ignored. If `comment_start` and `comment_end` are found inside an item, the the section
|
|
91
|
+
from `comment_start` to `comment_end` is ignored."""
|
|
92
|
+
if isinstance(update_values, str):
|
|
93
|
+
update_values = [update_values]
|
|
94
|
+
valid_entries = [
|
|
95
|
+
(parts[0].strip(), parts[1])
|
|
96
|
+
for update_value in update_values
|
|
97
|
+
if len(parts := update_value.split(str(sep[1]).strip())) == 2
|
|
98
|
+
]
|
|
99
|
+
value_paths, new_values = zip(*valid_entries) if valid_entries else ([], [])
|
|
100
|
+
processed_data, data = Json.read(json_file, comment_start, comment_end, return_original=True)
|
|
101
|
+
update = []
|
|
102
|
+
for value_path, new_value in zip(value_paths, new_values):
|
|
103
|
+
path_id = Data.get_path_id(processed_data, value_path)
|
|
104
|
+
update.append(f"{path_id}::{new_value}")
|
|
105
|
+
updated = Data.set_value_by_path_id(data, update)
|
|
106
|
+
Json.create(updated, json_file, force=True)
|
xulbux/xx_path.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import tempfile as _tempfile
|
|
2
|
+
import difflib as _difflib
|
|
3
|
+
import shutil as _shutil
|
|
4
|
+
import sys as _sys
|
|
5
|
+
import os as _os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Path:
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def get(cwd: bool = False, base_dir: bool = False) -> str | list[str]:
|
|
12
|
+
paths = []
|
|
13
|
+
if cwd:
|
|
14
|
+
paths.append(_os.getcwd())
|
|
15
|
+
if base_dir:
|
|
16
|
+
if getattr(_sys, "frozen", False):
|
|
17
|
+
base_path = _os.path.dirname(_sys.executable)
|
|
18
|
+
else:
|
|
19
|
+
main_module = _sys.modules["__main__"]
|
|
20
|
+
if hasattr(main_module, "__file__"):
|
|
21
|
+
base_path = _os.path.dirname(_os.path.abspath(main_module.__file__))
|
|
22
|
+
elif (
|
|
23
|
+
hasattr(main_module, "__spec__") and main_module.__spec__ and getattr(main_module.__spec__, "origin", None)
|
|
24
|
+
):
|
|
25
|
+
base_path = _os.path.dirname(_os.path.abspath(main_module.__spec__.origin))
|
|
26
|
+
else:
|
|
27
|
+
raise RuntimeError("Can only get base directory if ran from a file.")
|
|
28
|
+
paths.append(base_path)
|
|
29
|
+
return paths[0] if len(paths) == 1 else paths
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def extend(path: str, search_in: str | list[str] = None, raise_error: bool = False, correct_path: bool = False) -> str:
|
|
33
|
+
if path in (None, ""):
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
def get_closest_match(dir: str, part: str) -> str | None:
|
|
37
|
+
try:
|
|
38
|
+
files_and_dirs = _os.listdir(dir)
|
|
39
|
+
matches = _difflib.get_close_matches(part, files_and_dirs, n=1, cutoff=0.6)
|
|
40
|
+
return matches[0] if matches else None
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def find_path(start: str, parts: list[str]) -> str | None:
|
|
45
|
+
current = start
|
|
46
|
+
for part in parts:
|
|
47
|
+
if _os.path.isfile(current):
|
|
48
|
+
return current
|
|
49
|
+
closest_match = get_closest_match(current, part) if correct_path else part
|
|
50
|
+
current = _os.path.join(current, closest_match) if closest_match else None
|
|
51
|
+
if current is None:
|
|
52
|
+
return None
|
|
53
|
+
return current if _os.path.exists(current) and current != start else None
|
|
54
|
+
|
|
55
|
+
def expand_env_path(p: str) -> str:
|
|
56
|
+
if "%" not in p:
|
|
57
|
+
return p
|
|
58
|
+
parts = p.split("%")
|
|
59
|
+
for i in range(1, len(parts), 2):
|
|
60
|
+
if parts[i].upper() in _os.environ:
|
|
61
|
+
parts[i] = _os.environ[parts[i].upper()]
|
|
62
|
+
return "".join(parts)
|
|
63
|
+
|
|
64
|
+
path = _os.path.normpath(expand_env_path(path))
|
|
65
|
+
if _os.path.isabs(path):
|
|
66
|
+
drive, rel_path = _os.path.splitdrive(path)
|
|
67
|
+
rel_path = rel_path.lstrip(_os.sep)
|
|
68
|
+
search_dirs = (drive + _os.sep) if drive else [_os.sep]
|
|
69
|
+
else:
|
|
70
|
+
rel_path = path.lstrip(_os.sep)
|
|
71
|
+
base_dir = Path.get(base_dir=True)
|
|
72
|
+
search_dirs = (
|
|
73
|
+
_os.getcwd(),
|
|
74
|
+
base_dir,
|
|
75
|
+
_os.path.expanduser("~"),
|
|
76
|
+
_tempfile.gettempdir(),
|
|
77
|
+
)
|
|
78
|
+
if search_in:
|
|
79
|
+
search_dirs.extend([search_in] if isinstance(search_in, str) else search_in)
|
|
80
|
+
path_parts = rel_path.split(_os.sep)
|
|
81
|
+
for search_dir in search_dirs:
|
|
82
|
+
full_path = _os.path.join(search_dir, rel_path)
|
|
83
|
+
if _os.path.exists(full_path):
|
|
84
|
+
return full_path
|
|
85
|
+
match = find_path(search_dir, path_parts) if correct_path else None
|
|
86
|
+
if match:
|
|
87
|
+
return match
|
|
88
|
+
if raise_error:
|
|
89
|
+
raise FileNotFoundError(f"Path '{path}' not found in specified directories.")
|
|
90
|
+
return _os.path.join(search_dirs[0], rel_path)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def remove(path: str, only_content: bool = False) -> None:
|
|
94
|
+
if not _os.path.exists(path):
|
|
95
|
+
return None
|
|
96
|
+
if not only_content:
|
|
97
|
+
_shutil.rmtree(path)
|
|
98
|
+
elif _os.path.isdir(path):
|
|
99
|
+
for filename in _os.listdir(path):
|
|
100
|
+
file_path = _os.path.join(path, filename)
|
|
101
|
+
try:
|
|
102
|
+
if _os.path.isfile(file_path) or _os.path.islink(file_path):
|
|
103
|
+
_os.unlink(file_path)
|
|
104
|
+
elif _os.path.isdir(file_path):
|
|
105
|
+
_shutil.rmtree(file_path)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise Exception(f"Failed to delete {file_path}. Reason: {e}")
|