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
@@ -1,348 +1,923 @@
1
- # use sys_util and flat_util
2
1
  import asyncio
2
+ import functools
3
+ import logging
3
4
  import time
4
- from typing import Any, Callable, List, Optional, Union
5
+ from typing import Any, Callable, Generator, Iterable, List, Dict, Optional, Tuple
5
6
 
6
- from .sys_util import create_copy
7
- from .flat_util import to_list
7
+ from aiocache import cached
8
8
 
9
9
 
10
- def hcall(
11
- input: Any, func: Callable, sleep: int = 0.1,
12
- message: Optional[str] = None,
13
- ignore_error: bool = False, **kwargs
14
- ) -> Any:
10
+ def to_list(input: Any, flatten: bool = True, dropna: bool = False) -> List[Any]:
15
11
  """
16
- Executes a function after a specified delay, handling exceptions optionally.
12
+ Convert input to a list, with options to flatten and drop None values.
17
13
 
18
- Waits for 'sleep' seconds before calling 'func' with 'input'. Handles exceptions by
19
- printing a message and optionally re-raising them.
14
+ Args:
15
+ input (Any): The input to convert.
16
+ flatten (bool): If True, flattens the list if input is a list. Default is True.
17
+ dropna (bool): If True, None values are removed from the list. Default is False.
20
18
 
21
- Parameters:
22
- input (Any): Input to the function.
23
-
24
- func (Callable): Function to execute.
25
-
26
- sleep (int, optional): Time in seconds to wait before calling the function. Defaults to 0.1.
19
+ Returns:
20
+ List[Any]: The input converted to a list.
27
21
 
28
- message (Optional[str], optional): Message to print on exception. Defaults to None.
22
+ Examples:
23
+ >>> to_list([1, [2, None], 3], flatten=True, dropna=True)
24
+ [1, 2, 3]
25
+ >>> to_list("hello", flatten=False)
26
+ ["hello"]
27
+ """
28
+ if isinstance(input, list) and flatten:
29
+ input = _flatten_list(input)
30
+ elif isinstance(input, Iterable) and not isinstance(input, (str, dict)):
31
+ try:
32
+ input = list(input)
33
+ except Exception as e:
34
+ raise ValueError("Input cannot be converted to a list.") from e
35
+ else:
36
+ input = [input]
37
+ if dropna:
38
+ input = _dropna(input)
39
+ return input
29
40
 
30
- ignore_error (bool, optional): If True, ignores exceptions. Defaults to False.
41
+ def lcall(
42
+ input: Any, func: Callable, flatten: bool = False,
43
+ dropna: bool = False, **kwargs
44
+ ) -> List[Any]:
45
+ """
46
+ Apply a function to each element of the input list, with options to flatten and drop None values.
31
47
 
32
- **kwargs: Additional keyword arguments for the function.
48
+ Args:
49
+ input (Any): The input to process.
50
+ func (Callable): The function to apply to each element.
51
+ flatten (bool): If True, flattens the result. Default is False.
52
+ dropna (bool): If True, None values are removed from the input. Default is False.
53
+ **kwargs: Additional keyword arguments to pass to func.
33
54
 
34
55
  Returns:
35
- Any: Result of the function call.
56
+ List[Any]: A list of results after applying the function.
36
57
 
37
- Raises:
38
- Exception: Re-raises the exception unless 'ignore_error' is True.
39
-
40
- Example:
41
- >>> def add_one(x):
42
- ... return x + 1
43
- >>> hold_call(5, add_one, sleep=2)
44
- 6
58
+ Examples:
59
+ >>> lcall([1, 2, 3], lambda x: x * 2)
60
+ [2, 4, 6]
61
+ >>> lcall([1, 2, None], lambda x: x and x * 2, dropna=True)
62
+ [2, 4]
45
63
  """
46
- try:
47
- time.sleep(sleep)
48
- return func(input, **kwargs)
49
- except Exception as e:
50
- if message:
51
- print(f"{message} Error: {e}")
52
- else:
53
- print(f"An error occurred: {e}")
54
- if not ignore_error:
55
- raise
56
-
57
- async def ahcall(
58
- input_: Any, func: Callable, sleep: int = 5,
59
- message: Optional[str] = None,
60
- ignore_error: bool = False, **kwargs
61
- ) -> Any:
64
+ lst = to_list(input=input, dropna=dropna)
65
+ if len(to_list(func)) != 1:
66
+ raise ValueError("There must be one and only one function for list calling.")
67
+ if flatten:
68
+ return to_list([func(i, **kwargs) for i in lst])
69
+ return [func(i, **kwargs) for i in lst]
70
+
71
+ @functools.lru_cache(maxsize=None)
72
+ def is_coroutine_func(func: Callable) -> bool:
62
73
  """
63
- Asynchronously executes a function after a specified delay, handling exceptions optionally.
74
+ Check if the given function is a coroutine function.
64
75
 
65
- Waits for 'sleep' seconds before calling 'func' with 'input'. Handles exceptions by
66
- printing a message and optionally re-raising them.
76
+ Args:
77
+ func (Callable): The function to check.
67
78
 
68
- Parameters:
69
- input (Any): Input to the function.
79
+ Returns:
80
+ bool: True if the function is a coroutine function, False otherwise.
81
+
82
+ Examples:
83
+ >>> async def async_func(): pass
84
+ >>> def sync_func(): pass
85
+ >>> is_coroutine_func(async_func)
86
+ True
87
+ >>> is_coroutine_func(sync_func)
88
+ False
89
+ """
90
+ return asyncio.iscoroutinefunction(func)
70
91
 
