lionagi 0.0.201__py3-none-any.whl → 0.0.204__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. lionagi/_services/anthropic.py +79 -1
  2. lionagi/_services/base_service.py +1 -1
  3. lionagi/_services/services.py +61 -25
  4. lionagi/_services/transformers.py +46 -0
  5. lionagi/agents/__init__.py +0 -0
  6. lionagi/configs/oai_configs.py +1 -1
  7. lionagi/configs/openrouter_configs.py +1 -1
  8. lionagi/core/__init__.py +3 -7
  9. lionagi/core/branch/__init__.py +0 -0
  10. lionagi/core/branch/branch.py +589 -0
  11. lionagi/core/branch/branch_manager.py +139 -0
  12. lionagi/core/branch/cluster.py +1 -0
  13. lionagi/core/branch/conversation.py +484 -0
  14. lionagi/core/core_util.py +59 -0
  15. lionagi/core/flow/__init__.py +0 -0
  16. lionagi/core/flow/flow.py +19 -0
  17. lionagi/core/instruction_set/__init__.py +0 -0
  18. lionagi/core/instruction_set/instruction_set.py +343 -0
  19. lionagi/core/messages/__init__.py +0 -0
  20. lionagi/core/messages/messages.py +176 -0
  21. lionagi/core/sessions/__init__.py +0 -0
  22. lionagi/core/sessions/session.py +428 -0
  23. lionagi/models/__init__.py +0 -0
  24. lionagi/models/base_model.py +0 -0
  25. lionagi/models/imodel.py +53 -0
  26. lionagi/schema/data_logger.py +75 -155
  27. lionagi/tests/test_utils/test_call_util.py +658 -657
  28. lionagi/tools/tool_manager.py +121 -188
  29. lionagi/utils/__init__.py +5 -10
  30. lionagi/utils/call_util.py +667 -585
  31. lionagi/utils/io_util.py +3 -0
  32. lionagi/utils/nested_util.py +17 -211
  33. lionagi/utils/pd_util.py +57 -0
  34. lionagi/utils/sys_util.py +220 -184
  35. lionagi/utils/url_util.py +55 -0
  36. lionagi/version.py +1 -1
  37. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/METADATA +12 -8
  38. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/RECORD +47 -32
  39. lionagi/core/branch.py +0 -193
  40. lionagi/core/conversation.py +0 -341
  41. lionagi/core/flow.py +0 -8
  42. lionagi/core/instruction_set.py +0 -150
  43. lionagi/core/messages.py +0 -243
  44. lionagi/core/sessions.py +0 -474
  45. /lionagi/{tools → agents}/planner.py +0 -0
  46. /lionagi/{tools → agents}/prompter.py +0 -0
  47. /lionagi/{tools → agents}/scorer.py +0 -0
  48. /lionagi/{tools → agents}/summarizer.py +0 -0
  49. /lionagi/{tools → agents}/validator.py +0 -0
  50. /lionagi/core/{flow_util.py → flow/flow_util.py} +0 -0
  51. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/LICENSE +0 -0
  52. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/WHEEL +0 -0
  53. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/top_level.txt +0 -0
@@ -1,735 +1,648 @@
1
- from collections import OrderedDict
2
1
  import asyncio
3
- import functools as ft
4
- from aiocache import cached
5
- import concurrent.futures
2
+ import functools
3
+ import logging
6
4
  import time
7
- from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
8
- from .sys_util import create_copy
9
- from .nested_util import to_list
5
+ from typing import Any, Callable, Generator, Iterable, List, Dict, Optional, Tuple
6
+
7
+ from aiocache import cached
10
8
 
11
9
 
12
- def lcall(
13
- input_: Any, func_: Callable, flatten: bool = False,
14
- dropna: bool = False, **kwargs) -> List[Any]:
10
+ def to_list(input: Any, flatten: bool = True, dropna: bool = False) -> List[Any]:
15
11
  """
16
- list call: Applies a function to each element in a list, with options for flattening and dropping NAs.
12
+ Convert input to a list, with options to flatten and drop None values.
17
13
 
18
14
  Args:
19
- input_ (Any): The input, potentially a list, to process.
20
- func_ (Callable): The function to be applied to each element.
21
- flatten (bool, optional): If True, flattens the input.
22
- dropna (bool, optional): If True, drops None values from the list.
23
- **kwargs: Additional keyword arguments to pass to the function.
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.
24
18
 
25
19
  Returns:
26
- List[Any]: A list containing the results of the function call on each element.
27
-
28
- Raises:
29
- ValueError: If the function cannot be applied to an element in the list.
20
+ List[Any]: The input converted to a list.
30
21
 
31
22
  Examples:
32
- >>> lcall([1, 2, 3], lambda x: x + 1)
33
- [2, 3, 4]
34
- >>> lcall([1, None, 3], lambda x: x + 1, dropna=True)
35
- [2, 4]
23
+ >>> to_list([1, [2, None], 3], flatten=True, dropna=True)
24
+ [1, 2, 3]
25
+ >>> to_list("hello", flatten=False)
26
+ ["hello"]
36
27
  """
37
- try:
38
- lst = to_list(input_=input_, flatten=flatten, dropna=dropna)
39
- return [func_(i, **kwargs) for i in lst]
40
- except Exception as e:
41
- raise ValueError(f"Function {func_.__name__} cannot be applied: {e}")
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
42
40
 
