xulbux 1.5.5__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.
- xulbux/__help__.py +74 -0
- xulbux/__init__.py +47 -0
- xulbux/_consts_.py +147 -0
- xulbux/xx_cmd.py +240 -0
- xulbux/xx_code.py +102 -0
- xulbux/xx_color.py +799 -0
- xulbux/xx_data.py +431 -0
- xulbux/xx_env_vars.py +60 -0
- xulbux/xx_file.py +50 -0
- xulbux/xx_format_codes.py +212 -0
- xulbux/xx_json.py +81 -0
- xulbux/xx_path.py +97 -0
- xulbux/xx_regex.py +124 -0
- xulbux/xx_string.py +116 -0
- xulbux/xx_system.py +75 -0
- xulbux-1.5.5.dist-info/METADATA +97 -0
- xulbux-1.5.5.dist-info/RECORD +20 -0
- xulbux-1.5.5.dist-info/WHEEL +4 -0
- xulbux-1.5.5.dist-info/entry_points.txt +2 -0
- xulbux-1.5.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions to be able to use special (easy) formatting codes directly inside some message (string).<br>
|
|
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?<br>
|
|
9
|
+
**Example string with formatting codes:**<br>
|
|
10
|
+
> `[bold]This is bold text, [#F08]which is pink now [black|BG:#FF0088] and now it changed`<br>
|
|
11
|
+
> `to black with a pink background. [_]And this is the boring text, where everything is reset.`\n
|
|
12
|
+
⇾ **Instead of writing the formats all separate** `[…][…][…]` **you can join them like this** `[…|…|…]`\n
|
|
13
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
14
|
+
You can also automatically reset a certain format, behind text like shown in the following example:<br>
|
|
15
|
+
> `This is normal text [b](which is bold now) but now it was automatically reset to normal.`\n
|
|
16
|
+
This will only reset formats, that have a reset listed below. Colors and BG-colors won't be reset.<br>
|
|
17
|
+
This is what will happen, if you use it with a color-format:<br>
|
|
18
|
+
> `[cyan]This is cyan text [b](which is bold now.) Now it's not bold any more but still cyan.`\n
|
|
19
|
+
If you want to ignore the `()` brackets you can put a `\\` or `/` between:<br>
|
|
20
|
+
> `[cyan]This is cyan text [b]/(which is bold now.) And now it is still bold and cyan.`\n
|
|
21
|
+
⇾ **To see these examples in action, you can put them into the** `FormatCodes.print()` **function.**\n
|
|
22
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
23
|
+
**All possible formatting codes:**
|
|
24
|
+
- HEX colors: `[#F08]` or `[#FF0088]` (*with or without leading #*)
|
|
25
|
+
- RGB colors: `[rgb(255, 0, 136)]`
|
|
26
|
+
- bright colors: `[bright:#F08]`
|
|
27
|
+
- background colors: `[BG:#F08]`
|
|
28
|
+
- standard cmd colors:
|
|
29
|
+
- `[black]`
|
|
30
|
+
- `[red]`
|
|
31
|
+
- `[green]`
|
|
32
|
+
- `[yellow]`
|
|
33
|
+
- `[blue]`
|
|
34
|
+
- `[magenta]`
|
|
35
|
+
- `[cyan]`
|
|
36
|
+
- `[white]`
|
|
37
|
+
- bright cmd colors: `[bright:black]` or `[br:black]`, `[bright:red]` or `[br:red]`, ...
|
|
38
|
+
- background cmd colors: `[BG:black]`, `[BG:red]`, ...
|
|
39
|
+
- bright background cmd colors: `[BG:bright:black]` or `[BG:br:black]`, `[BG:bright:red]` or `[BG:br:red]`, ...<br>
|
|
40
|
+
⇾ **The order of** `BG:` **and** `bright:` or `br:` **does not matter.**
|
|
41
|
+
- text formats:
|
|
42
|
+
- `[bold]` or `[b]`
|
|
43
|
+
- `[dim]`
|
|
44
|
+
- `[italic]` or `[i]`
|
|
45
|
+
- `[underline]` or `[u]`
|
|
46
|
+
- `[inverse]`, `[invert]` or `[in]`
|
|
47
|
+
- `[hidden]`, `[hide]` or `[h]`
|
|
48
|
+
- `[strikethrough]` or `[s]`
|
|
49
|
+
- `[double-underline]` or `[du]`
|
|
50
|
+
- specific reset: `[_bold]` or `[_b]`, `[_dim]`, ... or `[_color]` or `[_c]`, `[_background]` or `[_bg]`
|
|
51
|
+
- total reset: `[_]` (only if no `default_color` is set, otherwise see **↓** )
|
|
52
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
53
|
+
**Special formatting when param `default_color` is set to a color:**
|
|
54
|
+
- `[*]` will reset everything, just like `[_]`, but the text-color will remain in `default_color`
|
|
55
|
+
- `[*color]` will reset the text-color, just like `[_color]`, but then also make it `default_color`
|
|
56
|
+
- `[default]` will just color the text in `default_color`,
|
|
57
|
+
- `[BG:default]` will color the background in `default_color`\n
|
|
58
|
+
Unlike the standard cmd colors, the default color can be changed by using the following modifiers:
|
|
59
|
+
- `[l]` will lighten the `default_color` text by `brightness_steps`%
|
|
60
|
+
- `[ll]` will lighten the `default_color` text by `2 × brightness_steps`%
|
|
61
|
+
- `[lll]` will lighten the `default_color` text by `3 × brightness_steps`%
|
|
62
|
+
- ... etc. Same thing for darkening:
|
|
63
|
+
- `[d]` will darken the `default_color` text by `brightness_steps`%
|
|
64
|
+
- `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
|
|
65
|
+
- `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
|
|
66
|
+
- ... etc.\n
|
|
67
|
+
Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.<br>
|
|
68
|
+
This can also be changed by changing the param `_modifiers = ('+l', '-d')`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
from ._consts_ import ANSI
|
|
73
|
+
from .xx_string import *
|
|
74
|
+
from .xx_regex import *
|
|
75
|
+
from .xx_color import *
|
|
76
|
+
from .xx_data import *
|
|
77
|
+
|
|
78
|
+
import ctypes as _ctypes
|
|
79
|
+
import regex as _rx
|
|
80
|
+
import sys as _sys
|
|
81
|
+
import re as _re
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FormatCodes:
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def print(*values:object, default_color:hexa|rgba = None, brightness_steps:int = 20, sep:str = ' ', end:str = '\n') -> None:
|
|
90
|
+
FormatCodes.__config_console()
|
|
91
|
+
_sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)), default_color, brightness_steps) + end)
|
|
92
|
+
_sys.stdout.flush()
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def input(prompt:object = '', default_color:hexa|rgba = None, brightness_steps:int = 20) -> str:
|
|
96
|
+
FormatCodes.__config_console()
|
|
97
|
+
return input(FormatCodes.to_ansi(prompt, default_color, brightness_steps))
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def to_ansi(string:str, default_color:hexa|rgba = None, brightness_steps:int = 20, _default_start:bool = True) -> str:
|
|
101
|
+
result, use_default = '', default_color and (Color.is_valid_rgba(default_color, False) or Color.is_valid_hexa(default_color, False))
|
|
102
|
+
if use_default:
|
|
103
|
+
string = _re.sub(r'\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]', r'[\1_|default\2]', string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
|
|
104
|
+
string = _re.sub(r'\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]', r'[\1default\2]', string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
|
|
105
|
+
def replace_keys(match:_rx.Match) -> str:
|
|
106
|
+
format_keys, esc, auto_reset_txt = match.group(1), match.group(2), match.group(3)
|
|
107
|
+
if not format_keys:
|
|
108
|
+
return match.group(0)
|
|
109
|
+
else:
|
|
110
|
+
format_keys = [k.replace(' ', '') for k in format_keys.split('|') if k.replace(' ', '')]
|
|
111
|
+
ansi_resets, ansi_formats = [], [FormatCodes.__get_replacement(k, default_color, brightness_steps) for k in format_keys]
|
|
112
|
+
if auto_reset_txt and not esc:
|
|
113
|
+
reset_keys = ['_color' if Color.is_valid(k) or k in ANSI.color_map
|
|
114
|
+
else '_bg' if (set(k.lower().split(':')) & {'bg', 'bright', 'br'} and len(k.split(':')) <= 3 and any(Color.is_valid(k[i:]) or k[i:] in ANSI.color_map for i in range(len(k))))
|
|
115
|
+
else f'_{k}' for k in format_keys]
|
|
116
|
+
ansi_resets = [r for k in reset_keys if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(f'{ANSI.char}{ANSI.start}')]
|
|
117
|
+
if not all(f.startswith(f'{ANSI.char}{ANSI.start}') for f in ansi_formats): return match.group(0)
|
|
118
|
+
return ''.join(ansi_formats) + ((f'({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})' if esc else auto_reset_txt) if auto_reset_txt else '') + ('' if esc else ''.join(ansi_resets))
|
|
119
|
+
result = '\n'.join(_rx.sub(Regex.brackets('[', ']', is_group=True) + r'(?:\s*([/\\]?)\s*' + Regex.brackets('(', ')', is_group=True) + r')?', replace_keys, line) for line in string.splitlines())
|
|
120
|
+
return (FormatCodes.__get_default_ansi(default_color) if _default_start else '') + result if use_default else result
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def __config_console() -> None:
|
|
124
|
+
_sys.stdout.flush()
|
|
125
|
+
kernel32 = _ctypes.windll.kernel32
|
|
126
|
+
h = kernel32.GetStdHandle(-11)
|
|
127
|
+
mode = _ctypes.c_ulong()
|
|
128
|
+
kernel32.GetConsoleMode(h, _ctypes.byref(mode))
|
|
129
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004) # ENABLE VIRTUAL TERMINAL PROCESSING
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def __get_default_ansi(default_color:hexa|rgba, format_key:str = None, brightness_steps:int = None, _modifiers:tuple[str,str] = ('+l', '-d')) -> str|None:
|
|
133
|
+
if Color.is_valid_hexa(default_color, False):
|
|
134
|
+
default_color = Color.to_rgba(default_color)
|
|
135
|
+
if not brightness_steps or (format_key and _re.search(r'(?i)((?:BG\s*:)?)\s*default', format_key)):
|
|
136
|
+
if format_key and _re.search(r'(?i)BG\s*:\s*default', format_key):
|
|
137
|
+
return ANSI.seq_bg_color.format(default_color[0], default_color[1], default_color[2])
|
|
138
|
+
return ANSI.seq_color.format(default_color[0], default_color[1], default_color[2])
|
|
139
|
+
match = _re.match(rf'(?i)((?:BG\s*:)?)\s*({"|".join([f"{_re.escape(m)}+" for m in _modifiers[0] + _modifiers[1]])})$', format_key)
|
|
140
|
+
if not match or not match.group(2):
|
|
141
|
+
return None
|
|
142
|
+
is_bg, modifier = match.group(1), match.group(2)
|
|
143
|
+
new_rgb, lighten, darken = None, None, None
|
|
144
|
+
for mod in _modifiers[0]:
|
|
145
|
+
lighten = String.get_repeated_symbol(modifier, mod)
|
|
146
|
+
if lighten and lighten > 0:
|
|
147
|
+
new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * lighten)
|
|
148
|
+
break
|
|
149
|
+
if not new_rgb:
|
|
150
|
+
for mod in _modifiers[1]:
|
|
151
|
+
darken = String.get_repeated_symbol(modifier, mod)
|
|
152
|
+
if darken and darken > 0:
|
|
153
|
+
new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * darken)
|
|
154
|
+
break
|
|
155
|
+
if new_rgb:
|
|
156
|
+
return ANSI.seq_bg_color.format(new_rgb[0], new_rgb[1], new_rgb[2]) if is_bg else ANSI.seq_color.format(new_rgb[0], new_rgb[1], new_rgb[2])
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def __get_replacement(format_key:str, default_color:hexa|rgba = None, brightness_steps:int = 20, _modifiers:tuple[str, str] = ('+l', '-d')) -> str:
|
|
160
|
+
"""Gives you the corresponding ANSI code for the given format key.<br>
|
|
161
|
+
If `default_color` is not `None`, the text color will be `default_color` if all formats<br>
|
|
162
|
+
are reset or you can get lighter or darker version of `default_color` (also as BG) by<br>
|
|
163
|
+
using one or more `_modifiers` symbols as a format key ()"""
|
|
164
|
+
def key_exists(key:str) -> bool:
|
|
165
|
+
for map_key in ANSI.codes_map:
|
|
166
|
+
if isinstance(map_key, tuple) and key in map_key:
|
|
167
|
+
return True
|
|
168
|
+
elif key == map_key:
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
171
|
+
def get_value(key:str) -> any:
|
|
172
|
+
for map_key in ANSI.codes_map:
|
|
173
|
+
if isinstance(map_key, tuple) and key in map_key:
|
|
174
|
+
return ANSI.codes_map[map_key]
|
|
175
|
+
elif key == map_key:
|
|
176
|
+
return ANSI.codes_map[map_key]
|
|
177
|
+
return None
|
|
178
|
+
use_default = default_color and (Color.is_valid_rgba(default_color, False) or Color.is_valid_hexa(default_color, False))
|
|
179
|
+
_format_key, format_key = format_key, FormatCodes.__normalize(format_key)
|
|
180
|
+
if use_default:
|
|
181
|
+
new_default_color = FormatCodes.__get_default_ansi(default_color, format_key, brightness_steps, _modifiers)
|
|
182
|
+
if new_default_color:
|
|
183
|
+
return new_default_color
|
|
184
|
+
if key_exists(format_key):
|
|
185
|
+
return ANSI.seq().format(get_value(format_key))
|
|
186
|
+
rgb_match = _re.match(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*', format_key)
|
|
187
|
+
hex_match = _re.match(r'(?i)\s*(BG\s*:)?\s*(?:#|0x)?([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3})\s*', format_key)
|
|
188
|
+
try:
|
|
189
|
+
if rgb_match:
|
|
190
|
+
is_bg = rgb_match.group(1)
|
|
191
|
+
r, g, b = map(int, rgb_match.groups()[1:])
|
|
192
|
+
if Color.is_valid_rgba((r, g, b)):
|
|
193
|
+
return ANSI.seq_bg_color.format(r, g, b) if is_bg else ANSI.seq_color.format(r, g, b)
|
|
194
|
+
elif hex_match:
|
|
195
|
+
is_bg = hex_match.group(1)
|
|
196
|
+
rgb = Color.to_rgba(hex_match.group(2))
|
|
197
|
+
return ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2]) if is_bg else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
|
|
198
|
+
except Exception: pass
|
|
199
|
+
return _format_key
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def __normalize(format_key:str) -> str:
|
|
203
|
+
"""Put the given format key in the correct format:<br>
|
|
204
|
+
`1` put `BG:` as first key-part<br>
|
|
205
|
+
`2` put `bright:` or `br:` as second key-part<br>
|
|
206
|
+
`3` put everything else behind<br>
|
|
207
|
+
`4` everything in lower case"""
|
|
208
|
+
format_key = format_key.replace(' ', '').lower().strip()
|
|
209
|
+
if ':' in format_key:
|
|
210
|
+
key_parts = format_key.split(':')
|
|
211
|
+
format_key = ('bg:' if 'bg' in key_parts else '') + ('bright:' if 'bright' in key_parts or 'br' in key_parts else '') + ''.join(Data.remove(key_parts, ['bg', 'bright', 'br']))
|
|
212
|
+
return format_key
|
xulbux/xx_json.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from .xx_data import *
|
|
2
|
+
from .xx_file import *
|
|
3
|
+
|
|
4
|
+
import json as _json
|
|
5
|
+
import os as _os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Json:
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def read(json_file:str, comment_start:str = '>>', comment_end:str = '<<', return_original:bool = False) -> dict|tuple[dict,dict]:
|
|
14
|
+
"""Read JSON files, ignoring comments.\n
|
|
15
|
+
-------------------------------------------------------------------------
|
|
16
|
+
If only `comment_start` is found at the beginning of an item,<br>
|
|
17
|
+
the whole item is counted as a comment and therefore ignored.<br>
|
|
18
|
+
If `comment_start` and `comment_end` are found inside an item,<br>
|
|
19
|
+
the the section from `comment_start` to `comment_end` is ignored.<br>
|
|
20
|
+
If `return_original` is set to `True`, the original JSON is returned<br>
|
|
21
|
+
additionally. (returns: `[processed_json, original_json]`)"""
|
|
22
|
+
file_path = File._make_path(json_file, 'json', prefer_base_dir=True)
|
|
23
|
+
with open(file_path, 'r') as f:
|
|
24
|
+
content = f.read()
|
|
25
|
+
try:
|
|
26
|
+
data = _json.loads(content)
|
|
27
|
+
except _json.JSONDecodeError as e:
|
|
28
|
+
raise ValueError(f"Error parsing JSON in '{file_path}': {str(e)}")
|
|
29
|
+
processed_data = Data.remove_comments(data, comment_start, comment_end)
|
|
30
|
+
if not processed_data:
|
|
31
|
+
raise ValueError(f"The JSON file '{file_path}' is empty or contains only comments.")
|
|
32
|
+
return (processed_data, data) if return_original else processed_data
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def create(content:dict, new_file:str = 'config', indent:int = 2, compactness:int = 1, force:bool = False) -> str:
|
|
36
|
+
file_path = File._make_path(new_file, 'json', prefer_base_dir=True)
|
|
37
|
+
if _os.path.exists(file_path) and not force:
|
|
38
|
+
with open(file_path, 'r', encoding='utf-8') as existing_f:
|
|
39
|
+
existing_content = _json.load(existing_f)
|
|
40
|
+
if existing_content == content:
|
|
41
|
+
raise FileExistsError(f'Already created this file. (nothing changed)')
|
|
42
|
+
raise FileExistsError('File already exists.')
|
|
43
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
44
|
+
f.write(Data.to_str(content, indent, compactness, as_json=True))
|
|
45
|
+
full_path = _os.path.abspath(file_path)
|
|
46
|
+
return full_path
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def update(json_file:str, update_values:str|list[str], comment_start:str = '>>', comment_end:str = '<<', sep:tuple[str,str] = ('->', '::')) -> None:
|
|
50
|
+
"""Function to easily update single/multiple values inside JSON files.\n
|
|
51
|
+
--------------------------------------------------------------------------------------------------------
|
|
52
|
+
The param `json_file` is the path to the JSON file or just the name of the JSON file to be updated.\n
|
|
53
|
+
--------------------------------------------------------------------------------------------------------
|
|
54
|
+
The param `update_values` is a sort of path (or a list of paths) to the value/s to be updated, with<br>
|
|
55
|
+
the new value at the end of the path.<br>
|
|
56
|
+
In this example:
|
|
57
|
+
```\n {
|
|
58
|
+
'healthy': {
|
|
59
|
+
'fruit': ['apples', 'bananas', 'oranges'],
|
|
60
|
+
'vegetables': ['carrots', 'broccoli', 'celery']
|
|
61
|
+
}
|
|
62
|
+
}\n```
|
|
63
|
+
... if you want to change the value of `'apples'` to `'strawberries'`, `update_values` would<br>
|
|
64
|
+
be `healthy->fruit->apples::strawberries` or if you don't know that the value to update<br>
|
|
65
|
+
is `apples` you can also use the position of the value, so `healthy->fruit->0::strawberries`.\n
|
|
66
|
+
⇾ **If the path from `update_values` doesn't exist, it will be created.**\n
|
|
67
|
+
--------------------------------------------------------------------------------------------------------
|
|
68
|
+
If only `comment_start` is found at the beginning of an item, the whole item is counted<br>
|
|
69
|
+
as a comment and therefore ignored. If `comment_start` and `comment_end` are found<br>
|
|
70
|
+
inside an item, the the section from `comment_start` to `comment_end` is ignored."""
|
|
71
|
+
if isinstance(update_values, str):
|
|
72
|
+
update_values = [update_values]
|
|
73
|
+
valid_entries = [(parts[0].strip(), parts[1]) for update_value in update_values if len(parts := update_value.split(str(sep[1]).strip())) == 2]
|
|
74
|
+
value_paths, new_values = (zip(*valid_entries) if valid_entries else ([], []))
|
|
75
|
+
processed_data, data = Json.read(json_file, comment_start, comment_end, return_original=True)
|
|
76
|
+
update = []
|
|
77
|
+
for value_path, new_value in zip(value_paths, new_values):
|
|
78
|
+
path_id = Data.get_path_id(processed_data, value_path)
|
|
79
|
+
update.append(f'{path_id}::{new_value}')
|
|
80
|
+
updated = Data.set_value_by_path_id(data, update)
|
|
81
|
+
Json.create(updated, json_file, force=True)
|
xulbux/xx_path.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
|
|
9
|
+
|
|
10
|
+
class Path:
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get(cwd:bool = False, base_dir:bool = False) -> str|list:
|
|
14
|
+
paths = []
|
|
15
|
+
if cwd:
|
|
16
|
+
paths.append(_os.getcwd())
|
|
17
|
+
if base_dir:
|
|
18
|
+
if getattr(_sys, 'frozen', False):
|
|
19
|
+
base_path = _os.path.dirname(_sys.executable)
|
|
20
|
+
else:
|
|
21
|
+
main_module = _sys.modules['__main__']
|
|
22
|
+
if hasattr(main_module, '__file__'):
|
|
23
|
+
base_path = _os.path.dirname(_os.path.abspath(main_module.__file__))
|
|
24
|
+
elif hasattr(main_module, '__spec__') and main_module.__spec__ and getattr(main_module.__spec__, 'origin', None):
|
|
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
|
+
def get_closest_match(dir:str, part:str) -> str|None:
|
|
36
|
+
try:
|
|
37
|
+
files_and_dirs = _os.listdir(dir)
|
|
38
|
+
matches = _difflib.get_close_matches(part, files_and_dirs, n=1, cutoff=0.6)
|
|
39
|
+
return matches[0] if matches else None
|
|
40
|
+
except:
|
|
41
|
+
return None
|
|
42
|
+
def find_path(start:str, parts:list[str]) -> str|None:
|
|
43
|
+
current = start
|
|
44
|
+
for part in parts:
|
|
45
|
+
if _os.path.isfile(current):
|
|
46
|
+
return current
|
|
47
|
+
closest_match = get_closest_match(current, part) if correct_path else part
|
|
48
|
+
current = _os.path.join(current, closest_match) if closest_match else None
|
|
49
|
+
if current is None:
|
|
50
|
+
return None
|
|
51
|
+
return current if _os.path.exists(current) and current != start else None
|
|
52
|
+
def expand_env_path(p:str) -> str:
|
|
53
|
+
if not '%' in p:
|
|
54
|
+
return p
|
|
55
|
+
parts = p.split('%')
|
|
56
|
+
for i in range(1, len(parts), 2):
|
|
57
|
+
if parts[i].upper() in _os.environ:
|
|
58
|
+
parts[i] = _os.environ[parts[i].upper()]
|
|
59
|
+
return ''.join(parts)
|
|
60
|
+
path = _os.path.normpath(expand_env_path(path))
|
|
61
|
+
if _os.path.isabs(path):
|
|
62
|
+
drive, rel_path = _os.path.splitdrive(path)
|
|
63
|
+
rel_path = rel_path.lstrip(_os.sep)
|
|
64
|
+
search_dirs = [drive + _os.sep] if drive else [_os.sep]
|
|
65
|
+
else:
|
|
66
|
+
rel_path = path.lstrip(_os.sep)
|
|
67
|
+
base_dir = Path.get(base_dir=True)
|
|
68
|
+
search_dirs = [_os.getcwd(), base_dir, _os.path.expanduser('~'), _tempfile.gettempdir()]
|
|
69
|
+
if search_in:
|
|
70
|
+
search_dirs.extend([search_in] if isinstance(search_in, str) else search_in)
|
|
71
|
+
path_parts = rel_path.split(_os.sep)
|
|
72
|
+
for search_dir in search_dirs:
|
|
73
|
+
full_path = _os.path.join(search_dir, rel_path)
|
|
74
|
+
if _os.path.exists(full_path):
|
|
75
|
+
return full_path
|
|
76
|
+
match = find_path(search_dir, path_parts) if correct_path else None
|
|
77
|
+
if match: return match
|
|
78
|
+
if raise_error:
|
|
79
|
+
raise FileNotFoundError(f'Path \'{path}\' not found in specified directories.')
|
|
80
|
+
return _os.path.join(search_dirs[0], rel_path)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def remove(path:str, only_content:bool = False) -> None:
|
|
84
|
+
if not _os.path.exists(path):
|
|
85
|
+
return None
|
|
86
|
+
if not only_content:
|
|
87
|
+
_shutil.rmtree(path)
|
|
88
|
+
elif _os.path.isdir(path):
|
|
89
|
+
for filename in _os.listdir(path):
|
|
90
|
+
file_path = _os.path.join(path, filename)
|
|
91
|
+
try:
|
|
92
|
+
if _os.path.isfile(file_path) or _os.path.islink(file_path):
|
|
93
|
+
_os.unlink(file_path)
|
|
94
|
+
elif _os.path.isdir(file_path):
|
|
95
|
+
_shutil.rmtree(file_path)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise Exception(f'Failed to delete {file_path}. Reason: {e}')
|
xulbux/xx_regex.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Really long regex code presets:<br>
|
|
3
|
+
`quotes` match everything inside quotes<br>
|
|
4
|
+
`brackets` match everything inside brackets<br>
|
|
5
|
+
`outside_strings` match the pattern but not inside strings<br>
|
|
6
|
+
`all_except` match everything except a certain pattern<br>
|
|
7
|
+
`func_call` match a function call<br>
|
|
8
|
+
`rgba_str` match an RGBA color<br>
|
|
9
|
+
`hsla_str` match a HSLA color
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import re as _re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Regex:
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def quotes() -> str:
|
|
22
|
+
"""Matches everything inside quotes. (Strings)\n
|
|
23
|
+
------------------------------------------------------------------------------------
|
|
24
|
+
Will create two named groups:<br>
|
|
25
|
+
**`quote`** the quote type (single or double)<br>
|
|
26
|
+
**`string`** everything inside the found quote pair\n
|
|
27
|
+
------------------------------------------------------------------------------------
|
|
28
|
+
**Attention:** Requires non standard library `regex` not standard library `re`!"""
|
|
29
|
+
return r'(?P<quote>[\'"])(?P<string>(?:\\.|(?!\g<quote>).)*?)\g<quote>'
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def brackets(bracket1:str = '(', bracket2:str = ')', is_group:bool = False) -> str:
|
|
33
|
+
"""Matches everything inside brackets, including other nested brackets.\n
|
|
34
|
+
------------------------------------------------------------------------------------
|
|
35
|
+
**Attention:** Requires non standard library `regex` not standard library `re`!"""
|
|
36
|
+
g, b1, b2 = '' if is_group else '?:', _re.escape(bracket1) if len(bracket1) == 1 else bracket1, _re.escape(bracket2) if len(bracket2) == 1 else bracket2
|
|
37
|
+
return rf'{b1}\s*({g}(?:[^{b1}{b2}"\']|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|{b1}(?:[^{b1}{b2}"\']|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|(?R))*{b2})*)\s*{b2}'
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def outside_strings(pattern:str = r'.*') -> str:
|
|
41
|
+
"""Matches the `pattern` only when it is not found inside a string (`'...'` or `"..."`)."""
|
|
42
|
+
return rf'(?<!["\'])(?:{pattern})(?!["\'])'
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def all_except(disallowed_pattern:str, ignore_pattern:str = '', is_group:bool = False) -> str:
|
|
46
|
+
"""Matches everything except `disallowed_pattern`, unless the `disallowed_pattern` is found inside a string (`'...'` or `"..."`).\n
|
|
47
|
+
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
48
|
+
The `ignore_pattern` is just always ignored. For example if `disallowed_pattern` is `>` and `ignore_pattern` is `->`, the `->`-arrows will be allowed, even though they have `>` in them.<br>
|
|
49
|
+
If `is_group` is `True`, you will be able to reference the matched content as a group (e.g. <code>match.group(<i>int</i>)</code> or <code>r'\\<i>int</i>'</code>)."""
|
|
50
|
+
return rf'({"" if is_group else "?:"}(?:(?!{ignore_pattern}).)*(?:(?!{Regex.outside_strings(disallowed_pattern)}).)*)'
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def func_call(func_name:str = None) -> str:
|
|
54
|
+
"""Match a function call<br>
|
|
55
|
+
**`1`** function name<br>
|
|
56
|
+
**`2`** the function's arguments\n
|
|
57
|
+
If no `func_name` is given, it will match any function call.\n
|
|
58
|
+
------------------------------------------------------------------------------------
|
|
59
|
+
**Attention:** Requires non standard library `regex` not standard library `re`!"""
|
|
60
|
+
return r'(?<=\b)(' + (func_name if func_name else r'[\w_]+') + r')\s*' + Regex.brackets("(", ")", is_group=True)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def rgba_str(fix_sep:str = ',', allow_alpha:bool = False) -> str:
|
|
64
|
+
"""Matches an RGBA color inside a string.\n
|
|
65
|
+
--------------------------------------------------------------------------------
|
|
66
|
+
The RGBA color can be in the formats (for `fix_sep = ','`):<br>
|
|
67
|
+
`rgba(r, g, b)`<br>
|
|
68
|
+
`rgba(r, g, b, a)` (if `allow_alpha=True`)<br>
|
|
69
|
+
`(r, g, b)`<br>
|
|
70
|
+
`(r, g, b, a)` (if `allow_alpha=True`)<br>
|
|
71
|
+
`r, g, b`<br>
|
|
72
|
+
`r, g, b, a` (if `allow_alpha=True`)\n
|
|
73
|
+
--------------------------------------------------------------------------------
|
|
74
|
+
### Valid ranges:<br>
|
|
75
|
+
`r` 0-255 (amount: red)<br>
|
|
76
|
+
`g` 0-255 (amount: green)<br>
|
|
77
|
+
`b` 0-255 (amount: blue)<br>
|
|
78
|
+
`a` 0-1 (float: opacity)\n
|
|
79
|
+
--------------------------------------------------------------------------------
|
|
80
|
+
If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
|
|
81
|
+
can be used to separate the RGB values, including just a space."""
|
|
82
|
+
if fix_sep in (None, ''):
|
|
83
|
+
fix_sep = r'[^0-9A-Z]'
|
|
84
|
+
else:
|
|
85
|
+
fix_sep = _re.escape(fix_sep)
|
|
86
|
+
rgb_part = rf'''((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
|
|
87
|
+
(?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
|
|
88
|
+
(?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))'''
|
|
89
|
+
return rf'''(?ix)
|
|
90
|
+
(?:rgb|rgba)?\s*(?:\(?\s*{rgb_part}
|
|
91
|
+
(?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
|
|
92
|
+
\s*\)?)''' if allow_alpha else rf'(?ix)(?:rgb|rgba)?\s*(?:\(?\s*{rgb_part}\s*\)?)'
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def hsla_str(fix_sep:str = ',', allow_alpha:bool = False) -> str:
|
|
96
|
+
"""Matches a HSLA color inside a string.\n
|
|
97
|
+
--------------------------------------------------------------------------------
|
|
98
|
+
The HSLA color can be in the formats (for `fix_sep = ','`):<br>
|
|
99
|
+
`hsla(h, s, l)`<br>
|
|
100
|
+
`hsla(h, s, l, a)` (if `allow_alpha=True`)<br>
|
|
101
|
+
`(h, s, l)`<br>
|
|
102
|
+
`(h, s, l, a)` (if `allow_alpha=True`)<br>
|
|
103
|
+
`h, s, l`<br>
|
|
104
|
+
`h, s, l, a` (if `allow_alpha=True`)\n
|
|
105
|
+
--------------------------------------------------------------------------------
|
|
106
|
+
### Valid ranges:<br>
|
|
107
|
+
`h` 0-360 (degrees: hue)<br>
|
|
108
|
+
`s` 0-100 (percentage: saturation)<br>
|
|
109
|
+
`l` 0-100 (percentage: lightness)<br>
|
|
110
|
+
`a` 0-1 (float: opacity)\n
|
|
111
|
+
--------------------------------------------------------------------------------
|
|
112
|
+
If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
|
|
113
|
+
can be used to separate the HSL values, including just a space."""
|
|
114
|
+
if fix_sep in (None, ''):
|
|
115
|
+
fix_sep = r'[^0-9A-Z]'
|
|
116
|
+
else:
|
|
117
|
+
fix_sep = _re.escape(fix_sep)
|
|
118
|
+
hsl_part = rf'''((?:0*(?:360|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])))
|
|
119
|
+
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))
|
|
120
|
+
(?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))'''
|
|
121
|
+
return rf'''(?ix)
|
|
122
|
+
(?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}
|
|
123
|
+
(?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
|
|
124
|
+
\s*\)?)''' if allow_alpha else rf'(?ix)(?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}\s*\)?)'
|
xulbux/xx_string.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import re as _re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class String:
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def to_type(value:str) -> any:
|
|
10
|
+
"""Will convert a string to a type."""
|
|
11
|
+
if value.lower() in ['true', 'false']: # BOOLEAN
|
|
12
|
+
return value.lower() == 'true'
|
|
13
|
+
elif value.lower() in ['none', 'null', 'undefined']: # NONE
|
|
14
|
+
return None
|
|
15
|
+
elif value.startswith('[') and value.endswith(']'): # LIST
|
|
16
|
+
return [String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip()]
|
|
17
|
+
elif value.startswith('(') and value.endswith(')'): # TUPLE
|
|
18
|
+
return tuple(String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip())
|
|
19
|
+
elif value.startswith('{') and value.endswith('}'): # SET
|
|
20
|
+
return {String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip()}
|
|
21
|
+
elif value.startswith('{') and value.endswith('}') and ':' in value: # DICTIONARY
|
|
22
|
+
return {String.to_type(k.strip()): String.to_type(v.strip()) for k, v in [item.split(':') for item in value[1:-1].split(',') if item.strip()]}
|
|
23
|
+
try: # NUMBER (INT OR FLOAT)
|
|
24
|
+
if '.' in value or 'e' in value.lower():
|
|
25
|
+
return float(value)
|
|
26
|
+
else:
|
|
27
|
+
return int(value)
|
|
28
|
+
except ValueError: pass
|
|
29
|
+
if value.startswith(("'", '"')) and value.endswith(("'", '"')): # STRING (WITH OR WITHOUT QUOTES)
|
|
30
|
+
return value[1:-1]
|
|
31
|
+
try: # COMPLEX
|
|
32
|
+
return complex(value)
|
|
33
|
+
except ValueError: pass
|
|
34
|
+
return value # IF NOTHING ELSE MATCHES, RETURN AS IS
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def get_repeated_symbol(string:str, symbol:str) -> int|bool:
|
|
38
|
+
"""If the string consists of one repeating `symbol`, it returns the number of times it is repeated.<br>
|
|
39
|
+
If the string doesn't consist of one repeating symbol, it returns `False`."""
|
|
40
|
+
if len(string) == len(symbol) * string.count(symbol):
|
|
41
|
+
return string.count(symbol)
|
|
42
|
+
else:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def decompose(case_string:str, seps:str = '-_', lower_all:bool = True) -> list[str]:
|
|
47
|
+
"""Will decompose the string (*any type of casing, also mixed*) into parts."""
|
|
48
|
+
return [(part.lower() if lower_all else part) for part in _re.split(rf'(?<=[a-z])(?=[A-Z])|[{seps}]', case_string)]
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def to_camel_case(string:str) -> str:
|
|
52
|
+
"""Will convert the string of any type of casing to camel case."""
|
|
53
|
+
return ''.join(part.capitalize() for part in String.decompose(string))
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def to_snake_case(string:str, sep:str = '_', screaming:bool = False) -> str:
|
|
57
|
+
"""Will convert the string of any type of casing to snake case."""
|
|
58
|
+
return sep.join(part.upper() if screaming else part for part in String.decompose(string))
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def get_string_lines(string:str, remove_empty_lines:bool = False) -> list[str]:
|
|
62
|
+
"""Will split the string into lines."""
|
|
63
|
+
if not remove_empty_lines:
|
|
64
|
+
return string.splitlines()
|
|
65
|
+
lines = string.splitlines()
|
|
66
|
+
if not lines:
|
|
67
|
+
return []
|
|
68
|
+
non_empty_lines = [line for line in lines if line.strip()]
|
|
69
|
+
if not non_empty_lines:
|
|
70
|
+
return []
|
|
71
|
+
return non_empty_lines
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def remove_consecutive_empty_lines(string:str, max_consecutive:int = 0) -> str:
|
|
75
|
+
"""Will remove consecutive empty lines from the string.\n
|
|
76
|
+
----------------------------------------------------------------------------------------------
|
|
77
|
+
If `max_consecutive` is `0`, it will remove all consecutive empty lines.<br>
|
|
78
|
+
If `max_consecutive` is bigger than `0`, it will only allow `max_consecutive` consecutive<br>
|
|
79
|
+
empty lines and everything above it will be cut down to `max_consecutive` empty lines."""
|
|
80
|
+
return _re.sub(r'(\n\s*){2,}', r'\1' * (max_consecutive + 1), string)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def split_every_chars(string:str, split:int) -> list[str]:
|
|
84
|
+
"""Will split the string every `split` characters."""
|
|
85
|
+
return [string[i:i + split] for i in range(0, len(string), split)]
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def multi_strip(string:str, strip_chars:str = ' _-') -> str:
|
|
89
|
+
"""Will remove all leading and trailing `strip_chars` from the string."""
|
|
90
|
+
for char in string:
|
|
91
|
+
if char in strip_chars:
|
|
92
|
+
string = string[1:]
|
|
93
|
+
else: break
|
|
94
|
+
for char in string[::-1]:
|
|
95
|
+
if char in strip_chars:
|
|
96
|
+
string = string[:-1]
|
|
97
|
+
else: break
|
|
98
|
+
return string
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def multi_lstrip(string:str, strip_chars:str = ' _-') -> str:
|
|
102
|
+
"""Will remove all leading `strip_chars` from the string."""
|
|
103
|
+
for char in string:
|
|
104
|
+
if char in strip_chars:
|
|
105
|
+
string = string[1:]
|
|
106
|
+
else: break
|
|
107
|
+
return string
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def multi_rstrip(string:str, strip_chars:str = ' _-') -> str:
|
|
111
|
+
"""Will remove all trailing `strip_chars` from the string."""
|
|
112
|
+
for char in string[::-1]:
|
|
113
|
+
if char in strip_chars:
|
|
114
|
+
string = string[:-1]
|
|
115
|
+
else: break
|
|
116
|
+
return string
|