71
- func (Callable): Asynchronous function to execute.
92
+ async def alcall(
93
+ input: Any, func: Callable, flatten: bool = False, **kwargs
94
+ )-> List[Any]:
95
+ """
96
+ Asynchronously apply a function to each element in the input.
72
97
 
73
- sleep (int, optional): Time in seconds to wait before calling the function. Defaults to 5.
98
+ Args:
99
+ input (Any): The input to process.
100
+ func (Callable): The function to apply.
101
+ flatten (bool, optional): Whether to flatten the result. Default is False.
102
+ **kwargs: Keyword arguments to pass to the function.
74
103
 
75
- message (Optional[str], optional): Message to print on exception. Defaults to None.
104
+ Returns:
105
+ List[Any]: A list of results after asynchronously applying the function.
76
106
 
77
- ignore_error (bool, optional): If True, ignores exceptions. Defaults to False.
107
+ Examples:
108
+ >>> async def square(x): return x * x
109
+ >>> asyncio.run(alcall([1, 2, 3], square))
110
+ [1, 4, 9]
111
+ """
112
+ lst = to_list(input=input)
113
+ tasks = [func(i, **kwargs) for i in lst]
114
+ outs = await asyncio.gather(*tasks)
115
+ return to_list(outs, flatten=flatten)
116
+
117
+ async def mcall(
118
+ input: Any, func: Any, explode: bool = False, **kwargs
119
+ ) -> List[Any]:
120
+ """
121
+ Asynchronously map a function or functions over an input or inputs.
78
122
 
79
- **kwargs: Additional keyword arguments for the function.
123
+ Args:
124
+ input (Any): The input or inputs to process.
125
+ func (Any): The function or functions to apply.
126
+ explode (bool, optional): Whether to apply each function to each input. Default is False.
127
+ **kwargs: Keyword arguments to pass to the function.
80
128
 
81
129
  Returns:
82
- Any: Result of the asynchronous function call.
83
-
84
- Raises:
85
- Exception: Re-raises the exception unless 'ignore_error' is True.
130
+ List[Any]: A list of results after applying the function(s).
86
131
 
87
- Example:
88
- >>> async def async_add_one(x):
89
- ... return x + 1
90
- >>> asyncio.run(ahold_call(5, async_add_one, sleep=2))
91
- 6
132
+ Examples:
133
+ >>> async def add_one(x): return x + 1
134
+ >>> asyncio.run(mcall([1, 2, 3], add_one))
135
+ [2, 3, 4]
136
+
92
137
  """
93
- try:
94
- if not asyncio.iscoroutinefunction(func):
95
- raise TypeError(f"The function {func} must be an asynchronous function.")
96
- await asyncio.sleep(sleep)
97
- return await func(input_, **kwargs)
98
- except Exception as e:
99
- if message:
100
- print(f"{message} Error: {e}")
101
- else:
102
- print(f"An error occurred: {e}")
103
- if not ignore_error:
104
- raise
138
+ input_ = to_list(input, dropna=True)
139
+ funcs_ = to_list(func, dropna=True)
140
+
141
+ if explode:
142
+ tasks = [
143
+ _alcall(input_, f, flatten=True, **kwargs)
144
+ for f in funcs_
145
+ ]
146
+ return await asyncio.gather(*tasks)
147
+ else:
148
+ if len(input_) != len(funcs_):
149
+ raise ValueError("Inputs and functions must be the same length for map calling.")
150
+ tasks = [
151
+ _call_handler(func, inp, **kwargs)
152
+ for inp, func in zip(input, func)
153
+ ]
154
+ return await asyncio.gather(*tasks)
105
155
 
106
- def lcall(
107
- input_: Any, func_: Callable, flatten: bool = False,
108
- dropna: bool = False, **kwargs
109
- ) -> List[Any]:
156
+ async def bcall(input: Any, func: Callable, batch_size: int, **kwargs) -> List[Any]:
110
157
  """
111
- Applies a function to each element of `input`, after converting it to a list.
112
-
113
- This function converts the `input` to a list, with options to flatten structures
114
- and lists, and then applies a given `func` to each element of the list.
115
-
116
- Parameters:
117
- input (Any): The input to be converted to a list and processed.
158
+ Asynchronously call a function on batches of inputs.
118
159
 
119
- func (Callable): The function to apply to each element of the list.
160
+ Args:
161
+ input (Any): The input to process.
162
+ func (Callable): The function to apply.
163
+ batch_size (int): The size of each batch.
164
+ **kwargs: Keyword arguments to pass to the function.
120
165
 
121
- flatten_dict (bool, optional): If True, flattens dictionaries in the input. Defaults to False.
122
-
123
- flat (bool, optional): If True, flattens nested lists in the input. Defaults to False.
166
+ Returns:
167
+ List[Any]: A list of results after applying the function in batches.
124
168
 
125
- dropna (bool, optional): If True, drops None values during flattening. Defaults to True.
169
+ Examples:
170
+ >>> async def sum_batch(batch): return sum(batch)
171
+ >>> asyncio.run(bcall([1, 2, 3, 4], sum_batch, batch_size=2))
172
+ [3, 7]
173
+ """
174
+ results = []
175
+ input = to_list(input)
176
+ for i in range(0, len(input), batch_size):
177
+ batch = input[i:i + batch_size]
178
+ batch_results = await alcall(batch, func, **kwargs)
179
+ results.extend(batch_results)
180
+
181
+ return results
182
+
183
+ async def tcall(
184
+ func: Callable, *args, delay: float = 0, err_msg: Optional[str] = None,
185
+ ignore_err: bool = False, timing: bool = False,
186
+ timeout: Optional[float] = None, **kwargs
187
+ ) -> Any:
188
+ """
189
+ Asynchronously call a function with optional delay, timeout, and error handling.
190
+
191
+ Args:
192
+ func (Callable): The function to call.
193
+ *args: Positional arguments to pass to the function.
194
+ delay (float): Delay before calling the function, in seconds.
195
+ err_msg (Optional[str]): Custom error message.
196
+ ignore_err (bool): If True, ignore errors and return default.
197
+ timing (bool): If True, return a tuple (result, duration).
198
+ default (Any): Default value to return on error.
199
+ timeout (Optional[float]): Timeout for the function call, in seconds.
200
+ **kwargs: Keyword arguments to pass to the function.
126
201
 
127
202
  Returns:
128
- List[Any]: A list containing the results of applying the `func` to each element.
129
-
130
- Raises:
131
- ValueError: If the `func` cannot be applied to the `input`.
203
+ Any: The result of the function call, or (result, duration) if timing is True.
132
204
 
133
- Example:
134
- >>> def square(x):
135
- ... return x * x
136
- >>> l_call([1, 2, 3], square)
137
- [1, 4, 9]
205
+ Examples:
206
+ >>> async def example_func(x): return x
207
+ >>> asyncio.run(tcall(example_func, 5, timing=True))
208
+ (5, duration)
138
209
  """
