"""Iris parser implementation using the Vectorize Iris HTTP API.""" import asyncio import logging import mimetypes import time import httpx from .base import FileParser, UnsupportedFileTypeError logger = logging.getLogger(__name__) _IRIS_BASE_URL = "https://api.vectorize.io/v1" _DEFAULT_POLL_INTERVAL = 2.0 # seconds _DEFAULT_TIMEOUT = 300.0 # seconds class IrisParser(FileParser): """ Iris file parser using the Vectorize Iris cloud extraction service. Uploads files to the Vectorize Iris API, starts an extraction job, and polls until the text is ready. The API determines which file types are supported — UnsupportedFileTypeError is raised if the file is rejected. Authentication: Requires HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN and HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID environment variables, or pass them explicitly via the constructor. """ def __init__( self, token: str, org_id: str, poll_interval: float = _DEFAULT_POLL_INTERVAL, timeout: float = _DEFAULT_TIMEOUT, ): """ Initialize iris parser. Args: token: Vectorize API token org_id: Vectorize organization ID poll_interval: Seconds between status poll requests (default: 2) timeout: Maximum seconds to wait for extraction (default: 300) """ self._token = token self._org_id = org_id self._poll_interval = poll_interval self._timeout = timeout self._auth_headers = {"Authorization": f"Bearer {token}"} async def convert(self, file_data: bytes, filename: str) -> str: """ Parse file to text using the Vectorize Iris API. Raises: UnsupportedFileTypeError: If the Iris API rejects the file type (4xx) RuntimeError: If extraction fails for another reason """ content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, read=120.0)) as client: # Step 1: Request a presigned upload URL init_resp = await client.post( f"{_IRIS_BASE_URL}/org/{self._org_id}/files", headers=self._auth_headers, json={"name": filename, "contentType": content_type}, ) _raise_for_status(init_resp, filename, "file upload init") init_data = init_resp.json() file_id: str = init_data["fileId"] upload_url: str = init_data["uploadUrl"] # Step 2: Upload the file bytes to the presigned URL (no auth header) # Ensure file_data is plain bytes (GCS storage may return obstore.Bytes) upload_resp = await client.put( upload_url, content=bytes(file_data), headers={"Content-Type": content_type}, ) _raise_for_status(upload_resp, filename, "file upload") # Step 3: Start extraction extract_resp = await client.post( f"{_IRIS_BASE_URL}/org/{self._org_id}/extraction", headers=self._auth_headers, json={"fileId": file_id}, ) _raise_for_status(extract_resp, filename, "start extraction") extraction_id: str = extract_resp.json()["extractionId"] # Step 4: Poll until ready or timeout deadline = time.monotonic() + self._timeout while True: status_resp = await client.get( f"{_IRIS_BASE_URL}/org/{self._org_id}/extraction/{extraction_id}", headers=self._auth_headers, ) _raise_for_status(status_resp, filename, "poll extraction status") status_data = status_resp.json() if status_data.get("ready"): data = status_data.get("data", {}) if not data.get("success"): error = data.get("error", "unknown error") raise RuntimeError(f"Iris extraction failed for '{filename}': {error}") text = data.get("text") if not text: raise RuntimeError(f"No content extracted from '{filename}'") return text if time.monotonic() >= deadline: raise RuntimeError(f"Iris extraction timed out after {self._timeout}s for '{filename}'") await asyncio.sleep(self._poll_interval) def name(self) -> str: """Get parser name.""" return "iris" def _raise_for_status(response: httpx.Response, filename: str, step: str) -> None: """ Raise an appropriate error including the response body on HTTP errors. Raises UnsupportedFileTypeError for 4xx responses (file rejected by the API), RuntimeError for other HTTP errors. """ if not response.is_error: return body = response.text or "" msg = f"Iris API error during {step} for '{filename}': {response.status_code} {response.reason_phrase} — {body}" if response.is_client_error: raise UnsupportedFileTypeError(msg) raise RuntimeError(msg)