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