139
- try:
140
- lst = to_list(input_=input_, flatten=flatten, dropna=dropna)
141
- return [func_(i, **kwargs) for i in lst]
142
- except Exception as e:
143
- raise ValueError(f"Given function cannot be applied to the input. Error: {e}")
210
+ async def async_call() -> Tuple[Any, float]:
211
+ start_time = time.time()
212
+ if timeout is not None:
213
+ result = await asyncio.wait_for(func(*args, **kwargs), timeout)
214
+ try:
215
+ await asyncio.sleep(delay)
216
+ result = await func(*args, **kwargs)
217
+ duration = time.time() - start_time
218
+ return (result, duration) if timing else result
219
+ except Exception as e:
220
+ handle_error(e)
221
+
222
+ def sync_call() -> Tuple[Any, float]:
223
+ start_time = time.time()
224
+ try:
225
+ time.sleep(delay)
226
+ result = func(*args, **kwargs)
227
+ duration = time.time() - start_time
228
+ return (result, duration) if timing else result
229
+ except Exception as e:
230
+ handle_error(e)
231
+
232
+ def handle_error(e: Exception):
233
+ _msg = f"{err_msg} Error: {e}" if err_msg else f"An error occurred: {e}"
234
+ print(_msg)
235
+ if not ignore_err:
236
+ raise
144
237
 
145
- async def alcall(
146
- input_: Any, func_: Callable, flatten: bool = False, dropna: bool = True, **kwargs
147
- ) -> List[Any]:
238
+ if asyncio.iscoroutinefunction(func):
239
+ return await async_call()
240
+ else:
241
+ return sync_call()
242
+
243
+ async def rcall(
244
+ func: Callable, *args, retries: int = 0, delay: float = 1.0,
245
+ backoff_factor: float = 2.0, default: Any = None,
246
+ timeout: Optional[float] = None, **kwargs
247
+ ) -> Any:
148
248
  """
149
- Asynchronously applies a function to each element of `input`, after converting it to a list.
249
+ Asynchronously retry a function call with exponential backoff.
250
+
251
+ Args:
252
+ func (Callable): The function to call.
253
+ *args: Positional arguments to pass to the function.
254
+ retries (int): Number of retry attempts.
255
+ delay (float): Initial delay between retries, in seconds.
256
+ backoff_factor (float): Factor by which to multiply delay for each retry.
257
+ default (Any): Default value to return if all retries fail.
258
+ timeout (Optional[float]): Timeout for each function call, in seconds.
259
+ **kwargs: Keyword arguments to pass to the function.
150
260
 
151
- This function converts the `input` to a list, with options to flatten
152
- dictionaries and lists, and then applies a given asynchronous `func` to
153
- each element of the list asynchronously.
261
+ Returns:
262
+ Any: The result of the function call, or default if retries are exhausted.
154
263
 
155
- Parameters:
156
- input (Any): The input to be converted to a list and processed.
264
+ Examples:
265
+ >>> async def example_func(x): return x
266
+ >>> asyncio.run(rcall(example_func, 5, retries=2))
267
+ 5
268
+ """
269
+ last_exception = None
270
+ result = None
271
+
272
+ for attempt in range(retries + 1) if retries == 0 else range(retries):
273
+ try:
274
+ # Using tcall for each retry attempt with timeout and delay
275
+ result = await _tcall(func, *args, timeout=timeout, **kwargs)
276
+ return result
277
+ except Exception as e:
278
+ last_exception = e
279
+ if attempt < retries:
280
+ await asyncio.sleep(delay)
281
+ delay *= backoff_factor
282
+ else:
283
+ break
284
+ if result is None and default is not None:
285
+ return default
286
+ elif last_exception is not None:
287
+ raise last_exception
288
+ else:
289
+ raise RuntimeError("rcall failed without catching an exception")
290
+
291
+ class CallDecorator:
157
292
 
158
- func (Callable): The asynchronous function to apply to each element of the list.
293
+ """
294
+ Call Decorators
295
+ """
159
296
 
160
- flatten_dict (bool, optional): If True, flattens dictionaries in the input. Defaults to False.
161
297
 
