Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 67 additions & 29 deletions src/infrastructure/bricks/bricks_api_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import json
import logging
import os
import random
import re
import time
import urllib.error
import urllib.parse
import urllib.request
Expand All @@ -16,6 +18,8 @@
logger = logging.getLogger(__name__)

_DEFAULT_TIMEOUT = 30
_MAX_RETRIES = 3
_RETRY_BACKOFF = 2


class BricksApiAdapter(BricksApiPort):
Expand All @@ -27,39 +31,67 @@ def __init__(self, config) -> None:
async def close(self) -> None:
pass

@staticmethod
def _is_retryable(exc: Exception) -> bool:
if isinstance(exc, urllib.error.HTTPError):
return exc.code >= 500
if isinstance(exc, (urllib.error.URLError, ConnectionError, TimeoutError, OSError)):
return True
return False

def _get(self, url: str, headers: dict | None = None) -> tuple[bytes, dict]:
logger.debug("GET %s", url)
req = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=_DEFAULT_TIMEOUT) as resp:
body = resp.read()
resp_headers = dict(resp.headers)
logger.debug("GET %s -> %d bytes (status=%s)", url, len(body), resp.status)
return body, resp_headers
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""
logger.error("GET %s -> HTTP %d: %s", url, e.code, error_body[:500])
raise
except Exception as e:
logger.error("GET %s -> error: %s", url, e)
raise
logger.info("GET %s", url)
for attempt in range(1, _MAX_RETRIES + 1):
try:
req = urllib.request.Request(url, headers=headers or {})
with urllib.request.urlopen(req, timeout=_DEFAULT_TIMEOUT) as resp:
body = resp.read()
resp_headers = dict(resp.headers)
logger.info("GET %s -> %d bytes (status=%s)", url, len(body), resp.status)
return body, resp_headers
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""
if self._is_retryable(e) and attempt < _MAX_RETRIES:
logger.warning("GET %s -> HTTP %d (attempt %d/%d), retrying", url, e.code, attempt, _MAX_RETRIES)
time.sleep(_RETRY_BACKOFF ** attempt + random.uniform(0, 1))
continue
logger.exception("GET %s -> HTTP %d: %s", url, e.code, error_body[:500])
raise
except Exception as e:
if self._is_retryable(e) and attempt < _MAX_RETRIES:
logger.warning("GET %s -> error (attempt %d/%d): %s, retrying", url, attempt, _MAX_RETRIES, e)
time.sleep(_RETRY_BACKOFF ** attempt + random.uniform(0, 1))
continue
logger.exception("GET %s -> error: %s", url, e)
raise
raise RuntimeError(f"GET {url} failed after {_MAX_RETRIES} retries")

def _post(self, url: str, payload: dict, headers: dict) -> bytes:
data = json.dumps(payload).encode("utf-8")
logger.debug("POST %s (body=%d bytes)", url, len(data))
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=_DEFAULT_TIMEOUT) as resp:
body = resp.read()
logger.debug("POST %s -> %d bytes (status=%s)", url, len(body), resp.status)
return body
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""
logger.error("POST %s -> HTTP %d: %s", url, e.code, error_body[:500])
raise
except Exception as e:
logger.error("POST %s -> error: %s", url, e)
raise
logger.info("POST %s (body=%d bytes)", url, len(data))
for attempt in range(1, _MAX_RETRIES + 1):
try:
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=_DEFAULT_TIMEOUT) as resp:
body = resp.read()
logger.info("POST %s -> %d bytes (status=%s)", url, len(body), resp.status)
return body
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""
if self._is_retryable(e) and attempt < _MAX_RETRIES:
logger.warning("POST %s -> HTTP %d (attempt %d/%d), retrying", url, e.code, attempt, _MAX_RETRIES)
time.sleep(_RETRY_BACKOFF ** attempt + random.uniform(0, 1))
continue
logger.exception("POST %s -> HTTP %d: %s", url, e.code, error_body[:500])
raise
except Exception as e:
if self._is_retryable(e) and attempt < _MAX_RETRIES:
logger.warning("POST %s -> error (attempt %d/%d): %s, retrying", url, attempt, _MAX_RETRIES, e)
time.sleep(_RETRY_BACKOFF ** attempt + random.uniform(0, 1))
continue
logger.exception("POST %s -> error: %s", url, e)
raise
raise RuntimeError(f"POST {url} failed after {_MAX_RETRIES} retries")

async def list_project_documents(self, project_id: str) -> list[BricksDocumentInfo]:
url = f"{self._base_url}/api/projects/{project_id}/documents/ai"
Expand All @@ -77,6 +109,8 @@ async def list_project_documents(self, project_id: str) -> list[BricksDocumentIn
raise FileNotFoundError(
f"Bricks project not found: {project_id}"
) from e
if e.code == 429:
logger.warning("Bricks API rate limited (HTTP 429) for project %s", project_id)
raise RuntimeError(f"Bricks API error (HTTP {e.code})") from e
except urllib.error.URLError as e:
raise ConnectionError(f"Bricks API connection failed: {e.reason}") from e
Expand Down Expand Up @@ -116,6 +150,8 @@ async def download_document(
raise FileNotFoundError(
f"Document {document_id} not found (project {project_id})"
) from e
if e.code == 429:
logger.warning("Bricks API rate limited (HTTP 429) for document %s download", document_id)
raise RuntimeError(
f"Failed to download document (HTTP {e.code})"
) from e
Expand Down Expand Up @@ -148,6 +184,8 @@ async def publish_section_version(self, payload: dict) -> SectionVersionResult:
raise PermissionError(
f"Publish authentication failed (HTTP {e.code})"
) from e
if e.code == 429:
logger.warning("Bricks API rate limited (HTTP 429) for publish: project=%s section=%s", payload.get("projectUniqueId"), payload.get("sectionKey"))
raise RuntimeError(f"Publish failed (HTTP {e.code})") from e
except urllib.error.URLError as e:
raise ConnectionError(f"Publish connection failed: {e.reason}") from e
Expand Down
Loading
Loading