43
- async def alcall(
44
- input_: Any, func_: Callable, flatten: bool = False,
45
- dropna: bool = False, **kwargs) -> List[Any]:
41
+ def lcall(
42
+ input: Any, func: Callable, flatten: bool = False,
43
+ dropna: bool = False, **kwargs
44
+ ) -> List[Any]:
46
45
  """
47
- Async list call: Asynchronously applies a function to each element in a list, with options for flattening and dropping NAs.
46
+ Apply a function to each element of the input list, with options to flatten and drop None values.
48
47
 
49
48
  Args:
50
- input_ (Any): The input, potentially a list, to process.
51
- func_ (Callable): The function (can be async or sync) to be applied to each element.
52
- flatten (bool, optional): If True, flattens the input.
53
- dropna (bool, optional): If True, drops None values from the list.
54
- **kwargs: Additional keyword arguments to pass to the function.
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.
55
54
 
56
55
  Returns:
57
- List[Any]: A list containing the results of the function call on each element.
58
-
59
- Raises:
60
- ValueError: If the function cannot be applied to an element in the list.
56
+ List[Any]: A list of results after applying the function.
61
57
 
62
58
  Examples:
63
- >>> async def add_one(x): return x + 1
64
- >>> asyncio.run(alcall([1, 2, 3], add_one))
65
- [2, 3, 4]
66
- >>> asyncio.run(alcall([1, None, 3], add_one, dropna=True))
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)
67
62
  [2, 4]
68
63
  """
69
- try:
70
- lst = to_list(input_=input_, flatten=flatten, dropna=dropna)
71
- if asyncio.iscoroutinefunction(func_):
72
- tasks = [func_(i, **kwargs) for i in lst]
73
- return await asyncio.gather(*tasks)
74
- else:
75
- return lcall(input_, func_, flatten=flatten, dropna=dropna, **kwargs)
76
- except Exception as e:
77
- raise ValueError(f"Function {func_.__name__} cannot be applied: {e}")
78
-
79
- # timed call
80
- async def tcall(input_: Any, func: Callable, sleep: float = 0.1,
81
- message: Optional[str] = None, ignore_error: bool = False,
82
- include_timing: bool = False, **kwargs
83
- ) -> Union[Any, Tuple[Any, float]]:
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:
84
73
  """
85
- Timed call: Handle both synchronous and asynchronous calls with optional delay, error handling, and execution timing.
74
+ Check if the given function is a coroutine function.
86
75
 
87
76
  Args:
88
- input_ (Any): The input to be passed to the function.
89
- func (Callable): The (async) function to be called.
90
- sleep (float, optional): Delay before the function call in seconds.
91
- message (Optional[str], optional): Custom message for error handling.
92
- ignore_error (bool, optional): If False, re-raises the caught exception.
93
- include_timing (bool, optional): If True, returns execution duration.
94
- **kwargs: Additional keyword arguments to pass to the function.
77
+ func (Callable): The function to check.
95
78
 
96
79
  Returns:
97
- Any: The result of the function call, optionally with execution duration.
98
-
99
- Raises:
100
- Exception: If the function raises an exception and ignore_error is False.
80
+ bool: True if the function is a coroutine function, False otherwise.
101
81
 
102
82
  Examples:
103
- >>> async def my_func(x): return x * 2
104
- >>> asyncio.run(tcall(3, my_func))
105
- 6
106
- >>> asyncio.run(tcall(3, my_func, include_timing=True))
107
- (6, <execution_duration>)
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
108
89
  """
109
- async def async_call() -> Tuple[Any, float]:
110
- start_time = time.time()
111
- try:
112
- await asyncio.sleep(sleep)
113
- result = await func(input_, **kwargs)
114
- duration = time.time() - start_time
115
- return (result, duration) if include_timing else result
116
- except Exception as e:
117
- handle_error(e)
90
+ return asyncio.iscoroutinefunction(func)
118
91
 
119
- def sync_call() -> Tuple[Any, float]:
120
- start_time = time.time()
121
- try:
122
- time.sleep(sleep)
123
- result = func(input_, **kwargs)
124
- duration = time.time() - start_time
125
- return (result, duration) if include_timing else result
126
- except Exception as e:
127
- handle_error(e)
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.
128
97
 
129
- def handle_error(e: Exception):
130
- err_msg = f"{message} Error: {e}" if message else f"An error occurred: {e}"
131
- print(err_msg)
132
- if not ignore_error:
133
- raise
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.
134
103
 
135
- if asyncio.iscoroutinefunction(func):
136
- return await async_call()
137
- else:
138
- return sync_call()
104
+ Returns:
105
+ List[Any]: A list of results after asynchronously applying the function.
139
106
 