162
- flat (bool, optional): If True, flattens nested lists in the input. Defaults to False.
298
+ @staticmethod
299
+ def timeout(timeout: int) -> Callable:
300
+ """
301
+ Decorator to apply a timeout to an asynchronous function.
302
+
303
+ Args:
304
+ timeout (int): Timeout duration in seconds.
305
+
306
+ Returns:
307
+ Callable: A decorated function with applied timeout.
308
+
309
+ Examples:
310
+ >>> @CallDecorator.timeout(5)
311
+ ... async def long_running_task(): pass
312
+ ... # The task will timeout after 5 seconds
313
+ """
314
+ def decorator(func: Callable[..., Any]) -> Callable:
315
+ @functools.wraps(func)
316
+ async def wrapper(*args, **kwargs) -> Any:
317
+ return await rcall(func, *args, timeout=timeout, **kwargs)
318
+ return wrapper
319
+ return decorator
320
+
321
+ @staticmethod
322
+ def retry(
323
+ retries: int = 3, delay: float = 2.0, backoff_factor: float = 2.0
324
+ ) -> Callable:
325
+ """
326
+ Decorator to retry an asynchronous function with exponential backoff.
327
+
328
+ Args:
329
+ retries (int): Number of retry attempts.
330
+ initial_delay (float): Initial delay between retries in seconds.
331
+ backoff_factor (float): Factor by which to multiply delay for each retry.
332
+
333
+ Returns:
334
+ Callable: A decorated function with applied retry logic.
335
+
336
+ Examples:
337
+ >>> @CallDecorator.retry(retries=2, initial_delay=1.0, backoff_factor=2.0)
338
+ ... async def fetch_data(): pass
339
+ ... # This function will retry up to 2 times with increasing delays
340
+ """
341
+ def decorator(func: Callable[..., Any]) -> Callable:
342
+ @functools.wraps(func)
343
+ async def wrapper(*args, **kwargs) -> Any:
344
+ return await rcall(
345
+ func, *args, retries=retries, delay=delay,
346
+ backoff_factor=backoff_factor, **kwargs
347
+ )
348
+ return wrapper
349
+ return decorator
350
+
351
+ @staticmethod
352
+ def default(default_value: Any) -> Callable:
353
+ def decorator(func: Callable[..., Any]) -> Callable:
354
+ @functools.wraps(func)
355
+ async def wrapper(*args, **kwargs) -> Any:
356
+ return await rcall(func, *args, default=default_value, **kwargs)
357
+ return wrapper
358
+ return decorator
359
+
360
+ @staticmethod
361
+ def throttle(period: int) -> Callable:
362
+ """
363
+ A static method to create a throttling decorator using the Throttle class.
364
+
365
+ Args:
366
+ period (int): The minimum time period (in seconds) between successive calls.
367
+
368
+ Returns:
369
+ Callable: A decorator that applies a throttling mechanism to the decorated
370
+ function.
371
+ """
372
+ return Throttle(period)
373
+
374
+ @staticmethod
375
+ def map(function: Callable[[Any], Any]) -> Callable:
376
+ """
377
+ Decorator that applies a mapping function to the results of an asynchronous
378
+ function.
379
+
380
+ This decorator transforms each element in the list returned by the decorated
381
+ function using the provided mapping function.
382
+
383
+ Args:
384
+ function (Callable[[Any], Any]): A function to apply to each element of the list.
385
+
386
+ Returns:
387
+ Callable: A decorated function that maps the provided function over its results.
388
+
389
+ Examples:
390
+ >>> @CallDecorator.map(lambda x: x * 2)
391
+ ... async def get_numbers(): return [1, 2, 3]
392
+ >>> asyncio.run(get_numbers())
393
+ [2, 4, 6]
394
+ """
395
+ def decorator(func: Callable[..., List[Any]]) -> Callable:
396
+ if is_coroutine_func(func):
397
+ @functools.wraps(func)
398
+ async def async_wrapper(*args, **kwargs) -> List[Any]:
399
+ values = await func(*args, **kwargs)
400
+ return [function(value) for value in values]
401
+ return async_wrapper
402
+ else:
403
+ @functools.wraps(func)
404
+ def sync_wrapper(*args, **kwargs) -> List[Any]:
405
+ values = func(*args, **kwargs)
406
+ return [function(value) for value in values]
407
+ return sync_wrapper
408
+ return decorator
409
+
410
+ @staticmethod
411
+ def compose(*functions: Callable[[Any], Any]) -> Callable:
412
+ """
413
+ Decorator factory that composes multiple functions. The output of each
414
+ function is passed as the input to the next, in the order they are provided.
415
+
416
+ Args:
417
+ *functions: Variable length list of functions to compose.
418
+
419
+ Returns:
420
+ Callable: A new function that is the composition of the given functions.
421
+ """
422
+ def decorator(func: Callable) -> Callable:
423
+ if not any(is_coroutine_func(f) for f in functions):
424
+ @functools.wraps(func)
425
+ def sync_wrapper(*args, **kwargs):
426
+ value = func(*args, **kwargs)
427
+ for function in functions:
428
+ try:
429
+ value = function(value)
430
+ except Exception as e:
431
+ raise ValueError(f"Error in function {function.__name__}: {e}")
432
+ return value
433
+ return sync_wrapper
434
+ elif all(is_coroutine_func(f) for f in functions):
435
+ @functools.wraps(func)
436
+ async def async_wrapper(*args, **kwargs):
437
+ value = func(*args, **kwargs)
438
+ for function in functions:
439
+ try:
440
+ value = await function(value)
441
+ except Exception as e:
442
+ raise ValueError(f"Error in function {function.__name__}: {e}")
443
+ return value
444
+ return async_wrapper
445
+ else:
446
+ raise ValueError("Cannot compose both synchronous and asynchronous functions.")
447
+ return decorator
448
+
449
+ @staticmethod
450
+ def pre_post_process(
451
+ preprocess: Callable[..., Any], postprocess: Callable[..., Any]
452
+ ) -> Callable:
453
+ """
454
+ Decorator that applies preprocessing and postprocessing functions to the arguments
455
+ and result of an asynchronous function.
456
+
457
+ Args:
458
+ preprocess (Callable[..., Any]): A function to preprocess each argument.
459
+ postprocess (Callable[..., Any]): A function to postprocess the result.
460
+
461
+ Returns:
462
+ Callable: A decorated function with preprocessing and postprocessing applied.
463
+
464
+ Examples:
465
+ >>> @CallDecorator.pre_post_process(lambda x: x * 2, lambda x: x + 1)
466
+ ... async def compute(x): return x
467
+ >>> asyncio.run(compute(5))
468
+ 21 # (5 * 2) -> 10, compute(10) -> 10, 10 + 1 -> 21
469
+ """
470
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
471
+ if is_coroutine_func(func):
472
+ @functools.wraps(func)
473
+ async def async_wrapper(*args, **kwargs) -> Any:
474
+ preprocessed_args = [preprocess(arg) for arg in args]
475
+ preprocessed_kwargs = {k: preprocess(v) for k, v in kwargs.items()}
476
+ result = await func(*preprocessed_args, **preprocessed_kwargs)
477
+ return postprocess(result)
478
+ return async_wrapper
479
+ else:
480
+ @functools.wraps(func)
481
+ def sync_wrapper(*args, **kwargs) -> Any:
482
+ preprocessed_args = [preprocess(arg) for arg in args]
483
+ preprocessed_kwargs = {k: preprocess(v) for k, v in kwargs.items()}
484
+ result = func(*preprocessed_args, **preprocessed_kwargs)
485
+ return postprocess(result)
486
+ return sync_wrapper
487
+
488
+ return decorator
489
+
490
+ @staticmethod
491
+ def cache(func: Callable, ttl=600, maxsize=None) -> Callable:
492
+ """
493
+ Decorator that caches the results of function calls (both sync and async).
494
+ If the function is called again with the same arguments,
495
+ the cached result is returned instead of re-executing the function.
496
+
497
+ Args:
498
+ func (Callable): The function (can be sync or async) whose results need to be cached.
499
+
500
+ Returns:
501
+ Callable: A decorated function with caching applied.
502
+ """
503
+
504
+ if is_coroutine_func(func):
505
+ # Asynchronous function handling
506
+ @cached(ttl=ttl)
507
+ async def cached_async(*args, **kwargs) -> Any:
508
+ return await func(*args, **kwargs)
509
+
510
+ @functools.wraps(func)
511
+ async def async_wrapper(*args, **kwargs) -> Any:
512
+ return await cached_async(*args, **kwargs)
513
+
514
+ return async_wrapper
163
515
 
