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 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
+
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"