140
- async def mcall(input_: Union[Any, List[Any]],
141
- funcs: Union[Callable, List[Callable]],
142
- explode: bool = False,
143
- flatten: bool = False,
144
- dropna: bool = False,
145
- **kwargs) -> List[Any]:
107
+ Examples:
108
+ >>> async def square(x): return x * x
109
+ >>> asyncio.run(alcall([1, 2, 3], square))
110
+ [1, 4, 9]
146
111
  """
147
- mapped call: handles both synchronous and asynchronous function calls
148
- on input elements with additional features such as flattening, dropping NAs, and
149
- applying multiple functions to each input.
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.
150
122
 
151
123
  Args:
152
- input_ (Union[Any, List[Any]]): Input or list of inputs.
153
- funcs (Union[Callable, List[Callable]]): Function or list of functions.
154
- explode (bool, optional): If True, applies each function to each input.
155
- flatten (bool, optional): If True, flattens the input list.
156
- dropna (bool, optional): If True, drops None values from the list.
157
- **kwargs: Additional keyword arguments for the function calls.
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.
158
128
 
159
129
  Returns:
160
- List[Any]: List of results from applying function(s) to input(s).
130
+ List[Any]: A list of results after applying the function(s).
161
131
 
162
132
  Examples:
163
- >>> async def increment(x): return x + 1
164
- >>> async def double(x): return x * 2
165
- >>> asyncio.run(mcall([1, 2, 3], [increment, double, increment]))
166
- [[2], [4], [4]]
167
- >>> asyncio.run(mcall([1, 2, 3], [increment, double, increment], explode=True))
168
- [[2, 4, 4], [3, 5, 5], [4, 6, 6]]
133
+ >>> async def add_one(x): return x + 1
134
+ >>> asyncio.run(mcall([1, 2, 3], add_one))
135
+ [2, 3, 4]
136
+
169
137
  """
138
+ input_ = to_list(input, dropna=True)
139
+ funcs_ = to_list(func, dropna=True)
140
+
170
141
  if explode:
171
- return await _explode_call(input_=input_, funcs=funcs, dropna=dropna, **kwargs)
142
+ tasks = [
143
+ _alcall(input_, f, flatten=True, **kwargs)
144
+ for f in funcs_
145
+ ]
146
+ return await asyncio.gather(*tasks)
172
147
  else:
173
- return await _mapped_call(input_=input_, funcs=funcs, flatten=flatten, dropna=dropna, **kwargs)
174
-
175
- async def bcall(inputs: List[Any], func: Callable[..., Any], batch_size: int, **kwargs) -> List[Any]:
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)
155
+
156
+ async def bcall(input: Any, func: Callable, batch_size: int, **kwargs) -> List[Any]:
176
157
  """
177
- batch call: Processes a list of inputs in batches, applying a function (sync or async) to each item in a batch.
158
+ Asynchronously call a function on batches of inputs.
178
159
 
179
160
  Args:
180
- inputs (List[Any]): The list of inputs to be processed.
181
- func (Callable[..., Any]): The function (can be sync or async) to be applied to each item.
182
- batch_size (int): The number of items to include in each batch.
183
- **kwargs: Additional keyword arguments to pass to the function.
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.
184
165
 
185
166
  Returns:
186
- List[Any]: A list of results from applying the function to each item in the batches.
187
-
188
- Raises:
189
- Exception: If an exception occurs during batch processing.
167
+ List[Any]: A list of results after applying the function in batches.
190
168
 
191
169
  Examples:
192
- >>> async def add_one(x): return x + 1
193
- >>> asyncio.run(bcall([1, 2, 3, 4], add_one, batch_size=2))
194
- [2, 3, 4, 5]
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]
195
173
  """
196
-
197
- async def process_batch(batch: List[Any]) -> List[Any]:
198
- if asyncio.iscoroutinefunction(func):
199
- # Process asynchronously if the function is a coroutine
200
- return await asyncio.gather(*(func(item, **kwargs) for item in batch))
201
- else:
202
- # Process synchronously otherwise
203
- return [func(item, **kwargs) for item in batch]
204
-
205
174
  results = []
206
- for i in range(0, len(inputs), batch_size):
207
- batch = inputs[i:i + batch_size]
208
- try:
209
- batch_results = await process_batch(batch)
210
- results.extend(batch_results)
211
- except Exception as e:
212
- # Handle exceptions or log errors here if needed
213
- raise e
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
+
214
181
  return results
215
182
 
