Skip to content

Commit 962104e

Browse files
beaufourclaude
andcommitted
Add retry logic for timeouts, connection errors, and server errors
Extract retry logic into a shared retry module that handles HTTP 429 (rate limit), 5xx (server errors), timeouts, and connection errors with exponential backoff. Apply retry behavior to both API calls and uploads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a1b5b7 commit 962104e

File tree

6 files changed

+618
-53
lines changed

6 files changed

+618
-53
lines changed

flickr_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@
4444
from .method_call import set_timeout as set_timeout
4545
from .keys import set_keys as set_keys
4646
from .flickrerrors import FlickrRateLimitError as FlickrRateLimitError
47+
from .flickrerrors import FlickrTimeoutError as FlickrTimeoutError
4748
from ._version import __version__ as __version__

flickr_api/flickrerrors.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,29 @@ def __init__(self, retry_after: float | None, content: str) -> None:
101101
FlickrError.__init__(self, msg)
102102
self.retry_after = retry_after
103103
self.content = content
104+
105+
106+
class FlickrTimeoutError(FlickrError):
107+
"""Exception for request timeout or connection errors.
108+
109+
Raised when a request times out or fails due to connection issues
110+
and max retries have been exhausted.
111+
112+
Parameters:
113+
-----------
114+
message: str
115+
Error message describing the timeout/connection issue
116+
"""
117+
118+
message: str
119+
120+
def __init__(self, message: str) -> None:
121+
"""Constructor
122+
123+
Parameters:
124+
-----------
125+
message: str
126+
Error message describing the timeout/connection issue
127+
"""
128+
FlickrError.__init__(self, f"Request failed: {message}")
129+
self.message = message

flickr_api/method_call.py

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020

2121
from . import keys
2222
from .utils import urlopen_and_read
23-
from .flickrerrors import FlickrError, FlickrAPIError, FlickrServerError, FlickrRateLimitError
23+
from .flickrerrors import (
24+
FlickrAPIError,
25+
FlickrError,
26+
FlickrServerError,
27+
)
28+
from . import retry as retry_module
2429
from .cache import SimpleCache
2530

2631
REST_URL = "https://api.flickr.com/services/rest/"
@@ -41,6 +46,13 @@
4146
_RATE_LIMIT_LAST_REQUEST: float | None = None
4247

4348

49+
# Initialize retry module with our config getters
50+
def _init_retry_module() -> None:
51+
"""Initialize retry module with config getters."""
52+
retry_module.set_retry_config_getter(get_retry_config)
53+
retry_module.set_rate_limit_wait_func(_maybe_wait_for_rate_limit)
54+
55+
4456
def enable_cache(cache_object: Any | None = None) -> None:
4557
"""enable caching
4658
Parameters:
@@ -208,12 +220,8 @@ def _calculate_retry_delay(attempt: int, retry_after: float | None) -> float:
208220
--------
209221
Delay in seconds
210222
"""
211-
if retry_after is not None and retry_after > 0:
212-
return min(retry_after, RETRY_MAX_DELAY)
213-
214-
# Exponential backoff: base_delay * 2^attempt
215-
delay = RETRY_BASE_DELAY * (2**attempt)
216-
return min(delay, RETRY_MAX_DELAY)
223+
# Delegate to retry module for consistency
224+
return retry_module.calculate_retry_delay(attempt, retry_after)
217225

218226

219227
def _parse_retry_after(response: requests.Response) -> float | None:
@@ -228,24 +236,19 @@ def _parse_retry_after(response: requests.Response) -> float | None:
228236
--------
229237
Seconds to wait, or None if header not present/parseable
230238
"""
231-
retry_after = response.headers.get("Retry-After")
232-
if retry_after is None:
233-
return None
234-
235-
try:
236-
return float(retry_after)
237-
except ValueError:
238-
# Could be an HTTP-date format, but Flickr typically uses seconds
239-
logger.warning("Could not parse Retry-After header: %s", retry_after)
240-
return None
239+
# Delegate to retry module for consistency
240+
return retry_module.parse_retry_after(response)
241241

242242

243243
def _make_request_with_retry(
244244
request_url: str,
245245
args: dict[str, Any],
246246
oauth_auth: Any,
247247
) -> requests.Response:
248-
"""Make HTTP request with automatic retry on rate limit errors.
248+
"""Make HTTP request with automatic retry on transient errors.
249+
250+
Handles HTTP 429 (rate limit), 5xx (server errors), timeouts, and
251+
connection errors with configurable retry behavior.
249252
250253
Parameters:
251254
-----------
@@ -263,40 +266,16 @@ def _make_request_with_retry(
263266
Raises:
264267
-------
265268
FlickrRateLimitError: If rate limit exceeded and max retries exhausted
269+
FlickrServerError: If server error and max retries exhausted
270+
FlickrTimeoutError: If timeout/connection error and max retries exhausted
266271
"""
267-
_maybe_wait_for_rate_limit()
268-
269-
last_error: FlickrRateLimitError | None = None
270-
271-
for attempt in range(MAX_RETRIES + 1):
272-
resp = requests.post(request_url, args, auth=oauth_auth, timeout=get_timeout())
273-
274-
if resp.status_code != 429:
275-
return resp
276-
277-
# Rate limited - parse retry info and potentially retry
278-
retry_after = _parse_retry_after(resp)
279-
content = resp.content.decode("utf8") if resp.content else "Too Many Requests"
280-
last_error = FlickrRateLimitError(retry_after, content)
281-
282-
if attempt >= MAX_RETRIES:
283-
logger.warning(
284-
"Rate limit exceeded, max retries (%d) exhausted",
285-
MAX_RETRIES,
286-
)
287-
break
288-
289-
delay = _calculate_retry_delay(attempt, retry_after)
290-
logger.warning(
291-
"Rate limit exceeded (attempt %d/%d), retrying in %.1f seconds",
292-
attempt + 1,
293-
MAX_RETRIES + 1,
294-
delay,
295-
)
296-
time.sleep(delay)
297-
298-
# If we get here, we've exhausted retries
299-
raise last_error # type: ignore[misc]
272+
# Ensure retry module is initialized
273+
_init_retry_module()
274+
275+
def make_request() -> requests.Response:
276+
return requests.post(request_url, args, auth=oauth_auth, timeout=get_timeout())
277+
278+
return retry_module.retry_request(make_request, operation_name="API call")
300279

301280

302281
def send_request(url, data):

0 commit comments

Comments
 (0)