lionagi 0.0.306__py3-none-any.whl → 0.0.308__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 (78) hide show
  1. lionagi/__init__.py +2 -5
  2. lionagi/core/__init__.py +7 -5
  3. lionagi/core/agent/__init__.py +3 -0
  4. lionagi/core/agent/base_agent.py +10 -12
  5. lionagi/core/branch/__init__.py +4 -0
  6. lionagi/core/branch/base_branch.py +81 -81
  7. lionagi/core/branch/branch.py +16 -28
  8. lionagi/core/branch/branch_flow_mixin.py +3 -7
  9. lionagi/core/branch/executable_branch.py +86 -56
  10. lionagi/core/branch/util.py +77 -162
  11. lionagi/core/{flow/direct → direct}/__init__.py +1 -1
  12. lionagi/core/{flow/direct/predict.py → direct/parallel_predict.py} +39 -17
  13. lionagi/core/direct/parallel_react.py +0 -0
  14. lionagi/core/direct/parallel_score.py +0 -0
  15. lionagi/core/direct/parallel_select.py +0 -0
  16. lionagi/core/direct/parallel_sentiment.py +0 -0
  17. lionagi/core/direct/predict.py +174 -0
  18. lionagi/core/{flow/direct → direct}/react.py +2 -2
  19. lionagi/core/{flow/direct → direct}/score.py +28 -23
  20. lionagi/core/{flow/direct → direct}/select.py +48 -45
  21. lionagi/core/direct/utils.py +83 -0
  22. lionagi/core/flow/monoflow/ReAct.py +6 -5
  23. lionagi/core/flow/monoflow/__init__.py +9 -0
  24. lionagi/core/flow/monoflow/chat.py +10 -10
  25. lionagi/core/flow/monoflow/chat_mixin.py +11 -10
  26. lionagi/core/flow/monoflow/followup.py +6 -5
  27. lionagi/core/flow/polyflow/__init__.py +1 -0
  28. lionagi/core/flow/polyflow/chat.py +15 -3
  29. lionagi/core/mail/mail_manager.py +18 -19
  30. lionagi/core/mail/schema.py +5 -4
  31. lionagi/core/messages/schema.py +18 -20
  32. lionagi/core/prompt/__init__.py +0 -0
  33. lionagi/core/prompt/prompt_template.py +0 -0
  34. lionagi/core/schema/__init__.py +2 -2
  35. lionagi/core/schema/action_node.py +11 -3
  36. lionagi/core/schema/base_mixin.py +56 -59
  37. lionagi/core/schema/base_node.py +34 -37
  38. lionagi/core/schema/condition.py +24 -0
  39. lionagi/core/schema/data_logger.py +96 -99
  40. lionagi/core/schema/data_node.py +19 -19
  41. lionagi/core/schema/prompt_template.py +0 -0
  42. lionagi/core/schema/structure.py +171 -169
  43. lionagi/core/session/__init__.py +1 -3
  44. lionagi/core/session/session.py +196 -214
  45. lionagi/core/tool/tool_manager.py +95 -103
  46. lionagi/integrations/__init__.py +1 -3
  47. lionagi/integrations/bridge/langchain_/documents.py +17 -18
  48. lionagi/integrations/bridge/langchain_/langchain_bridge.py +14 -14
  49. lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py +22 -22
  50. lionagi/integrations/bridge/llamaindex_/node_parser.py +12 -12
  51. lionagi/integrations/bridge/llamaindex_/reader.py +11 -11
  52. lionagi/integrations/bridge/llamaindex_/textnode.py +7 -7
  53. lionagi/integrations/config/openrouter_configs.py +0 -1
  54. lionagi/integrations/provider/oai.py +26 -26
  55. lionagi/integrations/provider/services.py +38 -38
  56. lionagi/libs/__init__.py +34 -1
  57. lionagi/libs/ln_api.py +211 -221
  58. lionagi/libs/ln_async.py +53 -60
  59. lionagi/libs/ln_convert.py +118 -120
  60. lionagi/libs/ln_dataframe.py +32 -33
  61. lionagi/libs/ln_func_call.py +334 -342
  62. lionagi/libs/ln_nested.py +99 -107
  63. lionagi/libs/ln_parse.py +161 -165
  64. lionagi/libs/sys_util.py +52 -52
  65. lionagi/tests/test_core/test_session.py +254 -266
  66. lionagi/tests/test_core/test_session_base_util.py +299 -300
  67. lionagi/tests/test_core/test_tool_manager.py +70 -74
  68. lionagi/tests/test_libs/test_nested.py +2 -7
  69. lionagi/tests/test_libs/test_parse.py +2 -2
  70. lionagi/version.py +1 -1
  71. {lionagi-0.0.306.dist-info → lionagi-0.0.308.dist-info}/METADATA +4 -2
  72. lionagi-0.0.308.dist-info/RECORD +115 -0
  73. lionagi/core/flow/direct/utils.py +0 -43
  74. lionagi-0.0.306.dist-info/RECORD +0 -106
  75. /lionagi/core/{flow/direct → direct}/sentiment.py +0 -0
  76. {lionagi-0.0.306.dist-info → lionagi-0.0.308.dist-info}/LICENSE +0 -0
  77. {lionagi-0.0.306.dist-info → lionagi-0.0.308.dist-info}/WHEEL +0 -0
  78. {lionagi-0.0.306.dist-info → lionagi-0.0.308.dist-info}/top_level.txt +0 -0