216
- async def rcall(func: Callable[..., Any], *args, timeout: Optional[int] = None,
217
- retries: Optional[int] = None, initial_delay: float = 2.0,
218
- backoff_factor: float = 2.0, default: Optional[Any] = None,
219
- **kwargs) -> Any:
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:
220
188
  """
221
- Retry call: Executes a function with optional timeout, retry, and default value mechanisms.
189
+ Asynchronously call a function with optional delay, timeout, and error handling.
222
190
 
223
191
  Args:
224
- func (Callable[..., Any]): The function to be executed.
192
+ func (Callable): The function to call.
225
193
  *args: Positional arguments to pass to the function.
226
- timeout (Optional[int]): Timeout in seconds for the function call.
227
- retries (Optional[int]): Number of times to retry the function call.
228
- initial_delay (float): Initial delay in seconds for retries.
229
- backoff_factor (float): Factor by which the delay is multiplied on each retry.
230
- default (Optional[Any]): Default value to return in case of an exception.
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.
231
200
  **kwargs: Keyword arguments to pass to the function.
232
201
 
233
202
  Returns:
234
- Any: The result of the function call, default value if specified, or raises an exception.
235
-
236
- Raises:
237
- Exception: If the function raises an exception beyond the specified retries.
203
+ Any: The result of the function call, or (result, duration) if timing is True.
238
204
 
239
205
  Examples:
240
- >>> async def my_func(x): return x * 2
241
- >>> asyncio.run(rcall(my_func, 3))
242
- 6
243
- >>> asyncio.run(rcall(my_func, 3, retries=2, initial_delay=1, backoff_factor=2))
244
- 6
245
- """
246
- async def async_call():
247
- return await asyncio.wait_for(func(*args, **kwargs), timeout) if timeout else await func(*args, **kwargs)
248
-
249
- def sync_call():
250
- with concurrent.futures.ThreadPoolExecutor() as executor:
251
- future = executor.submit(func, *args, **kwargs)
252
- try:
253
- return future.result(timeout=timeout)
254
- except concurrent.futures.TimeoutError:
255
- future.cancel()
256
- raise asyncio.TimeoutError("Function call timed out")
206
+ >>> async def example_func(x): return x
207
+ >>> asyncio.run(tcall(example_func, 5, timing=True))
208
+ (5, duration)
209
+ """
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)
257
221
 
258
- delay = initial_delay
259
- for attempt in range(retries or 1):
222
+ def sync_call() -> Tuple[Any, float]:
223
+ start_time = time.time()
260
224
  try:
261
- if asyncio.iscoroutinefunction(func):
262
- return await async_call()
263
- else:
264
- return sync_call()
225
+ time.sleep(delay)
226
+ result = func(*args, **kwargs)
227
+ duration = time.time() - start_time
228
+ return (result, duration) if timing else result
265
229
  except Exception as e:
266
- if retries is None or attempt >= retries - 1:
267
- if default is not None:
268
- return default
269
- raise e
270
- await asyncio.sleep(delay)
271
- delay *= backoff_factor
230
+ handle_error(e)
272
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
273
237
 
274
- class CallDecorator:
275
-
276
- @staticmethod
277
- def cache(func: Callable) -> Callable:
278
- """
279
- Decorator that caches the results of function calls (both sync and async).
280
- If the function is called again with the same arguments,
281
- the cached result is returned instead of re-executing the function.
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:
248
+ """
249
+ Asynchronously retry a function call with exponential backoff.
282
250
 
283
- Args:
284
- func (Callable): The function (can be sync or async) whose results need to be cached.
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.
285
260
 
286
- Returns:
287
- Callable: A decorated function with caching applied.
288
- """
261
+ Returns:
262
+ Any: The result of the function call, or default if retries are exhausted.
289
263
 
290
- if asyncio.iscoroutinefunction(func):
291
- # Asynchronous function handling
292
- @cached(ttl=10 * 60)
293
- async def cached_async(*args, **kwargs) -> Any:
294
- return await func(*args, **kwargs)
295
-
296
- @ft.wraps(func)
297
- async def async_wrapper(*args, **kwargs) -> Any:
298
- return await cached_async(*args, **kwargs)
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
299
271
 
300
- return async_wrapper
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")
301
290
 
302
- else:
303
- # Synchronous function handling
304
- @ft.lru_cache(maxsize=None)
305
- def cached_sync(*args, **kwargs) -> Any:
306
- return func(*args, **kwargs)
307
-
308
- @ft.wraps(func)
309
- def sync_wrapper(*args, **kwargs) -> Any:
310
- return cached_sync(*args, **kwargs)
291
+ class CallDecorator:
292
+
293
+ """
294
+ Call Decorators
295
+ """
311
296
 
312
- return sync_wrapper
313
297
 
314
298
  @staticmethod
315
299
  def timeout(timeout: int) -> Callable:
316
300
  """
317
- Decorator to apply a timeout to a function.
301
+ Decorator to apply a timeout to an asynchronous function.
318
302
 
319
303
  Args:
320
- timeout (int): Maximum execution time allowed for the function in seconds.
304
+ timeout (int): Timeout duration in seconds.
321
305
 
322
306
  Returns:
323
- Callable: A decorated function with a timeout mechanism applied.
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
324
313
  """
325
314
  def decorator(func: Callable[..., Any]) -> Callable:
326
- @ft.wraps(func)
327
- async def async_wrapper(*args, **kwargs) -> Any:
315
+ @functools.wraps(func)
316
+ async def wrapper(*args, **kwargs) -> Any:
328
317
  return await rcall(func, *args, timeout=timeout, **kwargs)
329
-
330
- @ft.wraps(func)
331
- def sync_wrapper(*args, **kwargs) -> Any:
332
- return asyncio.run(rcall(func, *args, timeout=timeout, **kwargs))
333
-
334
- if asyncio.iscoroutinefunction(func):
335
- return async_wrapper
336
- else:
337
- return sync_wrapper
318
+ return wrapper
338
319
  return decorator
339
320
 
340
321
  @staticmethod
341
- def retry(retries: int = 3, initial_delay: float = 2.0, backoff_factor: float = 2.0) -> Callable:
322
+ def retry(
323
+ retries: int = 3, delay: float = 2.0, backoff_factor: float = 2.0
324
+ ) -> Callable:
342
325
  """
343
- Decorator to apply a retry mechanism to a function.
326
+ Decorator to retry an asynchronous function with exponential backoff.
344
327
 
345
328
  Args:
346
- retries (int): Maximum number of retry attempts.
347
- initial_delay (float): Initial delay in seconds before the first retry.
348
- backoff_factor (float): Factor by which the delay is increased on each retry.
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.
349
332
 
350
333
  Returns:
351
- Callable: A decorated function with a retry mechanism applied.
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
352
340
  """
353
341
  def decorator(func: Callable[..., Any]) -> Callable:
354
- @ft.wraps(func)
355
- async def async_wrapper(*args, **kwargs) -> Any:
356
- return await rcall(func, *args, retries=retries, initial_delay=initial_delay, backoff_factor=backoff_factor, **kwargs)
357
-
358
- @ft.wraps(func)
359
- def sync_wrapper(*args, **kwargs) -> Any:
360
- return asyncio.run(rcall(func, *args, retries=retries, initial_delay=initial_delay, backoff_factor=backoff_factor, **kwargs))
361
-
362
- if asyncio.iscoroutinefunction(func):
363
- return async_wrapper
364
- else:
365
- return sync_wrapper
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
366
349
  return decorator
367
350
 
368
351
  @staticmethod
369
352
  def default(default_value: Any) -> Callable:
370
- """
371
- Decorator to apply a default value mechanism to a function.
372
-
373
- Args:
374
- default (Any): The default value to return in case the function execution fails.
375
-
376
- Returns:
377
- Callable: A decorated function that returns a default value on failure.
378
- """
379
353
  def decorator(func: Callable[..., Any]) -> Callable:
380
- @ft.wraps(func)
381
- async def async_wrapper(*args, **kwargs) -> Any:
354
+ @functools.wraps(func)
355
+ async def wrapper(*args, **kwargs) -> Any:
382
356
  return await rcall(func, *args, default=default_value, **kwargs)
383
-
384
- @ft.wraps(func)
385
- def sync_wrapper(*args, **kwargs) -> Any:
386
- return asyncio.run(rcall(func, *args, default=default_value, **kwargs))
387
-
388
- if asyncio.iscoroutinefunction(func):
389
- return async_wrapper
390
- else:
391
- return sync_wrapper
357
+ return wrapper
392
358
  return decorator
393
359
 
394
360
  @staticmethod
395
361
  def throttle(period: int) -> Callable:
396
362
  """
397
- A decorator factory that creates a throttling decorator for both synchronous and asynchronous functions.
363
+ A static method to create a throttling decorator using the Throttle class.
398
364
 
399
365
  Args:
400
- period (int): The minimum time period (in seconds) between successive calls of the decorated function.
366
+ period (int): The minimum time period (in seconds) between successive calls.
401
367
 
402
368
  Returns:
403
- Callable: A decorator that applies a throttling mechanism to the decorated function.
404
-
405
- Usage:
406
- @throttle(2)
407
- def my_function():
408
- # Function implementation
409
-
410
- @throttle(2)
411
- async def my_async_function():
412
- # Async function implementation
369
+ Callable: A decorator that applies a throttling mechanism to the decorated
370
+ function.
413
371
  """
414
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
415
- throttle_decorator = Throttle(period)
416
- if asyncio.iscoroutinefunction(func):
417
- return throttle_decorator.__call_async__(func)
418
- else:
419
- return throttle_decorator(func)
420
- return decorator
372
+ return Throttle(period)
421
373
 
422
374
  @staticmethod
423
- def pre_post_process(preprocess: Callable[..., Any], postprocess: Callable[..., Any]) -> Callable:
375
+ def map(function: Callable[[Any], Any]) -> Callable:
424
376
  """
425
- Decorator factory that applies preprocessing and postprocessing to a function (sync or async).
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.
426
382
 
427
383
  Args:
428
- preprocess (Callable[..., Any]): A function to preprocess each argument.
429
- postprocess (Callable[..., Any]): A function to postprocess the result.
384
+ function (Callable[[Any], Any]): A function to apply to each element of the list.
430
385
 
431
386
  Returns:
432
- Callable: A decorator that applies preprocessing and postprocessing to the decorated function.
433
- """
387
+ Callable: A decorated function that maps the provided function over its results.
434
388
 
435
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
436
- @ft.wraps(func)
437
- async def async_wrapper(*args, **kwargs) -> Any:
438
- preprocessed_args = [preprocess(arg) for arg in args]
439
- preprocessed_kwargs = {k: preprocess(v) for k, v in kwargs.items()}
440
- result = await func(*preprocessed_args, **preprocessed_kwargs)
441
- return postprocess(result)
442
-
443
- @ft.wraps(func)
444
- def sync_wrapper(*args, **kwargs) -> Any:
445
- preprocessed_args = [preprocess(arg) for arg in args]
446
- preprocessed_kwargs = {k: preprocess(v) for k, v in kwargs.items()}
447
- result = func(*preprocessed_args, **preprocessed_kwargs)
448
- return postprocess(result)
449
-
450
- if asyncio.iscoroutinefunction(func):
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]
451
401
  return async_wrapper
452
- else:
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]
453
407
  return sync_wrapper
454
-
455
408
  return decorator
456
409
 
457
410
  @staticmethod
458
- def filter(predicate: Callable[[Any], bool]) -> Callable:
411
+ def compose(*functions: Callable[[Any], Any]) -> Callable:
459
412
  """
460
- Decorator factory to filter values returned by a function based on a predicate.
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.
461
415
 
462
416
  Args:
463
- predicate (Callable[[Any], bool]): Predicate function to filter values.
417
+ *functions: Variable length list of functions to compose.
464
418
 
465
419
  Returns:
466
- Callable: Decorated function that filters its return values.
420
+ Callable: A new function that is the composition of the given functions.
467
421
  """
468
- def decorator(func: Callable[..., List[Any]]) -> Callable:
469
- @ft.wraps(func)
470
- def wrapper(*args, **kwargs) -> List[Any]:
471
- values = func(*args, **kwargs)
472
- return [value for value in values if predicate(value)]
473
- return wrapper
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.")
474
447
  return decorator
