lionagi 0.16.2__py3-none-any.whl → 0.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. lionagi/adapters/_utils.py +10 -23
  2. lionagi/adapters/async_postgres_adapter.py +83 -79
  3. lionagi/ln/__init__.py +4 -4
  4. lionagi/ln/_json_dump.py +0 -6
  5. lionagi/ln/fuzzy/__init__.py +4 -1
  6. lionagi/ln/fuzzy/_fuzzy_validate.py +109 -0
  7. lionagi/ln/fuzzy/_to_dict.py +388 -0
  8. lionagi/models/__init__.py +0 -2
  9. lionagi/operations/__init__.py +0 -6
  10. lionagi/operations/_visualize_graph.py +285 -0
  11. lionagi/operations/brainstorm/brainstorm.py +14 -12
  12. lionagi/operations/builder.py +23 -302
  13. lionagi/operations/communicate/communicate.py +1 -1
  14. lionagi/operations/flow.py +14 -11
  15. lionagi/operations/node.py +14 -3
  16. lionagi/operations/operate/operate.py +5 -11
  17. lionagi/operations/parse/parse.py +2 -3
  18. lionagi/operations/types.py +0 -2
  19. lionagi/operations/utils.py +11 -5
  20. lionagi/protocols/generic/pile.py +3 -7
  21. lionagi/protocols/graph/graph.py +23 -6
  22. lionagi/protocols/graph/node.py +0 -2
  23. lionagi/protocols/messages/message.py +0 -1
  24. lionagi/protocols/operatives/operative.py +2 -2
  25. lionagi/protocols/types.py +0 -15
  26. lionagi/service/connections/endpoint.py +11 -5
  27. lionagi/service/connections/match_endpoint.py +2 -10
  28. lionagi/service/connections/providers/types.py +1 -3
  29. lionagi/service/hooks/hook_event.py +1 -1
  30. lionagi/service/hooks/hook_registry.py +1 -1
  31. lionagi/service/rate_limited_processor.py +1 -1
  32. lionagi/session/branch.py +24 -18
  33. lionagi/session/session.py +2 -18
  34. lionagi/utils.py +3 -335
  35. lionagi/version.py +1 -1
  36. {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/METADATA +4 -13
  37. {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/RECORD +39 -61
  38. lionagi/adapters/postgres_model_adapter.py +0 -131
  39. lionagi/libs/concurrency.py +0 -1
  40. lionagi/libs/nested/__init__.py +0 -3
  41. lionagi/libs/nested/flatten.py +0 -172
  42. lionagi/libs/nested/nfilter.py +0 -59
  43. lionagi/libs/nested/nget.py +0 -45
  44. lionagi/libs/nested/ninsert.py +0 -104
  45. lionagi/libs/nested/nmerge.py +0 -158
  46. lionagi/libs/nested/npop.py +0 -69
  47. lionagi/libs/nested/nset.py +0 -94
  48. lionagi/libs/nested/unflatten.py +0 -83
  49. lionagi/libs/nested/utils.py +0 -189
  50. lionagi/libs/parse.py +0 -31
  51. lionagi/libs/schema/json_schema.py +0 -231
  52. lionagi/libs/unstructured/__init__.py +0 -0
  53. lionagi/libs/unstructured/pdf_to_image.py +0 -45
  54. lionagi/libs/unstructured/read_image_to_base64.py +0 -33
  55. lionagi/libs/validate/fuzzy_match_keys.py +0 -7
  56. lionagi/libs/validate/fuzzy_validate_mapping.py +0 -144
  57. lionagi/libs/validate/string_similarity.py +0 -7
  58. lionagi/libs/validate/xml_parser.py +0 -203
  59. lionagi/models/note.py +0 -387
  60. lionagi/protocols/graph/_utils.py +0 -22
  61. lionagi/service/connections/providers/claude_code_.py +0 -299
  62. {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/WHEEL +0 -0
  63. {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,104 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from typing import Any
6
-
7
- from lionagi.utils import to_list
8
-
9
-
10
- def ninsert(
11
- nested_structure: dict[Any, Any] | list[Any],
12
- /,
13
- indices: list[str | int],
14
- value: Any,
15
- *,
16
- current_depth: int = 0,
17
- ) -> None:
18
- """
19
- Inserts a value into a nested structure at a specified path.
20
-
21
- Navigates a nested dictionary or list based on a sequence of indices or
22
- keys and inserts `value` at the final location. This method can create
23
- intermediate dictionaries or lists as needed.
24
-
25
- Args:
26
- nested_structure: The nested structure to modify.
27
- indices: The sequence of keys or indices defining the insertion path.
28
- value: The value to insert at the specified location.
29
- current_depth: Internal use only; tracks the current depth during
30
- recursive calls.
31
-
32
- Raises:
33
- ValueError: If the indices list is empty.
34
- TypeError: If an invalid key or container type is encountered.
35
-
36
- Examples:
37
- >>> subject_ = {'a': {'b': [1, 2]}}
38
- >>> ninsert(subject_, ['a', 'b', 2], 3)
39
- >>> assert subject_ == {'a': {'b': [1, 2, 3]}}
40
-
41
- >>> subject_ = []
42
- >>> ninsert(subject_, [0, 'a'], 1)
43
- >>> assert subject_ == [{'a': 1}]
44
- """
45
- if not indices:
46
- raise ValueError("Indices list cannot be empty")
47
-
48
- indices = to_list(indices)
49
- for i, part in enumerate(indices[:-1]):
50
- if isinstance(part, int):
51
- if isinstance(nested_structure, dict):
52
- raise TypeError(
53
- f"Unsupported key type: {type(part).__name__}.Only string keys are acceptable.",
54
- )
55
- while len(nested_structure) <= part:
56
- nested_structure.append(None)
57
- if nested_structure[part] is None or not isinstance(
58
- nested_structure[part], (dict, list)
59
- ):
60
- next_part = indices[i + 1]
61
- nested_structure[part] = (
62
- [] if isinstance(next_part, int) else {}
63
- )
64
- elif isinstance(nested_structure, dict):
65
- if part is None:
66
- raise TypeError("Cannot use NoneType as a key in a dictionary")
67
- if isinstance(part, (float, complex)):
68
- raise TypeError(
69
- f"Unsupported key type: {type(part).__name__}.Only string keys are acceptable.",
70
- )
71
- if part not in nested_structure:
72
- next_part = indices[i + 1]
73
- nested_structure[part] = (
74
- [] if isinstance(next_part, int) else {}
75
- )
76
- else:
77
- raise TypeError(
78
- f"Invalid container type: {type(nested_structure)} encountered during insertion"
79
- )
80
-
81
- nested_structure = nested_structure[part]
82
- current_depth += 1
83
-
84
- last_part = indices[-1]
85
- if isinstance(last_part, int):
86
- if isinstance(nested_structure, dict):
87
- raise TypeError(
88
- f"Unsupported key type: {type(last_part).__name__}."
89
- "Only string keys are acceptable.",
90
- )
91
- while len(nested_structure) <= last_part:
92
- nested_structure.append(None)
93
- nested_structure[last_part] = value
94
- elif isinstance(nested_structure, list):
95
- raise TypeError("Cannot use non-integer index on a list")
96
- else:
97
- if last_part is None:
98
- raise TypeError("Cannot use NoneType as a key in a dictionary")
99
- if isinstance(last_part, (float, complex)):
100
- raise TypeError(
101
- f"Unsupported key type: {type(last_part).__name__}."
102
- "Only string keys are acceptable.",
103
- )
104
- nested_structure[last_part] = value
@@ -1,158 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from collections import defaultdict
6
- from collections.abc import Callable, Sequence
7
- from itertools import chain
8
- from typing import Any
9
-
10
- from .utils import is_homogeneous
11
-
12
-
13
- def nmerge(
14
- nested_structure: Sequence[dict[str, Any] | list[Any]],
15
- /,
16
- *,
17
- overwrite: bool = False,
18
- dict_sequence: bool = False,
19
- sort_list: bool = False,
20
- custom_sort: Callable[[Any], Any] | None = None,
21
- ) -> dict[str, Any] | list[Any]:
22
- """
23
- Merge multiple dictionaries, lists, or sequences into a unified structure.
24
-
25
- Args:
26
- nested_structure: A sequence containing dictionaries, lists, or other
27
- iterable objects to merge.
28
- overwrite: If True, overwrite existing keys in dictionaries with
29
- those from subsequent dictionaries.
30
- dict_sequence: Enables unique key generation for duplicate keys by
31
- appending a sequence number. Applicable only if `overwrite` is
32
- False.
33
- sort_list: When True, sort the resulting list after merging. It does
34
- not affect dictionaries.
35
- custom_sort: An optional callable that defines custom sorting logic
36
- for the merged list.
37
-
38
- Returns:
39
- A merged dictionary or list, depending on the types present in
40
- `nested_structure`.
41
-
42
- Raises:
43
- TypeError: If `nested_structure` contains objects of incompatible
44
- types that cannot be merged.
45
- """
46
- if not isinstance(nested_structure, list):
47
- raise TypeError("Please input a list")
48
- if is_homogeneous(nested_structure, dict):
49
- return _merge_dicts(nested_structure, overwrite, dict_sequence)
50
- elif is_homogeneous(nested_structure, list):
51
- return _merge_sequences(nested_structure, sort_list, custom_sort)
52
- else:
53
- raise TypeError(
54
- "All items in the input list must be of the same type, either dict, list, or Iterable."
55
- )
56
-
57
-
58
- def _deep_merge_dicts(
59
- dict1: dict[str, Any], dict2: dict[str, Any]
60
- ) -> dict[str, Any]:
61
- """
62
- Recursively merges two dictionaries, combining values where keys overlap.
63
-
64
- Args:
65
- dict1: The first dictionary.
66
- dict2: The second dictionary.
67
-
68
- Returns:
69
- The merged dictionary.
70
- """
71
- for key in dict2:
72
- if key in dict1:
73
- if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
74
- _deep_merge_dicts(dict1[key], dict2[key])
75
- else:
76
- if not isinstance(dict1[key], list):
77
- dict1[key] = [dict1[key]]
78
- dict1[key].append(dict2[key])
79
- else:
80
- dict1[key] = dict2[key]
81
- return dict1
82
-
83
-
84
- def _merge_dicts(
85
- iterables: list[dict[str, Any]],
86
- dict_update: bool,
87
- dict_sequence: bool,
88
- ) -> dict[str, Any]:
89
- """
90
- Merges a list of dictionaries into a single dictionary, with options for
91
- handling duplicate keys and sequences.
92
-
93
- Args:
94
- iterables: A list of dictionaries to merge.
95
- dict_update: If True, overwrite existing keys in dictionaries
96
- with those from subsequent dictionaries.
97
- dict_sequence: Enables unique key generation for duplicate keys
98
- by appending a sequence number
99
-
100
- Returns:
101
- The merged dictionary.
102
- """
103
- merged_dict = {} # {'a': [1, 2]}
104
- sequence_counters = defaultdict(int)
105
- list_values = {}
106
-
107
- for d in iterables: # [{'a': [1, 2]}, {'a': [3, 4]}]
108
- for key, value in d.items(): # {'a': [3, 4]}
109
- if key not in merged_dict or dict_update:
110
- if (
111
- key in merged_dict
112
- and isinstance(merged_dict[key], dict)
113
- and isinstance(value, dict)
114
- ):
115
- _deep_merge_dicts(merged_dict[key], value)
116
- else:
117
- merged_dict[key] = value # {'a': [1, 2]}
118
- if isinstance(value, list):
119
- list_values[key] = True
120
- elif dict_sequence:
121
- sequence_counters[key] += 1
122
- new_key = f"{key}{sequence_counters[key]}"
123
- merged_dict[new_key] = value
124
- else:
125
- if not isinstance(merged_dict[key], list) or list_values.get(
126
- key, False
127
- ):
128
- merged_dict[key] = [merged_dict[key]]
129
- merged_dict[key].append(value)
130
-
131
- return merged_dict
132
-
133
-
134
- def _merge_sequences(
135
- iterables: list[list[Any]],
136
- sort_list: bool,
137
- custom_sort: Callable[[Any], Any] | None = None,
138
- ) -> list[Any]:
139
- """
140
- Merges a list of lists into a single list, with options for sorting and
141
- custom sorting logic.
142
-
143
- Args:
144
- iterables: A list of lists to merge.
145
- sort_list: When True, sort the resulting list after merging.
146
- custom_sort: An optional callable that defines custom sorting logic
147
- for the merged list.
148
-
149
- Returns:
150
- The merged list.
151
- """
152
- merged_list = list(chain(*iterables))
153
- if sort_list:
154
- if custom_sort:
155
- return sorted(merged_list, key=custom_sort)
156
- else:
157
- return sorted(merged_list, key=lambda x: (isinstance(x, str), x))
158
- return merged_list
@@ -1,69 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from collections.abc import Sequence
6
- from typing import Any
7
-
8
- from lionagi.utils import UNDEFINED, to_list
9
-
10
-
11
- def npop(
12
- input_: dict[str, Any] | list[Any],
13
- /,
14
- indices: str | int | Sequence[str | int],
15
- default: Any = UNDEFINED,
16
- ) -> Any:
17
- """
18
- Perform a nested pop operation on the input structure.
19
-
20
- This function navigates through the nested structure using the provided
21
- indices and removes and returns the value at the final location.
22
-
23
- Args:
24
- input_: The input nested structure (dict or list) to pop from.
25
- indices: A single index or a sequence of indices to navigate the
26
- nested structure.
27
- default: The value to return if the key is not found. If not
28
- provided, a KeyError will be raised.
29
-
30
- Returns:
31
- The value at the specified nested location.
32
-
33
- Raises:
34
- ValueError: If the indices list is empty.
35
- KeyError: If a key is not found in a dictionary.
36
- IndexError: If an index is out of range for a list.
37
- TypeError: If an operation is not supported on the current data type.
38
- """
39
- if not indices:
40
- raise ValueError("Indices list cannot be empty")
41
-
42
- indices = to_list(indices)
43
-
44
- current = input_
45
- for key in indices[:-1]:
46
- if isinstance(current, dict):
47
- if current.get(key):
48
- current = current[key]
49
- else:
50
- raise KeyError(f"{key} is not found in {current}")
51
- elif isinstance(current, list) and isinstance(key, int):
52
- if key >= len(current):
53
- raise KeyError(
54
- f"{key} exceeds the length of the list {current}"
55
- )
56
- elif key < 0:
57
- raise ValueError("list index cannot be negative")
58
- current = current[key]
59
-
60
- last_key = indices[-1]
61
- try:
62
- return current.pop(
63
- last_key,
64
- )
65
- except Exception as e:
66
- if default is not UNDEFINED:
67
- return default
68
- else:
69
- raise KeyError(f"Invalid npop. Error: {e}")
@@ -1,94 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from collections.abc import Sequence
6
- from typing import Any
7
-
8
- from lionagi.utils import to_list
9
-
10
- from .utils import ensure_list_index
11
-
12
-
13
- def nset(
14
- nested_structure: dict[str, Any] | list[Any],
15
- /,
16
- indices: str | int | Sequence[str | int],
17
- value: Any,
18
- ) -> None:
19
- """Set a value within a nested structure at the specified path.
20
-
21
- This method allows setting a value deep within a nested dictionary or list
22
- by specifying a path to the target location using a sequence of indices.
23
- Each index in the sequence represents a level in the nested structure,
24
- with integers used for list indices and strings for dictionary keys.
25
-
26
- Args:
27
- nested_structure: The nested structure to modify.
28
- indices: The path of indices leading to the target location.
29
- value: The value to set at the specified location.
30
-
31
- Raises:
32
- ValueError: If the indices sequence is empty.
33
- TypeError: If the target container is not a list or dictionary,
34
- or if the index type is incorrect.
35
-
36
- Examples:
37
- >>> data = {'a': {'b': [10, 20]}}
38
- >>> nset(data, ['a', 'b', 1], 99)
39
- >>> assert data == {'a': {'b': [10, 99]}}
40
-
41
- >>> data = [0, [1, 2], 3]
42
- >>> nset(data, [1, 1], 99)
43
- >>> assert data == [0, [1, 99], 3]
44
- """
45
-
46
- if not indices:
47
- raise ValueError(
48
- "Indices list is empty, cannot determine target container"
49
- )
50
-
51
- _indices = to_list(indices)
52
- target_container = nested_structure
53
-
54
- for i, index in enumerate(_indices[:-1]):
55
- if isinstance(target_container, list):
56
- if not isinstance(index, int):
57
- raise TypeError("Cannot use non-integer index on a list")
58
- ensure_list_index(target_container, index)
59
- if target_container[index] is None:
60
- next_index = _indices[i + 1]
61
- target_container[index] = (
62
- [] if isinstance(next_index, int) else {}
63
- )
64
- elif isinstance(target_container, dict):
65
- if isinstance(index, int):
66
- raise TypeError(
67
- f"Unsupported key type: {type(index).__name__}. "
68
- "Only string keys are acceptable."
69
- )
70
- if index not in target_container:
71
- next_index = _indices[i + 1]
72
- target_container[index] = (
73
- [] if isinstance(next_index, int) else {}
74
- )
75
- else:
76
- raise TypeError("Target container is not a list or dictionary")
77
-
78
- target_container = target_container[index]
79
-
80
- last_index = _indices[-1]
81
- if isinstance(target_container, list):
82
- if not isinstance(last_index, int):
83
- raise TypeError("Cannot use non-integer index on a list")
84
- ensure_list_index(target_container, last_index)
85
- target_container[last_index] = value
86
- elif isinstance(target_container, dict):
87
- if not isinstance(last_index, str):
88
- raise TypeError(
89
- f"Unsupported key type: {type(last_index).__name__}. "
90
- "Only string keys are acceptable."
91
- )
92
- target_container[last_index] = value
93
- else:
94
- raise TypeError("Cannot set value on non-list/dict element")
@@ -1,83 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from typing import Any
6
-
7
-
8
- def unflatten(
9
- flat_dict: dict[str, Any], sep: str = "|", inplace: bool = False
10
- ) -> dict[str, Any] | list[Any]:
11
- """
12
- Unflatten a single-level dictionary into a nested dictionary or list.
13
-
14
- Args:
15
- flat_dict: The flattened dictionary to unflatten.
16
- sep: The separator used for joining keys.
17
- inplace: Whether to modify the input dictionary in place.
18
-
19
- Returns:
20
- The unflattened nested dictionary or list.
21
-
22
- Examples:
23
- >>> unflatten({"a|b|c": 1, "a|b|d": 2})
24
- {'a': {'b': {'c': 1, 'd': 2}}}
25
-
26
- >>> unflatten({"0": "a", "1": "b", "2": "c"})
27
- ['a', 'b', 'c']
28
- """
29
-
30
- def _unflatten(data: dict) -> dict | list:
31
- result = {}
32
- for key, value in data.items():
33
- parts = key.split(sep)
34
- current = result
35
- for part in parts[:-1]:
36
- if part not in current:
37
- current[part] = {}
38
- current = current[part]
39
- if isinstance(value, dict):
40
- current[parts[-1]] = _unflatten(value)
41
- else:
42
- current[parts[-1]] = value
43
-
44
- # Convert dictionary to list if keys are consecutive integers
45
- if result and all(
46
- isinstance(key, str) and key.isdigit() for key in result
47
- ):
48
- return [result[str(i)] for i in range(len(result))]
49
- return result
50
-
51
- if inplace:
52
- unflattened_dict = {}
53
- for key, value in flat_dict.items():
54
- parts = key.split(sep)
55
- current = unflattened_dict
56
- for part in parts[:-1]:
57
- if part not in current:
58
- current[part] = {}
59
- current = current[part]
60
- current[parts[-1]] = value
61
-
62
- unflattened_result = _unflatten(unflattened_dict)
63
- flat_dict.clear()
64
- if isinstance(unflattened_result, list):
65
- flat_dict.update(
66
- {str(i): v for i, v in enumerate(unflattened_result)}
67
- )
68
- else:
69
- flat_dict.update(unflattened_result)
70
- return flat_dict
71
-
72
- else:
73
- unflattened_dict = {}
74
- for key, value in flat_dict.items():
75
- parts = key.split(sep)
76
- current = unflattened_dict
77
- for part in parts[:-1]:
78
- if part not in current:
79
- current[part] = {}
80
- current = current[part]
81
- current[parts[-1]] = value
82
-
83
- return _unflatten(unflattened_dict)
@@ -1,189 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from typing import Any
6
-
7
- from lionagi.utils import UNDEFINED
8
-
9
-
10
- def is_homogeneous(
11
- iterables: list[Any] | dict[Any, Any], type_check: type | tuple[type, ...]
12
- ) -> bool:
13
- """
14
- Check if all elements in a list or all values in a dict are of same type.
15
-
16
- Args:
17
- iterables: The list or dictionary to check.
18
- type_check: The type to check against.
19
-
20
- Returns:
21
- True if all elements/values are of the same type, False otherwise.
22
- """
23
- if isinstance(iterables, list):
24
- return all(isinstance(it, type_check) for it in iterables)
25
-
26
- elif isinstance(iterables, dict):
27
- return all(isinstance(val, type_check) for val in iterables.values())
28
-
29
- else:
30
- return isinstance(iterables, type_check)
31
-
32
-
33
- def is_same_dtype(
34
- input_: list[Any] | dict[Any, Any],
35
- dtype: type | None = None,
36
- return_dtype: bool = False,
37
- ) -> bool | tuple[bool, type | None]:
38
- """
39
- Check if all elements in a list or dict values are of the same data type.
40
-
41
- Args:
42
- input_: The input list or dictionary to check.
43
- dtype: The data type to check against. If None, uses the type of the
44
- first element.
45
- return_dtype: If True, return the data type with the check result.
46
-
47
- Returns:
48
- If return_dtype is False, returns True if all elements are of the
49
- same type (or if the input is empty), False otherwise.
50
- If return_dtype is True, returns a tuple (bool, type | None).
51
- """
52
- if not input_:
53
- return True
54
-
55
- iterable = input_.values() if isinstance(input_, dict) else input_
56
- first_element_type = type(next(iter(iterable), None))
57
-
58
- dtype = dtype or first_element_type
59
-
60
- result = all(isinstance(element, dtype) for element in iterable)
61
- return (result, dtype) if return_dtype else result
62
-
63
-
64
- def is_structure_homogeneous(
65
- structure: Any, return_structure_type: bool = False
66
- ) -> bool | tuple[bool, type | None]:
67
- """
68
- Check if a nested structure is homogeneous (no mix of lists and dicts).
69
-
70
- Args:
71
- structure: The nested structure to check.
72
- return_structure_type: If True, return the type of the homogeneous
73
- structure.
74
-
75
- Returns:
76
- If return_structure_type is False, returns True if the structure is
77
- homogeneous, False otherwise.
78
- If True, returns a tuple (bool, type | None).
79
-
80
- Examples:
81
- >>> is_structure_homogeneous({'a': {'b': 1}, 'c': {'d': 2}})
82
- True
83
- >>> is_structure_homogeneous({'a': {'b': 1}, 'c': [1, 2]})
84
- False
85
- """
86
-
87
- def _check_structure(substructure):
88
- structure_type = None
89
- if isinstance(substructure, list):
90
- structure_type = list
91
- for item in substructure:
92
- if not isinstance(item, structure_type) and isinstance(
93
- item, list | dict
94
- ):
95
- return False, None
96
- result, _ = _check_structure(item)
97
- if not result:
98
- return False, None
99
- elif isinstance(substructure, dict):
100
- structure_type = dict
101
- for item in substructure.values():
102
- if not isinstance(item, structure_type) and isinstance(
103
- item, list | dict
104
- ):
105
- return False, None
106
- result, _ = _check_structure(item)
107
- if not result:
108
- return False, None
109
- return True, structure_type
110
-
111
- is_homogeneous, structure_type = _check_structure(structure)
112
- return (
113
- (is_homogeneous, structure_type)
114
- if return_structure_type
115
- else is_homogeneous
116
- )
117
-
118
-
119
- def deep_update(
120
- original: dict[Any, Any], update: dict[Any, Any]
121
- ) -> dict[Any, Any]:
122
- """
123
- Recursively merge two dicts, updating nested dicts instead of overwriting.
124
-
125
- Args:
126
- original: The dictionary to update.
127
- update: The dictionary containing updates to apply to `original`.
128
-
129
- Returns:
130
- The `original` dictionary after applying updates from `update`.
131
-
132
- Note:
133
- This method modifies the `original` dictionary in place.
134
- """
135
- for key, value in update.items():
136
- if isinstance(value, dict) and key in original:
137
- original[key] = deep_update(original.get(key, {}), value)
138
- else:
139
- original[key] = value
140
- return original
141
-
142
-
143
- def get_target_container(
144
- nested: list[Any] | dict[Any, Any], indices: list[int | str]
145
- ) -> list[Any] | dict[Any, Any]:
146
- """
147
- Retrieve the target container in a nested structure using indices.
148
-
149
- Args:
150
- nested: The nested structure to navigate.
151
- indices: A list of indices to navigate through the nested structure.
152
-
153
- Returns:
154
- The target container at the specified path.
155
-
156
- Raises:
157
- IndexError: If a list index is out of range.
158
- KeyError: If a dictionary key is not found.
159
- TypeError: If the current element is neither a list nor a dictionary.
160
- """
161
- current_element = nested
162
- for index in indices:
163
- if isinstance(current_element, list):
164
- if isinstance(index, str) and index.isdigit():
165
- index = int(index)
166
-
167
- if isinstance(index, int) and 0 <= index < len(current_element):
168
- current_element = current_element[index]
169
-
170
- else:
171
- raise IndexError("List index is invalid or out of range")
172
-
173
- elif isinstance(current_element, dict):
174
- if index in current_element:
175
- current_element = current_element.get(index, None)
176
- else:
177
- raise KeyError("Key not found in dictionary")
178
- else:
179
- raise TypeError(
180
- "Current element is neither a list nor a dictionary"
181
- )
182
- return current_element
183
-
184
-
185
- def ensure_list_index(
186
- lst: list[Any], index: int, default: Any = UNDEFINED
187
- ) -> None:
188
- while len(lst) <= index:
189
- lst.append(default if default is not UNDEFINED else None)