gllm-core-binary 0.4.4__py3-none-manylinux_2_31_x86_64.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. gllm_core/__init__.py +1 -0
  2. gllm_core/__init__.pyi +0 -0
  3. gllm_core/adapters/__init__.py +5 -0
  4. gllm_core/adapters/__init__.pyi +3 -0
  5. gllm_core/adapters/tool/__init__.py +6 -0
  6. gllm_core/adapters/tool/__init__.pyi +4 -0
  7. gllm_core/adapters/tool/google_adk.py +91 -0
  8. gllm_core/adapters/tool/google_adk.pyi +23 -0
  9. gllm_core/adapters/tool/langchain.py +130 -0
  10. gllm_core/adapters/tool/langchain.pyi +31 -0
  11. gllm_core/constants.py +55 -0
  12. gllm_core/constants.pyi +36 -0
  13. gllm_core/event/__init__.py +6 -0
  14. gllm_core/event/__init__.pyi +4 -0
  15. gllm_core/event/event_emitter.py +211 -0
  16. gllm_core/event/event_emitter.pyi +155 -0
  17. gllm_core/event/handler/__init__.py +7 -0
  18. gllm_core/event/handler/__init__.pyi +5 -0
  19. gllm_core/event/handler/console_event_handler.py +48 -0
  20. gllm_core/event/handler/console_event_handler.pyi +32 -0
  21. gllm_core/event/handler/event_handler.py +89 -0
  22. gllm_core/event/handler/event_handler.pyi +51 -0
  23. gllm_core/event/handler/print_event_handler.py +130 -0
  24. gllm_core/event/handler/print_event_handler.pyi +33 -0
  25. gllm_core/event/handler/stream_event_handler.py +85 -0
  26. gllm_core/event/handler/stream_event_handler.pyi +62 -0
  27. gllm_core/event/hook/__init__.py +5 -0
  28. gllm_core/event/hook/__init__.pyi +3 -0
  29. gllm_core/event/hook/event_hook.py +30 -0
  30. gllm_core/event/hook/event_hook.pyi +18 -0
  31. gllm_core/event/hook/json_stringify_event_hook.py +32 -0
  32. gllm_core/event/hook/json_stringify_event_hook.pyi +16 -0
  33. gllm_core/event/messenger.py +133 -0
  34. gllm_core/event/messenger.pyi +66 -0
  35. gllm_core/schema/__init__.py +8 -0
  36. gllm_core/schema/__init__.pyi +6 -0
  37. gllm_core/schema/chunk.py +148 -0
  38. gllm_core/schema/chunk.pyi +66 -0
  39. gllm_core/schema/component.py +546 -0
  40. gllm_core/schema/component.pyi +205 -0
  41. gllm_core/schema/event.py +50 -0
  42. gllm_core/schema/event.pyi +33 -0
  43. gllm_core/schema/schema_generator.py +150 -0
  44. gllm_core/schema/schema_generator.pyi +35 -0
  45. gllm_core/schema/tool.py +418 -0
  46. gllm_core/schema/tool.pyi +198 -0
  47. gllm_core/utils/__init__.py +32 -0
  48. gllm_core/utils/__init__.pyi +13 -0
  49. gllm_core/utils/analyzer.py +256 -0
  50. gllm_core/utils/analyzer.pyi +123 -0
  51. gllm_core/utils/binary_handler_factory.py +99 -0
  52. gllm_core/utils/binary_handler_factory.pyi +62 -0
  53. gllm_core/utils/chunk_metadata_merger.py +102 -0
  54. gllm_core/utils/chunk_metadata_merger.pyi +41 -0
  55. gllm_core/utils/concurrency.py +184 -0
  56. gllm_core/utils/concurrency.pyi +94 -0
  57. gllm_core/utils/event_formatter.py +69 -0
  58. gllm_core/utils/event_formatter.pyi +30 -0
  59. gllm_core/utils/google_sheets.py +115 -0
  60. gllm_core/utils/google_sheets.pyi +18 -0
  61. gllm_core/utils/imports.py +91 -0
  62. gllm_core/utils/imports.pyi +42 -0
  63. gllm_core/utils/logger_manager.py +339 -0
  64. gllm_core/utils/logger_manager.pyi +176 -0
  65. gllm_core/utils/main_method_resolver.py +185 -0
  66. gllm_core/utils/main_method_resolver.pyi +54 -0
  67. gllm_core/utils/merger_method.py +130 -0
  68. gllm_core/utils/merger_method.pyi +49 -0
  69. gllm_core/utils/retry.py +258 -0
  70. gllm_core/utils/retry.pyi +41 -0
  71. gllm_core/utils/similarity.py +29 -0
  72. gllm_core/utils/similarity.pyi +10 -0
  73. gllm_core/utils/validation.py +26 -0
  74. gllm_core/utils/validation.pyi +12 -0
  75. gllm_core_binary-0.4.4.dist-info/METADATA +177 -0
  76. gllm_core_binary-0.4.4.dist-info/RECORD +78 -0
  77. gllm_core_binary-0.4.4.dist-info/WHEEL +5 -0
  78. gllm_core_binary-0.4.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,185 @@
