| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import json |
| import os |
| from typing import Any, Dict, Optional |
|
|
| import aiohttp |
|
|
| from ._core import ( |
| extract_api_message_from_payload, |
| extract_job_id, |
| raise_for_status, |
| unwrap_api_response, |
| ) |
| from .errors import ( |
| NetworkError, |
| RequestTimeoutError, |
| ResponseFormatError, |
| ResultParseError, |
| ) |
| from ._http import API_PATH, DEFAULT_BASE_URL |
|
|
|
|
| class AsyncHTTPClient: |
| def __init__( |
| self, |
| token: str, |
| base_url: str, |
| timeout: float, |
| client_platform: Optional[str] = None, |
| ): |
| self._token = token |
| self._base_url = base_url.rstrip("/") |
| self._jobs_url = f"{self._base_url}{API_PATH}" |
| self._timeout = timeout |
| self._client_platform = client_platform |
| self._session = None |
|
|
| @property |
| def timeout(self) -> float: |
| return self._timeout |
|
|
| async def __aenter__(self): |
| await self._ensure_session() |
| return self |
|
|
| async def __aexit__(self, *args): |
| await self.close() |
|
|
| async def _ensure_session(self): |
| if self._session is None: |
| self._session = aiohttp.ClientSession( |
| headers=self._api_headers(), |
| timeout=aiohttp.ClientTimeout(total=self._timeout), |
| ) |
|
|
| async def close(self): |
| if self._session: |
| await self._session.close() |
| self._session = None |
|
|
| def _api_headers(self) -> dict: |
| headers = {"Authorization": f"Bearer {self._token}"} |
| if self._client_platform: |
| headers["Client-Platform"] = self._client_platform |
| return headers |
|
|
| async def submit_url( |
| self, |
| model: str, |
| file_url: str, |
| optional_payload: dict, |
| page_ranges: Optional[str] = None, |
| batch_id: Optional[str] = None, |
| ) -> str: |
| body = { |
| "fileUrl": file_url, |
| "model": model, |
| "optionalPayload": optional_payload, |
| } |
| if page_ranges is not None: |
| body["pageRanges"] = page_ranges |
| if batch_id is not None: |
| body["batchId"] = batch_id |
|
|
| await self._ensure_session() |
| async with self._session.post( |
| self._jobs_url, |
| json=body, |
| headers={"Content-Type": "application/json"}, |
| ) as resp: |
| await self._raise_for_response(resp) |
| data = await self._response_data(resp) |
| return extract_job_id(data) |
|
|
| async def submit_file( |
| self, |
| model: str, |
| file_path: str, |
| optional_payload: dict, |
| page_ranges: Optional[str] = None, |
| batch_id: Optional[str] = None, |
| ) -> str: |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(file_path) |
|
|
| form = aiohttp.FormData() |
| form.add_field("model", model) |
| form.add_field("optionalPayload", json.dumps(optional_payload)) |
| if page_ranges is not None: |
| form.add_field("pageRanges", page_ranges) |
| if batch_id is not None: |
| form.add_field("batchId", batch_id) |
|
|
| with open(file_path, "rb") as f: |
| file_data = f.read() |
| form.add_field( |
| "file", |
| file_data, |
| filename=os.path.basename(file_path), |
| ) |
|
|
| await self._ensure_session() |
| async with self._session.post(self._jobs_url, data=form) as resp: |
| await self._raise_for_response(resp) |
| data = await self._response_data(resp) |
| return extract_job_id(data) |
|
|
| async def get_job_status(self, job_id: str) -> Dict[str, Any]: |
| await self._ensure_session() |
| async with self._session.get(f"{self._jobs_url}/{job_id}") as resp: |
| await self._raise_for_response(resp) |
| return await self._response_data(resp) |
|
|
| async def get_batch_status(self, batch_id: str) -> Dict[str, Any]: |
| await self._ensure_session() |
| async with self._session.get(f"{self._jobs_url}/batch/{batch_id}") as resp: |
| await self._raise_for_response(resp) |
| return await self._response_data(resp) |
|
|
| async def fetch_jsonl(self, url: str) -> list: |
| timeout = aiohttp.ClientTimeout(total=self._timeout) |
| async with aiohttp.ClientSession(timeout=timeout) as bare_session: |
| async with bare_session.get(url) as resp: |
| await self._raise_for_response(resp) |
| text = await resp.text() |
| try: |
| lines = text.strip().split("\n") |
| return [json.loads(line) for line in lines if line.strip()] |
| except json.JSONDecodeError as e: |
| raise ResultParseError( |
| f"Malformed JSONL result payload: {e}" |
| ) from e |
|
|
| async def _raise_for_response(self, resp) -> None: |
| if 200 <= resp.status < 300: |
| return |
| try: |
| body = await resp.json() |
| msg = ( |
| extract_api_message_from_payload(body) |
| if isinstance(body, dict) |
| else None |
| ) |
| if not msg: |
| msg = await resp.text() |
| except Exception: |
| msg = await resp.text() |
| raise_for_status(resp.status, msg) |
|
|
| async def _response_data(self, resp) -> Dict[str, Any]: |
| try: |
| payload = await resp.json() |
| except Exception as e: |
| raise ResponseFormatError(f"Response body is not valid JSON: {e}") from e |
| return unwrap_api_response(payload, resp.status) |
|
|