475
448
 
476
449
  @staticmethod
477
- def map(function: Callable[[Any], Any]) -> Callable:
450
+ def pre_post_process(
451
+ preprocess: Callable[..., Any], postprocess: Callable[..., Any]
452
+ ) -> Callable:
478
453
  """
479
- Decorator factory to map values returned by a function using a provided function.
454
+ Decorator that applies preprocessing and postprocessing functions to the arguments
455
+ and result of an asynchronous function.
480
456
 
481
457
  Args:
482
- function (Callable[[Any], Any]): Function to map values.
458
+ preprocess (Callable[..., Any]): A function to preprocess each argument.
459
+ postprocess (Callable[..., Any]): A function to postprocess the result.
483
460
 
484
461
  Returns:
485
- Callable: Decorated function that maps its return values.
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
486
469
  """
487
- def decorator(func: Callable[..., List[Any]]) -> Callable:
488
- @ft.wraps(func)
489
- def wrapper(*args, **kwargs) -> List[Any]:
490
- values = func(*args, **kwargs)
491
- return [function(value) for value in values]
492
- return wrapper
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
+
493
488
  return decorator
494
489
 
495
490
  @staticmethod
496
- def reduce(function: Callable[[Any, Any], Any], initial: Any) -> Callable:
491
+ def cache(func: Callable, ttl=600, maxsize=None) -> Callable:
497
492
  """
498
- Decorator factory to reduce values returned by a function to a single value using the provided function.
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.
499
496
 
500
497
  Args:
501
- function (Callable[[Any, Any], Any]): Reducing function.
502
- initial (Any): Initial value for reduction.
498
+ func (Callable): The function (can be sync or async) whose results need to be cached.
503
499
 
504
500
  Returns:
505
- Callable: Decorated function that reduces its return values.
501
+ Callable: A decorated function with caching applied.
506
502
  """
507
- def decorator(func: Callable[..., List[Any]]) -> Callable:
508
- @ft.wraps(func)
509
- def wrapper(*args, **kwargs) -> Any:
510
- values = func(*args, **kwargs)
511
- return ft.reduce(function, values, initial)
512
- return wrapper
513
- return decorator
514
503
 
515
- @staticmethod
516
- def compose(*functions: Callable[[Any], Any]) -> Callable:
517
- """
518
- Decorator factory that composes multiple functions. The output of each function is passed as
519
- the input to the next, in the order they are provided.
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)
520
513
 
521
- Args:
522
- *functions: Variable length list of functions to compose.
514
+ return async_wrapper
523
515
 
524
- Returns:
525
- Callable: A new function that is the composition of the given functions.
526
- """
527
- def decorator(func: Callable) -> Callable:
528
- @ft.wraps(func)
529
- def wrapper(*args, **kwargs):
530
- value = func(*args, **kwargs)
531
- for function in functions:
532
- try:
533
- value = function(value)
534
- except Exception as e:
535
- raise ValueError(f"Error in function {function.__name__}: {e}")
536
- return value
537
- return wrapper
538
- return decorator
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
539
527
 
540
528
  @staticmethod
541
- def memorize(maxsize: int = 10_000) -> Callable:
529
+ def filter(predicate: Callable[[Any], bool]) -> Callable:
542
530
  """
543
- Decorator factory to memorize function calls. Caches the return values of the function for specific inputs.
531
+ Decorator that filters the results of an asynchronous function based on a predicate
532
+ function.
544
533
 
545
534
  Args:
546
- maxsize (int): Maximum size of the cache. Defaults to 10,000.
535
+ predicate (Callable[[Any], bool]): A function that returns True for items to keep.
547
536
 
548
537
  Returns:
549
- Callable: A memorized version of the function.
550
- """
551
- def decorator(function: Callable) -> Callable:
552
- cache = OrderedDict()
553
-
554
- @ft.wraps(function)
555
- def memorized_function(*args):
556
- if args in cache:
557
- cache.move_to_end(args) # Move the recently accessed item to the end
558
- return cache[args]
559
-
560
- if len(cache) >= maxsize:
561
- cache.popitem(last=False) # Remove oldest cache entry
538
+ Callable: A decorated function that filters its results based on the predicate.
562
539
 
563
- result = function(*args)
564
- cache[args] = result
565
- return result
566
-
567
- return memorized_function
568
-
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
569
559
  return decorator
570
560
 
571
561
  @staticmethod
572
- def validate(**config):
562
+ def reduce(function: Callable[[Any, Any], Any], initial: Any) -> Callable:
573
563
  """
574
- Decorator factory to process the return value of a function using specified validation and conversion functions.
564
+ Decorator that reduces the results of an asynchronous function to a single value using
565
+ the specified reduction function.
575
566
 
576
567
  Args:
577
- **config: Configuration dictionary specifying the processing functions and their settings.
568
+ function (Callable[[Any, Any], Any]): A reduction function to apply.
569
+ initial (Any): The initial value for the reduction.
578
570
 
579
571
  Returns:
580
- Callable: A decorator that applies specified processing to the function's return value.
581
- """
582
- def decorator(func: Callable) -> Callable:
583
- @ft.wraps(func)
584
- def wrapper(*args, **kwargs) -> Any:
585
- value = func(*args, **kwargs)
586
- return _process_value(value, config)
587
-
588
- return wrapper
572
+ Callable: A decorated function that reduces its results to a single value.
589
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
590
593
  return decorator
