openai-sdk-helpers 0.4.1__py3-none-any.whl → 0.4.3__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.
- openai_sdk_helpers/__init__.py +10 -36
- openai_sdk_helpers/agent/__init__.py +5 -6
- openai_sdk_helpers/agent/base.py +184 -39
- openai_sdk_helpers/agent/{config.py → configuration.py} +50 -75
- openai_sdk_helpers/agent/{coordination.py → coordinator.py} +12 -10
- openai_sdk_helpers/agent/search/__init__.py +4 -4
- openai_sdk_helpers/agent/search/base.py +16 -16
- openai_sdk_helpers/agent/search/vector.py +66 -42
- openai_sdk_helpers/agent/search/web.py +33 -29
- openai_sdk_helpers/agent/summarizer.py +6 -4
- openai_sdk_helpers/agent/translator.py +9 -5
- openai_sdk_helpers/agent/{validation.py → validator.py} +6 -4
- openai_sdk_helpers/cli.py +8 -22
- openai_sdk_helpers/environment.py +17 -0
- openai_sdk_helpers/prompt/vector_planner.jinja +7 -0
- openai_sdk_helpers/prompt/vector_search.jinja +6 -0
- openai_sdk_helpers/prompt/vector_writer.jinja +7 -0
- openai_sdk_helpers/response/__init__.py +1 -1
- openai_sdk_helpers/response/base.py +4 -4
- openai_sdk_helpers/response/{config.py → configuration.py} +9 -9
- openai_sdk_helpers/response/planner.py +12 -0
- openai_sdk_helpers/response/prompter.py +12 -0
- openai_sdk_helpers/streamlit_app/__init__.py +1 -1
- openai_sdk_helpers/streamlit_app/app.py +16 -17
- openai_sdk_helpers/streamlit_app/{config.py → configuration.py} +13 -13
- openai_sdk_helpers/streamlit_app/streamlit_web_search.py +3 -3
- openai_sdk_helpers/types.py +3 -3
- openai_sdk_helpers/utils/__init__.py +2 -6
- openai_sdk_helpers/utils/json/base_model.py +1 -1
- openai_sdk_helpers/utils/json/data_class.py +1 -1
- openai_sdk_helpers/utils/json/ref.py +3 -0
- openai_sdk_helpers/utils/registry.py +19 -15
- openai_sdk_helpers/vector_storage/storage.py +1 -1
- {openai_sdk_helpers-0.4.1.dist-info → openai_sdk_helpers-0.4.3.dist-info}/METADATA +8 -8
- {openai_sdk_helpers-0.4.1.dist-info → openai_sdk_helpers-0.4.3.dist-info}/RECORD +40 -40
- openai_sdk_helpers/agent/prompt_utils.py +0 -15
- openai_sdk_helpers/context_manager.py +0 -241
- openai_sdk_helpers/deprecation.py +0 -167
- openai_sdk_helpers/retry.py +0 -175
- openai_sdk_helpers/utils/deprecation.py +0 -167
- /openai_sdk_helpers/{logging_config.py → logging.py} +0 -0
- /openai_sdk_helpers/{config.py → settings.py} +0 -0
- {openai_sdk_helpers-0.4.1.dist-info → openai_sdk_helpers-0.4.3.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.4.1.dist-info → openai_sdk_helpers-0.4.3.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.4.1.dist-info → openai_sdk_helpers-0.4.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
"""Context manager utilities for resource cleanup.
|
|
2
|
-
|
|
3
|
-
Provides base classes and utilities for proper resource management
|
|
4
|
-
with guaranteed cleanup on exit or exception.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
from contextlib import asynccontextmanager
|
|
11
|
-
from types import TracebackType
|
|
12
|
-
from typing import Any, AsyncIterator, Generic, Optional, TypeVar
|
|
13
|
-
|
|
14
|
-
from openai_sdk_helpers.logging_config import log
|
|
15
|
-
|
|
16
|
-
T = TypeVar("T")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ManagedResource(Generic[T]):
|
|
20
|
-
"""Base class for resources that need cleanup.
|
|
21
|
-
|
|
22
|
-
Provides context manager support for guaranteed resource cleanup
|
|
23
|
-
even when exceptions occur.
|
|
24
|
-
|
|
25
|
-
Examples
|
|
26
|
-
--------
|
|
27
|
-
>>> class DatabaseConnection(ManagedResource[Connection]):
|
|
28
|
-
... def __init__(self, connection):
|
|
29
|
-
... self.connection = connection
|
|
30
|
-
...
|
|
31
|
-
... def close(self) -> None:
|
|
32
|
-
... if self.connection:
|
|
33
|
-
... self.connection.close()
|
|
34
|
-
|
|
35
|
-
>>> with DatabaseConnection(connect()) as db:
|
|
36
|
-
... db.query("SELECT ...")
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __enter__(self) -> T:
|
|
40
|
-
"""Enter context manager.
|
|
41
|
-
|
|
42
|
-
Returns
|
|
43
|
-
-------
|
|
44
|
-
T
|
|
45
|
-
The resource instance (self cast appropriately).
|
|
46
|
-
"""
|
|
47
|
-
return self # type: ignore
|
|
48
|
-
|
|
49
|
-
def __exit__(
|
|
50
|
-
self,
|
|
51
|
-
exc_type: Optional[type[BaseException]],
|
|
52
|
-
exc_val: Optional[BaseException],
|
|
53
|
-
exc_tb: Optional[TracebackType],
|
|
54
|
-
) -> bool:
|
|
55
|
-
"""Exit context manager with cleanup.
|
|
56
|
-
|
|
57
|
-
Parameters
|
|
58
|
-
----------
|
|
59
|
-
exc_type : type[BaseException] | None
|
|
60
|
-
Type of exception if one was raised, None otherwise.
|
|
61
|
-
exc_val : BaseException | None
|
|
62
|
-
Exception instance if one was raised, None otherwise.
|
|
63
|
-
exc_tb : TracebackType | None
|
|
64
|
-
Traceback if exception was raised, None otherwise.
|
|
65
|
-
|
|
66
|
-
Returns
|
|
67
|
-
-------
|
|
68
|
-
bool
|
|
69
|
-
False to re-raise exceptions, True to suppress them.
|
|
70
|
-
"""
|
|
71
|
-
try:
|
|
72
|
-
self.close()
|
|
73
|
-
except Exception as exc:
|
|
74
|
-
log(f"Error during cleanup: {exc}", level=30) # logging.WARNING
|
|
75
|
-
# Don't suppress cleanup errors
|
|
76
|
-
if exc_type is None:
|
|
77
|
-
raise
|
|
78
|
-
|
|
79
|
-
return False # Re-raise exceptions
|
|
80
|
-
|
|
81
|
-
def close(self) -> None:
|
|
82
|
-
"""Close and cleanup the resource.
|
|
83
|
-
|
|
84
|
-
Should be overridden by subclasses to perform actual cleanup.
|
|
85
|
-
Should not raise exceptions, but may log them.
|
|
86
|
-
|
|
87
|
-
Raises
|
|
88
|
-
------
|
|
89
|
-
Exception
|
|
90
|
-
May raise if cleanup fails catastrophically.
|
|
91
|
-
"""
|
|
92
|
-
pass
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class AsyncManagedResource(Generic[T]):
|
|
96
|
-
"""Base class for async resources that need cleanup.
|
|
97
|
-
|
|
98
|
-
Provides async context manager support for guaranteed resource cleanup
|
|
99
|
-
even when exceptions occur.
|
|
100
|
-
|
|
101
|
-
Examples
|
|
102
|
-
--------
|
|
103
|
-
>>> class AsyncDatabaseConnection(AsyncManagedResource[AsyncConnection]):
|
|
104
|
-
... def __init__(self, connection):
|
|
105
|
-
... self.connection = connection
|
|
106
|
-
...
|
|
107
|
-
... async def close(self) -> None:
|
|
108
|
-
... if self.connection:
|
|
109
|
-
... await self.connection.close()
|
|
110
|
-
|
|
111
|
-
>>> async with AsyncDatabaseConnection(await connect()) as db:
|
|
112
|
-
... await db.query("SELECT ...")
|
|
113
|
-
"""
|
|
114
|
-
|
|
115
|
-
async def __aenter__(self) -> T:
|
|
116
|
-
"""Enter async context manager.
|
|
117
|
-
|
|
118
|
-
Returns
|
|
119
|
-
-------
|
|
120
|
-
T
|
|
121
|
-
The resource instance (self cast appropriately).
|
|
122
|
-
"""
|
|
123
|
-
return self # type: ignore
|
|
124
|
-
|
|
125
|
-
async def __aexit__(
|
|
126
|
-
self,
|
|
127
|
-
exc_type: Optional[type[BaseException]],
|
|
128
|
-
exc_val: Optional[BaseException],
|
|
129
|
-
exc_tb: Optional[TracebackType],
|
|
130
|
-
) -> bool:
|
|
131
|
-
"""Exit async context manager with cleanup.
|
|
132
|
-
|
|
133
|
-
Parameters
|
|
134
|
-
----------
|
|
135
|
-
exc_type : type[BaseException] | None
|
|
136
|
-
Type of exception if one was raised, None otherwise.
|
|
137
|
-
exc_val : BaseException | None
|
|
138
|
-
Exception instance if one was raised, None otherwise.
|
|
139
|
-
exc_tb : TracebackType | None
|
|
140
|
-
Traceback if exception was raised, None otherwise.
|
|
141
|
-
|
|
142
|
-
Returns
|
|
143
|
-
-------
|
|
144
|
-
bool
|
|
145
|
-
False to re-raise exceptions, True to suppress them.
|
|
146
|
-
"""
|
|
147
|
-
try:
|
|
148
|
-
await self.close()
|
|
149
|
-
except Exception as exc:
|
|
150
|
-
log(f"Error during async cleanup: {exc}", level=30) # logging.WARNING
|
|
151
|
-
# Don't suppress cleanup errors
|
|
152
|
-
if exc_type is None:
|
|
153
|
-
raise
|
|
154
|
-
|
|
155
|
-
return False # Re-raise exceptions
|
|
156
|
-
|
|
157
|
-
async def close(self) -> None:
|
|
158
|
-
"""Close and cleanup the resource asynchronously.
|
|
159
|
-
|
|
160
|
-
Should be overridden by subclasses to perform actual cleanup.
|
|
161
|
-
Should not raise exceptions, but may log them.
|
|
162
|
-
|
|
163
|
-
Raises
|
|
164
|
-
------
|
|
165
|
-
Exception
|
|
166
|
-
May raise if cleanup fails catastrophically.
|
|
167
|
-
"""
|
|
168
|
-
pass
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def ensure_closed(resource: Any) -> None:
|
|
172
|
-
"""Safely close a resource if it has a close method.
|
|
173
|
-
|
|
174
|
-
Logs errors but doesn't raise them.
|
|
175
|
-
|
|
176
|
-
Parameters
|
|
177
|
-
----------
|
|
178
|
-
resource : Any
|
|
179
|
-
Object that may have a close() method.
|
|
180
|
-
"""
|
|
181
|
-
if resource is None:
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
close_method = getattr(resource, "close", None)
|
|
185
|
-
if callable(close_method):
|
|
186
|
-
try:
|
|
187
|
-
close_method()
|
|
188
|
-
except Exception as exc:
|
|
189
|
-
log(f"Error closing {type(resource).__name__}: {exc}", level=30)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
async def ensure_closed_async(resource: Any) -> None:
|
|
193
|
-
"""Safely close a resource asynchronously if it has an async close method.
|
|
194
|
-
|
|
195
|
-
Logs errors but doesn't raise them.
|
|
196
|
-
|
|
197
|
-
Parameters
|
|
198
|
-
----------
|
|
199
|
-
resource : Any
|
|
200
|
-
Object that may have an async close() method.
|
|
201
|
-
"""
|
|
202
|
-
if resource is None:
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
close_method = getattr(resource, "close", None)
|
|
206
|
-
if callable(close_method):
|
|
207
|
-
try:
|
|
208
|
-
if asyncio.iscoroutinefunction(close_method):
|
|
209
|
-
await close_method()
|
|
210
|
-
else:
|
|
211
|
-
close_method()
|
|
212
|
-
except Exception as exc:
|
|
213
|
-
log(
|
|
214
|
-
f"Error closing async {type(resource).__name__}: {exc}",
|
|
215
|
-
level=30,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@asynccontextmanager
|
|
220
|
-
async def async_context(resource: AsyncManagedResource[T]) -> AsyncIterator[T]:
|
|
221
|
-
"""Context manager for async resources.
|
|
222
|
-
|
|
223
|
-
Parameters
|
|
224
|
-
----------
|
|
225
|
-
resource : AsyncManagedResource
|
|
226
|
-
Async resource to manage.
|
|
227
|
-
|
|
228
|
-
Yields
|
|
229
|
-
------
|
|
230
|
-
T
|
|
231
|
-
The resource instance.
|
|
232
|
-
|
|
233
|
-
Examples
|
|
234
|
-
--------
|
|
235
|
-
>>> async with async_context(my_resource) as resource:
|
|
236
|
-
... await resource.do_something()
|
|
237
|
-
"""
|
|
238
|
-
try:
|
|
239
|
-
yield await resource.__aenter__()
|
|
240
|
-
finally:
|
|
241
|
-
await resource.__aexit__(None, None, None)
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"""Deprecation utilities for managing deprecated features.
|
|
2
|
-
|
|
3
|
-
This module provides infrastructure for marking and managing deprecated
|
|
4
|
-
functions, classes, and features with consistent warning messages.
|
|
5
|
-
|
|
6
|
-
Functions
|
|
7
|
-
---------
|
|
8
|
-
deprecated
|
|
9
|
-
Decorator to mark functions or classes as deprecated.
|
|
10
|
-
warn_deprecated
|
|
11
|
-
Emit a deprecation warning with optional custom message.
|
|
12
|
-
|
|
13
|
-
Classes
|
|
14
|
-
-------
|
|
15
|
-
DeprecationHelper
|
|
16
|
-
Utility class for managing deprecation warnings and versions.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import functools
|
|
22
|
-
import warnings
|
|
23
|
-
from typing import Any, Callable, TypeVar
|
|
24
|
-
|
|
25
|
-
F = TypeVar("F", bound=Callable[..., Any])
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class DeprecationHelper:
|
|
29
|
-
"""Utility class for managing deprecation warnings.
|
|
30
|
-
|
|
31
|
-
Provides consistent formatting and control of deprecation warnings
|
|
32
|
-
across the package.
|
|
33
|
-
|
|
34
|
-
Methods
|
|
35
|
-
-------
|
|
36
|
-
warn
|
|
37
|
-
Emit a deprecation warning with standard formatting.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def warn(
|
|
42
|
-
feature_name: str,
|
|
43
|
-
removal_version: str,
|
|
44
|
-
alternative: str | None = None,
|
|
45
|
-
extra_message: str | None = None,
|
|
46
|
-
) -> None:
|
|
47
|
-
"""Emit a deprecation warning for a feature.
|
|
48
|
-
|
|
49
|
-
Parameters
|
|
50
|
-
----------
|
|
51
|
-
feature_name : str
|
|
52
|
-
Name of the deprecated feature (e.g., "MyClass.old_method").
|
|
53
|
-
removal_version : str
|
|
54
|
-
Version in which the feature will be removed.
|
|
55
|
-
alternative : str, optional
|
|
56
|
-
Recommended alternative to use instead.
|
|
57
|
-
extra_message : str, optional
|
|
58
|
-
Additional context or migration instructions.
|
|
59
|
-
|
|
60
|
-
Raises
|
|
61
|
-
------
|
|
62
|
-
DeprecationWarning
|
|
63
|
-
Always issues a DeprecationWarning to stderr.
|
|
64
|
-
"""
|
|
65
|
-
msg = f"{feature_name} is deprecated and will be removed in version {removal_version}."
|
|
66
|
-
if alternative:
|
|
67
|
-
msg += f" Use {alternative} instead."
|
|
68
|
-
if extra_message:
|
|
69
|
-
msg += f" {extra_message}"
|
|
70
|
-
|
|
71
|
-
warnings.warn(msg, DeprecationWarning, stacklevel=3)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def deprecated(
|
|
75
|
-
removal_version: str,
|
|
76
|
-
alternative: str | None = None,
|
|
77
|
-
extra_message: str | None = None,
|
|
78
|
-
) -> Callable[[F], F]:
|
|
79
|
-
"""Mark a function or class as deprecated.
|
|
80
|
-
|
|
81
|
-
Parameters
|
|
82
|
-
----------
|
|
83
|
-
removal_version : str
|
|
84
|
-
Version in which the decorated feature will be removed.
|
|
85
|
-
alternative : str, optional
|
|
86
|
-
Recommended alternative to use instead.
|
|
87
|
-
extra_message : str, optional
|
|
88
|
-
Additional context or migration instructions.
|
|
89
|
-
|
|
90
|
-
Returns
|
|
91
|
-
-------
|
|
92
|
-
Callable
|
|
93
|
-
Decorator function that wraps the target function or class.
|
|
94
|
-
|
|
95
|
-
Examples
|
|
96
|
-
--------
|
|
97
|
-
>>> @deprecated("1.0.0", "new_function")
|
|
98
|
-
... def old_function():
|
|
99
|
-
... pass
|
|
100
|
-
|
|
101
|
-
>>> class OldClass:
|
|
102
|
-
... @deprecated("1.0.0", "NewClass")
|
|
103
|
-
... def old_method(self):
|
|
104
|
-
... pass
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
def decorator(func_or_class: F) -> F:
|
|
108
|
-
feature_name = f"{func_or_class.__module__}.{func_or_class.__qualname__}"
|
|
109
|
-
|
|
110
|
-
if isinstance(func_or_class, type):
|
|
111
|
-
# Handle class deprecation
|
|
112
|
-
original_init = func_or_class.__init__
|
|
113
|
-
|
|
114
|
-
@functools.wraps(original_init)
|
|
115
|
-
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
116
|
-
DeprecationHelper.warn(
|
|
117
|
-
feature_name,
|
|
118
|
-
removal_version,
|
|
119
|
-
alternative,
|
|
120
|
-
extra_message,
|
|
121
|
-
)
|
|
122
|
-
original_init(self, *args, **kwargs)
|
|
123
|
-
|
|
124
|
-
func_or_class.__init__ = new_init
|
|
125
|
-
else:
|
|
126
|
-
# Handle function deprecation
|
|
127
|
-
@functools.wraps(func_or_class)
|
|
128
|
-
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
129
|
-
DeprecationHelper.warn(
|
|
130
|
-
feature_name,
|
|
131
|
-
removal_version,
|
|
132
|
-
alternative,
|
|
133
|
-
extra_message,
|
|
134
|
-
)
|
|
135
|
-
return func_or_class(*args, **kwargs)
|
|
136
|
-
|
|
137
|
-
return wrapper # type: ignore
|
|
138
|
-
|
|
139
|
-
return func_or_class # type: ignore[return-value]
|
|
140
|
-
|
|
141
|
-
return decorator
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def warn_deprecated(
|
|
145
|
-
feature_name: str,
|
|
146
|
-
removal_version: str,
|
|
147
|
-
alternative: str | None = None,
|
|
148
|
-
extra_message: str | None = None,
|
|
149
|
-
) -> None:
|
|
150
|
-
"""Issue a deprecation warning.
|
|
151
|
-
|
|
152
|
-
Parameters
|
|
153
|
-
----------
|
|
154
|
-
feature_name : str
|
|
155
|
-
Name of the deprecated feature.
|
|
156
|
-
removal_version : str
|
|
157
|
-
Version in which the feature will be removed.
|
|
158
|
-
alternative : str, optional
|
|
159
|
-
Recommended alternative to use instead.
|
|
160
|
-
extra_message : str, optional
|
|
161
|
-
Additional context or migration instructions.
|
|
162
|
-
|
|
163
|
-
Examples
|
|
164
|
-
--------
|
|
165
|
-
>>> warn_deprecated("old_config_key", "1.0.0", "new_config_key")
|
|
166
|
-
"""
|
|
167
|
-
DeprecationHelper.warn(feature_name, removal_version, alternative, extra_message)
|
openai_sdk_helpers/retry.py
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"""Retry decorators with exponential backoff for API operations.
|
|
2
|
-
|
|
3
|
-
Provides decorators for retrying async and sync functions with
|
|
4
|
-
exponential backoff and jitter when rate limiting or transient
|
|
5
|
-
errors occur.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
import logging
|
|
10
|
-
import random
|
|
11
|
-
import time
|
|
12
|
-
from functools import wraps
|
|
13
|
-
from typing import Any, Callable, ParamSpec, TypeVar
|
|
14
|
-
|
|
15
|
-
from openai import APIError, RateLimitError
|
|
16
|
-
|
|
17
|
-
from openai_sdk_helpers.errors import AsyncExecutionError
|
|
18
|
-
from openai_sdk_helpers.logging_config import log
|
|
19
|
-
|
|
20
|
-
P = ParamSpec("P")
|
|
21
|
-
T = TypeVar("T")
|
|
22
|
-
|
|
23
|
-
# Default retry configuration constants
|
|
24
|
-
DEFAULT_MAX_RETRIES = 3
|
|
25
|
-
DEFAULT_BASE_DELAY = 1.0
|
|
26
|
-
DEFAULT_MAX_DELAY = 60.0
|
|
27
|
-
|
|
28
|
-
# HTTP status codes for transient errors
|
|
29
|
-
TRANSIENT_HTTP_STATUS_CODES = frozenset({408, 429, 500, 502, 503})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def with_exponential_backoff(
|
|
33
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
34
|
-
base_delay: float = DEFAULT_BASE_DELAY,
|
|
35
|
-
max_delay: float = DEFAULT_MAX_DELAY,
|
|
36
|
-
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
37
|
-
"""Decorate functions with exponential backoff on transient errors.
|
|
38
|
-
|
|
39
|
-
Retries on RateLimitError or transient API errors (5xx, 408, 429).
|
|
40
|
-
Uses exponential backoff with jitter to avoid thundering herd.
|
|
41
|
-
|
|
42
|
-
Parameters
|
|
43
|
-
----------
|
|
44
|
-
max_retries : int
|
|
45
|
-
Maximum number of retry attempts (total attempts = max_retries + 1).
|
|
46
|
-
Default is 3.
|
|
47
|
-
base_delay : float
|
|
48
|
-
Initial delay in seconds before first retry. Default is 1.0.
|
|
49
|
-
max_delay : float
|
|
50
|
-
Maximum delay in seconds between retries. Default is 60.0.
|
|
51
|
-
|
|
52
|
-
Returns
|
|
53
|
-
-------
|
|
54
|
-
Callable
|
|
55
|
-
Decorator function.
|
|
56
|
-
|
|
57
|
-
Examples
|
|
58
|
-
--------
|
|
59
|
-
>>> @with_exponential_backoff(max_retries=3, base_delay=1.0)
|
|
60
|
-
... def call_api(query: str) -> str:
|
|
61
|
-
... # API call that may fail with rate limiting
|
|
62
|
-
... return client.call(query)
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
66
|
-
"""Apply retry logic to function."""
|
|
67
|
-
if asyncio.iscoroutinefunction(func):
|
|
68
|
-
|
|
69
|
-
@wraps(func)
|
|
70
|
-
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
71
|
-
"""Async wrapper with retry logic."""
|
|
72
|
-
last_exc: Exception | None = None
|
|
73
|
-
for attempt in range(max_retries + 1):
|
|
74
|
-
try:
|
|
75
|
-
return await func(*args, **kwargs)
|
|
76
|
-
except RateLimitError as exc:
|
|
77
|
-
last_exc = exc
|
|
78
|
-
if attempt >= max_retries:
|
|
79
|
-
raise
|
|
80
|
-
delay = min(
|
|
81
|
-
base_delay * (2**attempt) + random.uniform(0, 1),
|
|
82
|
-
max_delay,
|
|
83
|
-
)
|
|
84
|
-
log(
|
|
85
|
-
f"Rate limited on {func.__name__}, retrying in "
|
|
86
|
-
f"{delay:.2f}s (attempt {attempt + 1}/{max_retries + 1})",
|
|
87
|
-
level=logging.WARNING,
|
|
88
|
-
)
|
|
89
|
-
await asyncio.sleep(delay)
|
|
90
|
-
except APIError as exc:
|
|
91
|
-
last_exc = exc
|
|
92
|
-
status_code: int | None = getattr(exc, "status_code", None)
|
|
93
|
-
# Only retry on transient errors
|
|
94
|
-
if (
|
|
95
|
-
not status_code
|
|
96
|
-
or status_code not in TRANSIENT_HTTP_STATUS_CODES
|
|
97
|
-
):
|
|
98
|
-
raise
|
|
99
|
-
if attempt >= max_retries:
|
|
100
|
-
raise
|
|
101
|
-
delay = min(
|
|
102
|
-
base_delay * (2**attempt),
|
|
103
|
-
max_delay,
|
|
104
|
-
)
|
|
105
|
-
log(
|
|
106
|
-
f"Transient API error on {func.__name__}: "
|
|
107
|
-
f"{status_code}, retrying in {delay:.2f}s "
|
|
108
|
-
f"(attempt {attempt + 1}/{max_retries + 1})",
|
|
109
|
-
level=logging.WARNING,
|
|
110
|
-
)
|
|
111
|
-
await asyncio.sleep(delay)
|
|
112
|
-
|
|
113
|
-
# Should never reach here, but handle edge case
|
|
114
|
-
if last_exc:
|
|
115
|
-
raise last_exc
|
|
116
|
-
raise AsyncExecutionError(
|
|
117
|
-
f"Unexpected state in {func.__name__} after retries"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
return async_wrapper # type: ignore
|
|
121
|
-
|
|
122
|
-
@wraps(func)
|
|
123
|
-
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
124
|
-
"""Sync wrapper with retry logic."""
|
|
125
|
-
last_exc: Exception | None = None
|
|
126
|
-
for attempt in range(max_retries + 1):
|
|
127
|
-
try:
|
|
128
|
-
return func(*args, **kwargs)
|
|
129
|
-
except RateLimitError as exc:
|
|
130
|
-
last_exc = exc
|
|
131
|
-
if attempt >= max_retries:
|
|
132
|
-
raise
|
|
133
|
-
delay = min(
|
|
134
|
-
base_delay * (2**attempt) + random.uniform(0, 1),
|
|
135
|
-
max_delay,
|
|
136
|
-
)
|
|
137
|
-
log(
|
|
138
|
-
f"Rate limited on {func.__name__}, retrying in "
|
|
139
|
-
f"{delay:.2f}s (attempt {attempt + 1}/{max_retries + 1})",
|
|
140
|
-
level=logging.WARNING,
|
|
141
|
-
)
|
|
142
|
-
time.sleep(delay)
|
|
143
|
-
except APIError as exc:
|
|
144
|
-
last_exc = exc
|
|
145
|
-
status_code: int | None = getattr(exc, "status_code", None)
|
|
146
|
-
# Only retry on transient errors
|
|
147
|
-
if (
|
|
148
|
-
not status_code
|
|
149
|
-
or status_code not in TRANSIENT_HTTP_STATUS_CODES
|
|
150
|
-
):
|
|
151
|
-
raise
|
|
152
|
-
if attempt >= max_retries:
|
|
153
|
-
raise
|
|
154
|
-
delay = min(
|
|
155
|
-
base_delay * (2**attempt),
|
|
156
|
-
max_delay,
|
|
157
|
-
)
|
|
158
|
-
log(
|
|
159
|
-
f"Transient API error on {func.__name__}: "
|
|
160
|
-
f"{status_code}, retrying in {delay:.2f}s "
|
|
161
|
-
f"(attempt {attempt + 1}/{max_retries + 1})",
|
|
162
|
-
level=logging.WARNING,
|
|
163
|
-
)
|
|
164
|
-
time.sleep(delay)
|
|
165
|
-
|
|
166
|
-
# Should never reach here, but handle edge case
|
|
167
|
-
if last_exc:
|
|
168
|
-
raise last_exc
|
|
169
|
-
raise AsyncExecutionError(
|
|
170
|
-
f"Unexpected state in {func.__name__} after retries"
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
return sync_wrapper # type: ignore
|
|
174
|
-
|
|
175
|
-
return decorator
|