164
- dropna (bool, optional): If True, drops None values during flattening. Defaults to True.
516
+ else:
517
+ # Synchronous function handling
518
+ @functools.lru_cache(maxsize=maxsize)
519
+ def cached_sync(*args, **kwargs) -> Any:
520
+ return func(*args, **kwargs)
521
+
522
+ @functools.wraps(func)
523
+ def sync_wrapper(*args, **kwargs) -> Any:
524
+ return cached_sync(*args, **kwargs)
525
+
526
+ return sync_wrapper
527
+
528
+ @staticmethod
529
+ def filter(predicate: Callable[[Any], bool]) -> Callable:
530
+ """
531
+ Decorator that filters the results of an asynchronous function based on a predicate
532
+ function.
533
+
534
+ Args:
535
+ predicate (Callable[[Any], bool]): A function that returns True for items to keep.
536
+
537
+ Returns:
538
+ Callable: A decorated function that filters its results based on the predicate.
539
+
540
+ Examples:
541
+ >>> @CallDecorator.filter(lambda x: x % 2 == 0)
542
+ ... async def get_numbers(): return [1, 2, 3, 4, 5]
543
+ >>> asyncio.run(get_numbers())
544
+ [2, 4]
545
+ """
546
+ def decorator(func: Callable[..., List[Any]]) -> Callable:
547
+ if is_coroutine_func(func):
548
+ @functools.wraps(func)
549
+ async def wrapper(*args, **kwargs) -> List[Any]:
550
+ values = await func(*args, **kwargs)
551
+ return [value for value in values if predicate(value)]
552
+ return wrapper
553
+ else:
554
+ @functools.wraps(func)
555
+ def wrapper(*args, **kwargs) -> List[Any]:
556
+ values = func(*args, **kwargs)
557
+ return [value for value in values if predicate(value)]
558
+ return wrapper
559
+ return decorator
560
+
561
+ @staticmethod
562
+ def reduce(function: Callable[[Any, Any], Any], initial: Any) -> Callable:
563
+ """
564
+ Decorator that reduces the results of an asynchronous function to a single value using
565
+ the specified reduction function.
566
+
567
+ Args:
568
+ function (Callable[[Any, Any], Any]): A reduction function to apply.
569
+ initial (Any): The initial value for the reduction.
570
+
571
+ Returns:
572
+ Callable: A decorated function that reduces its results to a single value.
573
+
574
+ Examples:
575
+ >>> @CallDecorator.reduce(lambda x, y: x + y, 0)
576
+ ... async def get_numbers(): return [1, 2, 3, 4]
577
+ >>> asyncio.run(get_numbers())
578
+ 10 # Sum of the numbers
579
+ """
580
+ def decorator(func: Callable[..., List[Any]]) -> Callable:
581
+ if is_coroutine_func(func):
582
+ @functools.wraps(func)
583
+ async def async_wrapper(*args, **kwargs) -> Any:
584
+ values = await func(*args, **kwargs)
585
+ return functools.reduce(function, values, initial)
586
+ return async_wrapper
587
+ else:
588
+ @functools.wraps(func)
589
+ def sync_wrapper(*args, **kwargs) -> Any:
590
+ values = func(*args, **kwargs)
591
+ return functools.reduce(function, values, initial)
592
+ return sync_wrapper
593
+ return decorator
594
+
595
+ @staticmethod
596
+ def max_concurrency(limit: int = 5) -> Callable:
597
+ """
598
+ Decorator to limit the maximum number of concurrent executions of an async function.
599
+
600
+ Args:
601
+ limit (int): The maximum number of concurrent tasks allowed.
602
+
603
+ Returns:
604
+ Callable: A decorated function with limited concurrency.
605
+
606
+ Examples:
607
+ >>> @CallDecorator.max_concurrency(3)
608
+ ... async def process_data(): pass
609
+ """
610
+ def decorator(func: Callable) -> Callable:
611
+ if not asyncio.iscoroutinefunction(func):
612
+ raise TypeError("max_concurrency decorator can only be used with async functions.")
613
+ semaphore = asyncio.Semaphore(limit)
614
+
615
+ @functools.wraps(func)
616
+ async def wrapper(*args, **kwargs):
617
+ async with semaphore:
618
+ return await func(*args, **kwargs)
619
+
620
+ return wrapper
621
+
622
+ return decorator
623
+
624
+ @staticmethod
625
+ def throttle(period: int) -> Callable:
626
+ """
627
+ A static method to create a throttling decorator. This method utilizes the Throttle
628
+ class to enforce a minimum time period between successive calls of the decorated function.
629
+
630
+ Args:
631
+ period (int): The minimum time period, in seconds, that must elapse between successive
632
+ calls to the decorated function.
633
+
634
+ Returns:
635
+ Callable: A decorator that applies a throttling mechanism to the decorated function,
636
+ ensuring that the function is not called more frequently than the specified period.
637
+
638
+ Examples:
639
+ >>> @CallDecorator.throttle(2) # Ensures at least 2 seconds between calls
640
+ ... async def fetch_data(): pass
641
+
642
+ This decorator is particularly useful in scenarios like rate-limiting API calls or
643
+ reducing the frequency of resource-intensive operations.
644
+ """
645
+ return Throttle(period)
646
+
647
+ class Throttle:
648
+ """
649
+ A class that provides a throttling mechanism for function calls.
165
650
 
166
- Returns:
167
- List[Any]: A list containing the results of applying the `func` to each element.
651
+ When used as a decorator, it ensures that the decorated function can only be called
652
+ once per specified period. Subsequent calls within this period are delayed to enforce
653
+ this constraint.
168
654
 
169
- Raises:
170
- ValueError: If the `func` cannot be applied to the `input`.
655
+ Attributes:
656
+ period (int): The minimum time period (in seconds) between successive calls.
171
657
 
172
- Example:
173
- >>> async def async_square(x):
174
- ... return x * x
175
- >>> asyncio.run(al_call([1, 2, 3], async_square))
176
- [1, 4, 9]
658
+ Methods:
659
+ __call__: Decorates a synchronous function with throttling.
660
+ __call_async__: Decorates an asynchronous function with throttling.
177
661
  """