591
-
594
+
592
595
  @staticmethod
593
- def max_concurrency(limit: int=5):
596
+ def max_concurrency(limit: int = 5) -> Callable:
594
597
  """
595
598
  Decorator to limit the maximum number of concurrent executions of an async function.
596
-
599
+
597
600
  Args:
598
601
  limit (int): The maximum number of concurrent tasks allowed.
599
- """
600
602
 
601
- def decorator(func: Callable):
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.")
602
613
  semaphore = asyncio.Semaphore(limit)
603
614
 
604
- @ft.wraps(func)
615
+ @functools.wraps(func)
605
616
  async def wrapper(*args, **kwargs):
606
617
  async with semaphore:
607
618
  return await func(*args, **kwargs)
608
619
 
609
620
  return wrapper
610
-
621
+
611
622
  return decorator
612
623
 
613
- def _handle_error(value: Any, config: Dict[str, Any]) -> Any:
614
- """Handle an error by logging and returning a default value if provided.
615
-
616
- Args:
617
- value: The value to check for an exception.
618
- config: A dictionary with optional keys 'log' and 'default'.
619
-
620
- Returns:
621
- The original value or the default value from config if value is an exception.
622
-
623
- Examples:
624
- >>> handle_error(ValueError("An error"), {'log': True, 'default': 'default_value'})
625
- Error: An error
626
- 'default_value'
627
- """
628
- if isinstance(value, Exception):
629
- if config.get('log', True):
630
- print(f"Error: {value}") # Replace with appropriate logging mechanism
631
- return config.get('default', None)
632
- return value
633
-
634
- def _validate_type(value: Any, expected_type: Type) -> Any:
635
- """Validate the type of value, raise TypeError if not expected type.
636
-
637
- Args:
638
- value: The value to validate.
639
- expected_type: The type that value is expected to be.
640
-
641
- Returns:
642
- The original value if it is of the expected type.
643
-
644
- Raises:
645
- TypeError: If value is not of the expected type.
646
-
647
- Examples:
648
- >>> validate_type(10, int)
649
- 10
650
- >>> validate_type("10", int)
651
- Traceback (most recent call last):
652
- ...
653
- TypeError: Invalid type: expected <class 'int'>, got <class 'str'>
654
- """
655
- if not isinstance(value, expected_type):
656
- raise TypeError(f"Invalid type: expected {expected_type}, got {type(value)}")
657
- return value
658
-
659
- def _convert_type(value: Any, target_type: Callable) -> Optional[Any]:
660
- """Convert the type of value to target_type, return None if conversion fails.
661
-
662
- Args:
663
- value: The value to convert.
664
- target_type: The type to convert value to.
665
-
666
- Returns:
667
- The converted value or None if conversion fails.
668
-
669
- Examples:
670
- >>> convert_type("10", int)
671
- 10
672
- >>> convert_type("abc", int)
673
- Conversion error: invalid literal for int() with base 10: 'abc'
674
- None
675
- """
676
- try:
677
- return target_type(value)
678
- except (ValueError, TypeError) as e:
679
- print(f"Conversion error: {e}") # Replace with appropriate logging mechanism
680
- return None
681
-
682
- def _process_value(value: Any, config: Dict[str, Any]) -> Any:
683
- """
684
- Processes a value using a chain of functions defined in config.
685
- """
686
- processing_functions = {
687
- 'handle_error': _handle_error,
688
- 'validate_type': _validate_type,
689
- 'convert_type': _convert_type
690
- }
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.
691
629
 
692
- try:
693
- for key, func_config in config.items():
694
- func = processing_functions.get(key)
695
- if func:
696
- value = func(value, func_config)
697
- return value
698
- except Exception as e:
699
- if 'handle_error' in config.keys():
700
- func = processing_functions.get('handle_error')
701
- return func(e, config['handle_error'])
702
- else:
703
- raise e
630
+ Args:
631
+ period (int): The minimum time period, in seconds, that must elapse between successive
632
+ calls to the decorated function.
704
633
 
705
- async def _mapped_call(input_: Union[Any, List[Any]],
706
- funcs: Union[Callable, List[Callable]],
707
- flatten: bool = False,
708
- dropna: bool = False,
709
- **kwargs) -> List[Any]:
710
-
711
- input_ = to_list(input_=input_, flatten=flatten, dropna=dropna)
712
- funcs = to_list(funcs)
713
- assert len(input_) == len(funcs), "The number of inputs and functions must be the same."
714
- return to_list(
715
- [
716
- await alcall(input_=inp, func_=f, flatten=flatten, dropna=dropna, **kwargs)
717
- for f, inp in zip(funcs, input_)
718
- ]
719
- )
720
-
721
- async def _explode_call(input_: Union[Any, List[Any]],
722
- funcs: Union[Callable, List[Callable]],
723
- dropna: bool = False,
724
- **kwargs) -> List[Any]:
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.
725
637
 
726
- async def _async_f(x, y):
727
- return await mcall(
728
- create_copy(x, len(to_list(y))), y, flatten=False, dropna=dropna, **kwargs
729
- )
638
+ Examples:
639
+ >>> @CallDecorator.throttle(2) # Ensures at least 2 seconds between calls
640
+ ... async def fetch_data(): pass
730
641
 
