pdf-accessibility/retry_helper.py
Vadym Samoilenko 0e24602096 Add production readiness: authentication, logging, retry logic, and test suite
Phase 1: Critical bug fixes
- Fix missing os/sys imports in pdf_remediation.py (line 427 crash)
- Install Python dependencies (venv with 11 packages)
- Create runtime directories (uploads, results, .cache)
- Configure environment (.env from .env.example)

Phase 2: Production features
- Add authentication module (auth.php) with API key support
- Integrate auth into api.php with CORS headers update
- Add structured logging framework (logger_config.py) with rotation
- Add retry helper (retry_helper.py) with exponential backoff
- Apply retry decorators to AI API calls (Claude and Google Vision)
- Create comprehensive test suite (31 tests, 34% coverage)
  * Unit tests for checker and remediation
  * Integration tests for API and authentication
  * pytest configuration with coverage reporting

Documentation:
- Add requirements specifications (BRS, FRS, SAD) to docs_req/
- Add PDF-UA-1 technical background
- Add sample accessibility report

All tests passing (31/31). Ready for production deployment.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-25 13:26:02 +00:00

242 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""
Retry Helper Module
Provides retry logic with exponential backoff for API calls and other operations.
Helps make the application more resilient to transient failures.
"""
import time
import functools
from typing import Callable, Any, Optional, Tuple, Type
from logger_config import setup_logger
logger = setup_logger(__name__, "retry_helper.log")
def retry_with_backoff(
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 60.0,
exponential_base: float = 2.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
"""
Decorator to retry a function with exponential backoff
Args:
max_retries: Maximum number of retry attempts (default: 3)
initial_delay: Initial delay in seconds (default: 1.0)
max_delay: Maximum delay in seconds (default: 60.0)
exponential_base: Base for exponential backoff (default: 2.0)
exceptions: Tuple of exceptions to catch and retry (default: all exceptions)
Returns:
Decorated function with retry logic
Example:
@retry_with_backoff(max_retries=3, initial_delay=1.0)
def call_api():
return api.get_data()
# Will retry up to 3 times with delays: 1s, 2s, 4s
result = call_api()
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
delay = initial_delay
last_exception = None
for attempt in range(max_retries + 1):
try:
# Try to execute the function
result = func(*args, **kwargs)
# If we retried at least once, log success
if attempt > 0:
logger.info(
f"{func.__name__} succeeded on attempt {attempt + 1}/{max_retries + 1}"
)
return result
except exceptions as e:
last_exception = e
# If this was the last attempt, don't retry
if attempt >= max_retries:
logger.error(
f"{func.__name__} failed after {max_retries + 1} attempts: {str(e)}"
)
raise
# Calculate delay with exponential backoff
current_delay = min(delay, max_delay)
logger.warning(
f"{func.__name__} failed on attempt {attempt + 1}/{max_retries + 1}: {str(e)}. "
f"Retrying in {current_delay:.1f}s..."
)
# Wait before retrying
time.sleep(current_delay)
# Increase delay for next attempt
delay *= exponential_base
# Should never reach here, but just in case
raise last_exception
return wrapper
return decorator
def retry_on_failure(
func: Callable,
max_retries: int = 3,
initial_delay: float = 1.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,)
) -> Any:
"""
Retry a function call with exponential backoff (non-decorator version)
Args:
func: Function to execute
max_retries: Maximum number of retry attempts
initial_delay: Initial delay in seconds
exceptions: Tuple of exceptions to catch and retry
Returns:
Result of the function call
Example:
def api_call():
return api.get_data()
result = retry_on_failure(api_call, max_retries=3)
"""
@retry_with_backoff(max_retries=max_retries, initial_delay=initial_delay, exceptions=exceptions)
def wrapped():
return func()
return wrapped()
class RetryableError(Exception):
"""Exception that indicates an operation should be retried"""
pass
class NonRetryableError(Exception):
"""Exception that indicates an operation should NOT be retried"""
pass
def is_retryable_error(error: Exception) -> bool:
"""
Determine if an error should be retried
Args:
error: Exception to check
Returns:
True if error should be retried, False otherwise
"""
# Don't retry explicit non-retryable errors
if isinstance(error, NonRetryableError):
return False
# Retry explicit retryable errors
if isinstance(error, RetryableError):
return True
# Check for common retryable error messages/types
error_str = str(error).lower()
retryable_patterns = [
'timeout',
'connection',
'network',
'temporary',
'unavailable',
'rate limit',
'too many requests',
'429',
'503',
'504',
]
return any(pattern in error_str for pattern in retryable_patterns)
def safe_execute(
func: Callable,
fallback_value: Any = None,
log_errors: bool = True
) -> Any:
"""
Execute a function and return a fallback value on error (graceful degradation)
Args:
func: Function to execute
fallback_value: Value to return if function fails (default: None)
log_errors: Whether to log errors (default: True)
Returns:
Result of function or fallback value on error
Example:
# If API fails, return empty list instead of crashing
results = safe_execute(
lambda: api.get_results(),
fallback_value=[],
log_errors=True
)
"""
try:
return func()
except Exception as e:
if log_errors:
logger.warning(f"Function {func.__name__} failed gracefully: {str(e)}")
return fallback_value
if __name__ == "__main__":
# Test the retry logic
print("Testing retry_with_backoff decorator...")
attempt_count = 0
@retry_with_backoff(max_retries=3, initial_delay=0.5)
def flaky_function():
"""Simulates a flaky API that fails twice then succeeds"""
global attempt_count
attempt_count += 1
if attempt_count < 3:
raise ConnectionError(f"Connection failed (attempt {attempt_count})")
return "Success!"
try:
result = flaky_function()
print(f"✅ Result: {result}")
print(f"✅ Took {attempt_count} attempts")
except Exception as e:
print(f"❌ Failed: {e}")
# Test safe_execute
print("\nTesting safe_execute...")
def failing_function():
raise ValueError("This always fails")
result = safe_execute(
failing_function,
fallback_value="Fallback value",
log_errors=True
)
print(f"✅ Graceful degradation result: {result}")
print("\n✅ All tests passed!")