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.
Files changed (123) hide show
  1. lionagi/__init__.py +1 -2
  2. lionagi/_services/__init__.py +5 -0
  3. lionagi/_services/anthropic.py +79 -0
  4. lionagi/_services/base_service.py +414 -0
  5. lionagi/_services/oai.py +98 -0
  6. lionagi/_services/openrouter.py +44 -0
  7. lionagi/_services/services.py +91 -0
  8. lionagi/_services/transformers.py +46 -0
  9. lionagi/bridge/langchain.py +26 -16
  10. lionagi/bridge/llama_index.py +50 -20
  11. lionagi/configs/oai_configs.py +2 -14
  12. lionagi/configs/openrouter_configs.py +2 -2
  13. lionagi/core/__init__.py +7 -8
  14. lionagi/core/branch/branch.py +589 -0
  15. lionagi/core/branch/branch_manager.py +139 -0
  16. lionagi/core/branch/conversation.py +484 -0
  17. lionagi/core/core_util.py +59 -0
  18. lionagi/core/flow/flow.py +19 -0
  19. lionagi/core/flow/flow_util.py +62 -0
  20. lionagi/core/instruction_set/__init__.py +0 -5
  21. lionagi/core/instruction_set/instruction_set.py +343 -0
  22. lionagi/core/messages/messages.py +176 -0
  23. lionagi/core/sessions/__init__.py +0 -5
  24. lionagi/core/sessions/session.py +428 -0
  25. lionagi/loaders/chunker.py +51 -47
  26. lionagi/loaders/load_util.py +2 -2
  27. lionagi/loaders/reader.py +45 -39
  28. lionagi/models/imodel.py +53 -0
  29. lionagi/schema/async_queue.py +158 -0
  30. lionagi/schema/base_node.py +318 -147
  31. lionagi/schema/base_tool.py +31 -1
  32. lionagi/schema/data_logger.py +74 -38
  33. lionagi/schema/data_node.py +57 -6
  34. lionagi/structures/graph.py +132 -10
  35. lionagi/structures/relationship.py +58 -20
  36. lionagi/structures/structure.py +36 -25
  37. lionagi/tests/test_utils/test_api_util.py +219 -0
  38. lionagi/tests/test_utils/test_call_util.py +785 -0
  39. lionagi/tests/test_utils/test_encrypt_util.py +323 -0
  40. lionagi/tests/test_utils/test_io_util.py +238 -0
  41. lionagi/tests/test_utils/test_nested_util.py +338 -0
  42. lionagi/tests/test_utils/test_sys_util.py +358 -0
  43. lionagi/tools/tool_manager.py +186 -0
  44. lionagi/tools/tool_util.py +266 -3
  45. lionagi/utils/__init__.py +21 -61
  46. lionagi/utils/api_util.py +359 -71
  47. lionagi/utils/call_util.py +839 -264
  48. lionagi/utils/encrypt_util.py +283 -16
  49. lionagi/utils/io_util.py +178 -93
  50. lionagi/utils/nested_util.py +672 -0
  51. lionagi/utils/pd_util.py +57 -0
  52. lionagi/utils/sys_util.py +284 -156
  53. lionagi/utils/url_util.py +55 -0
  54. lionagi/version.py +1 -1
  55. {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/METADATA +21 -17
  56. lionagi-0.0.204.dist-info/RECORD +106 -0
  57. lionagi/core/conversations/__init__.py +0 -5
  58. lionagi/core/conversations/conversation.py +0 -107
  59. lionagi/core/flows/__init__.py +0 -8
  60. lionagi/core/flows/flow.py +0 -8
  61. lionagi/core/flows/flow_util.py +0 -62
  62. lionagi/core/instruction_set/instruction_sets.py +0 -7
  63. lionagi/core/sessions/sessions.py +0 -185
  64. lionagi/endpoints/__init__.py +0 -5
  65. lionagi/endpoints/audio.py +0 -17
  66. lionagi/endpoints/chatcompletion.py +0 -54
  67. lionagi/messages/__init__.py +0 -11
  68. lionagi/messages/instruction.py +0 -15
  69. lionagi/messages/message.py +0 -110
  70. lionagi/messages/response.py +0 -33
  71. lionagi/messages/system.py +0 -12
  72. lionagi/objs/__init__.py +0 -11
  73. lionagi/objs/abc_objs.py +0 -39
  74. lionagi/objs/async_queue.py +0 -135
  75. lionagi/objs/messenger.py +0 -85
  76. lionagi/objs/tool_manager.py +0 -253
  77. lionagi/services/__init__.py +0 -11
  78. lionagi/services/base_api_service.py +0 -230
  79. lionagi/services/oai.py +0 -34
  80. lionagi/services/openrouter.py +0 -31
  81. lionagi/tests/test_api_util.py +0 -46
  82. lionagi/tests/test_call_util.py +0 -115
  83. lionagi/tests/test_convert_util.py +0 -202
  84. lionagi/tests/test_encrypt_util.py +0 -33
  85. lionagi/tests/test_flat_util.py +0 -426
  86. lionagi/tests/test_sys_util.py +0 -0
  87. lionagi/utils/convert_util.py +0 -229
  88. lionagi/utils/flat_util.py +0 -599
  89. lionagi-0.0.115.dist-info/RECORD +0 -110
  90. /lionagi/{services → _services}/anyscale.py +0 -0
  91. /lionagi/{services → _services}/azure.py +0 -0
  92. /lionagi/{services → _services}/bedrock.py +0 -0
  93. /lionagi/{services → _services}/everlyai.py +0 -0
  94. /lionagi/{services → _services}/gemini.py +0 -0
  95. /lionagi/{services → _services}/gpt4all.py +0 -0
  96. /lionagi/{services → _services}/huggingface.py +0 -0
  97. /lionagi/{services → _services}/litellm.py +0 -0
  98. /lionagi/{services → _services}/localai.py +0 -0
  99. /lionagi/{services → _services}/mistralai.py +0 -0
  100. /lionagi/{services → _services}/ollama.py +0 -0
  101. /lionagi/{services → _services}/openllm.py +0 -0
  102. /lionagi/{services → _services}/perplexity.py +0 -0
  103. /lionagi/{services → _services}/predibase.py +0 -0
  104. /lionagi/{services → _services}/rungpt.py +0 -0
  105. /lionagi/{services → _services}/vllm.py +0 -0
  106. /lionagi/{services → _services}/xinference.py +0 -0
  107. /lionagi/{endpoints/assistants.py → agents/__init__.py} +0 -0
  108. /lionagi/{tools → agents}/planner.py +0 -0
  109. /lionagi/{tools → agents}/prompter.py +0 -0
  110. /lionagi/{tools → agents}/scorer.py +0 -0
  111. /lionagi/{tools → agents}/summarizer.py +0 -0
  112. /lionagi/{tools → agents}/validator.py +0 -0
  113. /lionagi/{endpoints/embeddings.py → core/branch/__init__.py} +0 -0
  114. /lionagi/{services/anthropic.py → core/branch/cluster.py} +0 -0
  115. /lionagi/{endpoints/finetune.py → core/flow/__init__.py} +0 -0
  116. /lionagi/{endpoints/image.py → core/messages/__init__.py} +0 -0
  117. /lionagi/{endpoints/moderation.py → models/__init__.py} +0 -0
  118. /lionagi/{endpoints/vision.py → models/base_model.py} +0 -0
  119. /lionagi/{objs → schema}/status_tracker.py +0 -0
  120. /lionagi/tests/{test_io_util.py → test_utils/__init__.py} +0 -0
  121. {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/LICENSE +0 -0
  122. {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/WHEEL +0 -0
  123. {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