731
- tasks = [_async_f(inp, funcs) for inp in to_list(input_)]
732
- return await asyncio.gather(*tasks)
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)
733
646
 
734
647
  class Throttle:
735
648
  """
@@ -767,7 +680,7 @@ class Throttle:
767
680
  Returns:
768
681
  Callable[..., Any]: The throttled synchronous function.
769
682
  """
770
- @ft.wraps(func)
683
+ @functools.wraps(func)
771
684
  def wrapper(*args, **kwargs) -> Any:
772
685
  elapsed = time.time() - self.last_called
773
686
  if elapsed < self.period:
@@ -787,7 +700,7 @@ class Throttle:
787
700
  Returns:
788
701
  Callable[..., Any]: The throttled asynchronous function.
789
702
  """
790
- @ft.wraps(func)
703
+ @functools.wraps(func)
791
704
  async def wrapper(*args, **kwargs) -> Any:
792
705
  elapsed = time.time() - self.last_called
793
706
  if elapsed < self.period:
@@ -796,46 +709,215 @@ class Throttle:
796
709
  return await func(*args, **kwargs)
797
710
 
798
711
  return wrapper
712
+
713
+ def _dropna(l: List[Any]) -> List[Any]:
714
+ """
715
+ Remove None values from a list.
716
+
717
+ Args:
718
+ l (List[Any]): A list potentially containing None values.
719
+
720
+ Returns:
721
+ List[Any]: A list with None values removed.
722
+
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.
732
+
733
+ Args:
734
+ l (List[Any]): A nested list to flatten.
735
+ dropna (bool): If True, None values are removed. Default is True.
736
+
737
+ Returns:
738
+ List[Any]: A flattened list.
739
+
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]
745
+ """
746
+ flattened_list = list(_flatten_list_generator(l, dropna))
747
+ return _dropna(flattened_list) if dropna else flattened_list
748
+
749
+ def _flatten_list_generator(
750
+ l: List[Any], dropna: bool = True
751
+ ) -> Generator[Any, None, None]:
752
+ """
753
+ Generator for flattening a nested list.
754
+
755
+ Args:
756
+ l (List[Any]): A nested list to flatten.
757
+ dropna (bool): If True, None values are omitted. Default is True.
758
+
759
+ Yields:
760
+ Generator[Any, None, None]: A generator yielding flattened elements.
761
+
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
771
+
772
+
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.
798
+
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.
804
+
805
+ Returns:
806
+ Any: The result of the function call.
807
+
808
+ Raises:
809
+ Exception: Propagates any exceptions not handled by the error_map.
810
+
811
+ Examples:
812
+ >>> async def async_add(x, y): return x + y
813
+ >>> asyncio.run(call_handler(async_add, 1, 2))
814
+ 3
815
+ """
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)
835
+
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
842
+
843
+
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.
849
+
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.
799
855
 
856
+ Returns:
857
+ List[Any]: A list of results after asynchronously applying the function.
800
858
 
859
+ Examples:
860
+ >>> async def square(x): return x * x
861
+ >>> asyncio.run(alcall([1, 2, 3], square))
862
+ [1, 4, 9]
863
+ """
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)
801
868
 
802
- # # parallel call with control of max_concurrent
803
- # async def pcall(input_: Any,
804
- # funcs: Union[Callable, List[Callable]],
805
- # max_concurrent: Optional[int] = None,
806
- # flatten: bool = False,
807
- # dropna: bool = False,
808
- # **kwargs) -> List[Any]:
809
- # """
810
- # A unified function that handles both synchronous and asynchronous function calls
811
- # on input elements. It can process inputs in parallel or sequentially and can handle
812
- # both single and multiple functions with corresponding inputs.
813
-
814
- # Args:
815
- # input_ (Any): The input to process, potentially a list.
816
- # funcs (Union[Callable, List[Callable]]): A function or list of functions to apply.
817
- # max_concurrent (Optional[int]): Maximum number of concurrent executions for parallel processing.
818
- # flatten (bool): If True, flattens the input.
819
- # dropna (bool): If True, drops None values from the list.
820
- # **kwargs: Additional keyword arguments to pass to the function(s).
821
-
822
- # Returns:
823
- # List[Any]: A list containing the results of function call(s) on input elements.
824
- # """
825
-
826
- # async def async_wrapper(func, item):
827
- # return await func(item, **kwargs) if asyncio.iscoroutinefunction(func) else func(item, **kwargs)
828
-
829
- # try:
830
- # lst = to_list(input_=input_, flatten=flatten, dropna=dropna)
831
- # if not isinstance(funcs, list):
832
- # funcs = [funcs] * len(lst)
833
- # tasks = [async_wrapper(func, item) for func, item in zip(funcs, lst)]
834
- # if max_concurrent:
835
- # semaphore = asyncio.Semaphore(max_concurrent)
836
- # async with semaphore:
837
- # return await asyncio.gather(*tasks)
838
- # else:
839
- # return await asyncio.gather(*tasks)
840
- # except Exception as e:
841
- # raise ValueError(f"Error in unified_call: {e}")
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:
875
+ """
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.
888
+
889
+ Returns:
890
+ Any: The result of the function call, or (result, duration) if timing is True.
891
+
892
+ Examples:
893
+ >>> async def example_func(x): return x
894
+ >>> asyncio.run(tcall(example_func, 5, timing=True))
895
+ (5, duration)
896
+ """
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
+