|
import json
|
|
import logging
|
|
import socket
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import Dict, Any, Optional
|
|
|
|
logger = logging.getLogger("pr")
|
|
|
|
|
|
class SyncHTTPClient:
|
|
def __init__(self):
|
|
self.session_headers = {}
|
|
|
|
def request(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
headers: Optional[Dict[str, str]] = None,
|
|
data: Optional[bytes] = None,
|
|
json_data: Optional[Dict[str, Any]] = None,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
"""Make a sync HTTP request using urllib with retry logic."""
|
|
# Prepare headers
|
|
request_headers = {**self.session_headers}
|
|
if headers:
|
|
request_headers.update(headers)
|
|
|
|
# Prepare data
|
|
request_data = data
|
|
if json_data is not None:
|
|
request_data = json.dumps(json_data).encode("utf-8")
|
|
request_headers["Content-Type"] = "application/json"
|
|
|
|
# Create request object
|
|
req = urllib.request.Request(url, data=request_data, headers=request_headers, method=method)
|
|
|
|
attempt = 0
|
|
start_time = time.time()
|
|
|
|
while True:
|
|
attempt += 1
|
|
try:
|
|
# Execute request
|
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
response_data = response.read()
|
|
response_text = response_data.decode("utf-8")
|
|
|
|
return {
|
|
"status": response.status,
|
|
"headers": dict(response.headers),
|
|
"text": response_text,
|
|
"json": lambda: json.loads(response_text) if response_text else None,
|
|
}
|
|
|
|
except urllib.error.HTTPError as e:
|
|
error_body = e.read()
|
|
error_text = error_body.decode("utf-8")
|
|
return {
|
|
"status": e.code,
|
|
"error": True,
|
|
"text": error_text,
|
|
"json": lambda: json.loads(error_text) if error_text else None,
|
|
}
|
|
except socket.timeout:
|
|
# Handle socket timeouts specifically
|
|
elapsed = time.time() - start_time
|
|
elapsed_minutes = int(elapsed // 60)
|
|
elapsed_seconds = elapsed % 60
|
|
duration_str = (
|
|
f"{elapsed_minutes}m {elapsed_seconds:.1f}s"
|
|
if elapsed_minutes > 0
|
|
else f"{elapsed_seconds:.1f}s"
|
|
)
|
|
|
|
logger.warning(
|
|
f"Request timed out (attempt {attempt}, "
|
|
f"duration: {duration_str}). Retrying in {attempt} second(s)..."
|
|
)
|
|
|
|
# Exponential backoff starting at 1 second
|
|
time.sleep(attempt)
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
# For other exceptions, check if they might be timeout-related
|
|
if "timed out" in error_msg.lower() or "timeout" in error_msg.lower():
|
|
elapsed = time.time() - start_time
|
|
elapsed_minutes = int(elapsed // 60)
|
|
elapsed_seconds = elapsed % 60
|
|
duration_str = (
|
|
f"{elapsed_minutes}m {elapsed_seconds:.1f}s"
|
|
if elapsed_minutes > 0
|
|
else f"{elapsed_seconds:.1f}s"
|
|
)
|
|
|
|
logger.warning(
|
|
f"Request timed out (attempt {attempt}, "
|
|
f"duration: {duration_str}). Retrying in {attempt} second(s)..."
|
|
)
|
|
|
|
# Exponential backoff starting at 1 second
|
|
time.sleep(attempt)
|
|
else:
|
|
# Non-timeout errors should not be retried
|
|
return {"error": True, "exception": error_msg}
|
|
|
|
def get(
|
|
self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 30.0
|
|
) -> Dict[str, Any]:
|
|
return self.request("GET", url, headers=headers, timeout=timeout)
|
|
|
|
def post(
|
|
self,
|
|
url: str,
|
|
headers: Optional[Dict[str, str]] = None,
|
|
data: Optional[bytes] = None,
|
|
json_data: Optional[Dict[str, Any]] = None,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
return self.request(
|
|
"POST", url, headers=headers, data=data, json_data=json_data, timeout=timeout
|
|
)
|
|
|
|
def set_default_headers(self, headers: Dict[str, str]):
|
|
self.session_headers.update(headers)
|
|
|
|
|
|
# Global client instance
|
|
http_client = SyncHTTPClient()
|