@@ -36,33 +36,33 @@ def lcall(
36
36
  function can be passed dynamically, allowing for flexible function application.
37
37
 
38
38
  Args:
39
- input_ (Any):
40
- The input list or iterable to process. each element will be passed to the
41
- provided `func` Callable.
42
- func (Callable):
43
- The function to apply to each element of `input_`. this function can be any
44
- Callable that accepts the elements of `input_` as arguments.
45
- flatten (bool, optional):
46
- If True, the resulting list is flattened. useful when `func` returns a list.
47
- defaults to False.
48
- dropna (bool, optional):
49
- If True, None values are removed from the final list. defaults to False.
50
- **kwargs:
51
- Additional keyword arguments to be passed to `func`.
39
+ input_ (Any):
40
+ The input list or iterable to process. each element will be passed to the
41
+ provided `func` Callable.
42
+ func (Callable):
43
+ The function to apply to each element of `input_`. this function can be any
44
+ Callable that accepts the elements of `input_` as arguments.
45
+ flatten (bool, optional):
46
+ If True, the resulting list is flattened. useful when `func` returns a list.
47
+ defaults to False.
48
+ dropna (bool, optional):
49
+ If True, None values are removed from the final list. defaults to False.
50
+ **kwargs:
51
+ Additional keyword arguments to be passed to `func`.
52
52
 
53
53
  Returns:
54
- list[Any]:
55
- The list of results after applying `func` to each input element, modified
56
- according to `flatten` and `dropna` options.
54
+ list[Any]:
55
+ The list of results after applying `func` to each input element, modified
56
+ according to `flatten` and `dropna` options.
57
57
 
58
58
  Examples:
59
- Apply a doubling function to each element:
60
- >>> lcall([1, 2, 3], lambda x: x * 2)
61
- [2, 4, 6]
59
+ Apply a doubling function to each element:
60
+ >>> lcall([1, 2, 3], lambda x: x * 2)
61
+ [2, 4, 6]
62
62
 
63
- apply a function that returns lists, then flatten the result:
64
- >>> lcall([1, 2, None], lambda x: [x, x] if x else x, flatten=True, dropna=True)
65
- [1, 1, 2, 2]
63
+ apply a function that returns lists, then flatten the result:
64
+ >>> lcall([1, 2, None], lambda x: [x, x] if x else x, flatten=True, dropna=True)
65
+ [1, 1, 2, 2]
66
66
  """
67
67
  lst = to_list(input_, dropna=dropna)
68
68
  if len(to_list(func)) != 1:
@@ -91,26 +91,26 @@ async def alcall(
91
91
  efficiency and overall execution time.
92
92
 
93
93
  Args:
94
- input_ (Any, optional):
95
- The input to process. defaults to None, which requires `func` to be capable of
96
- handling the absence of explicit input.
97
- func (Callable, optional):
98
- The asynchronous function to apply. defaults to None.
99
- flatten (bool, optional):
100
- Whether to flatten the result. useful when `func` returns a list or iterable
101
- that should be merged into a single list. defaults to False.
102
- **kwargs:
103
- Keyword arguments to pass to the function.
94
+ input_ (Any, optional):
95
+ The input to process. defaults to None, which requires `func` to be capable of
96
+ handling the absence of explicit input.
97
+ func (Callable, optional):
98
+ The asynchronous function to apply. defaults to None.
99
+ flatten (bool, optional):
100
+ Whether to flatten the result. useful when `func` returns a list or iterable
101
+ that should be merged into a single list. defaults to False.
102
+ **kwargs:
103
+ Keyword arguments to pass to the function.
104
104
 
105
105
  Returns:
106
- list[Any]:
107
- A list of results after asynchronously applying the function to each element
108
- of the input, potentially flattened.
106
+ list[Any]:
107
+ A list of results after asynchronously applying the function to each element
108
+ of the input, potentially flattened.
109
109
 
110
110
  examples:
111
- >>> async def square(x): return x * x
112
- >>> await alcall([1, 2, 3], square)
113
- [1, 4, 9]
111
+ >>> async def square(x): return x * x
112
+ >>> await alcall([1, 2, 3], square)
113
+ [1, 4, 9]
114
114
  """
115
115
  tasks = []
116
116
  if input_ is not None:
@@ -123,7 +123,7 @@ async def alcall(
123
123
  outs = await AsyncUtil.execute_tasks(*tasks)
124
124
  outs_ = []
125
125
  for i in outs:
126
- outs_.append(i if not isinstance(i, (Coroutine, asyncio.Future)) else await i)
126
+ outs_.append(await i if isinstance(i, (Coroutine, asyncio.Future)) else i)
127
127
 
128
128
  return to_list(outs_, flatten=flatten, dropna=dropna)
129
129
 
@@ -135,23 +135,23 @@ async def mcall(
135
135
  asynchronously map a function or functions over an input_ or inputs.
136
136
 
137
137
  Args:
138
- input_ (Any):
139
- The input_ or inputs to process.
140
- func (Any):
141
- The function or functions to apply.
142
- explode (bool, optional):
143
- Whether to apply each function to each input_. default is False.
144
- **kwargs:
145
- Keyword arguments to pass to the function.
138
+ input_ (Any):
139
+ The input_ or inputs to process.
140
+ func (Any):
141
+ The function or functions to apply.
142
+ explode (bool, optional):
143
+ Whether to apply each function to each input_. default is False.
144
+ **kwargs:
145
+ Keyword arguments to pass to the function.
146
146
 
147
147
  Returns:
148
- list[Any]: A list of results after applying the function(s).
148
+ list[Any]: A list of results after applying the function(s).
149
149
 
150
150
  examples:
151
- >>> async def add_one(x):
152
- >>> return x + 1
153
- >>> asyncio.run(mcall([1, 2, 3], add_one))
154
- [2, 3, 4]
151
+ >>> async def add_one(x):
152
+ >>> return x + 1
153
+ >>> asyncio.run(mcall([1, 2, 3], add_one))
154
+ [2, 3, 4]
155
155
 
156
156
  """
157
157
  inputs_ = to_list(input_, dropna=True)
@@ -159,17 +159,16 @@ async def mcall(
159
159
 
160
160
  if explode:
161
161
  tasks = [_alcall(inputs_, f, flatten=True, **kwargs) for f in funcs_]
162
- return await AsyncUtil.execute_tasks(*tasks)
163
- else:
164
- if len(inputs_) != len(funcs_):
165
- raise ValueError(
166
- "Inputs and functions must be the same length for map calling."
167
- )
162
+ elif len(inputs_) == len(funcs_):
168
163
  tasks = [
169
164
  AsyncUtil.handle_async_sync(func, inp, **kwargs)
170
165
  for inp, func in zip(inputs_, funcs_)
171
166
  ]
172
- return await AsyncUtil.execute_tasks(*tasks)
167
+ else:
168
+ raise ValueError(
169
+ "Inputs and functions must be the same length for map calling."
170
+ )
171
+ return await AsyncUtil.execute_tasks(*tasks)
173
172
 
174
173
 
175
174
  async def bcall(
@@ -179,18 +178,18 @@ async def bcall(
179
178
  asynchronously call a function on batches of inputs.
180
179
 
181
180
  Args:
182
- input_ (Any): The input_ to process.
183
- func (Callable): The function to apply.
184
- batch_size (int): The size of each batch.
185
- **kwargs: Keyword arguments to pass to the function.
181
+ input_ (Any): The input_ to process.
182
+ func (Callable): The function to apply.
183
+ batch_size (int): The size of each batch.
184
+ **kwargs: Keyword arguments to pass to the function.
186
185
 
187
186
  Returns:
188
- list[Any]: A list of results after applying the function in batches.
187
+ list[Any]: A list of results after applying the function in batches.
189
188
 
190
189
  examples:
191
- >>> async def sum_batch(batch_): return sum(batch_)
192
- >>> asyncio.run(bcall([1, 2, 3, 4], sum_batch, batch_size=2))
193
- [3, 7]
190
+ >>> async def sum_batch(batch_): return sum(batch_)
191
+ >>> asyncio.run(bcall([1, 2, 3, 4], sum_batch, batch_size=2))
192
+ [3, 7]
194
193
  """
195
194
  results = []
196
195
  input_ = to_list(input_)
@@ -222,36 +221,36 @@ async def tcall(
222
221
  frame, or when monitoring execution duration.
223
222
 
224
223
  Args:
225
- func (Callable):
226
- The asynchronous function to be called.
227
- *args:
228
- Positional arguments to pass to the function.
229
- delay (float, optional):
230
- Time in seconds to wait before executing the function. default to 0.
231
- err_msg (str | None, optional):
232
- Custom error message to display if an error occurs. defaults to None.
233
- ignore_err (bool, optional):
234
- If True, suppresses any errors that occur during function execution,
235
- optionally returning a default value. defaults to False.
236
- timing (bool, optional):
237
- If True, returns a tuple containing the result of the function and the
238
- execution duration in seconds. defaults to False.
239
- timeout (float | None, optional):
240
- Maximum time in seconds allowed for the function execution. if the execution
241
- exceeds this time, a timeout error is raised. defaults to None.
242
- **kwargs:
243
- Keyword arguments to pass to the function.
224
+ func (Callable):
225
+ The asynchronous function to be called.
226
+ *args:
227
+ Positional arguments to pass to the function.
228
+ delay (float, optional):
229
+ Time in seconds to wait before executing the function. default to 0.
230
+ err_msg (str | None, optional):
231
+ Custom error message to display if an error occurs. defaults to None.
232
+ ignore_err (bool, optional):
233
+ If True, suppresses any errors that occur during function execution,
234
+ optionally returning a default value. defaults to False.
235
+ timing (bool, optional):
236
+ If True, returns a tuple containing the result of the function and the
237
+ execution duration in seconds. defaults to False.
238
+ timeout (float | None, optional):
239
+ Maximum time in seconds allowed for the function execution. if the execution
240
+ exceeds this time, a timeout error is raised. defaults to None.
241
+ **kwargs:
242
+ Keyword arguments to pass to the function.
244
243
 
245
244
  Returns:
246
- Any:
247
- The result of the function call. if `timing` is True, returns a tuple of
248
- (result, execution duration).
245
+ Any:
246
+ The result of the function call. if `timing` is True, returns a tuple of
247
+ (result, execution duration).
249
248
 
250
249
  examples:
251
- >>> async def sample_function(x):
252
- ... return x * x
253
- >>> await tcall(sample_function, 3, delay=1, timing=True)
254
- (9, execution_duration)
250
+ >>> async def sample_function(x):
251
+ ... return x * x
252
+ >>> await tcall(sample_function, 3, delay=1, timing=True)
253
+ (9, execution_duration)
255
254
  """
256
255
 
257
256
  async def async_call() -> tuple[Any, float]:
@@ -284,10 +283,7 @@ async def tcall(
284
283
  if not ignore_err:
285
284
  raise
286
285
 
287
- if AsyncUtil.is_coroutine_func(func):
288
- return await async_call()
289
- else:
290
- return sync_call()
286
+ return await async_call() if AsyncUtil.is_coroutine_func(func) else sync_call()
291
287
 
292
288
 
293
289
  async def rcall(
@@ -310,44 +306,42 @@ async def rcall(
310
306
  persistent failures, and a backoff factor to control the delay increase.
311
307
 
312
308
  Args:
313
- func (Callable):
314
- The asynchronous function to retry.
315
- *args:
316
- Positional arguments for the function.
317
- retries (int, optional):
318
- The number of retry attempts before giving up. default to 0.
319
- delay (float, optional):
320
- Initial delay between retries in seconds. default to 1.0.
321
- backoff_factor (float, optional):
322
- Multiplier for the delay between retries, for exponential backoff.
323
- default to 2.0.
324
- default (Any, optional):
325
- A value to return if all retries fail. defaults to None.
326
- timeout (float | None, optional):
327
- Maximum duration in seconds for each attempt. defaults to None.
328
- **kwargs:
329
- Keyword arguments for the function.
309
+ func (Callable):
310
+ The asynchronous function to retry.
311
+ *args:
312
+ Positional arguments for the function.
313
+ retries (int, optional):
314
+ The number of retry attempts before giving up. default to 0.
315
+ delay (float, optional):
316
+ Initial delay between retries in seconds. default to 1.0.
317
+ backoff_factor (float, optional):
318
+ Multiplier for the delay between retries, for exponential backoff.
319
+ default to 2.0.
320
+ default (Any, optional):
321
+ A value to return if all retries fail. defaults to None.
322
+ timeout (float | None, optional):
323
+ Maximum duration in seconds for each attempt. defaults to None.
324
+ **kwargs:
325
+ Keyword arguments for the function.
330
326
 
331
327
  Returns:
332
- Any:
333
- The result of the function call if successful within the retry attempts,
334
- otherwise the `default` value if specified.
328
+ Any:
329
+ The result of the function call if successful within the retry attempts,
330
+ otherwise the `default` value if specified.
335
331
 
336
332
  examples:
337
- >>> async def fetch_data():
338
- ... # Simulate a fetch operation that might fail
339
- ... raise Exception("temporary error")
340
- >>> await rcall(fetch_data, retries=3, delay=2, default="default value")
341
- 'default value'
333
+ >>> async def fetch_data():
334
+ ... # Simulate a fetch operation that might fail
335
+ ... raise Exception("temporary error")
336
+ >>> await rcall(fetch_data, retries=3, delay=2, default="default value")
337
+ 'default value'
342
338
  """
343
339
  last_exception = None
344
340
  result = None
345
341
 
346
342
  for attempt in range(retries + 1) if retries == 0 else range(retries):
347
343
  try:
348
- # Using tcall for each retry attempt with timeout and delay
349
- result = await _tcall(func, *args, timeout=timeout, **kwargs)
350
- return result
344
+ return await _tcall(func, *args, timeout=timeout, **kwargs)
351
345
  except Exception as e:
352
346
  last_exception = e
353
347
  if attempt < retries:
@@ -368,14 +362,14 @@ def _dropna(lst_: list[Any]) -> list[Any]:
368
362
  Remove None values from a list.
369
363
 
370
364
  Args:
371
- lst_ (list[Any]): A list potentially containing None values.
365
+ lst_ (list[Any]): A list potentially containing None values.
372
366
 
373
367
  Returns:
374
- list[Any]: A list with None values removed.
368
+ list[Any]: A list with None values removed.
375
369
 
376
370
  Examples:
377
- >>> _dropna([1, None, 3, None])
378
- [1, 3]
371
+ >>> _dropna([1, None, 3, None])
372
+ [1, 3]
379
373
  """
380
374
  return [item for item in lst_ if item is not None]
381
375
 
@@ -387,18 +381,18 @@ async def _alcall(
387
381
  asynchronously apply a function to each element in the input_.
388
382
 
389
383
  Args:
390
- input (Any): The input_ to process.
391
- func (Callable): The function to apply.
392
- flatten (bool, optional): Whether to flatten the result. default is False.
393
- **kwargs: Keyword arguments to pass to the function.
384
+ input (Any): The input_ to process.
385
+ func (Callable): The function to apply.
386
+ flatten (bool, optional): Whether to flatten the result. default is False.
387
+ **kwargs: Keyword arguments to pass to the function.
394
388
 
395
389
  Returns:
396
- list[Any]: A list of results after asynchronously applying the function.
390
+ list[Any]: A list of results after asynchronously applying the function.
397
391
 
398
392
  examples:
399
- >>> async def square(x): return x * x
400
- >>> asyncio.run(alcall([1, 2, 3], square))
401
- [1, 4, 9]
393
+ >>> async def square(x): return x * x
394
+ >>> asyncio.run(alcall([1, 2, 3], square))
395
+ [1, 4, 9]
402
396
  """
403
397
  lst = to_list(input_)
404
398
  tasks = [AsyncUtil.handle_async_sync(func, i, **kwargs) for i in lst]
@@ -421,23 +415,23 @@ async def _tcall(
421
415
  asynchronously call a function with optional delay, timeout, and error handling.
422
416
 
423
417
  Args:
424
- func (Callable): The function to call.
425
- *args: Positional arguments to pass to the function.
426
- delay (float): Delay before calling the function, in seconds.
427
- err_msg (str | None): Custom error message.
428
- ignore_err (bool): If True, ignore errors and return default.
429
- timing (bool): If True, return a tuple (result, duration).
430
- default (Any): Default value to return on error.
431
- timeout (float | None): Timeout for the function call, in seconds.
432
- **kwargs: Keyword arguments to pass to the function.
418
+ func (Callable): The function to call.
419
+ *args: Positional arguments to pass to the function.
420
+ delay (float): Delay before calling the function, in seconds.
421
+ err_msg (str | None): Custom error message.
422
+ ignore_err (bool): If True, ignore errors and return default.
423
+ timing (bool): If True, return a tuple (result, duration).
424
+ default (Any): Default value to return on error.
425
+ timeout (float | None): Timeout for the function call, in seconds.
426
+ **kwargs: Keyword arguments to pass to the function.
433
427
 
434
428
  Returns:
435
- Any: The result of the function call, or (result, duration) if timing is True.
429
+ Any: The result of the function call, or (result, duration) if timing is True.
436
430
 
437
431
  examples:
438
- >>> async def example_func(x): return x
439
- >>> asyncio.run(tcall(example_func, 5, timing=True))
440
- (5, duration)
432
+ >>> async def example_func(x): return x
433
+ >>> asyncio.run(tcall(example_func, 5, timing=True))
434
+ (5, duration)
441
435
  """
442
436
  start_time = SysUtil.get_now(datetime_=False)
443
437
  try:
@@ -515,21 +509,21 @@ class CallDecorator:
515
509
  asyncio.TimeoutError if the execution time exceeds the specified timeout.
516
510
 
517
511
  Args:
518
- timeout (int):
519
- The maximum duration, in seconds, that the function is allowed to execute.
512
+ timeout (int):
513
+ The maximum duration, in seconds, that the function is allowed to execute.
520
514
 
521
515
  Returns:
522
- Callable:
523
- A decorated function that enforces the specified execution timeout.
516
+ Callable:
517
+ A decorated function that enforces the specified execution timeout.
524
518
 
525
519
  Examples:
526
- >>> @CallDecorator.timeout(5)
527
- ... async def long_running_task():
528
- ... # Implementation that may exceed the timeout duration
529
- ... await asyncio.sleep(10)
530
- ... return "Completed"
531
- ... # Executing `long_running_task` will raise an asyncio.TimeoutError after 5
532
- ... # seconds
520
+ >>> @CallDecorator.timeout(5)
521
+ ... async def long_running_task():
522
+ ... # Implementation that may exceed the timeout duration
523
+ ... await asyncio.sleep(10)
524
+ ... return "Completed"
525
+ ... # Executing `long_running_task` will raise an asyncio.TimeoutError after 5
526
+ ... # seconds
533
527
  """
534
528
 
535
529
  def decorator(func: Callable[..., Any]) -> Callable:
@@ -556,26 +550,26 @@ class CallDecorator:
556
550
  mechanisms.
557
551
 
558
552
  Args:
559
- retries (int, optional):
560
- The number of retry attempts before giving up. Defaults to 3.
561
- delay (float, optional):
562
- The initial delay between retries, in seconds. Defaults to 2.0.
563
- backoff_factor (float, optional):
564
- The multiplier applied to the delay for each subsequent retry, for
565
- exponential backoff. Default to 2.0.
553
+ retries (int, optional):
554
+ The number of retry attempts before giving up. Defaults to 3.
555
+ delay (float, optional):
556
+ The initial delay between retries, in seconds. Defaults to 2.0.
557
+ backoff_factor (float, optional):
558
+ The multiplier applied to the delay for each subsequent retry, for
559
+ exponential backoff. Default to 2.0.
566
560
 
567
561
  Returns:
568
- Callable:
569
- A decorated asynchronous function with retry logic based on the specified
570
- parameters.
562
+ Callable:
563
+ A decorated asynchronous function with retry logic based on the specified
564
+ parameters.
571
565
 
572
566
  Examples:
573
- >>> @CallDecorator.retry(retries=2, delay=1, backoff_factor=2)
574
- ... async def fetch_data():
575
- ... # Implementation that might fail transiently
576
- ... raise ConnectionError("Temporary failure")
577
- ... # `fetch_data` will automatically retry on ConnectionError, up to 2 times,
578
- ... # with delays of 1s and 2s.
567
+ >>> @CallDecorator.retry(retries=2, delay=1, backoff_factor=2)
568
+ ... async def fetch_data():
569
+ ... # Implementation that might fail transiently
570
+ ... raise ConnectionError("Temporary failure")
571
+ ... # `fetch_data` will automatically retry on ConnectionError, up to 2 times,
572
+ ... # with delays of 1s and 2s.
579
573
  """
580
574
 
581
575
  def decorator(func: Callable[..., Any]) -> Callable:
@@ -607,21 +601,21 @@ class CallDecorator:
607
601
  value can prevent the application from crashing or halting due to minor errors.
608
602
 
609
603
  Args:
610
- default_value (Any):
611
- The value to return if the decorated function raises an exception.
604
+ default_value (Any):
605
+ The value to return if the decorated function raises an exception.
612
606
 
613
607
  Returns:
614
- Callable:
615
- A decorated asynchronous function that returns `default_value` in case of
616
- error.
608
+ Callable:
609
+ A decorated asynchronous function that returns `default_value` in case of
610
+ error.
617
611
 
618
612
  Examples:
619
- >>> @CallDecorator.default(default_value="Fetch failed")
620
- ... async def get_resource():
621
- ... # Implementation that might raise an exception
622
- ... raise RuntimeError("Resource not available")
623
- ... # Executing `get_resource` will return "Fetch failed" instead of raising
624
- ... # an error
613
+ >>> @CallDecorator.default(default_value="Fetch failed")
614
+ ... async def get_resource():
615
+ ... # Implementation that might raise an exception
616
+ ... raise RuntimeError("Resource not available")
617
+ ... # Executing `get_resource` will return "Fetch failed" instead of raising
618
+ ... # an error
625
619
  """
626
620
 
627
621
  def decorator(func: Callable[..., Any]) -> Callable:
@@ -646,21 +640,21 @@ class CallDecorator:
646
640
  function does not execute more frequently than the allowed rate.
647
641
 
648
642
  Args:
649
- period (int):
650
- The minimum time interval, in seconds, between consecutive calls to the
651
- decorated function.
643
+ period (int):
644
+ The minimum time interval, in seconds, between consecutive calls to the
645
+ decorated function.
652
646
 
653
647
  Returns:
654
- Callable:
655
- A decorated asynchronous function that adheres to the specified call
656
- frequency limit.
648
+ Callable:
649
+ A decorated asynchronous function that adheres to the specified call
650
+ frequency limit.
657
651
 
658
652
  Examples:
659
- >>> @CallDecorator.throttle(2)
660
- ... async def fetch_data():
661
- ... # Implementation that fetches data from an external source
662
- ... pass
663
- ... # `fetch_data` will not be called more often than once every 2 seconds.
653
+ >>> @CallDecorator.throttle(2)
654
+ ... async def fetch_data():
655
+ ... # Implementation that fetches data from an external source
656
+ ... pass
657
+ ... # `fetch_data` will not be called more often than once every 2 seconds.
664
658
  """
665
659
  return Throttle(period)
666
660
 
@@ -678,21 +672,21 @@ class CallDecorator:
678
672
  succinctly applied to a collection of asynchronous results.
679
673
 
680
674
  Args:
681
- function (Callable[[Any], Any]):
682
- A mapping function to apply to each element of the list returned by the
683
- decorated function.
675
+ function (Callable[[Any], Any]):
676
+ A mapping function to apply to each element of the list returned by the
677
+ decorated function.
684
678
 
685
679
  Returns:
686
- Callable:
687
- A decorated asynchronous function whose results are transformed by the
688
- specified mapping function.
680
+ Callable:
681
+ A decorated asynchronous function whose results are transformed by the
682
+ specified mapping function.
689
683
 
690
684
  Examples:
691
- >>> @CallDecorator.map(lambda x: x.upper())
692
- ... async def get_names():
693
- ... # Asynchronously fetches a list of names
694
- ... return ["alice", "bob", "charlie"]
695
- ... # `get_names` now returns ["ALICE", "BOB", "CHARLIE"]
685
+ >>> @CallDecorator.map(lambda x: x.upper())
686
+ ... async def get_names():
687
+ ... # Asynchronously fetches a list of names
688
+ ... return ["alice", "bob", "charlie"]
689
+ ... # `get_names` now returns ["ALICE", "BOB", "CHARLIE"]
696
690
  """
697
691
 
698
692
  def decorator(func: Callable[..., list[Any]]) -> Callable:
@@ -729,28 +723,28 @@ class CallDecorator:
729
723
  all asynchronous).
730
724
 
731
725
  Args:
732
- *functions (Callable[[Any], Any]):
733
- A variable number of functions that are to be composed together. Each
734
- function must accept a single argument and return a value.
726
+ *functions (Callable[[Any], Any]):
727
+ A variable number of functions that are to be composed together. Each
728
+ function must accept a single argument and return a value.
735
729
 
736
730
  Returns:
737
- Callable:
738
- A decorator that, when applied to a function, composes it with the
739
- specified functions, creating a pipeline of function calls.
731
+ Callable:
732
+ A decorator that, when applied to a function, composes it with the
733
+ specified functions, creating a pipeline of function calls.
740
734
 
741
735
  Raises:
742
- ValueError:
743
- If the provided functions mix synchronous and asynchronous types, as they
744
- cannot be composed together.
736
+ ValueError:
737
+ If the provided functions mix synchronous and asynchronous types, as they
738
+ cannot be composed together.
745
739
 
746
740
  Examples:
747
- >>> def double(x): return x * 2
748
- >>> def increment(x): return x + 1
749
- >>> @CallDecorator.compose(increment, double)
750
- ... def start_value(x):
751
- ... return x
752
- >>> start_value(3)
753
- 7 # The value is doubled to 6, then incremented to 7
741
+ >>> def double(x): return x * 2
742
+ >>> def increment(x): return x + 1
743
+ >>> @CallDecorator.compose(increment, double)
744
+ ... def start_value(x):
745
+ ... return x
746
+ >>> start_value(3)
747
+ 7 # The value is doubled to 6, then incremented to 7
754
748
  """
755
749
 
756
750
  def decorator(func: Callable) -> Callable:
@@ -807,24 +801,24 @@ class CallDecorator:
807
801
  function's output, such as formatting results or applying additional computations.
808
802
 
809
803
  Args:
810
- preprocess (Callable[..., Any]):
811
- A function to preprocess the arguments passed to the decorated function.
812
- It must accept the same arguments as the decorated function.
813
- postprocess (Callable[..., Any]):
814
- A function to postprocess the result of the decorated function. It must
815
- accept a single argument, which is the output of the decorated function.
804
+ preprocess (Callable[..., Any]):
805
+ A function to preprocess the arguments passed to the decorated function.
806
+ It must accept the same arguments as the decorated function.
807
+ postprocess (Callable[..., Any]):
808
+ A function to postprocess the result of the decorated function. It must
809
+ accept a single argument, which is the output of the decorated function.
816
810
 
817
811
  Returns:
818
- Callable:
819
- A decorated function that applies the specified preprocessing and
820
- postprocessing steps to its execution.
812
+ Callable:
813
+ A decorated function that applies the specified preprocessing and
814
+ postprocessing steps to its execution.
821
815
 
822
816
  Examples:
823
- >>> @CallDecorator.pre_post_process(lambda x: x - 1, lambda x: x * 2)
824
- ... async def process_value(x):
825
- ... return x + 2
826
- >>> asyncio.run(process_value(5))
827
- 12 # Input 5 is preprocessed to 4, processed to 6, and postprocessed to 12
817
+ >>> @CallDecorator.pre_post_process(lambda x: x - 1, lambda x: x * 2)
818
+ ... async def process_value(x):
819
+ ... return x + 2
820
+ >>> asyncio.run(process_value(5))
821
+ 12 # Input 5 is preprocessed to 4, processed to 6, and postprocessed to 12
828
822
  """
829
823
 
830
824
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -866,16 +860,16 @@ class CallDecorator:
866
860
  synchronous functions.
867
861
 
868
862
  Returns:
869
- Callable: A decorated version of the function with caching applied. Subsequent
870
- calls with the same arguments within the TTL will return the cached result.
863
+ Callable: A decorated version of the function with caching applied. Subsequent
864
+ calls with the same arguments within the TTL will return the cached result.
871
865
 
872
866
  Examples:
873
- >>> @CallDecorator.cache(ttl=10)
874
- ... async def fetch_data(key):
875
- ... # Simulate a database fetch
876
- ... return "data for " + key
877
- ... # Subsequent calls to `fetch_data` with the same `key` within 10 seconds
878
- ... # will return the cached result without re-executing the function body.
867
+ >>> @CallDecorator.cache(ttl=10)
868
+ ... async def fetch_data(key):
869
+ ... # Simulate a database fetch
870
+ ... return "data for " + key
871
+ ... # Subsequent calls to `fetch_data` with the same `key` within 10 seconds
872
+ ... # will return the cached result without re-executing the function body.
879
873
  """
880
874
 
881
875
  if AsyncUtil.is_coroutine_func(func):
@@ -911,21 +905,21 @@ class CallDecorator:
911
905
  functions returning lists.
912
906
 
913
907
  Args:
914
- predicate (Callable[[Any], bool]):
915
- A function that evaluates each input_ in the list. Items for which the
916
- predicate returns True are included in the final result.
908
+ predicate (Callable[[Any], bool]):
909
+ A function that evaluates each input_ in the list. Items for which the
910
+ predicate returns True are included in the final result.
917
911
 
918
912
  Returns:
919
- Callable:
920
- A decorated function that filters its list result according to the predicate.
913
+ Callable:
914
+ A decorated function that filters its list result according to the predicate.
921
915
 
922
916
  Examples:
923
- >>> @CallDecorator.filter(lambda x: x % 2 == 0)
924
- ... async def get_even_numbers():
925
- ... return [1, 2, 3, 4, 5]
926
- >>> asyncio.run(get_even_numbers())
927
- [2, 4]
928
- ... # The result list is filtered to include only even numbers.
917
+ >>> @CallDecorator.filter(lambda x: x % 2 == 0)
918
+ ... async def get_even_numbers():
919
+ ... return [1, 2, 3, 4, 5]
920
+ >>> asyncio.run(get_even_numbers())
921
+ [2, 4]
922
+ ... # The result list is filtered to include only even numbers.
929
923
  """
930
924
 
931
925
  def decorator(func: Callable[..., list[Any]]) -> Callable:
@@ -958,27 +952,27 @@ class CallDecorator:
958
952
  synchronous and asynchronous functions.
959
953
 
960
954
  Args:
961
- function (Callable[[Any, Any], Any]):
962
- The reduction function to apply to the list. It should take two arguments
963
- and return a single value that is the result of combining them.
964
- initial (Any):
965
- The initial value for the reduction process. This value is used as the
966
- starting point for the reduction and should be an identity value for the
967
- reduction operation.
955
+ function (Callable[[Any, Any], Any]):
956
+ The reduction function to apply to the list. It should take two arguments
957
+ and return a single value that is the result of combining them.
958
+ initial (Any):
959
+ The initial value for the reduction process. This value is used as the
960
+ starting point for the reduction and should be an identity value for the
961
+ reduction operation.
968
962
 
969
963
  Returns:
970
- Callable:
971
- A decorated function that applies the specified reduction to its list
972
- result,
973
- producing a single aggregated value.
964
+ Callable:
965
+ A decorated function that applies the specified reduction to its list
966
+ result,
967
+ producing a single aggregated value.
974
968
 
975
969
  Examples:
976
- >>> @CallDecorator.reduce(lambda x, y: x + y, 0)
977
- ... async def sum_numbers():
978
- ... return [1, 2, 3, 4]
979
- >>> asyncio.run(sum_numbers())
980
- 10
981
- ... # The numbers in the list are summed, resulting in a single value.
970
+ >>> @CallDecorator.reduce(lambda x, y: x + y, 0)
971
+ ... async def sum_numbers():
972
+ ... return [1, 2, 3, 4]
973
+ >>> asyncio.run(sum_numbers())
974
+ 10
975
+ ... # The numbers in the list are summed, resulting in a single value.
982
976
  """
983
977
 
984
978
  def decorator(func: Callable[..., list[Any]]) -> Callable:
@@ -1011,20 +1005,20 @@ class CallDecorator:
1011
1005
  that can only handle a limited amount of concurrent requests.
1012
1006
 
1013
1007
  Args:
1014
- limit (int):
1015
- The maximum number of concurrent executions allowed for the decorated
1016
- function.
1008
+ limit (int):
1009
+ The maximum number of concurrent executions allowed for the decorated
1010
+ function.
1017
1011
 
1018
1012
  Returns:
1019
- Callable:
1020
- An asynchronous function wrapper that enforces the concurrency limit.
1013
+ Callable:
1014
+ An asynchronous function wrapper that enforces the concurrency limit.
1021
1015
 
1022
1016
  Examples:
1023
- >>> @CallDecorator.max_concurrency(3)
1024
- ... async def process_data(input_):
1025
- ... # Asynchronous processing logic here
1026
- ... pass
1027
- ... # No more than 3 instances of `process_data` will run concurrently.
1017
+ >>> @CallDecorator.max_concurrency(3)
1018
+ ... async def process_data(input_):
1019
+ ... # Asynchronous processing logic here
1020
+ ... pass
1021
+ ... # No more than 3 instances of `process_data` will run concurrently.
1028
1022
  """
1029
1023
 
1030
1024
  def decorator(func: Callable) -> Callable:
@@ -1092,11 +1086,11 @@ class Throttle:
1092
1086
  this constraint.
1093
1087
 
1094
1088
  Attributes:
1095
- period (int): The minimum time period (in seconds) between successive calls.
1089
+ period (int): The minimum time period (in seconds) between successive calls.
1096
1090
 
1097
1091
  Methods:
1098
- __call__: Decorates a synchronous function with throttling.
1099
- __call_async__: Decorates an asynchronous function with throttling.
1092
+ __call__: Decorates a synchronous function with throttling.
1093
+ __call_async__: Decorates an asynchronous function with throttling.
1100
1094
  """
1101
1095
 
1102
1096
  def __init__(self, period: int) -> None:
@@ -1104,7 +1098,7 @@ class Throttle:
1104
1098
  Initializes a new instance of _Throttle.
1105
1099
 
1106
1100
  Args:
1107
- period (int): The minimum time period (in seconds) between successive calls.
1101
+ period (int): The minimum time period (in seconds) between successive calls.
1108
1102
  """
1109
1103
  self.period = period
1110
1104
  self.last_called = 0
@@ -1114,10 +1108,10 @@ class Throttle:
1114
1108
  Decorates a synchronous function with the throttling mechanism.
1115
1109
 
1116
1110
  Args:
1117
- func (Callable[..., Any]): The synchronous function to be throttled.
1111
+ func (Callable[..., Any]): The synchronous function to be throttled.
1118
1112
 
1119
1113
  Returns:
1120
- Callable[..., Any]: The throttled synchronous function.
1114
+ Callable[..., Any]: The throttled synchronous function.
1121
1115
  """
1122
1116
 
1123
1117
  @functools.wraps(func)
@@ -1135,10 +1129,10 @@ class Throttle:
1135
1129
  Decorates an asynchronous function with the throttling mechanism.
1136
1130
 
1137
1131
  Args:
1138
- func (Callable[..., Any]): The asynchronous function to be throttled.
1132
+ func (Callable[..., Any]): The asynchronous function to be throttled.
1139
1133
 
1140
1134
  Returns:
1141
- Callable[..., Any]: The throttled asynchronous function.
1135
+ Callable[..., Any]: The throttled asynchronous function.
1142
1136
  """
1143
1137
 
1144
1138
  @functools.wraps(func)
@@ -1158,18 +1152,17 @@ def _custom_error_handler(error: Exception, error_map: dict[type, Callable]) ->
1158
1152
  handle errors based on a given error mapping.
1159
1153
 
1160
1154
  Args:
1161
- error (Exception):
1162
- The error to handle.
1163
- error_map (Dict[type, Callable]):
1164
- A dictionary mapping error types to handler functions.
1155
+ error (Exception):
1156
+ The error to handle.
1157
+ error_map (Dict[type, Callable]):
1158
+ A dictionary mapping error types to handler functions.
1165
1159
 
1166
1160
  examples:
1167
- >>> def handle_value_error(e): print("ValueError occurred")
1168
- >>> custom_error_handler(ValueError(), {ValueError: handle_value_error})
1169
- ValueError occurred
1161
+ >>> def handle_value_error(e): print("ValueError occurred")
1162
+ >>> custom_error_handler(ValueError(), {ValueError: handle_value_error})
1163
+ ValueError occurred
1170
1164
  """
1171
- handler = error_map.get(type(error))
1172
- if handler:
1165
+ if handler := error_map.get(type(error)):
1173
1166
  handler(error)
1174
1167
  else:
1175
1168
  logging.error(f"Unhandled error: {error}")
@@ -1183,44 +1176,43 @@ async def call_handler(
1183
1176
  functions.
1184
1177
 
1185
1178
  Args:
1186
- func (Callable):
1187
- The function to call.
1188
- *args:
1189
- Positional arguments to pass to the function.
1190
- error_map (Dict[type, Callable], optional):
1191
- A dictionary mapping error types to handler functions.
1192
- **kwargs:
1193
- Keyword arguments to pass to the function.
1179
+ func (Callable):
1180
+ The function to call.
1181
+ *args:
1182
+ Positional arguments to pass to the function.
1183
+ error_map (Dict[type, Callable], optional):
1184
+ A dictionary mapping error types to handler functions.
1185
+ **kwargs:
1186
+ Keyword arguments to pass to the function.
1194
1187
 
1195
1188
  Returns:
1196
- Any: The result of the function call.
1189
+ Any: The result of the function call.
1197
1190
 
1198
1191
  Raises:
1199
- Exception: Propagates any exceptions not handled by the error_map.
1192
+ Exception: Propagates any exceptions not handled by the error_map.
1200
1193
 
1201
1194
  examples:
1202
- >>> async def async_add(x, y): return x + y
1203
- >>> asyncio.run(_call_handler(async_add, 1, 2))
1204
- 3
1195
+ >>> async def async_add(x, y): return x + y
1196
+ >>> asyncio.run(_call_handler(async_add, 1, 2))
1197
+ 3
1205
1198
  """
1206
1199
  try:
1207
- if is_coroutine_func(func):
1208
- # Checking for a running event loop
1209
- try:
1210
- loop = asyncio.get_running_loop()
1211
- except RuntimeError: # No running event loop
1212
- loop = asyncio.new_event_loop()
1213
- result = loop.run_until_complete(func(*args, **kwargs))
1214
-
1215
- loop.close()
1216
- return result
1217
-
1218
- if loop.is_running():
1219
- return await func(*args, **kwargs)
1220
-
1221
- else:
1200
+ if not is_coroutine_func(func):
1222
1201
  return func(*args, **kwargs)
1223
1202
 
1203
+ # Checking for a running event loop
1204
+ try:
1205
+ loop = asyncio.get_running_loop()
1206
+ except RuntimeError: # No running event loop
1207
+ loop = asyncio.new_event_loop()
1208
+ result = loop.run_until_complete(func(*args, **kwargs))
1209
+
1210
+ loop.close()
1211
+ return result
1212
+
1213
+ if loop.is_running():
1214
+ return await func(*args, **kwargs)
1215
+
1224
1216
  except Exception as e:
1225
1217
  if error_map:
1226
1218
  _custom_error_handler(e, error_map)
@@ -1241,20 +1233,20 @@ def is_coroutine_func(func: Callable) -> bool:
1241
1233
  asynchronous codebases correctly.
1242
1234
 
1243
1235
  Args:
1244
- func (Callable):
1245
- The function to check for coroutine compatibility.
1236
+ func (Callable):
1237
+ The function to check for coroutine compatibility.
1246
1238
 
1247
1239
  Returns:
1248
- bool:
1249
- True if `func` is an asyncio coroutine function, False otherwise. this
1250
- determination is based on whether the function is defined with `async def`.
1240
+ bool:
1241
+ True if `func` is an asyncio coroutine function, False otherwise. this
1242
+ determination is based on whether the function is defined with `async def`.
1251
1243
 
1252
1244
  examples:
1253
- >>> async def async_func(): pass
1254
- >>> def sync_func(): pass
1255
- >>> is_coroutine_func(async_func)
1256
- True
1257
- >>> is_coroutine_func(sync_func)
1258
- False
1245
+ >>> async def async_func(): pass
1246
+ >>> def sync_func(): pass
1247
+ >>> is_coroutine_func(async_func)
1248
+ True
1249
+ >>> is_coroutine_func(sync_func)
1250
+ False
1259
1251
  """
1260
1252
  return asyncio.iscoroutinefunction(func)