178
- try:
179
- lst = to_list(input_=input_, flatten=flatten, dropna=dropna)
180
- tasks = [func_(i, **kwargs) for i in lst]
181
- return await asyncio.gather(*tasks)
182
- except Exception as e:
183
- raise ValueError(f"Given function cannot be applied to the input. Error: {e}")
184
662
 
185
- def mcall(
186
- input_: Union[Any, List[Any]], func_: Union[Callable, List[Callable]],
187
- flatten: bool = True, dropna: bool = True, **kwargs
188
- ) -> List[Any]:
663
+ def __init__(self, period: int) -> None:
664
+ """
665
+ Initializes a new instance of Throttle.
666
+
667
+ Args:
668
+ period (int): The minimum time period (in seconds) between successive calls.
669
+ """
670
+ self.period = period
671
+ self.last_called = 0
672
+
673
+ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
674
+ """
675
+ Decorates a synchronous function with the throttling mechanism.
676
+
677
+ Args:
678
+ func (Callable[..., Any]): The synchronous function to be throttled.
679
+
680
+ Returns:
681
+ Callable[..., Any]: The throttled synchronous function.
682
+ """
683
+ @functools.wraps(func)
684
+ def wrapper(*args, **kwargs) -> Any:
685
+ elapsed = time.time() - self.last_called
686
+ if elapsed < self.period:
687
+ time.sleep(self.period - elapsed)
688
+ self.last_called = time.time()
689
+ return func(*args, **kwargs)
690
+
691
+ return wrapper
692
+
693
+ async def __call_async__(self, func: Callable[..., Any]) -> Callable[..., Any]:
694
+ """
695
+ Decorates an asynchronous function with the throttling mechanism.
696
+
697
+ Args:
698
+ func (Callable[..., Any]): The asynchronous function to be throttled.
699
+
700
+ Returns:
701
+ Callable[..., Any]: The throttled asynchronous function.
702
+ """
703
+ @functools.wraps(func)
704
+ async def wrapper(*args, **kwargs) -> Any:
705
+ elapsed = time.time() - self.last_called
706
+ if elapsed < self.period:
707
+ await asyncio.sleep(self.period - elapsed)
708
+ self.last_called = time.time()
709
+ return await func(*args, **kwargs)
710
+
711
+ return wrapper
712
+
713
+ def _dropna(l: List[Any]) -> List[Any]:
189
714
  """
190
- Maps multiple functions to corresponding elements of the input.
191
-
192
- This function applies a list of functions to a list of inputs, with each function
193
- being applied to its corresponding input element. It asserts that the number of inputs
194
- and functions are the same and raises an error if they are not.
195
-
196
- Parameters:
197
- input (Union[Any, List[Any]]): The input or list of inputs to be processed.
715
+ Remove None values from a list.
198
716
 
199
- func (Union[Callable, List[Callable]]): The function or list of functions to apply.
717
+ Args:
718
+ l (List[Any]): A list potentially containing None values.
200
719
 
201
- flatten_dict (bool, optional): Whether to flatten dictionaries in the input. Defaults to False.
720
+ Returns:
721
+ List[Any]: A list with None values removed.
202
722
 
203
- flat (bool, optional): Whether the output list should be flattened. Defaults to True.
723
+ Examples:
724
+ >>> _dropna([1, None, 3, None])
725
+ [1, 3]
726
+ """
727
+ return [item for item in l if item is not None]
728
+
729
+ def _flatten_list(l: List[Any], dropna: bool = True) -> List[Any]:
730
+ """
731
+ Flatten a nested list, optionally removing None values.
204
732
 
205
- dropna (bool, optional): Whether to drop None values during flattening. Defaults to True.
733
+ Args:
734
+ l (List[Any]): A nested list to flatten.
735
+ dropna (bool): If True, None values are removed. Default is True.
206
736
 
207
737
  Returns:
208
- List[Any]: A list containing the results from applying each function to its corresponding input.
209
-
210
- Raises:
211
- ValueError: If the number of provided inputs and functions are not the same.
738
+ List[Any]: A flattened list.
212
739
 
213
- Example:
214
- >>> def add_one(x):
215
- ... return x + 1
216
- >>> m_call([1, 2], [add_one, add_one])
217
- [2, 3]
740
+ Examples:
741
+ >>> _flatten_list([[1, 2], [3, None]], dropna=True)
742
+ [1, 2, 3]
743
+ >>> _flatten_list([[1, [2, None]], 3], dropna=False)
744
+ [1, 2, None, 3]
218
745
  """
