jbutils 0.1.1__tar.gz
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.
- jbutils-0.1.1/PKG-INFO +13 -0
- jbutils-0.1.1/README.md +0 -0
- jbutils-0.1.1/jbutils/__init__.py +48 -0
- jbutils-0.1.1/jbutils/consts.py +68 -0
- jbutils-0.1.1/jbutils/utils.py +611 -0
- jbutils-0.1.1/pyproject.toml +14 -0
jbutils-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: jbutils
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Joseph Bochinski
|
|
6
|
+
Author-email: stirgejr@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Dist: ruamel-yaml (>=0.18.10,<0.19.0)
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
|
jbutils-0.1.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Exports for jbutils"""
|
|
2
|
+
|
|
3
|
+
from jbutils.consts import STYLES, COLORS
|
|
4
|
+
from jbutils.utils import (
|
|
5
|
+
Consts,
|
|
6
|
+
copy_to_clipboard,
|
|
7
|
+
debug_print,
|
|
8
|
+
dedupe_in_place,
|
|
9
|
+
dedupe_list,
|
|
10
|
+
delete_nested,
|
|
11
|
+
find,
|
|
12
|
+
get_keys,
|
|
13
|
+
get_nested,
|
|
14
|
+
pretty_print,
|
|
15
|
+
print_stack_trace,
|
|
16
|
+
read_file,
|
|
17
|
+
remove_list_values,
|
|
18
|
+
set_encoding,
|
|
19
|
+
set_nested,
|
|
20
|
+
set_yaml_indent,
|
|
21
|
+
SetNestedOptions,
|
|
22
|
+
update_list_values,
|
|
23
|
+
write_file,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"COLORS",
|
|
28
|
+
"Consts",
|
|
29
|
+
"copy_to_clipboard",
|
|
30
|
+
"debug_print",
|
|
31
|
+
"dedupe_in_place",
|
|
32
|
+
"dedupe_list",
|
|
33
|
+
"delete_nested",
|
|
34
|
+
"find",
|
|
35
|
+
"get_keys",
|
|
36
|
+
"get_nested",
|
|
37
|
+
"pretty_print",
|
|
38
|
+
"print_stack_trace",
|
|
39
|
+
"read_file",
|
|
40
|
+
"remove_list_values",
|
|
41
|
+
"set_encoding",
|
|
42
|
+
"set_nested",
|
|
43
|
+
"set_yaml_indent",
|
|
44
|
+
"SetNestedOptions",
|
|
45
|
+
"STYLES",
|
|
46
|
+
"update_list_values",
|
|
47
|
+
"write_file",
|
|
48
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class STYLES:
|
|
2
|
+
BG_RED = "background-color: #FF0000"
|
|
3
|
+
BG_DARK_RED = "background-color: #8B0000"
|
|
4
|
+
BG_GREEN = "background-color: #00FF00"
|
|
5
|
+
BG_DARK_GREEN = "background-color: #006400"
|
|
6
|
+
BG_YELLOW = "background-color: #FFFF00"
|
|
7
|
+
BG_DARK_YELLOW = "background-color: #FFD700"
|
|
8
|
+
BG_BLUE = "background-color: #0000FF"
|
|
9
|
+
BG_DARK_BLUE = "background-color: #00008B"
|
|
10
|
+
BG_CYAN = "background-color: #00FFFF"
|
|
11
|
+
BG_DARK_CYAN = "background-color: #008B8B"
|
|
12
|
+
BG_MAGENTA = "background-color: #FF00FF"
|
|
13
|
+
BG_DARK_MAGENTA = "background-color: #8B008B"
|
|
14
|
+
BG_WHITE = "background-color: #FFFFFF"
|
|
15
|
+
BG_BLACK = "background-color: #000000"
|
|
16
|
+
BG_GRAY = "background-color: #808080"
|
|
17
|
+
BG_DARK_GRAY = "background-color: #A9A9A9"
|
|
18
|
+
BG_LIGHT_GRAY = "background-color: #D3D3D3"
|
|
19
|
+
BG_SILVER = "background-color: #C0C0C0"
|
|
20
|
+
BG_GOLD = "background-color: #FFD700"
|
|
21
|
+
BG_ORANGE = "background-color: #FFA500"
|
|
22
|
+
BG_BROWN = "background-color: #A52A2A"
|
|
23
|
+
BG_PINK = "background-color: #FFC0CB"
|
|
24
|
+
BG_PURPLE = "background-color: #800080"
|
|
25
|
+
BG_INDIGO = "background-color: #4B0082"
|
|
26
|
+
BG_VIOLET = "background-color: #EE82EE"
|
|
27
|
+
BG_LIME = "background-color: #00FF00"
|
|
28
|
+
BG_OLIVE = "background-color: #808000"
|
|
29
|
+
BG_TEAL = "background-color: #008080"
|
|
30
|
+
BG_AQUA = "background-color: #00FFFF"
|
|
31
|
+
BG_MAROON = "background-color: #800000"
|
|
32
|
+
BG_NAVY = "background-color: #000080"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class COLORS:
|
|
36
|
+
RED = "#FF0000"
|
|
37
|
+
DARK_RED = "#8B0000"
|
|
38
|
+
GREEN = "#00FF00"
|
|
39
|
+
DARK_GREEN = "#006400"
|
|
40
|
+
YELLOW = "#FFFF00"
|
|
41
|
+
DARK_YELLOW = "#FFD700"
|
|
42
|
+
BLUE = "#0000FF"
|
|
43
|
+
DARK_BLUE = "#00008B"
|
|
44
|
+
LIGHT_BLUE = "#6190ff"
|
|
45
|
+
CYAN = "#00FFFF"
|
|
46
|
+
DARK_CYAN = "#008B8B"
|
|
47
|
+
MAGENTA = "#FF00FF"
|
|
48
|
+
DARK_MAGENTA = "#8B008B"
|
|
49
|
+
WHITE = "#FFFFFF"
|
|
50
|
+
BLACK = "#000000"
|
|
51
|
+
GRAY = "#808080"
|
|
52
|
+
DARK_GRAY = "#A9A9A9"
|
|
53
|
+
LIGHT_GRAY = "#D3D3D3"
|
|
54
|
+
BLUE_GRAY = "#90909A"
|
|
55
|
+
SILVER = "#C0C0C0"
|
|
56
|
+
GOLD = "#FFD700"
|
|
57
|
+
ORANGE = "#FFA500"
|
|
58
|
+
BROWN = "#A52A2A"
|
|
59
|
+
PINK = "#FFC0CB"
|
|
60
|
+
PURPLE = "#800080"
|
|
61
|
+
INDIGO = "#4B0082"
|
|
62
|
+
VIOLET = "#EE82EE"
|
|
63
|
+
LIME = "#00FF00"
|
|
64
|
+
OLIVE = "#808000"
|
|
65
|
+
TEAL = "#008080"
|
|
66
|
+
AQUA = "#00FFFF"
|
|
67
|
+
MAROON = "#800000"
|
|
68
|
+
NAVY = "#000080"
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"""Collection of common utils functions for personal repeated use"""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from ruamel.yaml import YAML
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
15
|
+
|
|
16
|
+
yaml = YAML()
|
|
17
|
+
yaml.indent = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Consts:
|
|
21
|
+
encoding: str = "UTF-8"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_encoding(enc: str) -> None:
|
|
25
|
+
Consts.encoding = enc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_yaml_indent(indent: int) -> None:
|
|
29
|
+
yaml.indent = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_file(
|
|
33
|
+
path: str,
|
|
34
|
+
mode: str = "r",
|
|
35
|
+
encoding: str = Consts.encoding,
|
|
36
|
+
as_lines: bool = False,
|
|
37
|
+
default_val: Any = None,
|
|
38
|
+
as_dicts: bool = False,
|
|
39
|
+
) -> str | dict | list:
|
|
40
|
+
"""Read data from a file
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
path (str): The path to the file
|
|
44
|
+
mode (str, optional): IO mode to use. Defaults to "r".
|
|
45
|
+
encoding (str, optional): Encoding format to use.
|
|
46
|
+
Defaults to "latin-1".
|
|
47
|
+
as_lines (bool, optional): If reading a regular text file,
|
|
48
|
+
True will return the value of readlines() instead of read().
|
|
49
|
+
Defaults to False.
|
|
50
|
+
default_val (Any, optional): Value to return if the file is not found.
|
|
51
|
+
Defaults to None.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str | dict | list: The data read from the file. If the file is
|
|
55
|
+
not found, returns an empty dict
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
default_val = default_val or {}
|
|
59
|
+
|
|
60
|
+
if not os.path.exists(path):
|
|
61
|
+
print(f"Warning: Path '{path}' does not exist")
|
|
62
|
+
return default_val
|
|
63
|
+
|
|
64
|
+
_, ext = os.path.splitext(path)
|
|
65
|
+
|
|
66
|
+
with open(path, mode, encoding=encoding) as fs:
|
|
67
|
+
match ext.lower():
|
|
68
|
+
case ".yaml" | ".yml":
|
|
69
|
+
return yaml.load(stream=fs)
|
|
70
|
+
case ".json":
|
|
71
|
+
return json.load(fs)
|
|
72
|
+
case ".csv":
|
|
73
|
+
data = list(csv.reader(fs))
|
|
74
|
+
if as_dicts:
|
|
75
|
+
if not data:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
cols = data.pop(0)
|
|
79
|
+
|
|
80
|
+
if not data:
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
return [dict(zip(cols, vals)) for vals in data]
|
|
84
|
+
return data
|
|
85
|
+
case _:
|
|
86
|
+
if as_lines:
|
|
87
|
+
return fs.readlines()
|
|
88
|
+
else:
|
|
89
|
+
return fs.read()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_file(
|
|
93
|
+
path: str,
|
|
94
|
+
data: Any,
|
|
95
|
+
mode: str = "w",
|
|
96
|
+
encoding: str = Consts.encoding,
|
|
97
|
+
indent: int = 4,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Write text to a file
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
path (str): The path to the file
|
|
103
|
+
data (Any): The data to write
|
|
104
|
+
mode (str, optional): Read/write mode. Defaults to "w".
|
|
105
|
+
encoding (str, optional): Encoding to write with. Defaults to ENCODING.
|
|
106
|
+
indent (int, optional): Indent to apply if JSON. Defaults to 4.
|
|
107
|
+
sort_keys (bool, optional): Whether to sort the output keys for YAML.
|
|
108
|
+
Defaults to False.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
_, ext = os.path.splitext(path)
|
|
112
|
+
|
|
113
|
+
with open(path, mode, encoding=encoding) as fs:
|
|
114
|
+
match ext.lower():
|
|
115
|
+
case ".yml" | ".yaml":
|
|
116
|
+
yaml.dump(data, fs)
|
|
117
|
+
case ".json":
|
|
118
|
+
json.dump(data, fs, indent=indent)
|
|
119
|
+
case _:
|
|
120
|
+
if isinstance(data, list):
|
|
121
|
+
fs.writelines(data)
|
|
122
|
+
else:
|
|
123
|
+
fs.write(data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find(items: list, value: Any) -> int:
|
|
127
|
+
"""A 'not in list' safe version of list.index()
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
items (list): List to search
|
|
131
|
+
value (Any): Value to search for
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
int: Index of the first instance of value, or -1 if not found
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
return items.index(value)
|
|
139
|
+
except ValueError:
|
|
140
|
+
return -1
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def update_list_values(
|
|
144
|
+
items: list[Any],
|
|
145
|
+
new_items: list[Any],
|
|
146
|
+
sort: bool = False,
|
|
147
|
+
sort_func: Callable[[Any], bool] = None,
|
|
148
|
+
reverse: bool = False,
|
|
149
|
+
) -> list[Any]:
|
|
150
|
+
"""Add new items to a list and sort it
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
items (list[Any]): Items to add to
|
|
154
|
+
new_items (list[Any]): Items to add
|
|
155
|
+
sort (Callable[[Any], bool], optional): Custom sort function. Defaults to None.
|
|
156
|
+
reverse (bool, optional): If true, sort order is reversed. Defaults to False.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
list[Any]: The updated list
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
for item in new_items:
|
|
163
|
+
if item not in items:
|
|
164
|
+
items.append(item)
|
|
165
|
+
|
|
166
|
+
if sort:
|
|
167
|
+
if sort_func:
|
|
168
|
+
items.sort(key=sort_func, reverse=reverse)
|
|
169
|
+
else:
|
|
170
|
+
items.sort(reverse=reverse)
|
|
171
|
+
|
|
172
|
+
return items
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def remove_list_values(
|
|
176
|
+
items: list[Any],
|
|
177
|
+
del_items: list[Any],
|
|
178
|
+
sort: bool = False,
|
|
179
|
+
sort_func: Callable[[Any], bool] = None,
|
|
180
|
+
reverse: bool = False,
|
|
181
|
+
) -> list[Any]:
|
|
182
|
+
"""Remove items from a list and sort it
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
items (list[Any]): Items to remove from
|
|
186
|
+
del_items (list[Any]): Items to remove
|
|
187
|
+
sort (Callable[[Any], bool], optional): Custom sort function. Defaults to None.
|
|
188
|
+
reverse (bool, optional): If true, sort order is reversed. Defaults to False.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
list[Any]: The updated list
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
for item in del_items:
|
|
195
|
+
if item in items:
|
|
196
|
+
items.remove(item)
|
|
197
|
+
if sort:
|
|
198
|
+
if sort_func:
|
|
199
|
+
items.sort(key=sort_func, reverse=reverse)
|
|
200
|
+
else:
|
|
201
|
+
items.sort(reverse=reverse)
|
|
202
|
+
|
|
203
|
+
return items
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_nested(obj: dict, path: str | list, default=None):
|
|
207
|
+
"""
|
|
208
|
+
Retrieves a nested property from a dictionary.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
dict (dict): The dictionary instance to retrieve the property from.
|
|
212
|
+
path (str | list[str]): The path to the desired property. Can be a dot-separated string or a list of strings.
|
|
213
|
+
default (any, optional): The value to return if the property cannot be found. Defaults to None.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
any: The value at the specified path, or the default value if not found.
|
|
217
|
+
"""
|
|
218
|
+
# Convert the path to a list if it's a string
|
|
219
|
+
if isinstance(path, str):
|
|
220
|
+
path = path.split(".")
|
|
221
|
+
|
|
222
|
+
current_value = obj
|
|
223
|
+
|
|
224
|
+
for key in path:
|
|
225
|
+
try:
|
|
226
|
+
current_value = current_value[key]
|
|
227
|
+
except (KeyError, TypeError):
|
|
228
|
+
return default
|
|
229
|
+
|
|
230
|
+
return current_value
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _set_nested(obj: dict, path: str | list[str], value: Any):
|
|
234
|
+
"""Sets a nested property in a dictionary.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
obj (dict): The dictionary instance to update.
|
|
238
|
+
path (str or list of str): The path to the desired property. Can be a dot-separated string or a list of strings.
|
|
239
|
+
value: The value to set at the specified path.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
None
|
|
243
|
+
"""
|
|
244
|
+
# Convert the path to a list if it's a string
|
|
245
|
+
if isinstance(path, str):
|
|
246
|
+
path = path.split(".")
|
|
247
|
+
|
|
248
|
+
current_value = obj
|
|
249
|
+
|
|
250
|
+
for key in path[:-1]:
|
|
251
|
+
try:
|
|
252
|
+
current_value = current_value[key]
|
|
253
|
+
except KeyError:
|
|
254
|
+
current_value[key] = {}
|
|
255
|
+
current_value = current_value[key]
|
|
256
|
+
|
|
257
|
+
# Set the final value
|
|
258
|
+
current_value[path[-1]] = value
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def print_stack_trace():
|
|
262
|
+
# Get the current stack frame
|
|
263
|
+
stack = traceback.format_stack()
|
|
264
|
+
|
|
265
|
+
print("\n".join(stack[:-2]))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def copy_to_clipboard(text):
|
|
269
|
+
process = subprocess.Popen(
|
|
270
|
+
["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE
|
|
271
|
+
)
|
|
272
|
+
process.communicate(input=text.encode("utf-8"))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def dedupe_list(items: list) -> list:
|
|
276
|
+
new_list = []
|
|
277
|
+
for item in items:
|
|
278
|
+
if item not in new_list:
|
|
279
|
+
new_list.append(item)
|
|
280
|
+
return new_list
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def dedupe_in_place(items: list) -> list:
|
|
284
|
+
uniques = []
|
|
285
|
+
dupes = []
|
|
286
|
+
|
|
287
|
+
for item in items:
|
|
288
|
+
if item not in uniques:
|
|
289
|
+
uniques.append(item)
|
|
290
|
+
else:
|
|
291
|
+
dupes.append(item)
|
|
292
|
+
|
|
293
|
+
for item in dupes:
|
|
294
|
+
items.remove(item)
|
|
295
|
+
|
|
296
|
+
return dupes
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_keys(obj: dict, keys: list[str] = None) -> Any:
|
|
300
|
+
if not isinstance(obj, dict):
|
|
301
|
+
return keys
|
|
302
|
+
|
|
303
|
+
keys = keys or []
|
|
304
|
+
keys.extend(obj.keys())
|
|
305
|
+
for value in obj.values():
|
|
306
|
+
get_keys(value, keys)
|
|
307
|
+
|
|
308
|
+
return keys
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_nested(
|
|
312
|
+
obj: dict | list, path: list[str] | str, default_val: Any = None
|
|
313
|
+
) -> Any:
|
|
314
|
+
"""Get a nested value from a dictionary or list
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
obj (dict | list): The object to get the value from
|
|
318
|
+
path (list[str] | str): The path to the value
|
|
319
|
+
default_val (Any, optional): The default value to return if the path is not found.
|
|
320
|
+
Defaults to None.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Any: The value at the path or the default value
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
if isinstance(path, str):
|
|
327
|
+
path = path.split(".")
|
|
328
|
+
|
|
329
|
+
if len(path) == 1:
|
|
330
|
+
result = None
|
|
331
|
+
if isinstance(obj, dict):
|
|
332
|
+
result = obj.get(path[0], default_val)
|
|
333
|
+
|
|
334
|
+
elif isinstance(obj, list) and path[0].isdigit():
|
|
335
|
+
idx = int(path[0])
|
|
336
|
+
item = None
|
|
337
|
+
if idx < len(obj):
|
|
338
|
+
item = obj[idx]
|
|
339
|
+
if item is None:
|
|
340
|
+
item = default_val
|
|
341
|
+
|
|
342
|
+
result = item
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
key = path.pop(0)
|
|
346
|
+
if isinstance(obj, list) and key.isdigit():
|
|
347
|
+
if int(key) < len(obj):
|
|
348
|
+
return get_nested(obj[int(key)], path, default_val)
|
|
349
|
+
return default_val
|
|
350
|
+
if key not in obj:
|
|
351
|
+
return default_val
|
|
352
|
+
return get_nested(obj[key], path, default_val)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def delete_nested(obj: dict | list, path: list[str] | str) -> None:
|
|
356
|
+
"""Delete a nested value from a dictionary or list
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
obj (dict | list): The object to delete the value from
|
|
360
|
+
path (list[str] | str): The path to the value
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
if isinstance(path, str):
|
|
364
|
+
path = path.split(".")
|
|
365
|
+
|
|
366
|
+
if len(path) == 1:
|
|
367
|
+
print(obj)
|
|
368
|
+
if isinstance(obj, (dict, CommentedMap)):
|
|
369
|
+
obj.pop(path[0], None)
|
|
370
|
+
elif isinstance(obj, (list, CommentedSeq)):
|
|
371
|
+
if path[0].isdigit():
|
|
372
|
+
idx = int(path[0])
|
|
373
|
+
if idx < len(obj):
|
|
374
|
+
obj.pop(idx)
|
|
375
|
+
elif path[0] in obj:
|
|
376
|
+
print("removing", path[0])
|
|
377
|
+
obj.remove(path[0])
|
|
378
|
+
else:
|
|
379
|
+
key = path.pop(0)
|
|
380
|
+
if isinstance(obj, list) and key.isdigit():
|
|
381
|
+
if int(key) < len(obj):
|
|
382
|
+
delete_nested(obj[int(key)], path)
|
|
383
|
+
elif key in obj:
|
|
384
|
+
delete_nested(obj[key], path)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def debug_print(*args):
|
|
388
|
+
"""Print debug statements with a newline before and after"""
|
|
389
|
+
|
|
390
|
+
strings = list(args)
|
|
391
|
+
strings.insert(0, "\n")
|
|
392
|
+
strings.append("\n")
|
|
393
|
+
print(*strings)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def pretty_print(obj: any) -> None:
|
|
397
|
+
"""Prints a JSON serializable object with indentation"""
|
|
398
|
+
|
|
399
|
+
print(json.dumps(obj, indent=4))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass
|
|
403
|
+
class SetNestedOptions:
|
|
404
|
+
"""Options for the set_nested function"""
|
|
405
|
+
|
|
406
|
+
def __init__(self, debug: bool = False, create_lists: bool = False) -> None:
|
|
407
|
+
|
|
408
|
+
self.debug: bool = debug
|
|
409
|
+
self.create_lists: bool = create_lists
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _set_next_append(
|
|
413
|
+
obj: list,
|
|
414
|
+
path: list[str],
|
|
415
|
+
key: str | int,
|
|
416
|
+
value: Any,
|
|
417
|
+
debug: bool = False,
|
|
418
|
+
create_lists: bool = True,
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Set a value in a nested object when the index is out of bounds
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
obj (list): Object to set the value in
|
|
424
|
+
path (list[str]): Path to the value
|
|
425
|
+
key (str | int): Key to set the value at
|
|
426
|
+
value (Any): Value to set
|
|
427
|
+
debug (bool, optional): Flag to enable debug statements. Defaults to False.
|
|
428
|
+
create_lists (bool, optional): Flag to set whether to create a list or. Defaults to True.
|
|
429
|
+
"""
|
|
430
|
+
if debug:
|
|
431
|
+
debug_print("out of bounds", "inserting new value", f"path[0] = {path[0]}")
|
|
432
|
+
|
|
433
|
+
if path[0].isdigit() and create_lists:
|
|
434
|
+
obj.insert(int(key), [])
|
|
435
|
+
else:
|
|
436
|
+
obj.insert(int(key), {})
|
|
437
|
+
|
|
438
|
+
if debug:
|
|
439
|
+
debug_print("blank inserted", obj)
|
|
440
|
+
|
|
441
|
+
set_nested(
|
|
442
|
+
obj=obj[-1], path=path, value=value, debug=debug, create_lists=create_lists
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _set_next_list_item(
|
|
447
|
+
obj: list,
|
|
448
|
+
path: list[str],
|
|
449
|
+
key: str | int,
|
|
450
|
+
value: Any,
|
|
451
|
+
debug: bool = False,
|
|
452
|
+
create_lists: bool = True,
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Iterate through the next item in a list or dictionary
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
obj (list): Object to set the value in
|
|
458
|
+
path (list[str]): Path to the value
|
|
459
|
+
key (str | int): Key to set the value at
|
|
460
|
+
value (Any): Value to set
|
|
461
|
+
debug (bool, optional): Flag to enable debug statements. Defaults to False.
|
|
462
|
+
create_lists (bool, optional): Flag to set whether to create a list or. Defaults to True.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
sub = obj[int(key)]
|
|
466
|
+
if not sub or not isinstance(sub, (dict, list)):
|
|
467
|
+
if debug:
|
|
468
|
+
debug_print("sub is None or not an object", "creating new sub")
|
|
469
|
+
|
|
470
|
+
if path[0].isdigit() and create_lists:
|
|
471
|
+
obj[int(key)] = []
|
|
472
|
+
else:
|
|
473
|
+
obj[int(key)] = {}
|
|
474
|
+
|
|
475
|
+
set_nested(
|
|
476
|
+
obj=obj[int(key)],
|
|
477
|
+
path=path,
|
|
478
|
+
value=value,
|
|
479
|
+
debug=debug,
|
|
480
|
+
create_lists=create_lists,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _set_final_prop(obj: dict | list, path: list[str], value: Any) -> None:
|
|
485
|
+
"""Set a value in a nested object at the final path
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
obj (dict | list): Object to set the value in
|
|
489
|
+
path (list[str]): Path to the value
|
|
490
|
+
value (Any): Value to set
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
if isinstance(obj, dict):
|
|
494
|
+
obj[path[0]] = value
|
|
495
|
+
elif isinstance(obj, list) and path[0].isdigit():
|
|
496
|
+
if int(path[0]) < len(obj):
|
|
497
|
+
obj[int(path[0])] = value
|
|
498
|
+
else:
|
|
499
|
+
obj.append(value)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _set_next_nested(
|
|
503
|
+
obj: dict | list,
|
|
504
|
+
path: list[str],
|
|
505
|
+
value: Any,
|
|
506
|
+
key: str | int,
|
|
507
|
+
debug: bool = False,
|
|
508
|
+
create_lists: bool = True,
|
|
509
|
+
) -> None:
|
|
510
|
+
"""Set a value in a nested object
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
obj (dict | list): Object to set the value in
|
|
514
|
+
path (list[str]): Path to the value
|
|
515
|
+
value (Any): Value to set
|
|
516
|
+
key (str | int): Key to set the value at
|
|
517
|
+
debug (bool, optional): Flag to enable debug statements. Defaults to False.
|
|
518
|
+
create_lists (bool, optional): Flag to set whether to create a list or. Defaults to True.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
sub = obj.get(key)
|
|
522
|
+
|
|
523
|
+
if debug:
|
|
524
|
+
debug_print("obj is dict", "sub:", sub)
|
|
525
|
+
|
|
526
|
+
if not sub or not isinstance(sub, (dict, list)):
|
|
527
|
+
if debug:
|
|
528
|
+
debug_print("sub is None or not an object", "creating new sub")
|
|
529
|
+
|
|
530
|
+
if path[0].isdigit() and create_lists:
|
|
531
|
+
obj[key] = []
|
|
532
|
+
else:
|
|
533
|
+
obj[key] = {}
|
|
534
|
+
|
|
535
|
+
if debug:
|
|
536
|
+
debug_print("new sub created", obj)
|
|
537
|
+
|
|
538
|
+
set_nested(
|
|
539
|
+
obj=obj.get(key),
|
|
540
|
+
path=path,
|
|
541
|
+
value=value,
|
|
542
|
+
debug=debug,
|
|
543
|
+
create_lists=create_lists,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def set_nested(
|
|
548
|
+
obj: dict | list,
|
|
549
|
+
path: list[str] | str,
|
|
550
|
+
value: Any,
|
|
551
|
+
debug: bool = False,
|
|
552
|
+
create_lists: bool = True,
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Set a nested value in a dictionary or list
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
obj (dict | list): The object to set the value in
|
|
558
|
+
path (list[str] | str): The path to the value
|
|
559
|
+
value (Any): The value to set
|
|
560
|
+
debug (bool, optional): Flag to enable debug statements. Defaults to False.
|
|
561
|
+
create_lists (bool, optional): Flag to set whether to create a list or
|
|
562
|
+
a dict when the path fragment is a number. Defaults to True.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
if isinstance(path, str):
|
|
566
|
+
path = path.split(".")
|
|
567
|
+
|
|
568
|
+
if debug:
|
|
569
|
+
print_stack_trace()
|
|
570
|
+
debug_print("starting function")
|
|
571
|
+
pretty_print({"obj": obj, "path": path, "value": value})
|
|
572
|
+
|
|
573
|
+
if len(path) == 1:
|
|
574
|
+
_set_final_prop(obj, path, value)
|
|
575
|
+
else:
|
|
576
|
+
key = path.pop(0)
|
|
577
|
+
|
|
578
|
+
if debug:
|
|
579
|
+
debug_print("key", key)
|
|
580
|
+
|
|
581
|
+
if isinstance(obj, list) and key.isdigit(): # true
|
|
582
|
+
if debug:
|
|
583
|
+
debug_print("obj is list", "key < len", int(key) < len(obj))
|
|
584
|
+
|
|
585
|
+
if int(key) < len(obj):
|
|
586
|
+
_set_next_list_item(
|
|
587
|
+
obj=obj,
|
|
588
|
+
path=path,
|
|
589
|
+
key=key,
|
|
590
|
+
value=value,
|
|
591
|
+
debug=debug,
|
|
592
|
+
create_lists=create_lists,
|
|
593
|
+
)
|
|
594
|
+
else:
|
|
595
|
+
_set_next_append(
|
|
596
|
+
obj=obj,
|
|
597
|
+
path=path,
|
|
598
|
+
key=key,
|
|
599
|
+
value=value,
|
|
600
|
+
debug=debug,
|
|
601
|
+
create_lists=create_lists,
|
|
602
|
+
)
|
|
603
|
+
elif isinstance(obj, dict):
|
|
604
|
+
_set_next_nested(
|
|
605
|
+
obj=obj,
|
|
606
|
+
path=path,
|
|
607
|
+
value=value,
|
|
608
|
+
key=key,
|
|
609
|
+
debug=debug,
|
|
610
|
+
create_lists=create_lists,
|
|
611
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "poetry-core",]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "jbutils"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = ""
|
|
9
|
+
authors = [ "Joseph Bochinski <stirgejr@gmail.com>",]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.12"
|
|
14
|
+
ruamel-yaml = "^0.18.10"
|