init commit
This commit is contained in:
162
app/utils/http_client.py
Normal file
162
app/utils/http_client.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""Centralized HTTP client wrapper with retries and consistent error handling."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from app.utils.logging_config import get_logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpResponse:
|
||||
"""Wrapper for HTTP response data."""
|
||||
|
||||
status_code: int
|
||||
json_data: Optional[dict[str, Any]]
|
||||
text: str
|
||||
success: bool
|
||||
|
||||
|
||||
class HttpClient:
|
||||
"""HTTP client with automatic retries, timeouts, and logging."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout: int = 30,
|
||||
max_retries: int = 3,
|
||||
backoff_factor: float = 0.5,
|
||||
) -> None:
|
||||
"""Initialize the HTTP client.
|
||||
|
||||
Args:
|
||||
timeout: Request timeout in seconds.
|
||||
max_retries: Maximum number of retry attempts.
|
||||
backoff_factor: Multiplier for exponential backoff between retries.
|
||||
"""
|
||||
self.timeout = timeout
|
||||
self.logger = get_logger(__name__)
|
||||
|
||||
# Configure retry strategy
|
||||
retry_strategy = Retry(
|
||||
total=max_retries,
|
||||
backoff_factor=backoff_factor,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
allowed_methods=["GET", "POST"],
|
||||
)
|
||||
|
||||
# Create session with retry adapter
|
||||
self.session = requests.Session()
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
|
||||
def get(
|
||||
self,
|
||||
url: str,
|
||||
params: Optional[dict[str, Any]] = None,
|
||||
headers: Optional[dict[str, str]] = None,
|
||||
) -> HttpResponse:
|
||||
"""Perform a GET request.
|
||||
|
||||
Args:
|
||||
url: The URL to request.
|
||||
params: Optional query parameters.
|
||||
headers: Optional HTTP headers.
|
||||
|
||||
Returns:
|
||||
HttpResponse with status, data, and success flag.
|
||||
"""
|
||||
self.logger.debug("http_get", url=url, params=params)
|
||||
|
||||
try:
|
||||
response = self.session.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return self._build_response(response)
|
||||
except requests.RequestException as e:
|
||||
self.logger.error("http_get_failed", url=url, error=str(e))
|
||||
return HttpResponse(
|
||||
status_code=0,
|
||||
json_data=None,
|
||||
text=str(e),
|
||||
success=False,
|
||||
)
|
||||
|
||||
def post(
|
||||
self,
|
||||
url: str,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
json_data: Optional[dict[str, Any]] = None,
|
||||
headers: Optional[dict[str, str]] = None,
|
||||
) -> HttpResponse:
|
||||
"""Perform a POST request.
|
||||
|
||||
Args:
|
||||
url: The URL to request.
|
||||
data: Optional form data.
|
||||
json_data: Optional JSON data.
|
||||
headers: Optional HTTP headers.
|
||||
|
||||
Returns:
|
||||
HttpResponse with status, data, and success flag.
|
||||
"""
|
||||
self.logger.debug("http_post", url=url)
|
||||
|
||||
try:
|
||||
response = self.session.post(
|
||||
url,
|
||||
data=data,
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return self._build_response(response)
|
||||
except requests.RequestException as e:
|
||||
self.logger.error("http_post_failed", url=url, error=str(e))
|
||||
return HttpResponse(
|
||||
status_code=0,
|
||||
json_data=None,
|
||||
text=str(e),
|
||||
success=False,
|
||||
)
|
||||
|
||||
def _build_response(self, response: requests.Response) -> HttpResponse:
|
||||
"""Build an HttpResponse from a requests Response.
|
||||
|
||||
Args:
|
||||
response: The requests library Response object.
|
||||
|
||||
Returns:
|
||||
HttpResponse wrapper.
|
||||
"""
|
||||
json_data = None
|
||||
if response.headers.get("content-type", "").startswith("application/json"):
|
||||
try:
|
||||
json_data = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
success = 200 <= response.status_code < 300
|
||||
|
||||
self.logger.debug(
|
||||
"http_response",
|
||||
status_code=response.status_code,
|
||||
success=success,
|
||||
)
|
||||
|
||||
return HttpResponse(
|
||||
status_code=response.status_code,
|
||||
json_data=json_data,
|
||||
text=response.text,
|
||||
success=success,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the HTTP session."""
|
||||
self.session.close()
|
||||
Reference in New Issue
Block a user