1
+ """Main method resolver for Component classes.
2
+
3
+ This module provides the MainMethodResolver class which encapsulates the logic
4
+ for resolving the main entrypoint method for Component subclasses.
5
+
6
+ Authors:
7
+ Dimitrij Ray (dimitrij.ray@gdplabs.id)
8
+
9
+ References:
10
+ NONE
11
+ """
12
+
13
+ from abc import ABC
14
+ from typing import Callable
15
+
16
+ from gllm_core.utils.logger_manager import LoggerManager
17
+
18
+
19
+ class MainMethodResolver:
20
+ """Resolves the main entrypoint method for Component classes.
21
+
22
+ This resolver implements the precedence rules for determining which method
23
+ should be used as the main entrypoint:
24
+ 1. Method decorated with @main in the most derived class.
25
+ 2. Method named by __main_method__ property.
26
+ 3. _run method (with deprecation warning).
27
+
28
+ Attributes:
29
+ cls (type): The Component class to resolve the main method for.
30
+ """
31
+
32
+ def __init__(self, component_class: type):
33
+ """Initialize the resolver with a Component class.
34
+
35
+ Args:
36
+ component_class (type): The Component class to resolve the main method for.
37
+ """
38
+ self.cls = component_class
39
+ self._logger = LoggerManager().get_logger(f"MainMethodResolver.{component_class.__name__}")
40
+
41
+ @staticmethod
42
+ def validate_class(component_class: type) -> None:
43
+ """Validate main method configuration at class definition time.
44
+
45
+ This performs early validation that can be done when a Component subclass
46
+ is defined, before any instances are created or methods are called.
47
+
48
+ Validations performed:
49
+ 1. Check that __main_method__ property points to an existing method
50
+ 2. Check that only one @main decorator is used within the same class
51
+
52
+ Note: Multiple inheritance conflicts are intentionally NOT checked here,
53
+ as they are deferred to runtime (get_main()) to allow class definition
54
+ to succeed.
55
+
56
+ Args:
57
+ component_class (type): The Component class to validate.
58
+
59
+ Raises:
60
+ AttributeError: If __main_method__ refers to a non-existent method.
61
+ TypeError: If multiple methods are decorated with @main in the same class.
62
+ """
63
+ if (method_name := getattr(component_class, "__main_method__", None)) is not None:
64
+ if not hasattr(component_class, method_name):
65
+ raise AttributeError(
66
+ f"Method {method_name!r} specified in __main_method__ does not exist in {component_class.__name__}"
67
+ )
68
+
69
+ main_methods = []
70
+ for name, method in component_class.__dict__.items():
71
+ if callable(method) and hasattr(method, "__is_main__"):
72
+ main_methods.append(name)
73
+
74
+ if len(main_methods) > 1:
75
+ raise TypeError(f"Multiple main methods defined in {component_class.__name__}: {', '.join(main_methods)}")
76
+
77
+ def resolve(self) -> Callable | None:
78
+ """Resolve the main method following precedence rules.
79
+
80
+ Returns:
81
+ Callable | None: The resolved main method, or None if not found.
82
+
83
+ Raises:
84
+ TypeError: If conflicting main methods are inherited from multiple ancestors.
85
+ """
86
+ if decorated := self._resolve_decorated():
87
+ self._warn_if_redundant_property()
88
+ return decorated
89
+
90
+ if property_method := self._resolve_property():
91
+ return property_method
92
+
93
+ return self._resolve_legacy()
94
+
95
+ def _resolve_decorated(self) -> Callable | None:
96
+ """Find the most-derived @main decorated method in the MRO (method resolution order).
97
+
98
+ Returns:
99
+ Callable | None: The decorated main method, or None if not found.
100
+
101
+ Raises:
102
+ TypeError: If conflicting main methods are inherited from multiple ancestors.
103
+ """
104
+ classes_with_main = []
105
+ for base in self.cls.__mro__:
106
+ if base.__name__ == "Component" or base is ABC:
107
+ continue
108
+
109
+ main_method_name = next(
110
+ (name for name, method in base.__dict__.items() if callable(method) and hasattr(method, "__is_main__")),
111
+ None,
112
+ )
113
+
114
+ if main_method_name:
115
+ classes_with_main.append((base, main_method_name))
116
+
117
+ if not classes_with_main:
118
+ return None
119
+
120
+ most_derived_class, method_name = classes_with_main[0]
121
+ actual_method = getattr(self.cls, method_name)
122
+
123
+ self._validate_no_decorator_conflicts(classes_with_main, most_derived_class, actual_method)
124
+
125
+ return actual_method
126
+
127
+ def _validate_no_decorator_conflicts(
128
+ self, classes_with_main: list[tuple[type, str]], most_derived_class: type, actual_method: Callable
129
+ ) -> None:
130
+ """Validate that there are no conflicting @main decorators in multiple inheritance.
131
+
132
+ This method checks for conflicts only when multiple classes in the inheritance hierarchy have @main decorated
133
+ methods. It allows intentional overrides (when the most derived class defines its own @main method) but prevents
134
+ conflicts from multiple inheritance where different ancestors define different @main methods.
135
+
136
+ Args:
137
+ classes_with_main (list[tuple[type, str]]): List of (class, method_name) tuples for classes with @main.
138
+ most_derived_class (type): The most derived class in the hierarchy with @main.
139
+ actual_method (Callable): The resolved method from the most derived class.
140
+
141
+ Raises:
142
+ TypeError: If conflicting main methods are inherited from multiple ancestors and the most derived class
143
+ is not the current class (indicating a true conflict rather than an intentional override).
144
+ """
145
+ if len(classes_with_main) > 1 and most_derived_class is not self.cls:
146
+ for _, name in classes_with_main[1:]:
147
+ other_method = getattr(self.cls, name)
148
+ if other_method is not actual_method:
149
+ raise TypeError(
150
+ f"Conflicting main methods inherited from multiple ancestors in {self.cls.__name__}. "
151
+ "Please explicitly override with @main decorator."
152
+ )
153
+
154
+ def _resolve_property(self) -> Callable | None:
155
+ """Find method via __main_method__ property.
156
+
157
+ Returns:
158
+ Callable | None: The method named by __main_method__, or None if not found.
159
+ """
160
+ if hasattr(self.cls, "__main_method__") and self.cls.__main_method__ is not None:
161
+ method_name = self.cls.__main_method__
162
+ return getattr(self.cls, method_name, None)
163
+
164
+ def _resolve_legacy(self) -> Callable | None:
165
+ """Fall back to _run method with deprecation warning.
166
+
167
+ Returns:
168
+ Callable | None: The _run method, or None if not found.
169
+ """
170
+ if hasattr(self.cls, "_run"):
171
+ self._logger.warning(
172
+ f"Using legacy _run method for {self.cls.__name__}. "
173
+ f"Consider using @main decorator to explicitly declare the main entrypoint.",
174
+ stacklevel=4,
175
+ )
176
+ return self.cls._run
177
+
178
+ def _warn_if_redundant_property(self) -> None:
179
+ """Emit warning if both @main decorator and __main_method__ are defined."""
180
+ if self.cls.__dict__.get("__main_method__") is not None:
181
+ self._logger.warning(
182
+ f"Both @main decorator and __main_method__ property are defined in {self.cls.__name__}. "
183
+ "The @main decorator takes precedence. This redundant configuration should be resolved.",
184
+ stacklevel=4,
185
+ )
@@ -0,0 +1,54 @@
1
+ from _typeshed import Incomplete
2
+ from gllm_core.utils.logger_manager import LoggerManager as LoggerManager
3
+ from typing import Callable
4
+
5
+ class MainMethodResolver:
6
+ """Resolves the main entrypoint method for Component classes.
7
+
8
+ This resolver implements the precedence rules for determining which method
9
+ should be used as the main entrypoint:
10
+ 1. Method decorated with @main in the most derived class.
11
+ 2. Method named by __main_method__ property.
12
+ 3. _run method (with deprecation warning).
13
+
14
+ Attributes:
15
+ cls (type): The Component class to resolve the main method for.
16
+ """
17
+ cls: Incomplete
18
+ def __init__(self, component_class: type) -> None:
19
+ """Initialize the resolver with a Component class.
20
+
21
+ Args:
22
+ component_class (type): The Component class to resolve the main method for.
23
+ """
24
+ @staticmethod
25
+ def validate_class(component_class: type) -> None:
26
+ """Validate main method configuration at class definition time.
27
+
28
+ This performs early validation that can be done when a Component subclass
29
+ is defined, before any instances are created or methods are called.
30
+
31
+ Validations performed:
32
+ 1. Check that __main_method__ property points to an existing method
33
+ 2. Check that only one @main decorator is used within the same class
34
+
35
+ Note: Multiple inheritance conflicts are intentionally NOT checked here,
36
+ as they are deferred to runtime (get_main()) to allow class definition
37
+ to succeed.
38
+
39
+ Args:
40
+ component_class (type): The Component class to validate.
41
+
42
+ Raises:
43
+ AttributeError: If __main_method__ refers to a non-existent method.
44
+ TypeError: If multiple methods are decorated with @main in the same class.
45
+ """
46
+ def resolve(self) -> Callable | None:
47
+ """Resolve the main method following precedence rules.
48
+
49
+ Returns:
50
+ Callable | None: The resolved main method, or None if not found.
51
+
52
+ Raises:
53
+ TypeError: If conflicting main methods are inherited from multiple ancestors.
54
+ """
@@ -0,0 +1,130 @@
1
+ """Defines a collection of merger methods.
2
+
3
+ Authors:
4
+ Henry Wicaksono (henry.wicaksono@gdplabs.id)
5
+
6
+ References:
7
+ NONE
8
+ """
9
+
10
+ import os
11
+ from typing import Any, Callable
12
+
13
+
14
+ class MergerMethod:
15
+ """A collection of merger methods."""
16
+
17
+ @staticmethod
18
+ def concatenate(delimiter: str = "-") -> Callable[[list[Any]], str]:
19
+ """Creates a function that concatenates a list of values with a delimiter.
20
+
21
+ Args:
22
+ delimiter (str, optional): The delimiter to use when concatenating the values. Defaults to "-".
23
+
24
+ Returns:
25
+ Callable[[list[Any]], str]: A function that concatenates a list of values with the delimiter.
26
+ """
27
+
28
+ def _concat_with_delimiter(values: list[Any]) -> str:
29
+ return delimiter.join([str(value) for value in values])
30
+
31
+ return _concat_with_delimiter
32
+
33
+ @staticmethod
34
+ def pick_first(values: list[Any]) -> Any:
35
+ """Picks the first value from a list of values.
36
+
37
+ Args:
38
+ values (list[Any]): The values to pick from.
39
+
40
+ Returns:
41
+ Any: The first value from the list.
42
+ """
43
+ return values[0] if values else None
44
+
45
+ @staticmethod
46
+ def pick_last(values: list[Any]) -> Any:
47
+ """Picks the last value from a list of values.
48
+
49
+ Args:
50
+ values (list[Any]): The values to pick from.
51
+
52
+ Returns:
53
+ Any: The last value from the list.
54
+ """
55
+ return values[-1] if values else None
56
+
57
+ @staticmethod
58
+ def merge_overlapping_strings(delimiter: str = "\n") -> Callable[[list[str]], str]:
59
+ r"""Creates a function that merges a list of strings, handling common prefixes and overlaps.
60
+
61
+ The created function will:
62
+ - Identify and remove any common prefix shared by the strings.
63
+ - Process each pair of adjacent strings to remove overlapping strings.
64
+ - Join the cleaned strings together, including the common prefix at the beginning.
65
+
66
+ Args:
67
+ delimiter (str, optional): The delimiter to use when merging the values. Defaults to "\n".
68
+
69
+ Returns:
70
+ Callable[[list[str]], str]: A function that merges a list of strings, handling common prefixes and overlaps.
71
+ """
72
+
73
+ def _merge_overlapping_strings(values: list[str]) -> str:
74
+ values = [str(value) for value in values]
75
+ common_prefix = MergerMethod._get_common_prefix(values)
76
+ values = [string[len(common_prefix) :] for string in values]
77
+
78
+ for idx in range(len(values) - 1):
79
+ overlap = MergerMethod._find_overlap(values[idx], values[idx + 1])
80
+ values[idx] = values[idx].replace(overlap, "")
81
+
82
+ values.insert(0, common_prefix)
83
+
84
+ return delimiter.join([string.strip() for string in values if string.strip()])
85
+
86
+ return _merge_overlapping_strings
87
+
88
+ @staticmethod
89
+ def _get_common_prefix(strings: list[str]) -> str:
90
+ """Identifies the common prefix shared by a list of strings.
91
+
92
+ This method uses `os.path.commonprefix` to find the common prefix shared by all strings in the provided
93
+ `strings`. If the common prefix does not end with a newline, the method removes the last partial line to
94
+ ensure the prefix ends at a full line break.
95
+
96
+ Args:
97
+ strings (list[str]): A list of strings to find the common prefix.
98
+
99
+ Returns:
100
+ str: The common prefix shared by all strings, ending at the last full line.
101
+ """
102
+ common_prefix = os.path.commonprefix(strings)
103
+ last_newline_index = common_prefix.rfind("\n")
104
+
105
+ if last_newline_index != -1:
106
+ return common_prefix[:last_newline_index]
107
+
108
+ return ""
109
+
110
+ @staticmethod
111
+ def _find_overlap(first_string: str, second_string: str) -> str:
112
+ """Finds the overlapping portion between two strings.
113
+
114
+ This method compares the end of the `first_string` with the beginning of the `second_string` to find the
115
+ longest overlapping substring. It returns the overlap if found, otherwise returns an empty string.
116
+
117
+ Args:
118
+ first_string (str): The first string to compare.
119
+ second_string (str): The second string to compare.
120
+
121
+ Returns:
122
+ str: The overlapping substring between the two strings, or an empty string if no overlap is found.
123
+ """
124
+ max_overlap_len = min(len(first_string), len(second_string))
125
+
126
+ for i in range(max_overlap_len, 0, -1):
127
+ if first_string[-i:] == second_string[:i]:
128
+ return first_string[-i:]
129
+
130
+ return ""
@@ -0,0 +1,49 @@
1
+ from typing import Any, Callable
2
+
3
+ class MergerMethod:
4
+ """A collection of merger methods."""
5
+ @staticmethod
6
+ def concatenate(delimiter: str = '-') -> Callable[[list[Any]], str]:
7
+ '''Creates a function that concatenates a list of values with a delimiter.
8
+
9
+ Args:
10
+ delimiter (str, optional): The delimiter to use when concatenating the values. Defaults to "-".
11
+
12
+ Returns:
13
+ Callable[[list[Any]], str]: A function that concatenates a list of values with the delimiter.
14
+ '''
15
+ @staticmethod
16
+ def pick_first(values: list[Any]) -> Any:
17
+ """Picks the first value from a list of values.
18
+
19
+ Args:
20
+ values (list[Any]): The values to pick from.
21
+
22
+ Returns:
23
+ Any: The first value from the list.
24
+ """
25
+ @staticmethod
26
+ def pick_last(values: list[Any]) -> Any:
27
+ """Picks the last value from a list of values.
28
+
29
+ Args:
30
+ values (list[Any]): The values to pick from.
31
+
32
+ Returns:
33
+ Any: The last value from the list.
34
+ """
35
+ @staticmethod
36
+ def merge_overlapping_strings(delimiter: str = '\n') -> Callable[[list[str]], str]:
37
+ '''Creates a function that merges a list of strings, handling common prefixes and overlaps.
38
+
39
+ The created function will:
40
+ - Identify and remove any common prefix shared by the strings.
41
+ - Process each pair of adjacent strings to remove overlapping strings.
42
+ - Join the cleaned strings together, including the common prefix at the beginning.
43
+
44
+ Args:
45
+ delimiter (str, optional): The delimiter to use when merging the values. Defaults to "\\n".
46
+
47
+ Returns:
48
+ Callable[[list[str]], str]: A function that merges a list of strings, handling common prefixes and overlaps.
49
+ '''
@@ -0,0 +1,258 @@
1
+ """Defines retry and timeout utilities.
2
+
3
+ Authors:
4
+ Henry Wicaksono (henry.wicaksono@gdplabs.id)
5
+
6
+ References:
7
+ NONE
8
+ """
9
+
10
+ import asyncio
11
+ import functools
12
+ import inspect
13
+ import random
14
+ from typing import Any, Callable, TypeVar, overload
15
+
16
+ from pydantic import BaseModel, Field, model_validator
17
+
18
+ from gllm_core.utils import LoggerManager
19
+ from gllm_core.utils.concurrency import syncify
20
+
21
+ logger = LoggerManager().get_logger(__name__)
22
+
23
+ T = TypeVar("T")
24
+
25
+ BASE_EXPONENTIAL_BACKOFF = 2.0
26
+
27
+
28
+ class RetryConfig(BaseModel):
29
+ """Configuration for retry behavior.
30
+
31
+ Attributes:
32
+ max_retries (int): Maximum number of retry attempts.
33
+ base_delay (float): Base delay in seconds between retries.
34
+ max_delay (float): Maximum delay in seconds between retries.
35
+ jitter (bool): Whether to add random jitter to delays.
36
+ timeout (float | None): Overall timeout in seconds for the entire operation. If None, timeout is disabled.
37
+ retry_on_exceptions (tuple[type[Exception], ...]): Tuple of exception types to retry on.
38
+ """
39
+
40
+ max_retries: int = Field(default=0, ge=0)
41
+ base_delay: float = Field(default=1.0, gt=0.0)
42
+ max_delay: float = Field(default=10.0, gt=0.0)
43
+ jitter: bool = Field(default=True)
44
+ timeout: float | None = Field(default=None, gt=0.0)
45
+ retry_on_exceptions: tuple[type[Exception], ...] = Field(default=(Exception,))
46
+
47
+ @model_validator(mode="after")
48
+ def validate_delay_constraints(self) -> "RetryConfig":
49
+ """Validates that max_delay is greater than or equal to base_delay.
50
+
51
+ Returns:
52
+ RetryConfig: The validated configuration.
53
+
54
+ Raises:
55
+ ValueError: If max_delay is less than base_delay.
56
+ """
57
+ if self.timeout == 0:
58
+ logger.warning(
59
+ "Setting timeout=0.0 is deprecated and will raise an error in v0.4. "
60
+ "Please use timeout=None instead to disable timeout."
61
+ )
62
+ self.timeout = None
63
+
64
+ if self.max_delay < self.base_delay:
65
+ raise ValueError("The 'max_delay' parameter must be greater than or equal to 'base_delay'.")
66
+
67
+ return self
68
+
69
+
70
+ def _calculate_delay(attempt: int, config: RetryConfig) -> float:
71
+ """Calculates the delay for the next retry attempt.
72
+
73
+ Args:
74
+ attempt (int): The current attempt number (0-based).
75
+ config (RetryConfig): The retry configuration.
76
+
77
+ Returns:
78
+ float: The delay in seconds.
79
+ """
80
+ delay = config.base_delay * (BASE_EXPONENTIAL_BACKOFF**attempt)
81
+
82
+ if config.jitter:
83
+ jitter_factor = random.uniform(0, 0.25)
84
+ delay += delay * jitter_factor
85
+
86
+ return min(delay, config.max_delay)
87
+
88
+
89
+ async def _retry_async(
90
+ func: Callable[..., Any],
91
+ *args: Any,
92
+ retry_config: RetryConfig | None = None,
93
+ **kwargs: Any,
94
+ ) -> T:
95
+ """Executes a function with retry logic and exponential backoff.
96
+
97
+ This function executes the provided function with retry logic. It will first try to execute the function once.
98
+ If the function raises an exception that matches the retry_on_exceptions, it will retry up to max_retries times
99
+ with exponential backoff. Therefore, the max number of attempts is max_retries + 1. If provided, the timeout
100
+ applies to the entire retry operation, including all attempts and delays.
101
+
102
+ Example:
103
+ If you set timeout=10.0 and max_retries=3, the entire retry operation (including all attempts
104
+ and delays) will timeout after 10 seconds, not 10 seconds per attempt.
105
+
106
+ Args:
107
+ func (Callable[..., Any]): The function to execute.
108
+ *args (Any): Positional arguments to pass to the function.
109
+ retry_config (RetryConfig | None, optional): Retry configuration. If None, uses default config.
110
+ Defaults to None.
111
+ **kwargs (Any): Keyword arguments to pass to the function.
112
+
113
+ Returns:
114
+ T: The result of the function execution.
115
+
116
+ Raises:
117
+ Exception: The last exception raised by the function if all retries are exhausted.
118
+ asyncio.TimeoutError: If the overall timeout is exceeded.
119
+ """
120
+ config = retry_config or RetryConfig()
121
+
122
+ async def attempt_loop() -> T:
123
+ max_retries = config.max_retries + 1
124
+
125
+ for attempt in range(max_retries):
126
+ try:
127
+ result = func(*args, **kwargs)
128
+ if inspect.isawaitable(result):
129
+ return await result
130
+ return result
131
+
132
+ except asyncio.TimeoutError:
133
+ raise
134
+
135
+ except config.retry_on_exceptions as exc:
136
+ if attempt == config.max_retries:
137
+ logger.error(f"Function {func.__name__} failed after {max_retries} attempts. Last error: {exc}")
138
+ raise exc from None
139
+
140
+ delay = _calculate_delay(attempt, config)
141
+ logger.warning(
142
+ f"Function {func.__name__} failed on attempt {attempt + 1}/{max_retries}. "
143
+ f"Retrying in {delay:.2f} seconds. Error: {exc}"
144
+ )
145
+ await asyncio.sleep(delay)
146
+
147
+ if config.timeout is not None:
148
+ return await asyncio.wait_for(attempt_loop(), timeout=config.timeout)
149
+
150
+ return await attempt_loop()
151
+
152
+
153
+ @overload
154
+ async def retry(
155
+ func: Callable[..., Any],
156
+ *args: Any,
157
+ retry_config: RetryConfig | None = None,
158
+ **kwargs: Any,
159
+ ) -> T: ...
160
+
161
+
162
+ @overload
163
+ def retry(config: RetryConfig | None = None) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
164
+
165
+
166
+ def retry(
167
+ func_or_config: Callable[..., Any] | RetryConfig | None = None,
168
+ *args: Any,
169
+ retry_config: RetryConfig | None = None,
170
+ **kwargs: Any,
171
+ ) -> T | Callable[[Callable[..., Any]], Callable[..., Any]]:
172
+ """Executes a function with retry logic or creates a retry decorator.
173
+
174
+ This function supports two usage patterns:
175
+ 1. Direct function execution: await retry(func, *args, retry_config=config, **kwargs)
176
+ 2. Decorator factory: @retry() or @retry(config)
177
+
178
+ Args:
179
+ func_or_config (Callable[..., Any] | RetryConfig | None, optional): Either a function to execute or a
180
+ RetryConfig for decorator usage. Defaults to None.
181
+ *args (Any): Positional arguments (only used in direct execution mode).
182
+ retry_config (RetryConfig | None, optional): Retry configuration (only used in direct execution mode).
183
+ Defaults to None, in which case no retry nor timeout is applied.
184
+ **kwargs (Any): Keyword arguments (only used in direct execution mode).
185
+
186
+ Returns:
187
+ T | Callable[[Callable[..., Any]], Callable[..., Any]]: Either the result of function execution or
188
+ a decorator function.
189
+
190
+ Raises:
191
+ Exception: The last exception raised by the function if all retries are exhausted.
192
+ asyncio.TimeoutError: If the overall timeout is exceeded.
193
+
194
+ Examples:
195
+ # Direct function execution
196
+ ```python
197
+ result = await retry(my_function, arg1, arg2, retry_config=config)
198
+ ```
199
+
200
+ # Decorator usage - parameterless
201
+ ```python
202
+ @retry()
203
+ async def my_async_function():
204
+ # Use default settings, in which case no retry nor timeout is applied.
205
+ pass
206
+ ```
207
+
208
+ # Decorator usage - with custom configuration
209
+ ```python
210
+ @retry(RetryConfig(max_retries=3, timeout=120))
211
+ async def my_function():
212
+ # Will retry up to 3 times with 0.5s base delay
213
+ pass
214
+ ```
215
+
216
+ # Decorator on sync functions
217
+ ```python
218
+ @retry()
219
+ def my_sync_function():
220
+ # Works with sync functions too
221
+ return "success"
222
+ ```
223
+
224
+ # Decorator on class methods
225
+ ```python
226
+ class MyService:
227
+ @retry(RetryConfig(max_retries=2))
228
+ async def get_data(self, id: str):
229
+ return {"id": id, "data": "value"}
230
+ ```
231
+ """
232
+ if callable(func_or_config) and not isinstance(func_or_config, RetryConfig):
233
+ func = func_or_config
234
+
235
+ async def async_wrapper() -> T:
236
+ return await _retry_async(func, *args, retry_config=retry_config, **kwargs)
237
+
238
+ return async_wrapper()
239
+
240
+ config = func_or_config
241
+
242
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
243
+ if asyncio.iscoroutinefunction(func):
244
+
245
+ @functools.wraps(func)
246
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
247
+ return await _retry_async(func, *args, retry_config=config, **kwargs)
248
+
249
+ return async_wrapper
250
+
251
+ @functools.wraps(func)
252
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
253
+ sync_retry = syncify(lambda: _retry_async(func, *args, retry_config=config, **kwargs))
254
+ return sync_retry()
255
+
256
+ return sync_wrapper
257
+
258
+ return decorator