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,338 @@
|
|
1
|
+
import unittest
|
2
|
+
import lionagi.utils.nested_util as n_util
|
3
|
+
|
4
|
+
class TestNFilter(unittest.TestCase):
|
5
|
+
|
6
|
+
def test_filter_dictionary_with_condition(self):
|
7
|
+
test_dict = {'a': 1, 'b': 2, 'c': 3}
|
8
|
+
result = n_util.nfilter(test_dict, lambda item: item[1] > 1)
|
9
|
+
self.assertEqual(result, {'b': 2, 'c': 3})
|
10
|
+
|
11
|
+
def test_filter_list_with_condition(self):
|
12
|
+
test_list = [1, 2, 3, 4]
|
13
|
+
result = n_util.nfilter(test_list, lambda x: x % 2 == 0)
|
14
|
+
self.assertEqual(result, [2, 4])
|
15
|
+
|
16
|
+
def test_filter_empty_dictionary(self):
|
17
|
+
self.assertEqual(n_util.nfilter({}, lambda item: item[1] > 1), {})
|
18
|
+
|
19
|
+
def test_filter_empty_list(self):
|
20
|
+
self.assertEqual(n_util.nfilter([], lambda x: x % 2 == 0), [])
|
21
|
+
|
22
|
+
def test_filter_all_items_fail_condition(self):
|
23
|
+
test_dict = {'a': 1, 'b': 2, 'c': 3}
|
24
|
+
self.assertEqual(n_util.nfilter(test_dict, lambda item: item[1] > 5), {})
|
25
|
+
|
26
|
+
def test_filter_all_items_pass_condition(self):
|
27
|
+
test_list = [1, 2, 3, 4]
|
28
|
+
self.assertEqual(n_util.nfilter(test_list, lambda x: x > 0), test_list)
|
29
|
+
|
30
|
+
def test_filter_nested_collections(self):
|
31
|
+
nested_dict = {'a': [1, 2, 3], 'b': [4, 5]}
|
32
|
+
result = n_util.nfilter(nested_dict, lambda item: len(item[1]) > 2)
|
33
|
+
self.assertEqual(result, {'a': [1, 2, 3]})
|
34
|
+
|
35
|
+
def test_filter_invalid_collection_type(self):
|
36
|
+
with self.assertRaises(TypeError):
|
37
|
+
n_util.nfilter("not a dict or list", lambda x: x)
|
38
|
+
|
39
|
+
def test_filter_with_complex_conditions(self):
|
40
|
+
test_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
|
41
|
+
# Condition: key is not 'a' and value is an even number
|
42
|
+
result = n_util.nfilter(test_dict, lambda item: item[0] != 'a' and item[1] % 2 == 0)
|
43
|
+
self.assertEqual(result, {'b': 2, 'd': 4})
|
44
|
+
|
45
|
+
|
46
|
+
class TestNSet(unittest.TestCase):
|
47
|
+
|
48
|
+
def test_set_value_in_nested_dict(self):
|
49
|
+
nested_dict = {'a': {'b': 1}}
|
50
|
+
n_util.nset(nested_dict, ['a', 'b'], 2)
|
51
|
+
self.assertEqual(nested_dict['a']['b'], 2)
|
52
|
+
|
53
|
+
def test_set_value_in_nested_list(self):
|
54
|
+
nested_list = [[1, 2], [3, 4]]
|
55
|
+
n_util.nset(nested_list, [1, 0], 5)
|
56
|
+
self.assertEqual(nested_list[1][0], 5)
|
57
|
+
|
58
|
+
def test_set_with_empty_indices_list(self):
|
59
|
+
with self.assertRaises(ValueError):
|
60
|
+
n_util.nset({}, [], 1)
|
61
|
+
|
62
|
+
def test_set_in_non_list_dict(self):
|
63
|
+
with self.assertRaises(TypeError):
|
64
|
+
n_util.nset(1, [0], 'value')
|
65
|
+
|
66
|
+
def test_set_with_index_out_of_bounds(self):
|
67
|
+
test_list = [1, 2, 3]
|
68
|
+
n_util.nset(test_list, [5], 4)
|
69
|
+
self.assertEqual(test_list[5], 4)
|
70
|
+
|
71
|
+
def test_set_with_non_existent_dict_key(self):
|
72
|
+
test_dict = {'a': 1}
|
73
|
+
n_util.nset(test_dict, ['b'], 2)
|
74
|
+
self.assertEqual(test_dict['b'], 2)
|
75
|
+
|
76
|
+
def test_set_with_mixed_indices_types(self):
|
77
|
+
nested_structure = {'a': [1, {'b': 2}]}
|
78
|
+
n_util.nset(nested_structure, ['a', 1, 'b'], 3)
|
79
|
+
self.assertEqual(nested_structure['a'][1]['b'], 3)
|
80
|
+
|
81
|
+
|
82
|
+
class TestNGet(unittest.TestCase):
|
83
|
+
|
84
|
+
def test_get_value_from_nested_dict(self):
|
85
|
+
nested_dict = {'a': {'b': 1}}
|
86
|
+
result = n_util.nget(nested_dict, ['a', 'b'])
|
87
|
+
self.assertEqual(result, 1)
|
88
|
+
|
89
|
+
def test_get_value_from_nested_list(self):
|
90
|
+
nested_list = [[1, 2], [3, 4]]
|
91
|
+
result = n_util.nget(nested_list, [1, 0])
|
92
|
+
self.assertEqual(result, 3)
|
93
|
+
|
94
|
+
def test_get_with_non_existent_key_index(self):
|
95
|
+
nested_dict = {'a': 1}
|
96
|
+
result = n_util.nget(nested_dict, ['b'])
|
97
|
+
self.assertIsNone(result)
|
98
|
+
|
99
|
+
nested_list = [1, 2, 3]
|
100
|
+
result = n_util.nget(nested_list, [5])
|
101
|
+
self.assertIsNone(result)
|
102
|
+
|
103
|
+
def test_get_with_mixed_indices(self):
|
104
|
+
nested_structure = {'a': [1, {'b': 2}]}
|
105
|
+
result = n_util.nget(nested_structure, ['a', 1, 'b'])
|
106
|
+
self.assertEqual(result, 2)
|
107
|
+
|
108
|
+
def test_get_with_invalid_structure_type(self):
|
109
|
+
result = n_util.nget(1, [0]) # Attempting to retrieve from an integer, not a list/dict
|
110
|
+
self.assertIsNone(result)
|
111
|
+
|
112
|
+
def test_get_with_index_out_of_bounds(self):
|
113
|
+
test_list = [1, 2, 3]
|
114
|
+
result = n_util.nget(test_list, [5])
|
115
|
+
self.assertIsNone(result)
|
116
|
+
|
117
|
+
|
118
|
+
class TestIsStructureHomogeneous(unittest.TestCase):
|
119
|
+
|
120
|
+
def test_homogeneous_structure_list(self):
|
121
|
+
test_list = [[1, 2], [3, 4]]
|
122
|
+
self.assertTrue(n_util.is_structure_homogeneous(test_list))
|
123
|
+
|
124
|
+
def test_homogeneous_structure_dict(self):
|
125
|
+
test_dict = {'a': {'b': 1}, 'c': {'d': 2}}
|
126
|
+
self.assertTrue(n_util.is_structure_homogeneous(test_dict))
|
127
|
+
|
128
|
+
def test_heterogeneous_structure(self):
|
129
|
+
test_structure = {'a': [1, 2], 'b': {'c': 3}}
|
130
|
+
self.assertFalse(n_util.is_structure_homogeneous(test_structure))
|
131
|
+
|
132
|
+
def test_empty_structure(self):
|
133
|
+
self.assertTrue(n_util.is_structure_homogeneous([]))
|
134
|
+
self.assertTrue(n_util.is_structure_homogeneous({}))
|
135
|
+
|
136
|
+
def test_return_structure_type_flag(self):
|
137
|
+
test_list = [1, 2, 3]
|
138
|
+
is_homogeneous, structure_type = n_util.is_structure_homogeneous(test_list, return_structure_type=True)
|
139
|
+
self.assertTrue(is_homogeneous)
|
140
|
+
self.assertIs(structure_type, list)
|
141
|
+
|
142
|
+
test_dict = {'a': 1, 'b': 2}
|
143
|
+
is_homogeneous, structure_type = n_util.is_structure_homogeneous(test_dict, return_structure_type=True)
|
144
|
+
self.assertTrue(is_homogeneous)
|
145
|
+
self.assertIs(structure_type, dict)
|
146
|
+
|
147
|
+
test_mixed = [1, {'a': 2}]
|
148
|
+
is_homogeneous, structure_type = n_util.is_structure_homogeneous(test_mixed, return_structure_type=True)
|
149
|
+
self.assertFalse(is_homogeneous)
|
150
|
+
self.assertIsNone(structure_type)
|
151
|
+
|
152
|
+
def test_deeply_nested_structures(self):
|
153
|
+
test_structure = {'a': [{'b': 1}, {'c': [2, 3]}]}
|
154
|
+
self.assertFalse(n_util.is_structure_homogeneous(test_structure))
|
155
|
+
|
156
|
+
class TestNMerge(unittest.TestCase):
|
157
|
+
|
158
|
+
def test_merge_list_of_dictionaries(self):
|
159
|
+
dict_list = [{'a': 1}, {'b': 2}]
|
160
|
+
result = n_util.nmerge(dict_list)
|
161
|
+
self.assertEqual(result, {'a': 1, 'b': 2})
|
162
|
+
|
163
|
+
def test_merge_list_of_lists(self):
|
164
|
+
list_of_lists = [[1, 2], [3, 4]]
|
165
|
+
result = n_util.nmerge(list_of_lists)
|
166
|
+
self.assertEqual(result, [1, 2, 3, 4])
|
167
|
+
|
168
|
+
def test_merge_empty_list(self):
|
169
|
+
self.assertEqual(n_util.nmerge([]), {})
|
170
|
+
|
171
|
+
def test_dict_update_option(self):
|
172
|
+
dict_list = [{'a': 1}, {'a': 2, 'b': 3}]
|
173
|
+
result = n_util.nmerge(dict_list, dict_update=True)
|
174
|
+
self.assertEqual(result, {'a': 2, 'b': 3})
|
175
|
+
|
176
|
+
def test_dict_sequence_option(self):
|
177
|
+
dict_list = [{'a': 1}, {'a': 2}]
|
178
|
+
result = n_util.nmerge(dict_list, dict_sequence=True, sequence_separator='_')
|
179
|
+
self.assertIn('a', result)
|
180
|
+
self.assertIn('a_1', result)
|
181
|
+
|
182
|
+
def test_sort_list_option(self):
|
183
|
+
list_of_lists = [[3, 1], [4, 2]]
|
184
|
+
result = n_util.nmerge(list_of_lists, sort_list=True)
|
185
|
+
self.assertEqual(result, [1, 2, 3, 4])
|
186
|
+
|
187
|
+
def test_custom_sort_function(self):
|
188
|
+
list_of_lists = [['b', 'a'], ['d', 'c']]
|
189
|
+
custom_sort_func = lambda x: x
|
190
|
+
result = n_util.nmerge(list_of_lists, sort_list=True, custom_sort=custom_sort_func)
|
191
|
+
self.assertEqual(result, ['a', 'b', 'c', 'd'])
|
192
|
+
|
193
|
+
def test_inhomogeneous_type_error(self):
|
194
|
+
mixed_list = [{'a': 1}, [1, 2]]
|
195
|
+
with self.assertRaises(TypeError):
|
196
|
+
n_util.nmerge(mixed_list)
|
197
|
+
|
198
|
+
|
199
|
+
class TestFlatten(unittest.TestCase):
|
200
|
+
|
201
|
+
def test_flatten_nested_dict(self):
|
202
|
+
nested_dict = {'a': {'b': 1, 'c': {'d': 2}}}
|
203
|
+
result = n_util.flatten(nested_dict)
|
204
|
+
self.assertEqual(result, {'a_b': 1, 'a_c_d': 2})
|
205
|
+
|
206
|
+
def test_flatten_nested_list(self):
|
207
|
+
nested_list = [[1, 2], [3, [4, 5]]]
|
208
|
+
result = n_util.flatten(nested_list)
|
209
|
+
self.assertEqual(result, {'0_0': 1, '0_1': 2, '1_0': 3, '1_1_0': 4, '1_1_1': 5})
|
210
|
+
|
211
|
+
def test_flatten_with_max_depth(self):
|
212
|
+
nested_dict = {'a': {'b': {'c': 1}}}
|
213
|
+
result = n_util.flatten(nested_dict, max_depth=1)
|
214
|
+
self.assertEqual(result, {'a_b': {'c': 1}})
|
215
|
+
|
216
|
+
def test_flatten_dict_only(self):
|
217
|
+
nested_mix = {'a': [1, 2], 'b': {'c': 3}}
|
218
|
+
result = n_util.flatten(nested_mix, dict_only=True)
|
219
|
+
self.assertEqual(result, {'a': [1, 2], 'b_c': 3})
|
220
|
+
|
221
|
+
def test_flatten_inplace(self):
|
222
|
+
nested_dict = {'a': {'b': 1}}
|
223
|
+
n_util.flatten(nested_dict, inplace=True)
|
224
|
+
self.assertEqual(nested_dict, {'a_b': 1})
|
225
|
+
|
226
|
+
def test_flatten_empty_structure(self):
|
227
|
+
self.assertEqual(n_util.flatten({}), {})
|
228
|
+
self.assertEqual(n_util.flatten([]), {})
|
229
|
+
|
230
|
+
def test_flatten_deeply_nested_structure(self):
|
231
|
+
deeply_nested = {'a': {'b': {'c': {'d': 1}}}}
|
232
|
+
result = n_util.flatten(deeply_nested)
|
233
|
+
self.assertEqual(result, {'a_b_c_d': 1})
|
234
|
+
|
235
|
+
|
236
|
+
class TestUnflatten(unittest.TestCase):
|
237
|
+
|
238
|
+
def test_unflatten_to_nested_dict(self):
|
239
|
+
flat_dict = {'a_b': 1, 'a_c_d': 2}
|
240
|
+
result = n_util.unflatten(flat_dict)
|
241
|
+
self.assertEqual(result, {'a': {'b': 1, 'c': {'d': 2}}})
|
242
|
+
|
243
|
+
def test_unflatten_to_nested_list(self):
|
244
|
+
flat_dict = {'0_0': 1, '0_1': 2, '1': [3, 4]}
|
245
|
+
result = n_util.unflatten(flat_dict)
|
246
|
+
self.assertEqual(result, [[1, 2], [3, 4]])
|
247
|
+
|
248
|
+
def test_unflatten_with_custom_separator(self):
|
249
|
+
flat_dict = {'a-b': 1, 'a-c-d': 2}
|
250
|
+
result = n_util.unflatten(flat_dict, sep='-')
|
251
|
+
self.assertEqual(result, {'a': {'b': 1, 'c': {'d': 2}}})
|
252
|
+
|
253
|
+
def test_unflatten_with_custom_logic(self):
|
254
|
+
flat_dict = {'a_1': 'one', 'a_2': 'two'}
|
255
|
+
custom_logic = lambda key: int(key) if key.isdigit() else key
|
256
|
+
result = n_util.unflatten(flat_dict, custom_logic=custom_logic)
|
257
|
+
self.assertEqual(result, {'a': [None, 'one', 'two']})
|
258
|
+
|
259
|
+
def test_unflatten_with_max_depth(self):
|
260
|
+
flat_dict = {'a_b_c': 1, 'a_b_d': 2}
|
261
|
+
result = n_util.unflatten(flat_dict, max_depth=1)
|
262
|
+
self.assertEqual(result, {'a': {'b_c': 1, 'b_d': 2}})
|
263
|
+
|
264
|
+
def test_unflatten_with_max_depth_int(self):
|
265
|
+
flat_dict = {'0_0_0': 1, '0_1_0': 2}
|
266
|
+
result = n_util.unflatten(flat_dict, max_depth=1)
|
267
|
+
self.assertEqual(result, [[{'0_0': 1}, {'1_0': 2}]])
|
268
|
+
|
269
|
+
def test_unflatten_empty_flat_dict(self):
|
270
|
+
self.assertEqual(n_util.unflatten({}), [])
|
271
|
+
|
272
|
+
def test_unflatten_to_mixed_structure(self):
|
273
|
+
flat_dict = {'0_0': 1, '1_key': 2}
|
274
|
+
result = n_util.unflatten(flat_dict)
|
275
|
+
self.assertEqual(result, [[1], {'key': 2}])
|
276
|
+
|
277
|
+
|
278
|
+
class TestNInsert(unittest.TestCase):
|
279
|
+
|
280
|
+
def test_insert_into_nested_dict(self):
|
281
|
+
obj = {}
|
282
|
+
n_util.ninsert(obj, ['a', 'b'], 1)
|
283
|
+
self.assertEqual(obj, {'a': {'b': 1}})
|
284
|
+
|
285
|
+
def test_insert_into_nested_list(self):
|
286
|
+
obj = []
|
287
|
+
n_util.ninsert(obj, [0, 1], 'value')
|
288
|
+
self.assertEqual(obj, [[None, 'value']])
|
289
|
+
|
290
|
+
def test_insert_with_mixed_paths(self):
|
291
|
+
obj = {'a': [{}]}
|
292
|
+
n_util.ninsert(obj, ['a', 0, 'b'], 2)
|
293
|
+
self.assertEqual(obj, {'a': [{'b': 2}]})
|
294
|
+
|
295
|
+
def test_insert_with_max_depth(self):
|
296
|
+
obj = {}
|
297
|
+
n_util.ninsert(obj, ['a', 'b', 'c'], 'value', max_depth=1)
|
298
|
+
self.assertEqual(obj, {'a': {'b_c': 'value'}})
|
299
|
+
|
300
|
+
def test_insert_into_empty_structure(self):
|
301
|
+
obj = {}
|
302
|
+
n_util.ninsert(obj, ['a'], 1)
|
303
|
+
self.assertEqual(obj, {'a': 1})
|
304
|
+
|
305
|
+
|
306
|
+
class TestGetFlattenedKeys(unittest.TestCase):
|
307
|
+
|
308
|
+
def test_get_keys_from_nested_dict(self):
|
309
|
+
nested_dict = {'a': {'b': 1, 'c': {'d': 2}}}
|
310
|
+
expected_keys = ['a_b', 'a_c_d']
|
311
|
+
self.assertEqual(set(n_util.get_flattened_keys(nested_dict)), set(expected_keys))
|
312
|
+
|
313
|
+
def test_get_keys_from_nested_list(self):
|
314
|
+
nested_list = [[1, 2], [3, [4, 5]]]
|
315
|
+
expected_keys = ['0_0', '0_1', '1_0', '1_1_0', '1_1_1']
|
316
|
+
self.assertEqual(set(n_util.get_flattened_keys(nested_list)), set(expected_keys))
|
317
|
+
|
318
|
+
def test_get_keys_with_max_depth(self):
|
319
|
+
nested_dict = {'a': {'b': {'c': 1}}}
|
320
|
+
expected_keys = ['a_b']
|
321
|
+
self.assertEqual(set(n_util.get_flattened_keys(nested_dict, max_depth=1)), set(expected_keys))
|
322
|
+
|
323
|
+
def test_get_keys_dict_only(self):
|
324
|
+
nested_mix = {'a': [1, 2], 'b': {'c': 3}}
|
325
|
+
expected_keys = ['a', 'b_c']
|
326
|
+
self.assertEqual(set(n_util.get_flattened_keys(nested_mix, dict_only=True)), set(expected_keys))
|
327
|
+
|
328
|
+
def test_get_keys_from_empty_structure(self):
|
329
|
+
self.assertEqual(n_util.get_flattened_keys({}), [])
|
330
|
+
|
331
|
+
def test_keys_from_deeply_nested_structure(self):
|
332
|
+
deeply_nested = {'a': {'b': {'c': {'d': 1}}}}
|
333
|
+
expected_keys = ['a_b_c_d']
|
334
|
+
self.assertEqual(set(n_util.get_flattened_keys(deeply_nested)), set(expected_keys))
|
335
|
+
|
336
|
+
|
337
|
+
if __name__ == '__main__':
|
338
|
+
unittest.main()
|
@@ -0,0 +1,358 @@
|
|
1
|
+
import unittest
|
2
|
+
from typing import Dict, List, Union, Tuple, Optional, Type, Any
|
3
|
+
from pathlib import Path
|
4
|
+
import hashlib
|
5
|
+
import os
|
6
|
+
from datetime import datetime
|
7
|
+
from unittest.mock import patch
|
8
|
+
import re
|
9
|
+
from dateutil import parser
|
10
|
+
import copy
|
11
|
+
|
12
|
+
|
13
|
+
def get_timestamp() -> str:
|
14
|
+
"""
|
15
|
+
Generates a current timestamp in a file-safe string format.
|
16
|
+
|
17
|
+
This function creates a timestamp from the current time, formatted in ISO 8601 format,
|
18
|
+
and replaces characters that are typically problematic in filenames (like colons and periods)
|
19
|
+
with underscores.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
str: The current timestamp in a file-safe string format.
|
23
|
+
|
24
|
+
Example:
|
25
|
+
>>> get_timestamp() # Doctest: +ELLIPSIS
|
26
|
+
'...'
|
27
|
+
"""
|
28
|
+
return datetime.now().isoformat().replace(":", "_").replace(".", "_")
|
29
|
+
|
30
|
+
def create_copy(input: Any, n: int) -> Any:
|
31
|
+
"""
|
32
|
+
Creates a deep copy of the input object a specified number of times.
|
33
|
+
|
34
|
+
This function makes deep copies of the provided input. If the number of copies ('n')
|
35
|
+
is greater than 1, a list of deep copies is returned. For a single copy, it returns
|
36
|
+
the copy directly.
|
37
|
+
|
38
|
+
Parameters:
|
39
|
+
input (Any): The object to be copied.
|
40
|
+
|
41
|
+
n (int): The number of deep copies to create.
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
ValueError: If 'n' is not a positive integer.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Any: A deep copy of 'input' or a list of deep copies if 'n' > 1.
|
48
|
+
|
49
|
+
Example:
|
50
|
+
>>> sample_dict = {'key': 'value'}
|
51
|
+
>>> create_copy(sample_dict, 2)
|
52
|
+
[{'key': 'value'}, {'key': 'value'}]
|
53
|
+
"""
|
54
|
+
if not isinstance(n, int) or n < 1:
|
55
|
+
raise ValueError(f"'n' must be a positive integer: {n}")
|
56
|
+
return copy.deepcopy(input) if n == 1 else [copy.deepcopy(input) for _ in range(n)]
|
57
|
+
|
58
|
+
def create_path(dir: str, filename: str, timestamp: bool = True, dir_exist_ok: bool = True, time_prefix=False) -> str:
|
59
|
+
dir = dir if dir.endswith('/') else dir + '/'
|
60
|
+
filename, ext = os.path.splitext(filename)
|
61
|
+
ext = ext[1:] # remove the dot from extension
|
62
|
+
os.makedirs(dir, exist_ok=dir_exist_ok)
|
63
|
+
|
64
|
+
if timestamp:
|
65
|
+
timestamp_str = get_timestamp()
|
66
|
+
if time_prefix:
|
67
|
+
filename = f"{timestamp_str}_{filename}.{ext}"
|
68
|
+
else:
|
69
|
+
filename = f"{filename}_{timestamp_str}.{ext}"
|
70
|
+
else:
|
71
|
+
filename = f"{filename}.{ext}"
|
72
|
+
|
73
|
+
return os.path.join(dir, filename)
|
74
|
+
|
75
|
+
def split_path(path: Path) -> tuple:
|
76
|
+
folder_name = path.parent.name
|
77
|
+
file_name = path.name
|
78
|
+
return (folder_name, file_name)
|
79
|
+
|
80
|
+
def change_dict_key(dict_, old_key, new_key):
|
81
|
+
if old_key in dict_:
|
82
|
+
dict_[new_key] = dict_.pop(old_key)
|
83
|
+
else:
|
84
|
+
raise KeyError(f"Key '{old_key}' not found in dictionary.")
|
85
|
+
|
86
|
+
def create_id(n=32) -> str:
|
87
|
+
current_time = datetime.now().isoformat().encode('utf-8')
|
88
|
+
random_bytes = os.urandom(16)
|
89
|
+
return hashlib.sha256(current_time + random_bytes).hexdigest()[:n]
|
90
|
+
|
91
|
+
def str_to_datetime(datetime_str: str, fmt: Optional[str] = None) -> datetime:
|
92
|
+
if fmt:
|
93
|
+
return datetime.strptime(datetime_str, fmt)
|
94
|
+
else:
|
95
|
+
return parser.parse(datetime_str)
|
96
|
+
|
97
|
+
def str_to_num(input_: str,
|
98
|
+
upper_bound: Optional[Union[int, float]] = None,
|
99
|
+
lower_bound: Optional[Union[int, float]] = None,
|
100
|
+
num_type: Type[Union[int, float]] = int,
|
101
|
+
precision: Optional[int] = None) -> Union[int, float]:
|
102
|
+
numbers = re.findall(r'-?\d+\.?\d*', input_)
|
103
|
+
if not numbers:
|
104
|
+
raise ValueError(f"No numeric values found in the string: {input_}")
|
105
|
+
|
106
|
+
number = numbers[0]
|
107
|
+
if num_type is int:
|
108
|
+
number = int(float(number))
|
109
|
+
elif num_type is float:
|
110
|
+
number = round(float(number), precision) if precision is not None else float(number)
|
111
|
+
else:
|
112
|
+
raise ValueError(f"Invalid number type: {num_type}")
|
113
|
+
|
114
|
+
if upper_bound is not None and number > upper_bound:
|
115
|
+
raise ValueError(f"Number {number} is greater than the upper bound of {upper_bound}.")
|
116
|
+
if lower_bound is not None and number < lower_bound:
|
117
|
+
raise ValueError(f"Number {number} is less than the lower bound of {lower_bound}.")
|
118
|
+
|
119
|
+
return number
|
120
|
+
|
121
|
+
|
122
|
+
def find_depth(nested_obj: Union[Dict, List, Tuple], depth_strategy: str = 'uniform', ignore_non_iterable: bool = False) -> int:
|
123
|
+
if depth_strategy not in {'uniform', 'mixed'}:
|
124
|
+
raise ValueError("Unsupported depth strategy. Choose 'uniform' or 'mixed'.")
|
125
|
+
|
126
|
+
def _uniform_depth(obj: Union[Dict, List, Tuple], current_depth: int = 0) -> int:
|
127
|
+
if isinstance(obj, (list, tuple)):
|
128
|
+
return max((_uniform_depth(item, current_depth + 1) for item in obj), default=current_depth)
|
129
|
+
elif isinstance(obj, dict):
|
130
|
+
return max((_uniform_depth(value, current_depth + 1) for value in obj.values()), default=current_depth)
|
131
|
+
else:
|
132
|
+
return current_depth
|
133
|
+
|
134
|
+
def _mixed_depth(obj: Union[Dict, List, Tuple], current_depth: int = 0) -> int:
|
135
|
+
if isinstance(obj, (list, tuple)):
|
136
|
+
return max((_mixed_depth(item, current_depth + 1) for item in obj), default=current_depth)
|
137
|
+
elif isinstance(obj, dict):
|
138
|
+
return max((_mixed_depth(value, current_depth + 1) for value in obj.values()), default=current_depth)
|
139
|
+
else:
|
140
|
+
return current_depth if ignore_non_iterable else current_depth + 1
|
141
|
+
|
142
|
+
if depth_strategy == 'uniform':
|
143
|
+
return _uniform_depth(nested_obj)
|
144
|
+
else:
|
145
|
+
return _mixed_depth(nested_obj)
|
146
|
+
|
147
|
+
def _is_schema(dict_: Dict, schema: Dict) -> bool:
|
148
|
+
for key, expected_type in schema.items():
|
149
|
+
if key not in dict_ or not isinstance(dict_[key], expected_type):
|
150
|
+
return False
|
151
|
+
return True
|
152
|
+
|
153
|
+
class TestFindDepth(unittest.TestCase):
|
154
|
+
def test_uniform_depth_list(self):
|
155
|
+
self.assertEqual(find_depth([[[1]], 2, 3], 'uniform'), 3)
|
156
|
+
|
157
|
+
def test_uniform_depth_mixed(self):
|
158
|
+
self.assertEqual(find_depth({'a': [1, [2]]}, 'uniform'), 3)
|
159
|
+
|
160
|
+
def test_mixed_depth(self):
|
161
|
+
self.assertEqual(find_depth([{'a': [1]}, 2, 3], 'mixed', True), 3)
|
162
|
+
|
163
|
+
def test_ignore_non_iterable(self):
|
164
|
+
self.assertEqual(find_depth([1, [2, [3]]], 'uniform', True), 3)
|
165
|
+
|
166
|
+
def test_invalid_depth_strategy(self):
|
167
|
+
with self.assertRaises(ValueError):
|
168
|
+
find_depth([], 'invalid')
|
169
|
+
|
170
|
+
class TestIsSchema(unittest.TestCase):
|
171
|
+
def test_valid_schema(self):
|
172
|
+
self.assertTrue(_is_schema({'a': 1, 'b': 'test'}, {'a': int, 'b': str}))
|
173
|
+
|
174
|
+
def test_invalid_schema(self):
|
175
|
+
self.assertFalse(_is_schema({'a': 1, 'b': 'test'}, {'a': str, 'b': str}))
|
176
|
+
|
177
|
+
def test_missing_key(self):
|
178
|
+
self.assertFalse(_is_schema({'a': 1}, {'a': int, 'b': str}))
|
179
|
+
|
180
|
+
if __name__ == '__main__':
|
181
|
+
unittest.main()
|
182
|
+
class TestStrToDatetime(unittest.TestCase):
|
183
|
+
def test_str_to_datetime_without_format(self):
|
184
|
+
self.assertEqual(
|
185
|
+
str_to_datetime("2023-01-01 12:00:00"),
|
186
|
+
datetime(2023, 1, 1, 12, 0)
|
187
|
+
)
|
188
|
+
|
189
|
+
def test_str_to_datetime_with_format(self):
|
190
|
+
self.assertEqual(
|
191
|
+
str_to_datetime("January 1, 2023, 12:00 PM", "%B %d, %Y, %I:%M %p"),
|
192
|
+
datetime(2023, 1, 1, 12, 0)
|
193
|
+
)
|
194
|
+
|
195
|
+
def test_str_to_datetime_invalid_format_raises_value_error(self):
|
196
|
+
with self.assertRaises(ValueError):
|
197
|
+
str_to_datetime("2023/01/01 12:00:00", "%Y-%m-%d %H:%M:%S")
|
198
|
+
|
199
|
+
class TestStrToNum(unittest.TestCase):
|
200
|
+
def test_str_to_num_int_default(self):
|
201
|
+
self.assertEqual(str_to_num('Value is 123'), 123)
|
202
|
+
|
203
|
+
def test_str_to_num_float_with_precision(self):
|
204
|
+
self.assertEqual(str_to_num('Value is -123.456', num_type=float, precision=2), -123.46)
|
205
|
+
|
206
|
+
def test_str_to_num_with_upper_bound_violation(self):
|
207
|
+
with self.assertRaises(ValueError):
|
208
|
+
str_to_num('Value is 100', upper_bound=99)
|
209
|
+
|
210
|
+
def test_str_to_num_with_lower_bound_violation(self):
|
211
|
+
with self.assertRaises(ValueError):
|
212
|
+
str_to_num('Value is -1', lower_bound=0)
|
213
|
+
|
214
|
+
def test_str_to_num_no_numeric_value_found_raises_value_error(self):
|
215
|
+
with self.assertRaises(ValueError):
|
216
|
+
str_to_num('No numbers here')
|
217
|
+
|
218
|
+
def test_str_to_num_invalid_num_type_raises_value_error(self):
|
219
|
+
with self.assertRaises(ValueError):
|
220
|
+
str_to_num('Value is 123', num_type=str)
|
221
|
+
|
222
|
+
|
223
|
+
class TestChangeDictKey(unittest.TestCase):
|
224
|
+
def test_change_dict_key_existing(self):
|
225
|
+
sample_dict = {'old_key': 'value'}
|
226
|
+
change_dict_key(sample_dict, 'old_key', 'new_key')
|
227
|
+
self.assertNotIn('old_key', sample_dict)
|
228
|
+
self.assertIn('new_key', sample_dict)
|
229
|
+
self.assertEqual(sample_dict['new_key'], 'value')
|
230
|
+
|
231
|
+
def test_change_dict_key_non_existing_raises_key_error(self):
|
232
|
+
sample_dict = {'old_key': 'value'}
|
233
|
+
with self.assertRaises(KeyError):
|
234
|
+
change_dict_key(sample_dict, 'non_existing_key', 'new_key')
|
235
|
+
|
236
|
+
class TestCreateId(unittest.TestCase):
|
237
|
+
def test_create_id_default_length(self):
|
238
|
+
unique_id = create_id()
|
239
|
+
self.assertEqual(len(unique_id), 32)
|
240
|
+
|
241
|
+
def test_create_id_custom_length(self):
|
242
|
+
lengths = [8, 16, 64]
|
243
|
+
for length in lengths:
|
244
|
+
unique_id = create_id(n=length)
|
245
|
+
self.assertEqual(len(unique_id), length)
|
246
|
+
|
247
|
+
def test_create_id_uniqueness(self):
|
248
|
+
id_set = set(create_id() for _ in range(100))
|
249
|
+
self.assertEqual(len(id_set), 100)
|
250
|
+
|
251
|
+
|
252
|
+
class TestSplitPath(unittest.TestCase):
|
253
|
+
def test_split_path(self):
|
254
|
+
# Test with a standard path
|
255
|
+
path = Path('/home/user/documents/report.txt')
|
256
|
+
result = split_path(path)
|
257
|
+
self.assertEqual(result, ('documents', 'report.txt'))
|
258
|
+
|
259
|
+
# Test with a root path
|
260
|
+
path = Path('/filename.ext')
|
261
|
+
result = split_path(path)
|
262
|
+
self.assertEqual(result, ('', 'filename.ext'))
|
263
|
+
|
264
|
+
# Test with a path with no file extension
|
265
|
+
path = Path('/home/user/foldername/filename')
|
266
|
+
result = split_path(path)
|
267
|
+
self.assertEqual(result, ('foldername', 'filename'))
|
268
|
+
|
269
|
+
# # Test with an empty path
|
270
|
+
# path = Path('')
|
271
|
+
# result = split_path(path)
|
272
|
+
# self.assertEqual(result, ('.', ''))
|
273
|
+
|
274
|
+
|
275
|
+
class TestCreatePath(unittest.TestCase):
|
276
|
+
@patch('os.makedirs')
|
277
|
+
def test_create_path_without_timestamp(self, mock_makedirs):
|
278
|
+
expected_path = '/tmp/testfile.txt'
|
279
|
+
actual_path = create_path('/tmp', 'testfile.txt', timestamp=False)
|
280
|
+
self.assertEqual(actual_path, expected_path)
|
281
|
+
mock_makedirs.assert_called_once_with('/tmp/', exist_ok=True)
|
282
|
+
|
283
|
+
# @patch('os.makedirs')
|
284
|
+
# def test_create_path_with_timestamp(self, mock_makedirs):
|
285
|
+
# with patch('datetime.datetime') as mock_datetime:
|
286
|
+
# mock_datetime.utcnow.return_value = datetime(2021, 1, 1, 12, 0, 0)
|
287
|
+
# mock_datetime.strftime.return_value = '20210101120000'
|
288
|
+
# expected_path = '/tmp/testfile_20210101120000.txt'
|
289
|
+
# actual_path = create_path('/tmp', 'testfile.txt')
|
290
|
+
# self.assertEqual(actual_path, expected_path)
|
291
|
+
# mock_makedirs.assert_called_once_with('/tmp/', exist_ok=True)
|
292
|
+
|
293
|
+
# @patch('os.makedirs')
|
294
|
+
# def test_create_path_with_time_prefix(self, mock_makedirs):
|
295
|
+
# with patch('datetime.datetime') as mock_datetime:
|
296
|
+
# mock_datetime.utcnow.return_value = datetime(2021, 1, 1, 12, 0, 0)
|
297
|
+
# mock_datetime.strftime.return_value = '20210101120000'
|
298
|
+
# expected_path = '/tmp/20210101120000_testfile.txt'
|
299
|
+
# actual_path = create_path('/tmp', 'testfile.txt', time_prefix=True)
|
300
|
+
# self.assertEqual(actual_path, expected_path)
|
301
|
+
# mock_makedirs.assert_called_once_with('/tmp/', exist_ok=True)
|
302
|
+
|
303
|
+
# @patch('os.makedirs')
|
304
|
+
# def test_create_path_dir_exist_ok_false(self, mock_makedirs):
|
305
|
+
# with self.assertRaises(FileNotFoundError):
|
306
|
+
# create_path('/nonexistent', 'testfile.txt', dir_exist_ok=False)
|
307
|
+
# mock_makedirs.assert_called_once_with('/nonexistent/', exist_ok=False)
|
308
|
+
|
309
|
+
|
310
|
+
class TestCreateCopy(unittest.TestCase):
|
311
|
+
|
312
|
+
def test_create_single_copy(self):
|
313
|
+
sample_dict = {'key': 'value'}
|
314
|
+
result = create_copy(sample_dict, 1)
|
315
|
+
self.assertEqual(result, sample_dict)
|
316
|
+
self.assertIsNot(result, sample_dict)
|
317
|
+
|
318
|
+
def test_create_multiple_copies(self):
|
319
|
+
sample_list = ['element']
|
320
|
+
n_copies = 3
|
321
|
+
result = create_copy(sample_list, n_copies)
|
322
|
+
self.assertEqual(len(result), n_copies)
|
323
|
+
for copy_item in result:
|
324
|
+
self.assertEqual(copy_item, sample_list)
|
325
|
+
self.assertIsNot(copy_item, sample_list)
|
326
|
+
|
327
|
+
def test_create_zero_copies_raises_value_error(self):
|
328
|
+
with self.assertRaises(ValueError):
|
329
|
+
create_copy({}, 0)
|
330
|
+
|
331
|
+
def test_create_negative_copies_raises_value_error(self):
|
332
|
+
with self.assertRaises(ValueError):
|
333
|
+
create_copy({}, -1)
|
334
|
+
|
335
|
+
def test_create_non_integer_copies_raises_value_error(self):
|
336
|
+
with self.assertRaises(ValueError):
|
337
|
+
create_copy({}, '2')
|
338
|
+
|
339
|
+
|
340
|
+
# class TestGetTimestamp(unittest.TestCase):
|
341
|
+
|
342
|
+
# @patch('datetime.datetime')
|
343
|
+
# def test_get_timestamp(self, mock_datetime):
|
344
|
+
# # Define a fixed datetime
|
345
|
+
# mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0)
|
346
|
+
|
347
|
+
# # Expected timestamp based on the fixed datetime
|
348
|
+
# expected_timestamp = "2023-01-01T12_00_00"
|
349
|
+
|
350
|
+
# # Get the actual timestamp from the function
|
351
|
+
# actual_timestamp = get_timestamp()
|
352
|
+
|
353
|
+
# # Assert that the actual timestamp matches the expected timestamp
|
354
|
+
# self.assertEqual(actual_timestamp, expected_timestamp)
|
355
|
+
|
356
|
+
|
357
|
+
if __name__ == '__main__':
|
358
|
+
unittest.main()
|