lionagi 0.0.115__py3-none-any.whl → 0.0.204__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- lionagi/__init__.py +1 -2
- lionagi/_services/__init__.py +5 -0
- lionagi/_services/anthropic.py +79 -0
- lionagi/_services/base_service.py +414 -0
- lionagi/_services/oai.py +98 -0
- lionagi/_services/openrouter.py +44 -0
- lionagi/_services/services.py +91 -0
- lionagi/_services/transformers.py +46 -0
- lionagi/bridge/langchain.py +26 -16
- lionagi/bridge/llama_index.py +50 -20
- lionagi/configs/oai_configs.py +2 -14
- lionagi/configs/openrouter_configs.py +2 -2
- lionagi/core/__init__.py +7 -8
- lionagi/core/branch/branch.py +589 -0
- lionagi/core/branch/branch_manager.py +139 -0
- lionagi/core/branch/conversation.py +484 -0
- lionagi/core/core_util.py +59 -0
- lionagi/core/flow/flow.py +19 -0
- lionagi/core/flow/flow_util.py +62 -0
- lionagi/core/instruction_set/__init__.py +0 -5
- lionagi/core/instruction_set/instruction_set.py +343 -0
- lionagi/core/messages/messages.py +176 -0
- lionagi/core/sessions/__init__.py +0 -5
- lionagi/core/sessions/session.py +428 -0
- lionagi/loaders/chunker.py +51 -47
- lionagi/loaders/load_util.py +2 -2
- lionagi/loaders/reader.py +45 -39
- lionagi/models/imodel.py +53 -0
- lionagi/schema/async_queue.py +158 -0
- lionagi/schema/base_node.py +318 -147
- lionagi/schema/base_tool.py +31 -1
- lionagi/schema/data_logger.py +74 -38
- lionagi/schema/data_node.py +57 -6
- lionagi/structures/graph.py +132 -10
- lionagi/structures/relationship.py +58 -20
- lionagi/structures/structure.py +36 -25
- lionagi/tests/test_utils/test_api_util.py +219 -0
- lionagi/tests/test_utils/test_call_util.py +785 -0
- lionagi/tests/test_utils/test_encrypt_util.py +323 -0
- lionagi/tests/test_utils/test_io_util.py +238 -0
- lionagi/tests/test_utils/test_nested_util.py +338 -0
- lionagi/tests/test_utils/test_sys_util.py +358 -0
- lionagi/tools/tool_manager.py +186 -0
- lionagi/tools/tool_util.py +266 -3
- lionagi/utils/__init__.py +21 -61
- lionagi/utils/api_util.py +359 -71
- lionagi/utils/call_util.py +839 -264
- lionagi/utils/encrypt_util.py +283 -16
- lionagi/utils/io_util.py +178 -93
- lionagi/utils/nested_util.py +672 -0
- lionagi/utils/pd_util.py +57 -0
- lionagi/utils/sys_util.py +284 -156
- lionagi/utils/url_util.py +55 -0
- lionagi/version.py +1 -1
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/METADATA +21 -17
- lionagi-0.0.204.dist-info/RECORD +106 -0
- lionagi/core/conversations/__init__.py +0 -5
- lionagi/core/conversations/conversation.py +0 -107
- lionagi/core/flows/__init__.py +0 -8
- lionagi/core/flows/flow.py +0 -8
- lionagi/core/flows/flow_util.py +0 -62
- lionagi/core/instruction_set/instruction_sets.py +0 -7
- lionagi/core/sessions/sessions.py +0 -185
- lionagi/endpoints/__init__.py +0 -5
- lionagi/endpoints/audio.py +0 -17
- lionagi/endpoints/chatcompletion.py +0 -54
- lionagi/messages/__init__.py +0 -11
- lionagi/messages/instruction.py +0 -15
- lionagi/messages/message.py +0 -110
- lionagi/messages/response.py +0 -33
- lionagi/messages/system.py +0 -12
- lionagi/objs/__init__.py +0 -11
- lionagi/objs/abc_objs.py +0 -39
- lionagi/objs/async_queue.py +0 -135
- lionagi/objs/messenger.py +0 -85
- lionagi/objs/tool_manager.py +0 -253
- lionagi/services/__init__.py +0 -11
- lionagi/services/base_api_service.py +0 -230
- lionagi/services/oai.py +0 -34
- lionagi/services/openrouter.py +0 -31
- lionagi/tests/test_api_util.py +0 -46
- lionagi/tests/test_call_util.py +0 -115
- lionagi/tests/test_convert_util.py +0 -202
- lionagi/tests/test_encrypt_util.py +0 -33
- lionagi/tests/test_flat_util.py +0 -426
- lionagi/tests/test_sys_util.py +0 -0
- lionagi/utils/convert_util.py +0 -229
- lionagi/utils/flat_util.py +0 -599
- lionagi-0.0.115.dist-info/RECORD +0 -110
- /lionagi/{services → _services}/anyscale.py +0 -0
- /lionagi/{services → _services}/azure.py +0 -0
- /lionagi/{services → _services}/bedrock.py +0 -0
- /lionagi/{services → _services}/everlyai.py +0 -0
- /lionagi/{services → _services}/gemini.py +0 -0
- /lionagi/{services → _services}/gpt4all.py +0 -0
- /lionagi/{services → _services}/huggingface.py +0 -0
- /lionagi/{services → _services}/litellm.py +0 -0
- /lionagi/{services → _services}/localai.py +0 -0
- /lionagi/{services → _services}/mistralai.py +0 -0
- /lionagi/{services → _services}/ollama.py +0 -0
- /lionagi/{services → _services}/openllm.py +0 -0
- /lionagi/{services → _services}/perplexity.py +0 -0
- /lionagi/{services → _services}/predibase.py +0 -0
- /lionagi/{services → _services}/rungpt.py +0 -0
- /lionagi/{services → _services}/vllm.py +0 -0
- /lionagi/{services → _services}/xinference.py +0 -0
- /lionagi/{endpoints/assistants.py → agents/__init__.py} +0 -0
- /lionagi/{tools → agents}/planner.py +0 -0
- /lionagi/{tools → agents}/prompter.py +0 -0
- /lionagi/{tools → agents}/scorer.py +0 -0
- /lionagi/{tools → agents}/summarizer.py +0 -0
- /lionagi/{tools → agents}/validator.py +0 -0
- /lionagi/{endpoints/embeddings.py → core/branch/__init__.py} +0 -0
- /lionagi/{services/anthropic.py → core/branch/cluster.py} +0 -0
- /lionagi/{endpoints/finetune.py → core/flow/__init__.py} +0 -0
- /lionagi/{endpoints/image.py → core/messages/__init__.py} +0 -0
- /lionagi/{endpoints/moderation.py → models/__init__.py} +0 -0
- /lionagi/{endpoints/vision.py → models/base_model.py} +0 -0
- /lionagi/{objs → schema}/status_tracker.py +0 -0
- /lionagi/tests/{test_io_util.py → test_utils/__init__.py} +0 -0
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/LICENSE +0 -0
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/WHEEL +0 -0
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,672 @@
|
|
1
|
+
import json
|
2
|
+
from collections import defaultdict
|
3
|
+
from copy import deepcopy
|
4
|
+
|
5
|
+
from typing import Any, Callable, Dict, Generator, List, Tuple, Union, Iterable, Optional
|
6
|
+
from itertools import chain
|
7
|
+
|
8
|
+
def to_readable_dict(input: Union[Dict, List]) -> Union[str, List]:
|
9
|
+
"""
|
10
|
+
Converts a dictionary to a JSON-formatted string with indentation or returns the input list as is.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
input: A dictionary or list.
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
A JSON-formatted string if the input is a dictionary, or the input list as is.
|
17
|
+
|
18
|
+
Examples:
|
19
|
+
>>> to_readable_dict({'a': 1})
|
20
|
+
'{\\n "a": 1\\n}'
|
21
|
+
|
22
|
+
>>> to_readable_dict([1, 2, 3])
|
23
|
+
[1, 2, 3]
|
24
|
+
"""
|
25
|
+
if isinstance(input, dict):
|
26
|
+
return json.dumps(input, indent=4)
|
27
|
+
else:
|
28
|
+
return input
|
29
|
+
|
30
|
+
def nfilter(collection: Union[Dict, List], condition: Callable[[Any], bool]) -> Union[Dict, List]:
|
31
|
+
"""
|
32
|
+
nested filter: Filters a collection (either a dictionary or a list) based on a given condition.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
collection: A collection to filter, either a dictionary or a list.
|
36
|
+
condition: A callable that takes an item (key-value pair for dictionaries, single item for lists)
|
37
|
+
and returns a boolean. If True, the item is included in the result.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
A new collection of the same type as the input, containing only the items that meet the condition.
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
TypeError: If the collection is neither a dictionary nor a list.
|
44
|
+
|
45
|
+
Examples:
|
46
|
+
>>> nfilter({'a': 1, 'b': 2, 'c': 3}, lambda x: x[1] > 1)
|
47
|
+
{'b': 2, 'c': 3}
|
48
|
+
|
49
|
+
>>> nfilter([1, 2, 3, 4], lambda x: x % 2 == 0)
|
50
|
+
[2, 4]
|
51
|
+
"""
|
52
|
+
if isinstance(collection, Dict):
|
53
|
+
return _filter_dict(collection, condition)
|
54
|
+
elif isinstance(collection, List):
|
55
|
+
return _filter_list(collection, condition)
|
56
|
+
else:
|
57
|
+
raise TypeError("The collection must be either a Dict or a List.")
|
58
|
+
|
59
|
+
def nset(nested_structure: Union[List, Dict], indices: List[Union[int, str]], value: Any) -> None:
|
60
|
+
"""
|
61
|
+
nested set: Sets a value in a nested list or dictionary structure.
|
62
|
+
|
63
|
+
Navigates through the nested structure according to the specified indices and sets the
|
64
|
+
value at the target location. The indices can be a mix of integers (for lists) and
|
65
|
+
strings (for dictionaries).
|
66
|
+
|
67
|
+
Args:
|
68
|
+
nested_structure: The nested list or dictionary structure.
|
69
|
+
indices: A list of indices to navigate through the structure.
|
70
|
+
value: The value to set at the target location.
|
71
|
+
|
72
|
+
Raises:
|
73
|
+
ValueError: If the indices list is empty.
|
74
|
+
TypeError: If the target container or the last index is of an incorrect type.
|
75
|
+
|
76
|
+
Examples:
|
77
|
+
>>> data = {'a': {'b': [10, 20]}}
|
78
|
+
>>> nset(data, ['a', 'b', 1], 99)
|
79
|
+
>>> data
|
80
|
+
{'a': {'b': [10, 99]}}
|
81
|
+
|
82
|
+
>>> data = [0, [1, 2], 3]
|
83
|
+
>>> nset(data, [1, 1], 99)
|
84
|
+
>>> data
|
85
|
+
[0, [1, 99], 3]
|
86
|
+
"""
|
87
|
+
if not indices:
|
88
|
+
raise ValueError("Indices list is empty, cannot determine target container")
|
89
|
+
|
90
|
+
target_container = _get_target_container(nested_structure, indices[:-1])
|
91
|
+
last_index = indices[-1]
|
92
|
+
|
93
|
+
if isinstance(target_container, list):
|
94
|
+
_ensure_list_index(target_container, last_index)
|
95
|
+
target_container[last_index] = value
|
96
|
+
elif isinstance(target_container, dict):
|
97
|
+
target_container[last_index] = value
|
98
|
+
else:
|
99
|
+
raise TypeError("Cannot set value on non-list/dict element")
|
100
|
+
|
101
|
+
def nget(nested_structure: Union[List, Dict], indices: List[Union[int, str]]) -> Any:
|
102
|
+
"""
|
103
|
+
nested get: Retrieves a value from a nested list or dictionary structure.
|
104
|
+
|
105
|
+
Navigates through the nested structure using the specified indices and retrieves the value
|
106
|
+
at the target location. The indices can be integers for lists and strings for dictionaries.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
nested_structure: The nested list or dictionary structure.
|
110
|
+
indices: A list of indices to navigate through the structure.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
The value at the target location, or None if the target cannot be reached or does not exist.
|
114
|
+
|
115
|
+
Examples:
|
116
|
+
>>> data = {'a': {'b': [10, 20]}}
|
117
|
+
>>> nget(data, ['a', 'b', 1])
|
118
|
+
20
|
119
|
+
|
120
|
+
>>> data = [0, [1, 2], 3]
|
121
|
+
>>> nget(data, [1, 0])
|
122
|
+
1
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
target_container = _get_target_container(nested_structure, indices[:-1])
|
126
|
+
last_index = indices[-1]
|
127
|
+
|
128
|
+
if isinstance(target_container, list) and isinstance(last_index, int) and last_index < len(target_container):
|
129
|
+
return target_container[last_index]
|
130
|
+
elif isinstance(target_container, dict) and last_index in target_container:
|
131
|
+
return target_container[last_index]
|
132
|
+
else:
|
133
|
+
return None # Index out of bounds or not found
|
134
|
+
except (IndexError, KeyError, TypeError):
|
135
|
+
return None
|
136
|
+
|
137
|
+
def is_structure_homogeneous(
|
138
|
+
structure: Any,
|
139
|
+
return_structure_type: bool = False
|
140
|
+
) -> Union[bool, Tuple[bool, Optional[type]]]:
|
141
|
+
"""
|
142
|
+
Checks if a nested structure is homogeneous, meaning it doesn't contain a mix of lists and dictionaries.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
structure: The nested structure to check.
|
146
|
+
return_structure_type: Flag to indicate whether to return the type of homogeneous structure.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
If return_structure_type is False, returns a boolean indicating whether the structure is homogeneous.
|
150
|
+
If return_structure_type is True, returns a tuple containing a boolean indicating whether the structure is homogeneous, and the type of the homogeneous structure if it is homogeneous (either list, dict, or None).
|
151
|
+
|
152
|
+
Examples:
|
153
|
+
>>> is_structure_homogeneous({'a': {'b': 1}, 'c': {'d': 2}})
|
154
|
+
True
|
155
|
+
|
156
|
+
>>> is_structure_homogeneous({'a': {'b': 1}, 'c': [1, 2]})
|
157
|
+
False
|
158
|
+
"""
|
159
|
+
def _check_structure(substructure):
|
160
|
+
structure_type = None
|
161
|
+
if isinstance(substructure, list):
|
162
|
+
structure_type = list
|
163
|
+
for item in substructure:
|
164
|
+
if not isinstance(item, structure_type) and isinstance(item, (list, dict)):
|
165
|
+
return False, None
|
166
|
+
result, _ = _check_structure(item)
|
167
|
+
if not result:
|
168
|
+
return False, None
|
169
|
+
elif isinstance(substructure, dict):
|
170
|
+
structure_type = dict
|
171
|
+
for item in substructure.values():
|
172
|
+
if not isinstance(item, structure_type) and isinstance(item, (list, dict)):
|
173
|
+
return False, None
|
174
|
+
result, _ = _check_structure(item)
|
175
|
+
if not result:
|
176
|
+
return False, None
|
177
|
+
return True, structure_type
|
178
|
+
|
179
|
+
is_homogeneous, structure_type = _check_structure(structure)
|
180
|
+
if return_structure_type:
|
181
|
+
return is_homogeneous, structure_type
|
182
|
+
else:
|
183
|
+
return is_homogeneous
|
184
|
+
|
185
|
+
# nested merge
|
186
|
+
def nmerge(iterables: List[Union[Dict, List, Iterable]],
|
187
|
+
dict_update: bool = False,
|
188
|
+
dict_sequence: bool = False,
|
189
|
+
sequence_separator: str = '_',
|
190
|
+
sort_list: bool = False,
|
191
|
+
custom_sort: Callable[[Any], Any] = None) -> Union[Dict, List]:
|
192
|
+
"""
|
193
|
+
Merges a list of dictionaries or sequences into a single dictionary or list.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
iterables: A list of dictionaries or sequences to be merged.
|
197
|
+
dict_update: If merging dictionaries, whether to overwrite values of the same key.
|
198
|
+
dict_sequence: If merging dictionaries, whether to create unique keys for duplicate keys.
|
199
|
+
sequence_separator: Separator for creating unique keys when dict_sequence is True.
|
200
|
+
sort_list: If merging sequences, whether to sort the merged list.
|
201
|
+
custom_sort: Custom sorting function for sorting the merged sequence.
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
A merged dictionary or list based on the type of elements in iterables.
|
205
|
+
|
206
|
+
Raises:
|
207
|
+
TypeError: If the elements of iterables are not all of the same type.
|
208
|
+
|
209
|
+
Examples:
|
210
|
+
>>> nmerge([{'a': 1}, {'b': 2}], dict_update=True)
|
211
|
+
{'a': 1, 'b': 2}
|
212
|
+
|
213
|
+
>>> nmerge([[1, 2], [3, 4]], sort_list=True)
|
214
|
+
[1, 2, 3, 4]
|
215
|
+
"""
|
216
|
+
if _is_homogeneous(iterables, Dict):
|
217
|
+
return _merge_dicts(iterables, dict_update, dict_sequence, sequence_separator)
|
218
|
+
elif _is_homogeneous(iterables, List) and not any(isinstance(it, (Dict, str)) for it in iterables):
|
219
|
+
return _merge_sequences(iterables, sort_list, custom_sort)
|
220
|
+
else:
|
221
|
+
raise TypeError("All items in the input list must be of the same type, either Dict, List, or Iterable.")
|
222
|
+
|
223
|
+
# flatten dictionary
|
224
|
+
def flatten(obj: Any, parent_key: str = '', sep: str = '_',
|
225
|
+
max_depth: Union[int, None] = None, inplace: bool = False,
|
226
|
+
dict_only: bool = False) -> Union[Dict, None]:
|
227
|
+
"""
|
228
|
+
Flattens a nested dictionary or list into a flat dictionary with composite keys.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
obj (Any): The dictionary or list to flatten.
|
232
|
+
|
233
|
+
parent_key (str, optional): The base key to prepend to each key in the flattened dictionary. Defaults to ''.
|
234
|
+
|
235
|
+
sep (str, optional): Separator to join keys. Defaults to '_'.
|
236
|
+
|
237
|
+
max_depth (Union[int, None], optional): Maximum depth to flatten. Defaults to None.
|
238
|
+
|
239
|
+
inplace (bool, optional): If True, the original object is modified in place. Defaults to False.
|
240
|
+
|
241
|
+
dict_only (bool, optional): If True, only dictionaries are flattened, lists are left as-is. Defaults to False.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Union[Dict, None]: The flattened dictionary, or None if inplace is True and the operation is performed in place.
|
245
|
+
|
246
|
+
Raises:
|
247
|
+
ValueError: If inplace is True and the provided object is not a dictionary.
|
248
|
+
|
249
|
+
Examples:
|
250
|
+
>>> nested_dict = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}}
|
251
|
+
>>> flat_dict = flatten(nested_dict)
|
252
|
+
>>> print(flat_dict) # Output: {'a': 1, 'b_c': 2, 'b_d_e': 3}
|
253
|
+
|
254
|
+
>>> nested_list = [{'a': 1}, {'b': 2}]
|
255
|
+
>>> flat_dict = flatten(nested_list)
|
256
|
+
>>> print(flat_dict) # Output: {'0_a': 1, '1_b': 2}
|
257
|
+
"""
|
258
|
+
if inplace:
|
259
|
+
if not isinstance(obj, dict):
|
260
|
+
raise ValueError("Object must be a dictionary when 'inplace' is True.")
|
261
|
+
_dynamic_flatten_in_place(obj, parent_key, sep, max_depth, dict_only=dict_only)
|
262
|
+
else:
|
263
|
+
parent_key_tuple = tuple(parent_key.split(sep)) if parent_key else ()
|
264
|
+
return dict(_dynamic_flatten_generator(obj, parent_key_tuple, sep, max_depth, dict_only=dict_only))
|
265
|
+
|
266
|
+
# unflatten dictionary
|
267
|
+
def unflatten(
|
268
|
+
flat_dict: Dict[str, Any], sep: str = '_',
|
269
|
+
custom_logic: Union[Callable[[str], Any], None] = None,
|
270
|
+
max_depth: Union[int, None] = None
|
271
|
+
) -> Union[Dict, List]:
|
272
|
+
"""
|
273
|
+
Converts a flat dictionary with composite keys into a nested dictionary or list.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
flat_dict (Dict[str, Any]): The flat dictionary to unflatten.
|
277
|
+
sep (str, optional): Separator used in composite keys. Defaults to '_'.
|
278
|
+
custom_logic (Union[Callable[[str], Any], None], optional): Custom function to process parts of the keys. Defaults to None.
|
279
|
+
max_depth (Union[int, None], optional): Maximum depth for nesting. Defaults to None.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
Union[Dict, List]: The unflattened dictionary or list.
|
283
|
+
|
284
|
+
Examples:
|
285
|
+
>>> flat_dict = {'a': 1, 'b_c': 2, 'b_d_e': 3}
|
286
|
+
>>> nested_dict = unflatten(flat_dict)
|
287
|
+
>>> print(nested_dict) # Output: {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}}
|
288
|
+
|
289
|
+
>>> flat_dict = {'0_a': 1, '1_b': 2}
|
290
|
+
>>> nested_list = unflatten(flat_dict)
|
291
|
+
>>> print(nested_list) # Output: [{'a': 1}, {'b': 2}]
|
292
|
+
"""
|
293
|
+
unflattened = {}
|
294
|
+
for composite_key, value in flat_dict.items():
|
295
|
+
parts = composite_key.split(sep)
|
296
|
+
if custom_logic:
|
297
|
+
parts = [custom_logic(part) for part in parts]
|
298
|
+
else:
|
299
|
+
parts = [int(part) if part.isdigit() else part for part in parts]
|
300
|
+
|
301
|
+
if not unflattened and all(isinstance(part, int) for part in parts):
|
302
|
+
unflattened = []
|
303
|
+
|
304
|
+
ninsert(unflattened, parts, value, sep, max_depth)
|
305
|
+
|
306
|
+
if isinstance(unflattened, dict) and all(isinstance(k, int) for k in unflattened.keys()):
|
307
|
+
max_index = max(unflattened.keys(), default=-1)
|
308
|
+
return [unflattened.get(i) for i in range(max_index + 1)]
|
309
|
+
if not unflattened:
|
310
|
+
return {}
|
311
|
+
return unflattened
|
312
|
+
|
313
|
+
|
314
|
+
def ninsert(sub_obj: Union[Dict, List], parts: List[Union[str, int]],
|
315
|
+
value: Any, sep: str = '_', max_depth: Union[int, None] = None, current_depth: int = 0):
|
316
|
+
"""
|
317
|
+
Inserts a value into a nested structure (dictionary or list) based on the specified path (parts).
|
318
|
+
|
319
|
+
Args:
|
320
|
+
sub_obj (Union[Dict, List]): The sub-object (dict or list) to insert into.
|
321
|
+
parts (List[Union[str, int]]): The parts of the key representing the path to insert the value at.
|
322
|
+
value (Any): The value to insert.
|
323
|
+
sep (str, optional): Separator used in composite keys. Defaults to '_'.
|
324
|
+
max_depth (Union[int, None], optional): Maximum depth for nesting. Defaults to None.
|
325
|
+
current_depth (int, optional): The current depth in the nested structure. Defaults to 0.
|
326
|
+
|
327
|
+
Examples:
|
328
|
+
>>> sub_obj = {}
|
329
|
+
>>> ninsert(sub_obj, ['a', 'b'], 2)
|
330
|
+
>>> print(sub_obj) # Output: {'a': {'b': 2}}
|
331
|
+
|
332
|
+
>>> sub_obj = []
|
333
|
+
>>> ninsert(sub_obj, [0, 'a'], 1)
|
334
|
+
>>> print(sub_obj) # Output: [{'a': 1}]
|
335
|
+
"""
|
336
|
+
parts_len = len(parts)
|
337
|
+
parts_depth = 0
|
338
|
+
for i, part in enumerate(parts[:-1]):
|
339
|
+
if max_depth is not None and current_depth >= max_depth:
|
340
|
+
break
|
341
|
+
|
342
|
+
if isinstance(part, int):
|
343
|
+
while len(sub_obj) <= part:
|
344
|
+
sub_obj.append(None)
|
345
|
+
if sub_obj[part] is None or not isinstance(sub_obj[part], (dict, list)):
|
346
|
+
next_part = parts[i + 1]
|
347
|
+
sub_obj[part] = [] if isinstance(next_part, int) else {}
|
348
|
+
sub_obj = sub_obj[part]
|
349
|
+
else:
|
350
|
+
if part not in sub_obj:
|
351
|
+
next_part = parts[i + 1]
|
352
|
+
sub_obj[part] = [] if isinstance(next_part, int) else {}
|
353
|
+
sub_obj = sub_obj[part]
|
354
|
+
current_depth += 1
|
355
|
+
parts_depth += 1
|
356
|
+
|
357
|
+
if parts_depth < parts_len - 1:
|
358
|
+
last_part = sep.join([str(part) for part in parts[parts_depth:]])
|
359
|
+
else:
|
360
|
+
last_part = parts[-1]
|
361
|
+
if isinstance(last_part, int):
|
362
|
+
_handle_list_insert(sub_obj, last_part, value)
|
363
|
+
else:
|
364
|
+
if isinstance(sub_obj, list):
|
365
|
+
sub_obj.append({last_part: value})
|
366
|
+
else:
|
367
|
+
sub_obj[last_part] = value
|
368
|
+
|
369
|
+
def get_flattened_keys(obj: Any, sep: str = '_', max_depth: Union[int, None] = None,
|
370
|
+
dict_only: bool = False, inplace: bool = False) -> List[str]:
|
371
|
+
"""
|
372
|
+
Retrieves a list of keys from a flattened dictionary or list.
|
373
|
+
|
374
|
+
Args:
|
375
|
+
obj (Any): The dictionary or list to retrieve keys from.
|
376
|
+
sep (str, optional): Separator to join keys. Defaults to '_'.
|
377
|
+
max_depth (Union[int, None], optional): Maximum depth to consider for flattening. Defaults to None.
|
378
|
+
dict_only (bool, optional): If True, only dictionaries are considered for flattening. Defaults to False.
|
379
|
+
inplace (bool, optional): If True, the original object is modified in place. Defaults to False.
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
List[str]: A list of keys from the flattened object.
|
383
|
+
|
384
|
+
Raises:
|
385
|
+
ValueError: If inplace is True and the provided object is not a dictionary.
|
386
|
+
|
387
|
+
Examples:
|
388
|
+
>>> nested_dict = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}}
|
389
|
+
>>> keys = get_flattened_keys(nested_dict)
|
390
|
+
>>> print(keys) # Output: ['a', 'b_c', 'b_d_e']
|
391
|
+
|
392
|
+
>>> nested_list = [{'a': 1}, {'b': 2}]
|
393
|
+
>>> keys = get_flattened_keys(nested_list)
|
394
|
+
>>> print(keys) # Output: ['0_a', '1_b']
|
395
|
+
"""
|
396
|
+
if inplace:
|
397
|
+
obj_copy = deepcopy(obj)
|
398
|
+
flatten(obj_copy, sep=sep, max_depth=max_depth, inplace=True, dict_only=dict_only)
|
399
|
+
return list(obj_copy.keys())
|
400
|
+
else:
|
401
|
+
return list(flatten(obj, sep=sep, max_depth=max_depth, dict_only=dict_only).keys())
|
402
|
+
|
403
|
+
def _dynamic_flatten_in_place(
|
404
|
+
obj: Any, parent_key: str = '', sep: str = '_',
|
405
|
+
max_depth: Union[int, None] = None, current_depth: int = 0,
|
406
|
+
dict_only: bool = False
|
407
|
+
) -> None:
|
408
|
+
"""
|
409
|
+
Helper function to flatten the object in place.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
obj (Any): The object to flatten.
|
413
|
+
parent_key (str, optional): The base key for the flattened dictionary. Defaults to ''.
|
414
|
+
sep (str, optional): Separator used in keys. Defaults to '_'.
|
415
|
+
max_depth (Union[int, None], optional): Maximum depth to flatten. Defaults to None.
|
416
|
+
current_depth (int, optional): Current depth in recursion. Defaults to 0.
|
417
|
+
dict_only (bool, optional): If True, flattens only dictionaries. Defaults to False.
|
418
|
+
|
419
|
+
Returns:
|
420
|
+
None: This function modifies the input object in place and returns None.
|
421
|
+
"""
|
422
|
+
if isinstance(obj, dict):
|
423
|
+
keys_to_delete = []
|
424
|
+
items = list(obj.items()) # Create a copy of the dictionary items
|
425
|
+
|
426
|
+
for k, v in items:
|
427
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
428
|
+
|
429
|
+
if isinstance(v, dict) and (max_depth is None or current_depth < max_depth):
|
430
|
+
_dynamic_flatten_in_place(v, new_key, sep, max_depth, current_depth + 1, dict_only)
|
431
|
+
keys_to_delete.append(k)
|
432
|
+
obj.update(v)
|
433
|
+
elif not dict_only and (isinstance(v, list) or not isinstance(v, (dict, list))):
|
434
|
+
obj[new_key] = v
|
435
|
+
if parent_key:
|
436
|
+
keys_to_delete.append(k)
|
437
|
+
|
438
|
+
for k in keys_to_delete:
|
439
|
+
del obj[k]
|
440
|
+
|
441
|
+
def _handle_list_insert(sub_obj: List, part: int, value: Any):
|
442
|
+
"""
|
443
|
+
Inserts or replaces a value in a list at a specified index. If the index is
|
444
|
+
beyond the current size of the list, the list is extended with `None` values.
|
445
|
+
|
446
|
+
Args:
|
447
|
+
sub_obj (List): The list in which the value needs to be inserted.
|
448
|
+
|
449
|
+
part (int): The index at which the value should be inserted.
|
450
|
+
|
451
|
+
value (Any): The value to insert into the list.
|
452
|
+
|
453
|
+
Returns:
|
454
|
+
None: This function modifies the input list in place and returns None.
|
455
|
+
"""
|
456
|
+
while len(sub_obj) <= part:
|
457
|
+
sub_obj.append(None)
|
458
|
+
sub_obj[part] = value
|
459
|
+
|
460
|
+
def _ensure_list_index(
|
461
|
+
lst: List, index: int, default=None
|
462
|
+
) -> None:
|
463
|
+
"""Ensures that a list is at least as long as a specified index.
|
464
|
+
|
465
|
+
This method appends the default value to the list until its length is at least
|
466
|
+
equal to the specified index + 1. This ensures that accessing the list at the
|
467
|
+
specified index will not result in an IndexError.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
lst: The list to be modified.
|
471
|
+
|
472
|
+
index: The index the list should extend to.
|
473
|
+
|
474
|
+
default: The default value to append to the list. Defaults to None.
|
475
|
+
|
476
|
+
"""
|
477
|
+
while len(lst) <= index:
|
478
|
+
lst.append(default)
|
479
|
+
|
480
|
+
def _deep_update(original: Dict, update: Dict) -> Dict:
|
481
|
+
"""Recursively updates a dictionary with another dictionary.
|
482
|
+
|
483
|
+
For each key-value pair in the update dictionary, this method updates the original dictionary.
|
484
|
+
If the value corresponding to a key is itself a dictionary, the update is applied recursively.
|
485
|
+
|
486
|
+
Args:
|
487
|
+
original: The original dictionary to be updated.
|
488
|
+
|
489
|
+
update: The dictionary with updates to be applied.
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
The updated dictionary with the values from the update dictionary applied.
|
493
|
+
|
494
|
+
"""
|
495
|
+
for key, value in update.items():
|
496
|
+
if isinstance(value, dict) and key in original:
|
497
|
+
original[key] = _deep_update(original.get(key, {}), value)
|
498
|
+
else:
|
499
|
+
original[key] = value
|
500
|
+
return original
|
501
|
+
|
502
|
+
def _dynamic_flatten_generator(obj: Any, parent_key: Tuple[str, ...], sep: str = '_',
|
503
|
+
max_depth: Union[int, None] = None, current_depth: int = 0,
|
504
|
+
dict_only: bool = False
|
505
|
+
) -> Generator[Tuple[str, Any], None, None]:
|
506
|
+
"""A generator to flatten a nested dictionary or list into key-value pairs.
|
507
|
+
|
508
|
+
This method recursively traverses the nested dictionary or list and yields flattened key-value pairs.
|
509
|
+
|
510
|
+
Args:
|
511
|
+
obj: The nested object (dictionary or list) to flatten.
|
512
|
+
parent_key: Tuple of parent keys leading to the current object.
|
513
|
+
sep: Separator used between keys in the flattened output.
|
514
|
+
max_depth: Maximum depth to flatten. None indicates no depth limit.
|
515
|
+
current_depth: The current depth level in the nested object.
|
516
|
+
dict_only: If True, only processes nested dictionaries, skipping other types in lists.
|
517
|
+
|
518
|
+
Yields:
|
519
|
+
Tuple[str, Any]: Flattened key-value pair as a tuple.
|
520
|
+
"""
|
521
|
+
if max_depth is not None and current_depth > max_depth:
|
522
|
+
yield sep.join(parent_key), obj
|
523
|
+
return
|
524
|
+
|
525
|
+
if isinstance(obj, dict):
|
526
|
+
for k, v in obj.items():
|
527
|
+
new_key = parent_key + (k,)
|
528
|
+
yield from _dynamic_flatten_generator(v, new_key, sep, max_depth, current_depth + 1, dict_only)
|
529
|
+
elif isinstance(obj, list) and not dict_only:
|
530
|
+
for i, item in enumerate(obj):
|
531
|
+
new_key = parent_key + (str(i),)
|
532
|
+
yield from _dynamic_flatten_generator(item, new_key, sep, max_depth, current_depth + 1, dict_only)
|
533
|
+
else:
|
534
|
+
yield sep.join(parent_key), obj
|
535
|
+
|
536
|
+
def _deep_merge_dicts(dict1: Dict, dict2: Dict) -> Dict:
|
537
|
+
"""Merges two dictionaries deeply.
|
538
|
+
|
539
|
+
For each key in `dict2`, updates or adds the key-value pair to `dict1`. If values
|
540
|
+
for the same key are dictionaries, merge them recursively.
|
541
|
+
|
542
|
+
Args:
|
543
|
+
dict1: The dictionary to be updated.
|
544
|
+
dict2: The dictionary with values to update `dict1`.
|
545
|
+
|
546
|
+
Returns:
|
547
|
+
The updated dictionary `dict1`.
|
548
|
+
"""
|
549
|
+
for key in dict2:
|
550
|
+
if key in dict1:
|
551
|
+
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
552
|
+
_deep_merge_dicts(dict1[key], dict2[key])
|
553
|
+
else:
|
554
|
+
dict1[key] = dict2[key]
|
555
|
+
else:
|
556
|
+
dict1[key] = dict2[key]
|
557
|
+
return dict1
|
558
|
+
|
559
|
+
def _is_homogeneous(
|
560
|
+
iterables: List[Union[Dict, List, Iterable]], type_check: type
|
561
|
+
) -> bool:
|
562
|
+
"""Checks if all elements in a list are of the same specified type.
|
563
|
+
|
564
|
+
Args:
|
565
|
+
iterables: A list of elements to check.
|
566
|
+
type_check: The type to check against.
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
True if all elements in the list are of the specified type; otherwise False.
|
570
|
+
"""
|
571
|
+
return all(isinstance(it, type_check) for it in iterables)
|
572
|
+
|
573
|
+
def _merge_dicts(iterables: List[Dict[Any, Any]], dict_update: bool, dict_sequence: bool, sequence_separator: str) -> Dict[Any, Any]:
|
574
|
+
"""Merges a list of dictionaries into a single dictionary.
|
575
|
+
|
576
|
+
Args:
|
577
|
+
iterables: A list of dictionaries to be merged.
|
578
|
+
dict_update: If True, the value of a key in a later dictionary overwrites the previous one.
|
579
|
+
dict_sequence: If True, instead of overwriting, keys are made unique by appending a sequence number.
|
580
|
+
sequence_separator: The separator to use when creating unique keys in case of dict_sequence.
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
A merged dictionary containing the combined key-value pairs from all dictionaries in the list.
|
584
|
+
"""
|
585
|
+
merged_dict = {}
|
586
|
+
sequence_counters = defaultdict(int)
|
587
|
+
|
588
|
+
for d in iterables:
|
589
|
+
for key, value in d.items():
|
590
|
+
if key not in merged_dict or dict_update:
|
591
|
+
merged_dict[key] = value
|
592
|
+
elif dict_sequence:
|
593
|
+
sequence_counters[key] += 1
|
594
|
+
new_key = f"{key}{sequence_separator}{sequence_counters[key]}"
|
595
|
+
merged_dict[new_key] = value
|
596
|
+
|
597
|
+
return merged_dict
|
598
|
+
|
599
|
+
def _merge_sequences(iterables: List[Iterable], sort_list: bool, custom_sort: Callable[[Any], Any] = None) -> List:
|
600
|
+
"""Merges a list of sequences (like lists) into a single list.
|
601
|
+
|
602
|
+
Args:
|
603
|
+
iterables: A list of iterables to be merged.
|
604
|
+
sort_list: If True, the merged list will be sorted.
|
605
|
+
custom_sort: A custom sorting function; used only if sort_list is True.
|
606
|
+
|
607
|
+
Returns:
|
608
|
+
A merged list containing elements from all iterables in the list.
|
609
|
+
"""
|
610
|
+
merged_list = list(chain(*iterables))
|
611
|
+
if sort_list:
|
612
|
+
if custom_sort:
|
613
|
+
return sorted(merged_list, key=custom_sort)
|
614
|
+
|
615
|
+
else:
|
616
|
+
return sorted(merged_list, key=lambda x: (isinstance(x, str), x))
|
617
|
+
return merged_list
|
618
|
+
|
619
|
+
def _filter_dict(dictionary: Dict[Any, Any], condition: Callable[[Any], bool]) -> Dict[Any, Any]:
|
620
|
+
"""Filters a dictionary based on a given condition.
|
621
|
+
|
622
|
+
This static method iterates over each key-value pair in the dictionary and retains
|
623
|
+
those pairs where the condition function returns True.
|
624
|
+
|
625
|
+
Args:
|
626
|
+
dictionary: A dictionary to filter.
|
627
|
+
|
628
|
+
condition: A callable that takes a key-value pair tuple and returns a boolean.
|
629
|
+
If True, the pair is included in the result.
|
630
|
+
|
631
|
+
Returns:
|
632
|
+
A new dictionary containing only the key-value pairs that meet the condition.
|
633
|
+
|
634
|
+
"""
|
635
|
+
return {k: v for k, v in dictionary.items() if condition((k, v))}
|
636
|
+
|
637
|
+
def _filter_list(lst: List[Any], condition: Callable[[Any], bool]) -> List[Any]:
|
638
|
+
"""Filters a list based on a given condition.
|
639
|
+
|
640
|
+
Iterates over each item in the list and includes it in the result if the condition function
|
641
|
+
returns True for that item.
|
642
|
+
|
643
|
+
Args:
|
644
|
+
lst: A list to filter.
|
645
|
+
|
646
|
+
condition: A callable that takes an item from the list and returns a boolean.
|
647
|
+
If True, the item is included in the result.
|
648
|
+
|
649
|
+
Returns:
|
650
|
+
A new list containing only the items that meet the condition.
|
651
|
+
|
652
|
+
"""
|
653
|
+
return [item for item in lst if condition(item)]
|
654
|
+
|
655
|
+
def _get_target_container(
|
656
|
+
nested_list: Union[List, Dict], indices: List[Union[int, str]]
|
657
|
+
) -> Union[List, Dict]:
|
658
|
+
current_element = nested_list
|
659
|
+
for index in indices:
|
660
|
+
if isinstance(current_element, list):
|
661
|
+
if isinstance(index, int) and 0 <= index < len(current_element):
|
662
|
+
current_element = current_element[index]
|
663
|
+
else:
|
664
|
+
raise IndexError("List index out of range")
|
665
|
+
elif isinstance(current_element, dict):
|
666
|
+
if index in current_element:
|
667
|
+
current_element = current_element[index]
|
668
|
+
else:
|
669
|
+
raise KeyError("Key not found in dictionary")
|
670
|
+
else:
|
671
|
+
raise TypeError("Current element is neither a list nor a dictionary")
|
672
|
+
return current_element
|