xulbux 1.5.5__py3-none-any.whl → 1.5.8__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 +28 -18
- xulbux/{__help__.py → _cli_.py} +23 -45
- xulbux/_consts_.py +108 -111
- xulbux/xx_code.py +31 -28
- xulbux/xx_color.py +364 -205
- xulbux/xx_console.py +381 -0
- xulbux/xx_data.py +358 -261
- xulbux/xx_env_path.py +113 -0
- xulbux/xx_file.py +22 -16
- xulbux/xx_format_codes.py +156 -91
- xulbux/xx_json.py +38 -18
- xulbux/xx_path.py +38 -23
- xulbux/xx_regex.py +64 -29
- xulbux/xx_string.py +104 -47
- xulbux/xx_system.py +37 -26
- {xulbux-1.5.5.dist-info → xulbux-1.5.8.dist-info}/METADATA +23 -19
- xulbux-1.5.8.dist-info/RECORD +20 -0
- {xulbux-1.5.5.dist-info → xulbux-1.5.8.dist-info}/WHEEL +1 -1
- xulbux-1.5.8.dist-info/entry_points.txt +3 -0
- xulbux/xx_cmd.py +0 -240
- xulbux/xx_env_vars.py +0 -60
- xulbux-1.5.5.dist-info/RECORD +0 -20
- xulbux-1.5.5.dist-info/entry_points.txt +0 -2
- {xulbux-1.5.5.dist-info → xulbux-1.5.8.dist-info}/licenses/LICENSE +0 -0
xulbux/xx_data.py
CHANGED
|
@@ -1,184 +1,223 @@
|
|
|
1
|
+
from .xx_string import String
|
|
2
|
+
|
|
3
|
+
from typing import TypeAlias, Union
|
|
1
4
|
import math as _math
|
|
2
5
|
import re as _re
|
|
3
6
|
|
|
4
7
|
|
|
8
|
+
DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class Data:
|
|
8
12
|
|
|
9
13
|
@staticmethod
|
|
10
|
-
def chars_count(data:
|
|
11
|
-
"""The sum of all the characters including the keys in dictionaries."""
|
|
14
|
+
def chars_count(data: DataStructure) -> int:
|
|
15
|
+
"""The sum of all the characters amount including the keys in dictionaries."""
|
|
12
16
|
if isinstance(data, dict):
|
|
13
17
|
return sum(len(str(k)) + len(str(v)) for k, v in data.items())
|
|
14
18
|
return sum(len(str(item)) for item in data)
|
|
15
19
|
|
|
16
20
|
@staticmethod
|
|
17
|
-
def strip(data:
|
|
21
|
+
def strip(data: DataStructure) -> DataStructure:
|
|
22
|
+
"""Removes leading and trailing whitespaces from the data structure's items."""
|
|
18
23
|
if isinstance(data, dict):
|
|
19
|
-
return {k:
|
|
20
|
-
|
|
21
|
-
stripped = [item.strip() if isinstance(item, str) else Data.strip(item) for item in data]
|
|
22
|
-
return tuple(stripped) if isinstance(data, tuple) else stripped
|
|
23
|
-
return data.strip() if isinstance(data, str) else data
|
|
24
|
-
|
|
25
|
-
@staticmethod
|
|
26
|
-
def remove(data:list|tuple|dict, items:list[str]) -> list|tuple|dict:
|
|
27
|
-
"""Remove multiple items from lists and tuples or keys from dictionaries."""
|
|
28
|
-
if isinstance(data, (list, tuple)):
|
|
29
|
-
result = [k for k in data if k not in items]
|
|
30
|
-
return result if isinstance(data, list) else tuple(result)
|
|
31
|
-
elif isinstance(data, dict):
|
|
32
|
-
return {k: v for k, v in data.items() if k not in items}
|
|
24
|
+
return {k: Data.strip(v) for k, v in data.items()}
|
|
25
|
+
return type(data)(map(Data.strip, data))
|
|
33
26
|
|
|
34
27
|
@staticmethod
|
|
35
|
-
def remove_empty_items(data:
|
|
28
|
+
def remove_empty_items(data: DataStructure, spaces_are_empty: bool = False) -> DataStructure:
|
|
29
|
+
"""Removes empty items from the data structure.<br>
|
|
30
|
+
If `spaces_are_empty` is true, it will count items with only spaces as empty."""
|
|
36
31
|
if isinstance(data, dict):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
return {
|
|
33
|
+
k: (
|
|
34
|
+
v
|
|
35
|
+
if not isinstance(v, (list, tuple, set, frozenset, dict))
|
|
36
|
+
else Data.remove_empty_items(v, spaces_are_empty)
|
|
37
|
+
)
|
|
38
|
+
for k, v in data.items()
|
|
39
|
+
if not String.is_empty(v, spaces_are_empty)
|
|
40
|
+
}
|
|
41
|
+
if isinstance(data, (list, tuple, set, frozenset)):
|
|
42
|
+
return type(data)(
|
|
43
|
+
item
|
|
44
|
+
for item in (
|
|
45
|
+
(
|
|
46
|
+
item
|
|
47
|
+
if not isinstance(item, (list, tuple, set, frozenset, dict))
|
|
48
|
+
else Data.remove_empty_items(item, spaces_are_empty)
|
|
49
|
+
)
|
|
50
|
+
for item in data
|
|
51
|
+
if not String.is_empty(item, spaces_are_empty)
|
|
52
|
+
)
|
|
53
|
+
if item not in ((), {}, set(), frozenset())
|
|
54
|
+
)
|
|
55
|
+
return data
|
|
56
56
|
|
|
57
57
|
@staticmethod
|
|
58
|
-
def remove_duplicates(data:
|
|
58
|
+
def remove_duplicates(data: DataStructure) -> DataStructure:
|
|
59
|
+
"""Removes all duplicates from the data structure."""
|
|
59
60
|
if isinstance(data, dict):
|
|
60
61
|
return {k: Data.remove_duplicates(v) for k, v in data.items()}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
if isinstance(data, (list, tuple)):
|
|
63
|
+
return type(data)(
|
|
64
|
+
Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
|
|
65
|
+
for item in dict.fromkeys(data)
|
|
66
|
+
)
|
|
67
|
+
if isinstance(data, (set, frozenset)):
|
|
68
|
+
return type(data)(
|
|
69
|
+
Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
|
|
70
|
+
for item in data
|
|
71
|
+
)
|
|
71
72
|
return data
|
|
72
73
|
|
|
73
74
|
@staticmethod
|
|
74
|
-
def remove_comments(
|
|
75
|
+
def remove_comments(
|
|
76
|
+
data: DataStructure,
|
|
77
|
+
comment_start: str = ">>",
|
|
78
|
+
comment_end: str = "<<",
|
|
79
|
+
comment_sep: str = "",
|
|
80
|
+
) -> DataStructure:
|
|
75
81
|
"""Remove comments from a list, tuple or dictionary.\n
|
|
76
|
-
|
|
82
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
77
83
|
The `data` parameter is your list, tuple or dictionary, where the comments should get removed from.<br>
|
|
78
84
|
The `comment_start` parameter is the string that marks the start of a comment inside `data`. (default: `>>`)<br>
|
|
79
85
|
The `comment_end` parameter is the string that marks the end of a comment inside `data`. (default: `<<`)<br>
|
|
80
|
-
The `comment_sep` parameter is a string with which a comment will be replaced, if it is
|
|
81
|
-
|
|
86
|
+
The `comment_sep` parameter is a string with which a comment will be replaced, if it is in the middle of a value.\n
|
|
87
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
82
88
|
Examples:\n
|
|
83
89
|
```python\n data = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
"key1": [
|
|
91
|
+
">> COMMENT IN THE BEGINNING OF THE STRING << value1",
|
|
92
|
+
"value2 >> COMMENT IN THE END OF THE STRING",
|
|
93
|
+
"val>> COMMENT IN THE MIDDLE OF THE STRING <<ue3",
|
|
94
|
+
">> FULL VALUE IS A COMMENT value4"
|
|
89
95
|
],
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
">> FULL KEY + ALL ITS VALUES ARE A COMMENT key2": [
|
|
97
|
+
"value",
|
|
98
|
+
"value",
|
|
99
|
+
"value"
|
|
94
100
|
],
|
|
95
|
-
|
|
101
|
+
"key3": ">> ALL THE KEYS VALUES ARE COMMENTS value"
|
|
96
102
|
}
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
|
|
104
|
+
processed_data = Data.remove_comments(
|
|
105
|
+
data,
|
|
106
|
+
comment_start=">>",
|
|
107
|
+
comment_end="<<",
|
|
108
|
+
comment_sep="__"
|
|
109
|
+
)\n```
|
|
110
|
+
--------------------------------------------------------------------------------------------------------------------
|
|
99
111
|
For this example, `processed_data` will be:
|
|
100
112
|
```python\n {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
"key1": [
|
|
114
|
+
"value1",
|
|
115
|
+
"value2",
|
|
116
|
+
"val__ue3"
|
|
105
117
|
],
|
|
106
|
-
|
|
118
|
+
"key3": None
|
|
107
119
|
}\n```
|
|
108
120
|
For `key1`, all the comments will just be removed, except at `value3` and `value4`:<br>
|
|
109
121
|
`value3` The comment is removed and the parts left and right are joined through `comment_sep`.<br>
|
|
110
122
|
`value4` The whole value is removed, since the whole value was a comment.<br>
|
|
111
123
|
For `key2`, the key, including its whole values will be removed.<br>
|
|
112
|
-
For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
127
|
-
elif isinstance(item, list):
|
|
128
|
-
return [v for v in (process_item(val) for val in item) if v is not None]
|
|
129
|
-
elif isinstance(item, tuple):
|
|
130
|
-
return tuple(v for v in (process_item(val) for val in item) if v is not None)
|
|
131
|
-
elif isinstance(item, str):
|
|
132
|
-
if comment_end:
|
|
133
|
-
no_comments = _re.sub(rf'^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$',
|
|
134
|
-
lambda m: f'{m.group(1).strip()}{comment_sep if (m.group(1).strip() not in ["", None]) and (m.group(2).strip() not in ["", None]) else ""}{m.group(2).strip()}', item)
|
|
135
|
-
else:
|
|
136
|
-
no_comments = None if item.lstrip().startswith(comment_start) else item
|
|
137
|
-
return no_comments.strip() if no_comments and no_comments.strip() != '' else None
|
|
124
|
+
For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if comment_end:
|
|
128
|
+
pattern = _re.compile(
|
|
129
|
+
rf"^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def process_string(s: str) -> str | None:
|
|
133
|
+
if comment_end:
|
|
134
|
+
match = pattern.match(s)
|
|
135
|
+
if match:
|
|
136
|
+
start, end = match.group(1).strip(), match.group(2).strip()
|
|
137
|
+
return f"{start}{comment_sep if start and end else ''}{end}" or None
|
|
138
|
+
return s.strip() or None
|
|
138
139
|
else:
|
|
139
|
-
return
|
|
140
|
+
return None if s.lstrip().startswith(comment_start) else s.strip() or None
|
|
141
|
+
|
|
142
|
+
def process_item(item: any) -> any:
|
|
143
|
+
if isinstance(item, dict):
|
|
144
|
+
return {
|
|
145
|
+
k: v for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
|
|
146
|
+
}
|
|
147
|
+
if isinstance(item, (list, tuple, set, frozenset)):
|
|
148
|
+
processed = (v for v in map(process_item, item) if v is not None)
|
|
149
|
+
return type(item)(processed)
|
|
150
|
+
if isinstance(item, str):
|
|
151
|
+
return process_string(item)
|
|
152
|
+
return item
|
|
153
|
+
|
|
140
154
|
return process_item(data)
|
|
141
155
|
|
|
142
156
|
@staticmethod
|
|
143
|
-
def is_equal(
|
|
157
|
+
def is_equal(
|
|
158
|
+
data1: DataStructure,
|
|
159
|
+
data2: DataStructure,
|
|
160
|
+
ignore_paths: str | list[str] = "",
|
|
161
|
+
path_sep: str = "->",
|
|
162
|
+
comment_start: str = ">>",
|
|
163
|
+
comment_end: str = "<<",
|
|
164
|
+
) -> bool:
|
|
144
165
|
"""Compares two structures and returns `True` if they are equal and `False` otherwise.\n
|
|
145
166
|
⇾ **Will not detect, if a key-name has changed, only if removed or added.**\n
|
|
146
167
|
------------------------------------------------------------------------------------------------
|
|
147
168
|
Ignores the specified (found) key/s or item/s from `ignore_paths`. Comments are not ignored<br>
|
|
148
|
-
when comparing. `comment_start` and `comment_end` are only used
|
|
169
|
+
when comparing. `comment_start` and `comment_end` are only used to correctly recognize the<br>
|
|
170
|
+
keys in the `ignore_paths`.\n
|
|
149
171
|
------------------------------------------------------------------------------------------------
|
|
150
|
-
The paths from `ignore_paths` work exactly the same way as
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def process_ignore_paths(
|
|
172
|
+
The paths from `ignore_paths` and the `path_sep` parameter work exactly the same way as for<br>
|
|
173
|
+
the function `Data.get_path_id()`. See its documentation for more details."""
|
|
174
|
+
|
|
175
|
+
def process_ignore_paths(
|
|
176
|
+
ignore_paths: str | list[str],
|
|
177
|
+
) -> list[list[str]]:
|
|
154
178
|
if isinstance(ignore_paths, str):
|
|
155
179
|
ignore_paths = [ignore_paths]
|
|
156
|
-
return [path.split(
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
return [path.split(path_sep) for path in ignore_paths if path]
|
|
181
|
+
|
|
182
|
+
def compare(
|
|
183
|
+
d1: DataStructure,
|
|
184
|
+
d2: DataStructure,
|
|
185
|
+
ignore_paths: list[list[str]],
|
|
186
|
+
current_path: list[str] = [],
|
|
187
|
+
) -> bool:
|
|
188
|
+
if any(current_path == path[: len(current_path)] for path in ignore_paths):
|
|
159
189
|
return True
|
|
160
|
-
if
|
|
190
|
+
if type(d1) != type(d2):
|
|
191
|
+
return False
|
|
192
|
+
if isinstance(d1, dict):
|
|
161
193
|
if set(d1.keys()) != set(d2.keys()):
|
|
162
194
|
return False
|
|
163
195
|
return all(compare(d1[key], d2[key], ignore_paths, current_path + [key]) for key in d1)
|
|
164
|
-
|
|
196
|
+
if isinstance(d1, (list, tuple)):
|
|
165
197
|
if len(d1) != len(d2):
|
|
166
198
|
return False
|
|
167
|
-
return all(
|
|
168
|
-
|
|
199
|
+
return all(
|
|
200
|
+
compare(item1, item2, ignore_paths, current_path + [str(i)])
|
|
201
|
+
for i, (item1, item2) in enumerate(zip(d1, d2))
|
|
202
|
+
)
|
|
203
|
+
if isinstance(d1, (set, frozenset)):
|
|
169
204
|
return d1 == d2
|
|
170
|
-
|
|
205
|
+
return d1 == d2
|
|
171
206
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
elif isinstance(data, (list, tuple)):
|
|
177
|
-
return {i: type(v).__name__ for i, v in enumerate(data)}
|
|
178
|
-
return None
|
|
207
|
+
processed_data1 = Data.remove_comments(data1, comment_start, comment_end)
|
|
208
|
+
processed_data2 = Data.remove_comments(data2, comment_start, comment_end)
|
|
209
|
+
processed_ignore_paths = process_ignore_paths(ignore_paths)
|
|
210
|
+
return compare(processed_data1, processed_data2, processed_ignore_paths)
|
|
179
211
|
|
|
180
212
|
@staticmethod
|
|
181
|
-
def get_path_id(
|
|
213
|
+
def get_path_id(
|
|
214
|
+
data: DataStructure,
|
|
215
|
+
value_paths: str | list[str],
|
|
216
|
+
path_sep: str = "->",
|
|
217
|
+
comment_start: str = ">>",
|
|
218
|
+
comment_end: str = "<<",
|
|
219
|
+
ignore_not_found: bool = False,
|
|
220
|
+
) -> str | list[str]:
|
|
182
221
|
"""Generates a unique ID based on the path to a specific value within a nested data structure.\n
|
|
183
222
|
-------------------------------------------------------------------------------------------------
|
|
184
223
|
The `data` parameter is the list, tuple, or dictionary, which the id should be generated for.\n
|
|
@@ -186,79 +225,78 @@ class Data:
|
|
|
186
225
|
The param `value_path` is a sort of path (or a list of paths) to the value/s to be updated.<br>
|
|
187
226
|
In this example:
|
|
188
227
|
```\n {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
"healthy": {
|
|
229
|
+
"fruit": ["apples", "bananas", "oranges"],
|
|
230
|
+
"vegetables": ["carrots", "broccoli", "celery"]
|
|
231
|
+
}
|
|
193
232
|
}\n```
|
|
194
|
-
... if you want to change the value of `
|
|
195
|
-
would be `healthy->fruit->apples` or if you don't know that the value is `apples`<br>
|
|
196
|
-
you can also use the
|
|
233
|
+
... if you want to change the value of `"apples"` to `"strawberries"`, the value path<br>
|
|
234
|
+
would be `healthy->fruit->apples` or if you don't know that the value is `"apples"`<br>
|
|
235
|
+
you can also use the index of the value, so `healthy->fruit->0`.\n
|
|
236
|
+
-------------------------------------------------------------------------------------------------
|
|
237
|
+
The comments marked with `comment_start` and `comment_end` will be removed,<br>
|
|
238
|
+
before trying to get the path id.\n
|
|
197
239
|
-------------------------------------------------------------------------------------------------
|
|
198
|
-
The `
|
|
240
|
+
The `path_sep` param is the separator between the keys/indexes in the path<br>
|
|
199
241
|
(default is `->` just like in the example above).\n
|
|
200
242
|
-------------------------------------------------------------------------------------------------
|
|
201
243
|
If `ignore_not_found` is `True`, the function will return `None` if the value is not<br>
|
|
202
244
|
found instead of raising an error."""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
245
|
+
|
|
246
|
+
def process_path(path: str, data_obj: list | tuple | set | frozenset | dict) -> str | None:
|
|
247
|
+
keys = path.split(path_sep)
|
|
248
|
+
path_ids = []
|
|
249
|
+
max_id_length = 0
|
|
250
|
+
for key in keys:
|
|
251
|
+
if isinstance(data_obj, dict):
|
|
252
|
+
if key.isdigit():
|
|
253
|
+
if ignore_not_found:
|
|
254
|
+
return None
|
|
255
|
+
raise TypeError(f"Key '{key}' is invalid for a dict type.")
|
|
256
|
+
try:
|
|
257
|
+
idx = list(data_obj.keys()).index(key)
|
|
258
|
+
data_obj = data_obj[key]
|
|
259
|
+
except (ValueError, KeyError):
|
|
260
|
+
if ignore_not_found:
|
|
261
|
+
return None
|
|
262
|
+
raise KeyError(f"Key '{key}' not found in dict.")
|
|
263
|
+
elif isinstance(data_obj, (list, tuple, set, frozenset)):
|
|
264
|
+
try:
|
|
265
|
+
idx = int(key)
|
|
266
|
+
data_obj = list(data_obj)[idx] # CONVERT TO LIST FOR INDEXING
|
|
267
|
+
except ValueError:
|
|
223
268
|
try:
|
|
224
|
-
idx =
|
|
225
|
-
|
|
226
|
-
_obj = _obj[idx]
|
|
269
|
+
idx = list(data_obj).index(key)
|
|
270
|
+
data_obj = list(data_obj)[idx]
|
|
227
271
|
except ValueError:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
path_ids.append(None)
|
|
245
|
-
except (KeyError, ValueError, TypeError) as e:
|
|
246
|
-
if ignore_not_found:
|
|
247
|
-
path_ids.append(None)
|
|
248
|
-
else: raise e
|
|
249
|
-
return path_ids if len(path_ids) > 1 else path_ids[0] if len(path_ids) == 1 else None
|
|
272
|
+
if ignore_not_found:
|
|
273
|
+
return None
|
|
274
|
+
raise ValueError(f"Value '{key}' not found in '{type(data_obj).__name__}'")
|
|
275
|
+
else:
|
|
276
|
+
break
|
|
277
|
+
path_ids.append(str(idx))
|
|
278
|
+
max_id_length = max(max_id_length, len(str(idx)))
|
|
279
|
+
if not path_ids:
|
|
280
|
+
return None
|
|
281
|
+
return f"{max_id_length}>{''.join(id.zfill(max_id_length) for id in path_ids)}"
|
|
282
|
+
|
|
283
|
+
data = Data.remove_comments(data, comment_start, comment_end)
|
|
284
|
+
if isinstance(value_paths, str):
|
|
285
|
+
return process_path(value_paths, data)
|
|
286
|
+
results = [process_path(path, data) for path in value_paths]
|
|
287
|
+
return results if len(results) > 1 else results[0] if results else None
|
|
250
288
|
|
|
251
289
|
@staticmethod
|
|
252
|
-
def get_value_by_path_id(data:
|
|
290
|
+
def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) -> any:
|
|
253
291
|
"""Retrieves the value from `data` using the provided `path_id`.\n
|
|
254
|
-
|
|
255
|
-
Input
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def get_nested(data:list|tuple|dict, path:list[int], get_key:bool) -> any:
|
|
292
|
+
----------------------------------------------------------------------------------------------------
|
|
293
|
+
Input your `data` along with a `path_id` that was created before using `Data.get_path_id()`.<br>
|
|
294
|
+
If `get_key` is true and the final item is in a dict, it returns the key instead of the value.\n
|
|
295
|
+
----------------------------------------------------------------------------------------------------
|
|
296
|
+
The function will return the value (or key) from the path ID location, as long as the structure<br>
|
|
297
|
+
of `data` hasn't changed since creating the path ID to that value."""
|
|
298
|
+
|
|
299
|
+
def get_nested(data: list | tuple | set | frozenset | dict, path: list[int], get_key: bool) -> any:
|
|
262
300
|
parent = None
|
|
263
301
|
for i, idx in enumerate(path):
|
|
264
302
|
if isinstance(data, dict):
|
|
@@ -267,75 +305,109 @@ class Data:
|
|
|
267
305
|
return keys[idx]
|
|
268
306
|
parent = data
|
|
269
307
|
data = data[keys[idx]]
|
|
270
|
-
elif isinstance(data, (list, tuple)):
|
|
308
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
271
309
|
if i == len(path) - 1 and get_key:
|
|
272
310
|
if parent is None or not isinstance(parent, dict):
|
|
273
|
-
raise ValueError(
|
|
311
|
+
raise ValueError("Cannot get key from a non-dict parent")
|
|
274
312
|
return next(key for key, value in parent.items() if value is data)
|
|
275
313
|
parent = data
|
|
276
|
-
data = data[idx]
|
|
314
|
+
data = list(data)[idx] # CONVERT TO LIST FOR INDEXING
|
|
277
315
|
else:
|
|
278
|
-
raise TypeError(f
|
|
316
|
+
raise TypeError(f"Unsupported type '{type(data)}' at path '{path[:i+1]}'")
|
|
279
317
|
return data
|
|
280
|
-
|
|
281
|
-
return get_nested(data,
|
|
318
|
+
|
|
319
|
+
return get_nested(data, Data.__sep_path_id(path_id), get_key)
|
|
282
320
|
|
|
283
321
|
@staticmethod
|
|
284
|
-
def set_value_by_path_id(
|
|
322
|
+
def set_value_by_path_id(
|
|
323
|
+
data: DataStructure,
|
|
324
|
+
update_values: str | list[str],
|
|
325
|
+
sep: str = "::",
|
|
326
|
+
) -> list | tuple | dict:
|
|
285
327
|
"""Updates the value/s from `update_values` in the `data`.\n
|
|
286
328
|
--------------------------------------------------------------------------------
|
|
287
329
|
Input a list, tuple or dict as `data`, along with `update_values`, which is<br>
|
|
288
|
-
a path
|
|
289
|
-
with the new value to be inserted where the path
|
|
330
|
+
a path ID that was created before using `Data.get_path_id()`, together<br>
|
|
331
|
+
with the new value to be inserted where the path ID points to. The path ID<br>
|
|
290
332
|
and the new value are separated by `sep`, which per default is `::`.\n
|
|
291
333
|
--------------------------------------------------------------------------------
|
|
292
|
-
The value from path
|
|
293
|
-
structure of `data` hasn't changed since creating the path
|
|
294
|
-
|
|
334
|
+
The value from path ID will be changed to the new value, as long as the<br>
|
|
335
|
+
structure of `data` hasn't changed since creating the path ID to that value."""
|
|
336
|
+
|
|
337
|
+
def update_nested(
|
|
338
|
+
data: list | tuple | set | frozenset | dict, path: list[int], value: any
|
|
339
|
+
) -> list | tuple | set | frozenset | dict:
|
|
295
340
|
if len(path) == 1:
|
|
296
341
|
if isinstance(data, dict):
|
|
297
342
|
keys = list(data.keys())
|
|
343
|
+
data = dict(data)
|
|
298
344
|
data[keys[path[0]]] = value
|
|
299
|
-
elif isinstance(data, (list, tuple)):
|
|
345
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
300
346
|
data = list(data)
|
|
301
347
|
data[path[0]] = value
|
|
302
348
|
data = type(data)(data)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
else:
|
|
350
|
+
if isinstance(data, dict):
|
|
351
|
+
keys = list(data.keys())
|
|
352
|
+
key = keys[path[0]]
|
|
353
|
+
data = dict(data)
|
|
354
|
+
data[key] = update_nested(data[key], path[1:], value)
|
|
355
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
356
|
+
data = list(data)
|
|
357
|
+
data[path[0]] = update_nested(data[path[0]], path[1:], value)
|
|
358
|
+
data = type(data)(data)
|
|
311
359
|
return data
|
|
360
|
+
|
|
312
361
|
if isinstance(update_values, str):
|
|
313
362
|
update_values = [update_values]
|
|
314
|
-
valid_entries = [
|
|
363
|
+
valid_entries = [
|
|
364
|
+
(parts[0].strip(), parts[1])
|
|
365
|
+
for update_value in update_values
|
|
366
|
+
if len(parts := update_value.split(str(sep).strip())) == 2
|
|
367
|
+
]
|
|
315
368
|
if not valid_entries:
|
|
316
|
-
raise ValueError(f
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
path = Data._sep_path_id(path_id)
|
|
369
|
+
raise ValueError(f"No valid update_values found: {update_values}")
|
|
370
|
+
for path_id, new_val in valid_entries:
|
|
371
|
+
path = Data.__sep_path_id(path_id)
|
|
320
372
|
data = update_nested(data, path, new_val)
|
|
321
373
|
return data
|
|
322
374
|
|
|
323
375
|
@staticmethod
|
|
324
|
-
def print(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
376
|
+
def print(
|
|
377
|
+
data: DataStructure,
|
|
378
|
+
indent: int = 4,
|
|
379
|
+
compactness: int = 1,
|
|
380
|
+
sep: str = ", ",
|
|
381
|
+
max_width: int = 127,
|
|
382
|
+
as_json: bool = False,
|
|
383
|
+
end: str = "\n",
|
|
384
|
+
) -> None:
|
|
385
|
+
"""Print nicely formatted data structures.\n
|
|
386
|
+
------------------------------------------------------------------------------------
|
|
387
|
+
The indentation spaces-amount can be set with with `indent`.<br>
|
|
388
|
+
There are three different levels of `compactness`:<br>
|
|
389
|
+
`0` expands everything possible<br>
|
|
390
|
+
`1` only expands if there's other lists, tuples or dicts inside of data or,<br>
|
|
391
|
+
⠀if the data's content is longer than `max_width`<br>
|
|
392
|
+
`2` keeps everything collapsed (all on one line)\n
|
|
393
|
+
------------------------------------------------------------------------------------
|
|
394
|
+
If `as_json` is set to `True`, the output will be in valid JSON format.
|
|
395
|
+
"""
|
|
396
|
+
print(
|
|
397
|
+
Data.to_str(data, indent, compactness, sep, max_width, as_json),
|
|
398
|
+
end=end,
|
|
399
|
+
flush=True,
|
|
400
|
+
)
|
|
336
401
|
|
|
337
402
|
@staticmethod
|
|
338
|
-
def to_str(
|
|
403
|
+
def to_str(
|
|
404
|
+
data: DataStructure,
|
|
405
|
+
indent: int = 4,
|
|
406
|
+
compactness: int = 1,
|
|
407
|
+
sep: str = ", ",
|
|
408
|
+
max_width: int = 127,
|
|
409
|
+
as_json: bool = False,
|
|
410
|
+
) -> str:
|
|
339
411
|
"""Get nicely formatted data structure-strings.\n
|
|
340
412
|
------------------------------------------------------------------------------------
|
|
341
413
|
The indentation spaces-amount can be set with with `indent`.<br>
|
|
@@ -346,86 +418,111 @@ class Data:
|
|
|
346
418
|
`2` keeps everything collapsed (all on one line)\n
|
|
347
419
|
------------------------------------------------------------------------------------
|
|
348
420
|
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if str_quotes == '"': s = s.replace(r"\\'", "'").replace(r'"', r'\"')
|
|
352
|
-
elif str_quotes == "'": s = s.replace(r'\\"', '"').replace(r"'", r"\'")
|
|
353
|
-
return s
|
|
354
|
-
def format_value(value:any, current_indent:int) -> str:
|
|
421
|
+
|
|
422
|
+
def format_value(value: any, current_indent: int) -> str:
|
|
355
423
|
if isinstance(value, dict):
|
|
356
424
|
return format_dict(value, current_indent + indent)
|
|
357
|
-
elif hasattr(value,
|
|
425
|
+
elif hasattr(value, "__dict__"):
|
|
358
426
|
return format_dict(value.__dict__, current_indent + indent)
|
|
359
427
|
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
360
428
|
return format_sequence(value, current_indent + indent)
|
|
361
429
|
elif isinstance(value, bool):
|
|
362
430
|
return str(value).lower() if as_json else str(value)
|
|
363
431
|
elif isinstance(value, (int, float)):
|
|
364
|
-
return
|
|
432
|
+
return "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
|
|
365
433
|
elif isinstance(value, complex):
|
|
366
|
-
return f
|
|
434
|
+
return f"[{value.real}, {value.imag}]" if as_json else str(value)
|
|
367
435
|
elif value is None:
|
|
368
|
-
return
|
|
436
|
+
return "null" if as_json else "None"
|
|
369
437
|
else:
|
|
370
|
-
return '"' +
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if compactness ==
|
|
438
|
+
return '"' + String.escape(str(value), '"') + '"' if as_json else "'" + String.escape(str(value), "'") + "'"
|
|
439
|
+
|
|
440
|
+
def should_expand(seq: list | tuple | dict) -> bool:
|
|
441
|
+
if compactness == 0:
|
|
442
|
+
return True
|
|
443
|
+
if compactness == 2:
|
|
444
|
+
return False
|
|
374
445
|
complex_items = sum(1 for item in seq if isinstance(item, (list, tuple, dict, set, frozenset)))
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
446
|
+
return (
|
|
447
|
+
complex_items > 1
|
|
448
|
+
or (complex_items == 1 and len(seq) > 1)
|
|
449
|
+
or Data.chars_count(seq) + (len(seq) * len(sep)) > max_width
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def format_key(k: any) -> str:
|
|
453
|
+
return (
|
|
454
|
+
'"' + String.escape(str(k), '"') + '"'
|
|
455
|
+
if as_json
|
|
456
|
+
else ("'" + String.escape(str(k), "'") + "'" if isinstance(k, str) else str(k))
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def format_dict(d: dict, current_indent: int) -> str:
|
|
379
460
|
if not d or compactness == 2:
|
|
380
|
-
return
|
|
461
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
381
462
|
if not should_expand(d.values()):
|
|
382
|
-
return
|
|
463
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
383
464
|
items = []
|
|
384
465
|
for key, value in d.items():
|
|
385
466
|
formatted_value = format_value(value, current_indent)
|
|
386
467
|
items.append(f'{" " * (current_indent + indent)}{format_key(key)}: {formatted_value}')
|
|
387
|
-
return
|
|
388
|
-
|
|
468
|
+
return "{\n" + ",\n".join(items) + f'\n{" " * current_indent}}}'
|
|
469
|
+
|
|
470
|
+
def format_sequence(seq, current_indent: int) -> str:
|
|
389
471
|
if as_json:
|
|
390
472
|
seq = list(seq)
|
|
391
473
|
if not seq or compactness == 2:
|
|
392
|
-
return
|
|
474
|
+
return (
|
|
475
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
476
|
+
if isinstance(seq, list)
|
|
477
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
478
|
+
)
|
|
393
479
|
if not should_expand(seq):
|
|
394
|
-
return
|
|
480
|
+
return (
|
|
481
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
482
|
+
if isinstance(seq, list)
|
|
483
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
484
|
+
)
|
|
395
485
|
items = [format_value(item, current_indent) for item in seq]
|
|
396
|
-
formatted_items =
|
|
486
|
+
formatted_items = ",\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
|
|
397
487
|
if isinstance(seq, list):
|
|
398
|
-
return
|
|
488
|
+
return "[\n" + formatted_items + f'\n{" " * current_indent}]'
|
|
399
489
|
else:
|
|
400
|
-
return
|
|
490
|
+
return "(\n" + formatted_items + f'\n{" " * current_indent})'
|
|
491
|
+
|
|
401
492
|
return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
|
|
402
493
|
|
|
403
494
|
@staticmethod
|
|
404
|
-
def _is_key(data:
|
|
405
|
-
"""Returns `True` if the path
|
|
495
|
+
def _is_key(data: DataStructure, path_id: str) -> bool:
|
|
496
|
+
"""Returns `True` if the path ID points to a key in `data` and `False` otherwise.\n
|
|
406
497
|
------------------------------------------------------------------------------------
|
|
407
|
-
Input a list, tuple or dict as `data`, along with `path_id`, which is a path
|
|
408
|
-
that was created before using `
|
|
409
|
-
|
|
498
|
+
Input a list, tuple or dict as `data`, along with `path_id`, which is a path ID<br>
|
|
499
|
+
that was created before using `Data.get_path_id()`."""
|
|
500
|
+
|
|
501
|
+
def check_nested(data: list | tuple | set | frozenset | dict, path: list[int]) -> bool:
|
|
410
502
|
for i, idx in enumerate(path):
|
|
411
503
|
if isinstance(data, dict):
|
|
412
504
|
keys = list(data.keys())
|
|
413
505
|
if i == len(path) - 1:
|
|
414
506
|
return True
|
|
415
|
-
|
|
416
|
-
|
|
507
|
+
try:
|
|
508
|
+
data = data[keys[idx]]
|
|
509
|
+
except IndexError:
|
|
510
|
+
return False
|
|
511
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
417
512
|
return False
|
|
418
513
|
else:
|
|
419
|
-
raise TypeError(f
|
|
514
|
+
raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
|
|
420
515
|
return False
|
|
421
|
-
|
|
516
|
+
|
|
517
|
+
if not isinstance(data, dict):
|
|
422
518
|
return False
|
|
423
|
-
path = Data.
|
|
519
|
+
path = Data.__sep_path_id(path_id)
|
|
424
520
|
return check_nested(data, path)
|
|
425
521
|
|
|
426
522
|
@staticmethod
|
|
427
|
-
def
|
|
428
|
-
if path_id.count(
|
|
429
|
-
raise ValueError(f
|
|
430
|
-
id_part_len
|
|
431
|
-
|
|
523
|
+
def __sep_path_id(path_id: str) -> list[int]:
|
|
524
|
+
if path_id.count(">") != 1:
|
|
525
|
+
raise ValueError(f"Invalid path ID: {path_id}")
|
|
526
|
+
id_part_len = int(path_id.split(">")[0])
|
|
527
|
+
path_ids_str = path_id.split(">")[1]
|
|
528
|
+
return [int(path_ids_str[i : i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
|