cohere-parse / parser_client.py
dennis-zhao's picture
Migrate to the Parse API
270d701 verified
Raw
History Blame Contribute Delete
5.51 kB
from __future__ import annotations
import base64
import os
import time
from dataclasses import dataclass
import requests
from parser_output import shape_parse_response
DEFAULT_ENVIRONMENT = "staging"
ENVIRONMENT_CONFIG = {
"production": ("COHERE_API_KEY", "https://api.cohere.com/v2/parse"),
"staging": ("COHERE_STG_API_KEY", "https://stg.api.cohere.ai/v2/parse"),
}
DEFAULT_MODEL = "parse-v5.0"
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
class ParserServiceError(RuntimeError):
"""A safe parser-service error that may be displayed to a user."""
def selected_environment() -> str:
environment = os.environ.get("COHERE_ENV", DEFAULT_ENVIRONMENT).strip().lower()
if environment not in ENVIRONMENT_CONFIG:
raise ParserServiceError(
"COHERE_ENV must be either 'staging' or 'production'."
)
return environment
def api_key_name_for_environment(environment: str | None = None) -> str:
selected = environment or selected_environment()
try:
return ENVIRONMENT_CONFIG[selected][0]
except KeyError as error:
raise ParserServiceError(
"COHERE_ENV must be either 'staging' or 'production'."
) from error
@dataclass(frozen=True)
class ParserConfig:
api_url: str
api_key: str
model: str
timeout: float
max_retries: int = 4
@classmethod
def from_environment(cls) -> "ParserConfig":
environment = selected_environment()
api_key_name, default_api_url = ENVIRONMENT_CONFIG[environment]
api_key = os.environ.get(api_key_name, "").strip()
if not api_key:
raise ParserServiceError(
f"Cohere Parse is not configured for {environment}."
)
return cls(
api_url=os.environ.get("COHERE_API_URL", "").strip()
or default_api_url,
api_key=api_key,
model=os.environ.get("COHERE_MODEL", DEFAULT_MODEL).strip(),
timeout=float(os.environ.get("COHERE_TIMEOUT", "300")),
)
def build_payload(model: str, png: bytes) -> dict:
encoded = base64.b64encode(png).decode("ascii")
return {
"model": model,
"document": {
"type": "image_url",
"image_url": f"data:image/png;base64,{encoded}",
},
"output_format": "blocks",
}
class ParserClient:
def __init__(
self,
config: ParserConfig | None = None,
*,
session: requests.Session | None = None,
sleeper=time.sleep,
) -> None:
self.config = config or ParserConfig.from_environment()
self.session = session or requests.Session()
self.sleeper = sleeper
def _retry_delay(self, response: requests.Response, attempt: int) -> float:
header = response.headers.get("Retry-After", "")
try:
return min(60.0, max(0.0, float(header)))
except ValueError:
return min(60.0, 2.0 ** (attempt + 1))
def parse_page(self, png: bytes) -> dict:
started = time.perf_counter()
headers = {
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {self.config.api_key}",
}
payload = build_payload(self.config.model, png)
for attempt in range(self.config.max_retries):
try:
response = self.session.post(
self.config.api_url,
headers=headers,
json=payload,
timeout=self.config.timeout,
)
except (requests.Timeout, requests.ConnectionError) as exc:
if attempt + 1 < self.config.max_retries:
self.sleeper(min(60.0, 2.0 ** (attempt + 1)))
continue
raise ParserServiceError(
"Cohere Parse could not be reached. Try again shortly."
) from exc
request_id = (
response.headers.get("x-request-id")
or response.headers.get("request-id")
)
if response.status_code in RETRYABLE_STATUSES:
if attempt + 1 < self.config.max_retries:
self.sleeper(self._retry_delay(response, attempt))
continue
raise ParserServiceError(
f"Cohere Parse is temporarily unavailable ({response.status_code})."
)
if not response.ok:
raise ParserServiceError(
f"Cohere Parse rejected the request ({response.status_code})."
)
try:
body = response.json()
except requests.JSONDecodeError as exc:
raise ParserServiceError(
"Cohere Parse returned an invalid response."
) from exc
try:
parsed = shape_parse_response(body)
except (TypeError, ValueError) as exc:
raise ParserServiceError(
"Cohere Parse returned an invalid response."
) from exc
return {
**parsed,
"elapsed_ms": round((time.perf_counter() - started) * 1000),
"model": self.config.model,
"usage": body.get("meta"),
"request_id": request_id or body.get("id"),
}
raise ParserServiceError("Cohere Parse is temporarily unavailable.")