219
- input_ = to_list(input_=input_, flatten=flatten, dropna=dropna)
220
- func_ = to_list(func_)
221
- assert len(input_) == len(func_), "The number of inputs and functions must be the same."
222
-
223
- return to_list(
224
- [
225
- lcall(input_=inp, func_=f, flatten=flatten, dropna=dropna, **kwargs)
226
- for f, inp in zip(func_, input_)
227
- ]
228
- )
746
+ flattened_list = list(_flatten_list_generator(l, dropna))
747
+ return _dropna(flattened_list) if dropna else flattened_list
229
748
 
230
- async def amcall(
231
- input_: Union[Any, List[Any]], func_: Union[Callable, List[Callable]],
232
- flatten: bool = True, dropna: bool = True, **kwargs
233
- ) -> List[Any]:
749
+ def _flatten_list_generator(
750
+ l: List[Any], dropna: bool = True
751
+ ) -> Generator[Any, None, None]:
234
752
  """
235
- Asynchronously applies multiple functions to corresponding elements of the input.
753
+ Generator for flattening a nested list.
236
754
 
237
- This asynchronous function maps a list of functions to a list of inputs, with each
238
- function being applied to its corresponding input element asynchronously. It ensures
239
- that the number of inputs and functions are the same, raising a `ValueError` if not.
755
+ Args:
756
+ l (List[Any]): A nested list to flatten.
757
+ dropna (bool): If True, None values are omitted. Default is True.
240
758
 
241
- Parameters:
242
- input (Union[Any, List[Any]]): The input or list of inputs to be processed.
759
+ Yields:
760
+ Generator[Any, None, None]: A generator yielding flattened elements.
243
761
 
244
- func (Union[Callable, List[Callable]]): The function or list of functions to apply.
762
+ Examples:
763
+ >>> list(_flatten_list_generator([[1, [2, None]], 3], dropna=False))
764
+ [1, 2, None, 3]
765
+ """
766
+ for i in l:
767
+ if isinstance(i, list):
768
+ yield from _flatten_list_generator(i, dropna)
769
+ else:
770
+ yield i
245
771
 
246
- flatten_dict (bool, optional): Whether to flatten dictionaries in the input. Defaults to False.
247
772
 
248
- flat (bool, optional): Whether the output list should be flattened. Defaults to True.
773
+ def _custom_error_handler(error: Exception, error_map: Dict[type, Callable]) -> None:
774
+ """
775
+ Handle errors based on a given error mapping.
776
+
777
+ Args:
778
+ error (Exception): The error to handle.
779
+ error_map (Dict[type, Callable]): A dictionary mapping error types to handler functions.
780
+
781
+ Examples:
782
+ >>> def handle_value_error(e): print("ValueError occurred")
783
+ >>> custom_error_handler(ValueError(), {ValueError: handle_value_error})
784
+ ValueError occurred
785
+ """
786
+ handler = error_map.get(type(error))
787
+ if handler:
788
+ handler(error)
789
+ else:
790
+ logging.error(f"Unhandled error: {error}")
791
+
792
+ async def _call_handler(
793
+ func: Callable, *args, error_map: Dict[type, Callable] = None,
794
+ **kwargs
795
+ ) -> Any:
796
+ """
797
+ Call a function with error handling, supporting both synchronous and asynchronous functions.
249
798
 
250
- dropna (bool, optional): Whether to drop None values during flattening. Defaults to True.
799
+ Args:
800
+ func (Callable): The function to call.
801
+ *args: Positional arguments to pass to the function.
802
+ error_map (Dict[type, Callable], optional): A dictionary mapping error types to handler functions.
803
+ **kwargs: Keyword arguments to pass to the function.
251
804
 
252
805
  Returns:
253
- List[Any]: A list containing the results from applying each function to its corresponding input.
806
+ Any: The result of the function call.
254
807
 
255
808
  Raises:
256
- ValueError: If the number of inputs and functions do not match.
257
-
258
- Example:
259
- >>> async def async_add_one(x):
260
- ... return x + 1
261
- >>> asyncio.run(am_call([1, 2], [async_add_one, async_add_one]))
262
- [2, 3]
263
- """
264
- input = to_list(input_=input_, flatten=flatten, dropna=dropna)
265
- func_ = to_list(func_)
266
- assert len(input) == len(func_), "Input and function counts must match."
267
-
268
- tasks = [
269
- alcall(input_=inp, func_=f, flatten=flatten, dropna=dropna, **kwargs)
270
- for f, inp in zip(func_, input)
271
- ]
272
-
273
- out = await asyncio.gather(*tasks)
274
- return to_list(out, flat=True)
809
+ Exception: Propagates any exceptions not handled by the error_map.
275
810
 
276
- def ecall(
277
- input_: Union[Any, List[Any]], func_: Union[Callable, List[Callable]],
278
- flatten: bool = True, dropna: bool = True, **kwargs
279
- ) -> List[Any]:
811
+ Examples:
812
+ >>> async def async_add(x, y): return x + y
813
+ >>> asyncio.run(call_handler(async_add, 1, 2))
814
+ 3
280
815
  """
281
- Applies each function in a list of functions to all elements in the input.
282
-
283
- This function expands the input to match the number of functions and then
284
- applies each function to the entire input. It is useful for applying a series
285
- of different transformations to the same input.
286
-
287
- Parameters:
288
- input (Union[Any, List[Any]]): The input or list of inputs to be processed.
816
+ try:
817
+ if is_coroutine_func(func):
818
+ # Checking for a running event loop
819
+ try:
820
+ loop = asyncio.get_running_loop()
821
+ except RuntimeError: # No running event loop
822
+ loop = asyncio.new_event_loop()
823
+ asyncio.set_event_loop(loop)
824
+ # Running the coroutine in the new loop
825
+ result = loop.run_until_complete(func(*args, **kwargs))
826
+ loop.close()
827
+ return result
828
+
829
+ if loop.is_running():
830
+ return asyncio.ensure_future(func(*args, **kwargs))
831
+ else:
832
+ return await func(*args, **kwargs)
833
+ else:
834
+ return func(*args, **kwargs)
289
835
 
290
- func (Union[Callable, List[Callable]]): The function or list of functions to apply.
836
+ except Exception as e:
837
+ if error_map:
838
+ _custom_error_handler(e, error_map)
839
+ else:
840
+ logging.error(f"Error in call_handler: {e}")
841
+ raise
291
842
 
292
- flatten_dict (bool, optional): Whether to flatten dictionaries in the input. Defaults to False.
293
843
 
294
- flat (bool, optional): Whether the output list should be flattened. Defaults to True.
844
+ async def _alcall(
845
+ input: Any, func: Callable, flatten: bool = False, **kwargs
846
+ )-> List[Any]:
847
+ """
848
+ Asynchronously apply a function to each element in the input.
295
849
 
