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,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()