lionagi 0.0.201__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 (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
+