296
- dropna (bool, optional): Whether to drop None values during flattening. Defaults to True.
850
+ Args:
851
+ input (Any): The input to process.
852
+ func (Callable): The function to apply.
853
+ flatten (bool, optional): Whether to flatten the result. Default is False.
854
+ **kwargs: Keyword arguments to pass to the function.
297
855
 
298
856
  Returns:
299
- List[Any]: A list of results after applying each function to the input.
857
+ List[Any]: A list of results after asynchronously applying the function.
300
858
 
301
- Example:
302
- >>> def square(x):
303
- ... return x**2
304
- >>> e_call([1, 2, 3], [square])
305
- [[1], [4], [9]]
859
+ Examples:
860
+ >>> async def square(x): return x * x
861
+ >>> asyncio.run(alcall([1, 2, 3], square))
862
+ [1, 4, 9]
306
863
  """
307
-
308
- _f = lambda x, y: mcall(
309
- input_=create_copy(x, len(to_list(y))), func_=y,
310
- flatten=False, dropna=dropna, **kwargs
311
- )
312
- return [_f(x=inp, y=func_) for inp in to_list(input_)]
313
-
314
- async def aecall(
315
- input_: Union[Any, List[Any]], func_: Union[Callable, List[Callable]],
316
- dropna: bool = True, **kwargs
317
- ) -> List[Any]:
864
+ lst = to_list(input=input)
865
+ tasks = [_call_handler(func, i, **kwargs) for i in lst]
866
+ outs = await asyncio.gather(*tasks)
867
+ return to_list(outs, flatten=flatten)
868
+
869
+
870
+ async def _tcall(
871
+ func: Callable, *args, delay: float = 0, err_msg: Optional[str] = None,
872
+ ignore_err: bool = False, timing: bool = False,
873
+ default: Any = None, timeout: Optional[float] = None, **kwargs
874
+ ) -> Any:
318
875
  """
319
- Asynchronously applies each function in a list of functions to all elements in the input.
320
-
321
- This asynchronous function expands the input to match the number of functions and
322
- then asynchronously applies each function to the entire input. It is useful for applying a series
323
- of different asynchronous transformations to the same input.
324
-
325
- Parameters:
326
- input_ (Union[Any, List[Any]]): The input or list of inputs to be processed.
327
-
328
- func_ (Union[Callable, List[Callable]]): The function or list of functions to apply.
876
+ Asynchronously call a function with optional delay, timeout, and error handling.
877
+
878
+ Args:
879
+ func (Callable): The function to call.
880
+ *args: Positional arguments to pass to the function.
881
+ delay (float): Delay before calling the function, in seconds.
882
+ err_msg (Optional[str]): Custom error message.
883
+ ignore_err (bool): If True, ignore errors and return default.
884
+ timing (bool): If True, return a tuple (result, duration).
885
+ default (Any): Default value to return on error.
886
+ timeout (Optional[float]): Timeout for the function call, in seconds.
887
+ **kwargs: Keyword arguments to pass to the function.
329
888
 
330
- flatten_dict (bool, optional): Whether to flatten dictionaries in the input. Defaults to False.
331
-
332
- flat (bool, optional): Whether the output list should be flattened. Defaults to True.
333
-
334
- dropna (bool, optional): Whether to drop None values during flattening. Defaults to True.
889
+ Returns:
890
+ Any: The result of the function call, or (result, duration) if timing is True.
335
891
 
336
- Example:
337
- >>> async def async_square(x):
338
- ... return x**2
339
- >>> asyncio.run(ae_call([1, 2, 3], [async_square]))
340
- [[1, 4, 9]]
892
+ Examples:
893
+ >>> async def example_func(x): return x
894
+ >>> asyncio.run(tcall(example_func, 5, timing=True))
895
+ (5, duration)
341
896
  """
342
- async def _async_f(x, y):
343
- return await amcall(
344
- create_copy(x, len(to_list(y))), y, flatten=False, dropna=dropna, **kwargs
345
- )
346
-
347
- tasks = [_async_f(inp, func_) for inp in to_list(input_)]
348
- return await asyncio.gather(*tasks)
897
+ start_time = time.time()
898
+ try:
899
+ await asyncio.sleep(delay)
900
+ # Apply timeout to the function call
901
+ if timeout is not None:
902
+ result = await asyncio.wait_for(func(*args, **kwargs), timeout)
903
+ else:
904
+ if is_coroutine_func(func):
905
+ return await func( *args, **kwargs)
906
+ return func(*args, **kwargs)
907
+ duration = time.time() - start_time
908
+ return (result, duration) if timing else result
909
+ except asyncio.TimeoutError as e:
910
+ err_msg = f"{err_msg} Error: {e}" if err_msg else f"An error occurred: {e}"
911
+ print(err_msg)
912
+ if ignore_err:
913
+ return (default, time.time() - start_time) if timing else default
914
+ else:
915
+ raise e # Re-raise the timeout exception
916
+ except Exception as e:
917
+ err_msg = f"{err_msg} Error: {e}" if err_msg else f"An error occurred: {e}"
918
+ print(err_msg)
919
+ if ignore_err:
920
+ return (default, time.time() - start_time) if timing else default
921
+ else:
922
+ raise e
923
+