openparser-sdk 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openparser/__init__.py +54 -0
- openparser/_files.py +43 -0
- openparser/_generated/__init__.py +8 -0
- openparser/_generated/api/__init__.py +1 -0
- openparser/_generated/api/extract/__init__.py +1 -0
- openparser/_generated/api/extract/extract_async.py +280 -0
- openparser/_generated/api/extract/extract_batch.py +284 -0
- openparser/_generated/api/extract/extract_sync.py +315 -0
- openparser/_generated/api/extract/suggest_schema.py +260 -0
- openparser/_generated/api/files/__init__.py +1 -0
- openparser/_generated/api/files/create_file.py +237 -0
- openparser/_generated/api/files/delete_file.py +200 -0
- openparser/_generated/api/files/get_file.py +200 -0
- openparser/_generated/api/files/get_file_content.py +198 -0
- openparser/_generated/api/jobs/__init__.py +1 -0
- openparser/_generated/api/jobs/get_job.py +261 -0
- openparser/_generated/api/jobs/get_job_result.py +305 -0
- openparser/_generated/api/jobs/get_job_source.py +233 -0
- openparser/_generated/api/jobs/list_jobs.py +340 -0
- openparser/_generated/api/models/__init__.py +1 -0
- openparser/_generated/api/models/list_llm_models.py +288 -0
- openparser/_generated/api/models/list_ocr_models.py +184 -0
- openparser/_generated/api/parse/__init__.py +1 -0
- openparser/_generated/api/parse/parse_async.py +316 -0
- openparser/_generated/api/parse/parse_batch.py +280 -0
- openparser/_generated/api/parse/parse_sync.py +369 -0
- openparser/_generated/api/pipelines/__init__.py +1 -0
- openparser/_generated/api/pipelines/create_extraction_pipeline.py +242 -0
- openparser/_generated/api/pipelines/delete_extraction_pipeline.py +212 -0
- openparser/_generated/api/pipelines/get_extraction_pipeline.py +212 -0
- openparser/_generated/api/pipelines/list_extraction_pipelines.py +180 -0
- openparser/_generated/api/pipelines/update_extraction_pipeline.py +258 -0
- openparser/_generated/client.py +270 -0
- openparser/_generated/errors.py +14 -0
- openparser/_generated/models/__init__.py +261 -0
- openparser/_generated/models/batch_child_page.py +97 -0
- openparser/_generated/models/batch_child_summary.py +195 -0
- openparser/_generated/models/batch_job_accepted.py +139 -0
- openparser/_generated/models/batch_job_accepted_operation.py +8 -0
- openparser/_generated/models/batch_summary_counts.py +97 -0
- openparser/_generated/models/bounding_box.py +82 -0
- openparser/_generated/models/chunk_provenance_span.py +97 -0
- openparser/_generated/models/content_kind.py +10 -0
- openparser/_generated/models/create_extraction_pipeline_request.py +177 -0
- openparser/_generated/models/create_extraction_pipeline_request_grounding.py +8 -0
- openparser/_generated/models/create_extraction_pipeline_request_llm_options.py +84 -0
- openparser/_generated/models/create_extraction_pipeline_request_llm_options_reasoning_effort_type_1.py +13 -0
- openparser/_generated/models/create_extraction_pipeline_request_ocr_options.py +78 -0
- openparser/_generated/models/create_extraction_pipeline_request_schema.py +68 -0
- openparser/_generated/models/create_file_body.py +78 -0
- openparser/_generated/models/delete_extraction_pipeline_response.py +66 -0
- openparser/_generated/models/delete_file_response.py +57 -0
- openparser/_generated/models/error_body.py +106 -0
- openparser/_generated/models/error_body_details.py +65 -0
- openparser/_generated/models/error_response.py +65 -0
- openparser/_generated/models/extract_async_body.py +136 -0
- openparser/_generated/models/extract_batch_body.py +143 -0
- openparser/_generated/models/extract_batch_item.py +207 -0
- openparser/_generated/models/extract_batch_item_grounding.py +8 -0
- openparser/_generated/models/extract_batch_item_llm_options.py +84 -0
- openparser/_generated/models/extract_batch_item_llm_options_reasoning_effort_type_1.py +13 -0
- openparser/_generated/models/extract_batch_item_ocr_options.py +78 -0
- openparser/_generated/models/extract_batch_item_schema.py +65 -0
- openparser/_generated/models/extract_batch_request.py +99 -0
- openparser/_generated/models/extract_batch_request_items_item.py +201 -0
- openparser/_generated/models/extract_batch_request_items_item_grounding.py +8 -0
- openparser/_generated/models/extract_batch_request_items_item_llm_options.py +84 -0
- openparser/_generated/models/extract_batch_request_items_item_llm_options_reasoning_effort_type_1.py +13 -0
- openparser/_generated/models/extract_batch_request_items_item_ocr_options.py +78 -0
- openparser/_generated/models/extract_batch_request_items_item_schema.py +65 -0
- openparser/_generated/models/extract_batch_request_output_format.py +8 -0
- openparser/_generated/models/extract_request.py +226 -0
- openparser/_generated/models/extract_request_grounding.py +8 -0
- openparser/_generated/models/extract_request_llm_options.py +84 -0
- openparser/_generated/models/extract_request_llm_options_reasoning_effort_type_1.py +13 -0
- openparser/_generated/models/extract_request_ocr_options.py +78 -0
- openparser/_generated/models/extract_request_output_format.py +8 -0
- openparser/_generated/models/extract_request_schema.py +65 -0
- openparser/_generated/models/extract_sync_body.py +136 -0
- openparser/_generated/models/extraction_attempt.py +122 -0
- openparser/_generated/models/extraction_attempt_kind.py +8 -0
- openparser/_generated/models/extraction_attempt_status.py +9 -0
- openparser/_generated/models/extraction_chunk.py +184 -0
- openparser/_generated/models/extraction_citation.py +169 -0
- openparser/_generated/models/extraction_citation_granularity.py +8 -0
- openparser/_generated/models/extraction_citation_source_type.py +16 -0
- openparser/_generated/models/extraction_grounding_field.py +102 -0
- openparser/_generated/models/extraction_grounding_mode.py +8 -0
- openparser/_generated/models/extraction_grounding_result.py +87 -0
- openparser/_generated/models/extraction_pipeline.py +201 -0
- openparser/_generated/models/extraction_pipeline_list_response.py +76 -0
- openparser/_generated/models/extraction_pipeline_llm_options.py +84 -0
- openparser/_generated/models/extraction_pipeline_llm_options_reasoning_effort_type_0_type_1.py +13 -0
- openparser/_generated/models/extraction_pipeline_ocr_options.py +73 -0
- openparser/_generated/models/extraction_pipeline_schema.py +67 -0
- openparser/_generated/models/extraction_terminal_result.py +206 -0
- openparser/_generated/models/extraction_terminal_result_reasoning_effort_type_0.py +13 -0
- openparser/_generated/models/extraction_usage_totals.py +80 -0
- openparser/_generated/models/job.py +437 -0
- openparser/_generated/models/job_accepted.py +131 -0
- openparser/_generated/models/job_accepted_operation.py +8 -0
- openparser/_generated/models/job_extraction_schema.py +65 -0
- openparser/_generated/models/job_failure.py +98 -0
- openparser/_generated/models/job_failure_details.py +65 -0
- openparser/_generated/models/job_list_response.py +91 -0
- openparser/_generated/models/job_operation.py +10 -0
- openparser/_generated/models/job_progress.py +68 -0
- openparser/_generated/models/job_related_extractions_item.py +145 -0
- openparser/_generated/models/job_status.py +11 -0
- openparser/_generated/models/job_summary.py +264 -0
- openparser/_generated/models/json_schema_object.py +66 -0
- openparser/_generated/models/list_llm_models_mode.py +8 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry.py +257 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry_pricing.py +76 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0.py +134 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0_default_effort_type_0.py +13 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0_supported_efforts_type_0_item.py +13 -0
- openparser/_generated/models/ocr_llm_model_catalog_entry_recommendation.py +8 -0
- openparser/_generated/models/ocr_llm_models_response.py +150 -0
- openparser/_generated/models/ocr_llm_models_response_mode.py +8 -0
- openparser/_generated/models/ocr_model.py +7 -0
- openparser/_generated/models/ocr_model_catalog_entry.py +131 -0
- openparser/_generated/models/ocr_model_catalog_entry_availability.py +9 -0
- openparser/_generated/models/ocr_model_catalog_entry_capabilities.py +97 -0
- openparser/_generated/models/ocr_model_catalog_entry_capabilities_options.py +73 -0
- openparser/_generated/models/ocr_model_catalog_entry_option_defaults.py +73 -0
- openparser/_generated/models/ocr_model_catalog_entry_pricing.py +68 -0
- openparser/_generated/models/ocr_models_response.py +76 -0
- openparser/_generated/models/ocr_output_format.py +8 -0
- openparser/_generated/models/paddle_raw_profile.py +76 -0
- openparser/_generated/models/paddle_raw_profile_options.py +115 -0
- openparser/_generated/models/page_block.py +332 -0
- openparser/_generated/models/page_block_kind.py +9 -0
- openparser/_generated/models/page_block_polygon_type_0_item.py +65 -0
- openparser/_generated/models/parse_async_body.py +133 -0
- openparser/_generated/models/parse_batch_body.py +143 -0
- openparser/_generated/models/parse_batch_item.py +109 -0
- openparser/_generated/models/parse_batch_item_ocr_options.py +78 -0
- openparser/_generated/models/parse_batch_request.py +100 -0
- openparser/_generated/models/parse_batch_request_items_item.py +108 -0
- openparser/_generated/models/parse_batch_request_items_item_ocr_options.py +78 -0
- openparser/_generated/models/parse_batch_request_output_format.py +8 -0
- openparser/_generated/models/parse_request.py +112 -0
- openparser/_generated/models/parse_request_ocr_options.py +78 -0
- openparser/_generated/models/parse_request_output_format.py +8 -0
- openparser/_generated/models/parse_sync_body.py +133 -0
- openparser/_generated/models/parsed_document.py +187 -0
- openparser/_generated/models/public_file.py +117 -0
- openparser/_generated/models/raw_parse_result.py +110 -0
- openparser/_generated/models/raw_parse_result_result.py +65 -0
- openparser/_generated/models/region.py +224 -0
- openparser/_generated/models/region_content.py +155 -0
- openparser/_generated/models/region_polygon_type_0_item.py +65 -0
- openparser/_generated/models/region_type.py +16 -0
- openparser/_generated/models/suggest_schema_request.py +69 -0
- openparser/_generated/models/suggest_schema_response.py +74 -0
- openparser/_generated/models/suggest_schema_response_schema.py +66 -0
- openparser/_generated/models/update_extraction_pipeline_request.py +227 -0
- openparser/_generated/models/update_extraction_pipeline_request_grounding.py +8 -0
- openparser/_generated/models/update_extraction_pipeline_request_llm_options_type_0.py +84 -0
- openparser/_generated/models/update_extraction_pipeline_request_llm_options_type_0_reasoning_effort_type_1.py +13 -0
- openparser/_generated/models/update_extraction_pipeline_request_ocr_options_type_0.py +78 -0
- openparser/_generated/models/update_extraction_pipeline_request_schema.py +67 -0
- openparser/_generated/py.typed +1 -0
- openparser/_generated/types.py +53 -0
- openparser/_telemetry.py +34 -0
- openparser/client.py +541 -0
- openparser/errors.py +172 -0
- openparser/py.typed +0 -0
- openparser_sdk-0.0.1.dist-info/METADATA +178 -0
- openparser_sdk-0.0.1.dist-info/RECORD +173 -0
- openparser_sdk-0.0.1.dist-info/WHEEL +4 -0
- openparser_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
openparser/client.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""Hand-written Python facade for the OpenParser API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from email.utils import parsedate_to_datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, BinaryIO, Literal, Optional, Sequence, Union
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from openparser._files import to_upload_tuple
|
|
19
|
+
from openparser._telemetry import build_telemetry_headers
|
|
20
|
+
from openparser.errors import (
|
|
21
|
+
OpenParserError,
|
|
22
|
+
OpenParserTimeoutError,
|
|
23
|
+
error_from_response,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.openparser.dev"
|
|
27
|
+
DEFAULT_TIMEOUT_SECONDS = 300.0
|
|
28
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 2.0
|
|
29
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS = 300.0
|
|
30
|
+
TERMINAL_JOB_STATUSES = frozenset({"succeeded", "failed", "indeterminate", "partial"})
|
|
31
|
+
|
|
32
|
+
FileInput = Union[Path, dict[str, Any], BinaryIO]
|
|
33
|
+
ParseResultFormat = Literal["openparser@1", "raw"]
|
|
34
|
+
LlmModelsMode = Literal["suggested", "search"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AttrDict(dict[str, Any]):
|
|
38
|
+
def __getattr__(self, name: str) -> Any:
|
|
39
|
+
try:
|
|
40
|
+
return self[name]
|
|
41
|
+
except KeyError as exc:
|
|
42
|
+
raise AttributeError(name) from exc
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _to_attr(value: Any) -> Any:
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
return AttrDict({key: _to_attr(item) for key, item in value.items()})
|
|
48
|
+
if isinstance(value, list):
|
|
49
|
+
return [_to_attr(item) for item in value]
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _headers(api_key: str, default_headers: Optional[dict[str, str]]) -> dict[str, str]:
|
|
54
|
+
return {
|
|
55
|
+
"Authorization": f"Bearer {api_key}",
|
|
56
|
+
**build_telemetry_headers(),
|
|
57
|
+
**(default_headers or {}),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _idempotency_key(value: Optional[str]) -> str:
|
|
62
|
+
return value or str(uuid.uuid4())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _assert_json_response(response: httpx.Response) -> None:
|
|
66
|
+
if response.status_code == 204:
|
|
67
|
+
return
|
|
68
|
+
content_type = (response.headers.get("content-type") or "").lower()
|
|
69
|
+
if content_type == "" or "json" in content_type or "octet-stream" in content_type:
|
|
70
|
+
return
|
|
71
|
+
raise OpenParserError(
|
|
72
|
+
f'Expected a JSON response from the API but got Content-Type "{content_type}". '
|
|
73
|
+
'Set `base_url` to your OpenParser API root, e.g. "https://api.openparser.dev".',
|
|
74
|
+
status=response.status_code,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_json_error(response: httpx.Response) -> Optional[dict[str, Any]]:
|
|
79
|
+
try:
|
|
80
|
+
parsed = response.json()
|
|
81
|
+
except Exception:
|
|
82
|
+
return None
|
|
83
|
+
return parsed if isinstance(parsed, dict) else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _backoff(attempt: int) -> float:
|
|
87
|
+
return 0.25 * (2**attempt)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
91
|
+
"""Parse Retry-After as nonnegative delta-seconds or HTTP-date.
|
|
92
|
+
|
|
93
|
+
Returns delay seconds, or None when the header is missing/malformed/negative
|
|
94
|
+
so callers can fall back to exponential backoff.
|
|
95
|
+
"""
|
|
96
|
+
if not value:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
seconds = float(value)
|
|
101
|
+
except ValueError:
|
|
102
|
+
seconds = None
|
|
103
|
+
else:
|
|
104
|
+
if math.isfinite(seconds) and seconds >= 0:
|
|
105
|
+
return seconds
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
when = parsedate_to_datetime(value)
|
|
110
|
+
except (TypeError, ValueError, IndexError, OverflowError):
|
|
111
|
+
return None
|
|
112
|
+
if when.tzinfo is None:
|
|
113
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
114
|
+
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _retry_delay(response: httpx.Response, attempt: int) -> float:
|
|
118
|
+
parsed = _parse_retry_after(response.headers.get("retry-after"))
|
|
119
|
+
return parsed if parsed is not None else _backoff(attempt)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _check_response(response: httpx.Response) -> Any:
|
|
123
|
+
_assert_json_response(response)
|
|
124
|
+
if 200 <= response.status_code < 300:
|
|
125
|
+
if response.status_code == 204 or not response.content:
|
|
126
|
+
return None
|
|
127
|
+
return _to_attr(response.json())
|
|
128
|
+
|
|
129
|
+
parsed = _parse_retry_after(response.headers.get("retry-after"))
|
|
130
|
+
retry_after = int(parsed) if parsed is not None else None
|
|
131
|
+
raise error_from_response(
|
|
132
|
+
response.status_code, _parse_json_error(response), retry_after
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _is_retriable(status: int) -> bool:
|
|
137
|
+
return status >= 500 or status == 429
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
SAFE_HTTP_METHODS = frozenset({"GET", "HEAD"})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _header_value(headers: Any, name: str) -> Optional[str]:
|
|
144
|
+
if headers is None:
|
|
145
|
+
return None
|
|
146
|
+
target = name.lower()
|
|
147
|
+
if isinstance(headers, dict):
|
|
148
|
+
for key, value in headers.items():
|
|
149
|
+
if str(key).lower() == target and value is not None:
|
|
150
|
+
return value if isinstance(value, str) else str(value)
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
value = headers.get(name)
|
|
154
|
+
except Exception:
|
|
155
|
+
return None
|
|
156
|
+
if value is None:
|
|
157
|
+
return None
|
|
158
|
+
return value if isinstance(value, str) else str(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_retryable_request(method: str, headers: Any = None) -> bool:
|
|
162
|
+
"""Whether an ambiguous failure may be retried for this request.
|
|
163
|
+
|
|
164
|
+
Safe reads (GET/HEAD) always retry. Mutations retry only when an
|
|
165
|
+
Idempotency-Key is present so the server can dedupe (parse/extract
|
|
166
|
+
admission). Unprotected mutations fail fast to avoid duplicate writes.
|
|
167
|
+
"""
|
|
168
|
+
if method.upper() in SAFE_HTTP_METHODS:
|
|
169
|
+
return True
|
|
170
|
+
key = _header_value(headers, "Idempotency-Key")
|
|
171
|
+
return bool(key and key.strip())
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class OpenParserClient:
|
|
175
|
+
"""Top-level OpenParser API client."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
api_key: Optional[str] = None,
|
|
181
|
+
base_url: Optional[str] = None,
|
|
182
|
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
|
183
|
+
max_retries: int = 3,
|
|
184
|
+
default_headers: Optional[dict[str, str]] = None,
|
|
185
|
+
httpx_args: Optional[dict[str, Any]] = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
resolved_key = api_key or os.getenv("OPENPARSER_API_KEY")
|
|
188
|
+
if not resolved_key:
|
|
189
|
+
raise OpenParserError(
|
|
190
|
+
"Missing API key. Pass `OpenParserClient(api_key=...)` or set OPENPARSER_API_KEY.",
|
|
191
|
+
status=0,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self.base_url = (
|
|
195
|
+
base_url or os.getenv("OPENPARSER_BASE_URL") or DEFAULT_BASE_URL
|
|
196
|
+
).rstrip("/")
|
|
197
|
+
self.timeout_seconds = timeout_seconds
|
|
198
|
+
self.max_retries = max_retries
|
|
199
|
+
self._http = httpx.Client(
|
|
200
|
+
base_url=self.base_url,
|
|
201
|
+
timeout=timeout_seconds,
|
|
202
|
+
headers=_headers(resolved_key, default_headers),
|
|
203
|
+
**(httpx_args or {}),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
self.parse = ParseResource(self)
|
|
207
|
+
self.extract = ExtractResource(self)
|
|
208
|
+
self.jobs = JobsResource(self)
|
|
209
|
+
self.files = FilesResource(self)
|
|
210
|
+
self.models = ModelsResource(self)
|
|
211
|
+
self.pipelines = PipelinesResource(self)
|
|
212
|
+
|
|
213
|
+
def close(self) -> None:
|
|
214
|
+
self._http.close()
|
|
215
|
+
|
|
216
|
+
def __enter__(self) -> "OpenParserClient":
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def __exit__(self, *_: object) -> None:
|
|
220
|
+
self.close()
|
|
221
|
+
|
|
222
|
+
def _send_with_retries(
|
|
223
|
+
self, method: str, url: str, **kwargs: Any
|
|
224
|
+
) -> httpx.Response:
|
|
225
|
+
may_retry = _is_retryable_request(method, kwargs.get("headers"))
|
|
226
|
+
for attempt in range(self.max_retries + 1):
|
|
227
|
+
try:
|
|
228
|
+
response = self._http.request(method, url, **kwargs)
|
|
229
|
+
if (
|
|
230
|
+
may_retry
|
|
231
|
+
and _is_retriable(response.status_code)
|
|
232
|
+
and attempt < self.max_retries
|
|
233
|
+
):
|
|
234
|
+
time.sleep(_retry_delay(response, attempt))
|
|
235
|
+
continue
|
|
236
|
+
return response
|
|
237
|
+
except httpx.TimeoutException as exc:
|
|
238
|
+
if may_retry and attempt < self.max_retries:
|
|
239
|
+
time.sleep(_backoff(attempt))
|
|
240
|
+
continue
|
|
241
|
+
raise OpenParserTimeoutError() from exc
|
|
242
|
+
except httpx.TransportError:
|
|
243
|
+
if may_retry and attempt < self.max_retries:
|
|
244
|
+
time.sleep(_backoff(attempt))
|
|
245
|
+
continue
|
|
246
|
+
raise
|
|
247
|
+
|
|
248
|
+
raise AssertionError("unreachable")
|
|
249
|
+
|
|
250
|
+
def _request(self, method: str, url: str, **kwargs: Any) -> Any:
|
|
251
|
+
return _check_response(self._send_with_retries(method, url, **kwargs))
|
|
252
|
+
|
|
253
|
+
def _request_bytes(self, method: str, url: str, **kwargs: Any) -> bytes:
|
|
254
|
+
response = self._send_with_retries(method, url, **kwargs)
|
|
255
|
+
if response.status_code >= 400:
|
|
256
|
+
_check_response(response)
|
|
257
|
+
return response.content
|
|
258
|
+
|
|
259
|
+
def wait_for_job(
|
|
260
|
+
self,
|
|
261
|
+
job_id: str,
|
|
262
|
+
*,
|
|
263
|
+
timeout_seconds: float = DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
264
|
+
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
265
|
+
) -> Any:
|
|
266
|
+
deadline = time.monotonic() + timeout_seconds
|
|
267
|
+
while time.monotonic() < deadline:
|
|
268
|
+
job = self.jobs.get(job_id)
|
|
269
|
+
status = (
|
|
270
|
+
job.get("status")
|
|
271
|
+
if isinstance(job, dict)
|
|
272
|
+
else getattr(job, "status", None)
|
|
273
|
+
)
|
|
274
|
+
if status in TERMINAL_JOB_STATUSES:
|
|
275
|
+
return job
|
|
276
|
+
time.sleep(poll_interval_seconds)
|
|
277
|
+
raise OpenParserTimeoutError(
|
|
278
|
+
f"Job {job_id} did not finish within {timeout_seconds:g}s"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ParseResource:
|
|
283
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
284
|
+
self._root = root
|
|
285
|
+
|
|
286
|
+
def sync(
|
|
287
|
+
self,
|
|
288
|
+
request: dict[str, Any],
|
|
289
|
+
*,
|
|
290
|
+
file: Optional[FileInput] = None,
|
|
291
|
+
idempotency_key: Optional[str] = None,
|
|
292
|
+
) -> Any:
|
|
293
|
+
return self._submit(
|
|
294
|
+
"/parse", request, file=file, idempotency_key=idempotency_key
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def async_(
|
|
298
|
+
self,
|
|
299
|
+
request: dict[str, Any],
|
|
300
|
+
*,
|
|
301
|
+
file: Optional[FileInput] = None,
|
|
302
|
+
idempotency_key: Optional[str] = None,
|
|
303
|
+
) -> Any:
|
|
304
|
+
return self._submit(
|
|
305
|
+
"/parse/async", request, file=file, idempotency_key=idempotency_key
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def batch(
|
|
309
|
+
self,
|
|
310
|
+
request: dict[str, Any],
|
|
311
|
+
*,
|
|
312
|
+
files: Sequence[FileInput],
|
|
313
|
+
idempotency_key: Optional[str] = None,
|
|
314
|
+
) -> Any:
|
|
315
|
+
multipart: list[tuple[str, Any]] = [
|
|
316
|
+
("request", (None, json.dumps(request), "application/json")),
|
|
317
|
+
]
|
|
318
|
+
for item in files:
|
|
319
|
+
multipart.append(("files", to_upload_tuple(item)))
|
|
320
|
+
return self._root._request(
|
|
321
|
+
"POST",
|
|
322
|
+
"/parse/batch",
|
|
323
|
+
files=multipart,
|
|
324
|
+
headers={"Idempotency-Key": _idempotency_key(idempotency_key)},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _submit(
|
|
328
|
+
self,
|
|
329
|
+
path: str,
|
|
330
|
+
request: dict[str, Any],
|
|
331
|
+
*,
|
|
332
|
+
file: Optional[FileInput],
|
|
333
|
+
idempotency_key: Optional[str],
|
|
334
|
+
) -> Any:
|
|
335
|
+
multipart: dict[str, Any] = {
|
|
336
|
+
"request": (None, json.dumps(request), "application/json"),
|
|
337
|
+
}
|
|
338
|
+
if file is not None:
|
|
339
|
+
multipart["file"] = to_upload_tuple(file)
|
|
340
|
+
return self._root._request(
|
|
341
|
+
"POST",
|
|
342
|
+
path,
|
|
343
|
+
files=multipart,
|
|
344
|
+
headers={"Idempotency-Key": _idempotency_key(idempotency_key)},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class ExtractResource:
|
|
349
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
350
|
+
self._root = root
|
|
351
|
+
|
|
352
|
+
def sync(
|
|
353
|
+
self,
|
|
354
|
+
request: dict[str, Any],
|
|
355
|
+
*,
|
|
356
|
+
file: Optional[FileInput] = None,
|
|
357
|
+
idempotency_key: Optional[str] = None,
|
|
358
|
+
) -> Any:
|
|
359
|
+
return self._submit(
|
|
360
|
+
"/extract", request, file=file, idempotency_key=idempotency_key
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def async_(
|
|
364
|
+
self,
|
|
365
|
+
request: dict[str, Any],
|
|
366
|
+
*,
|
|
367
|
+
file: Optional[FileInput] = None,
|
|
368
|
+
idempotency_key: Optional[str] = None,
|
|
369
|
+
) -> Any:
|
|
370
|
+
return self._submit(
|
|
371
|
+
"/extract/async", request, file=file, idempotency_key=idempotency_key
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def batch(
|
|
375
|
+
self,
|
|
376
|
+
request: dict[str, Any],
|
|
377
|
+
*,
|
|
378
|
+
files: Sequence[FileInput],
|
|
379
|
+
idempotency_key: Optional[str] = None,
|
|
380
|
+
) -> Any:
|
|
381
|
+
multipart: list[tuple[str, Any]] = [
|
|
382
|
+
("request", (None, json.dumps(request), "application/json")),
|
|
383
|
+
]
|
|
384
|
+
for item in files:
|
|
385
|
+
multipart.append(("files", to_upload_tuple(item)))
|
|
386
|
+
return self._root._request(
|
|
387
|
+
"POST",
|
|
388
|
+
"/extract/batch",
|
|
389
|
+
files=multipart,
|
|
390
|
+
headers={"Idempotency-Key": _idempotency_key(idempotency_key)},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def suggest_schema(self, body: dict[str, Any]) -> Any:
|
|
394
|
+
return self._root._request("POST", "/suggest-schema", json=body)
|
|
395
|
+
|
|
396
|
+
def _submit(
|
|
397
|
+
self,
|
|
398
|
+
path: str,
|
|
399
|
+
request: dict[str, Any],
|
|
400
|
+
*,
|
|
401
|
+
file: Optional[FileInput],
|
|
402
|
+
idempotency_key: Optional[str],
|
|
403
|
+
) -> Any:
|
|
404
|
+
multipart: dict[str, Any] = {
|
|
405
|
+
"request": (None, json.dumps(request), "application/json"),
|
|
406
|
+
}
|
|
407
|
+
if file is not None:
|
|
408
|
+
multipart["file"] = to_upload_tuple(file)
|
|
409
|
+
return self._root._request(
|
|
410
|
+
"POST",
|
|
411
|
+
path,
|
|
412
|
+
files=multipart,
|
|
413
|
+
headers={"Idempotency-Key": _idempotency_key(idempotency_key)},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class JobsResource:
|
|
418
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
419
|
+
self._root = root
|
|
420
|
+
|
|
421
|
+
def list(
|
|
422
|
+
self,
|
|
423
|
+
*,
|
|
424
|
+
cursor: Optional[str] = None,
|
|
425
|
+
limit: Optional[int] = None,
|
|
426
|
+
status: Optional[str] = None,
|
|
427
|
+
operation: Optional[str] = None,
|
|
428
|
+
) -> Any:
|
|
429
|
+
params = {
|
|
430
|
+
"cursor": cursor,
|
|
431
|
+
"limit": limit,
|
|
432
|
+
"status": status,
|
|
433
|
+
"operation": operation,
|
|
434
|
+
}
|
|
435
|
+
return self._root._request(
|
|
436
|
+
"GET",
|
|
437
|
+
"/jobs",
|
|
438
|
+
params={k: v for k, v in params.items() if v is not None} or None,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def get(
|
|
442
|
+
self,
|
|
443
|
+
job_id: str,
|
|
444
|
+
*,
|
|
445
|
+
cursor: Optional[str] = None,
|
|
446
|
+
limit: Optional[int] = None,
|
|
447
|
+
) -> Any:
|
|
448
|
+
params = {"cursor": cursor, "limit": limit}
|
|
449
|
+
return self._root._request(
|
|
450
|
+
"GET",
|
|
451
|
+
f"/jobs/{quote(job_id, safe='')}",
|
|
452
|
+
params={k: v for k, v in params.items() if v is not None} or None,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def result(
|
|
456
|
+
self,
|
|
457
|
+
job_id: str,
|
|
458
|
+
*,
|
|
459
|
+
format: Optional[ParseResultFormat] = None,
|
|
460
|
+
) -> Any:
|
|
461
|
+
params = {"format": format} if format is not None else None
|
|
462
|
+
return self._root._request(
|
|
463
|
+
"GET",
|
|
464
|
+
f"/jobs/{quote(job_id, safe='')}/result",
|
|
465
|
+
params=params,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def source(self, job_id: str) -> bytes:
|
|
469
|
+
return self._root._request_bytes(
|
|
470
|
+
"GET", f"/jobs/{quote(job_id, safe='')}/source"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class FilesResource:
|
|
475
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
476
|
+
self._root = root
|
|
477
|
+
|
|
478
|
+
def upload(self, file: FileInput) -> Any:
|
|
479
|
+
return self._root._request(
|
|
480
|
+
"POST", "/files", files={"file": to_upload_tuple(file)}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def get(self, file_id: str) -> Any:
|
|
484
|
+
return self._root._request("GET", f"/files/{quote(file_id, safe='')}")
|
|
485
|
+
|
|
486
|
+
def download(self, file_id: str) -> bytes:
|
|
487
|
+
return self._root._request_bytes(
|
|
488
|
+
"GET", f"/files/{quote(file_id, safe='')}/content"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def delete(self, file_id: str) -> Any:
|
|
492
|
+
return self._root._request("DELETE", f"/files/{quote(file_id, safe='')}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class ModelsResource:
|
|
496
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
497
|
+
self._root = root
|
|
498
|
+
|
|
499
|
+
def list_ocr(self) -> Any:
|
|
500
|
+
return self._root._request("GET", "/models/ocr")
|
|
501
|
+
|
|
502
|
+
def list_llm(
|
|
503
|
+
self,
|
|
504
|
+
*,
|
|
505
|
+
mode: LlmModelsMode = "suggested",
|
|
506
|
+
q: Optional[str] = None,
|
|
507
|
+
page: Optional[int] = None,
|
|
508
|
+
limit: Optional[int] = None,
|
|
509
|
+
) -> Any:
|
|
510
|
+
params = {"mode": mode, "q": q, "page": page, "limit": limit}
|
|
511
|
+
return self._root._request(
|
|
512
|
+
"GET",
|
|
513
|
+
"/models/llm",
|
|
514
|
+
params={k: v for k, v in params.items() if v is not None} or None,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class PipelinesResource:
|
|
519
|
+
def __init__(self, root: OpenParserClient) -> None:
|
|
520
|
+
self._root = root
|
|
521
|
+
|
|
522
|
+
def list(self) -> Any:
|
|
523
|
+
return self._root._request("GET", "/pipelines")
|
|
524
|
+
|
|
525
|
+
def get(self, pipeline_id: str) -> Any:
|
|
526
|
+
return self._root._request("GET", f"/pipelines/{quote(pipeline_id, safe='')}")
|
|
527
|
+
|
|
528
|
+
def create(self, body: dict[str, Any]) -> Any:
|
|
529
|
+
return self._root._request("POST", "/pipelines", json=body)
|
|
530
|
+
|
|
531
|
+
def update(self, pipeline_id: str, body: dict[str, Any]) -> Any:
|
|
532
|
+
return self._root._request(
|
|
533
|
+
"PATCH",
|
|
534
|
+
f"/pipelines/{quote(pipeline_id, safe='')}",
|
|
535
|
+
json=body,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def delete(self, pipeline_id: str) -> Any:
|
|
539
|
+
return self._root._request(
|
|
540
|
+
"DELETE", f"/pipelines/{quote(pipeline_id, safe='')}"
|
|
541
|
+
)
|
openparser/errors.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Typed exceptions raised by the OpenParser SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenParserError(Exception):
|
|
9
|
+
"""Base class for every error the SDK raises."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status: int,
|
|
16
|
+
envelope: Optional[dict[str, Any]] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status = status
|
|
20
|
+
self.envelope = envelope
|
|
21
|
+
self.request_id: Optional[str] = (envelope or {}).get("request_id")
|
|
22
|
+
self.code: Optional[str] = (envelope or {}).get("code")
|
|
23
|
+
self.retryable: Optional[bool] = (envelope or {}).get("retryable")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenParserAuthError(OpenParserError):
|
|
27
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
28
|
+
msg = _error_message(envelope) or (
|
|
29
|
+
"Invalid or missing API key. Pass OpenParserClient(api_key=...) "
|
|
30
|
+
"or set OPENPARSER_API_KEY."
|
|
31
|
+
)
|
|
32
|
+
super().__init__(msg, status=401, envelope=envelope)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OpenParserForbiddenError(OpenParserError):
|
|
36
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
37
|
+
msg = _error_message(envelope) or "forbidden"
|
|
38
|
+
super().__init__(msg, status=403, envelope=envelope)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OpenParserNotFoundError(OpenParserError):
|
|
42
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
43
|
+
msg = _error_message(envelope) or "not found"
|
|
44
|
+
super().__init__(msg, status=404, envelope=envelope)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OpenParserValidationError(OpenParserError):
|
|
48
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
49
|
+
msg = _error_message(envelope) or "validation error"
|
|
50
|
+
super().__init__(msg, status=400, envelope=envelope)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OpenParserPaymentRequiredError(OpenParserError):
|
|
54
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
55
|
+
msg = _error_message(envelope) or "insufficient credits"
|
|
56
|
+
super().__init__(msg, status=402, envelope=envelope)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OpenParserConflictError(OpenParserError):
|
|
60
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
61
|
+
msg = _error_message(envelope) or "conflict"
|
|
62
|
+
super().__init__(msg, status=409, envelope=envelope)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OpenParserLimitExceededError(OpenParserError):
|
|
66
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
67
|
+
msg = _error_message(envelope) or "limit exceeded"
|
|
68
|
+
super().__init__(msg, status=413, envelope=envelope)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OpenParserUnsupportedMediaError(OpenParserError):
|
|
72
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
73
|
+
msg = _error_message(envelope) or "unsupported media type"
|
|
74
|
+
super().__init__(msg, status=415, envelope=envelope)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class OpenParserUnprocessableError(OpenParserError):
|
|
78
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
79
|
+
msg = _error_message(envelope) or "unprocessable"
|
|
80
|
+
super().__init__(msg, status=422, envelope=envelope)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OpenParserRateLimitError(OpenParserError):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
envelope: Optional[dict[str, Any]] = None,
|
|
87
|
+
retry_after: Optional[int] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__(_error_message(envelope) or "rate limit exceeded", status=429, envelope=envelope)
|
|
90
|
+
self.retry_after = retry_after
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OpenParserServiceUnavailableError(OpenParserError):
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
envelope: Optional[dict[str, Any]] = None,
|
|
97
|
+
retry_after: Optional[int] = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
super().__init__(
|
|
100
|
+
_error_message(envelope) or "service unavailable",
|
|
101
|
+
status=503,
|
|
102
|
+
envelope=envelope,
|
|
103
|
+
)
|
|
104
|
+
self.retry_after = retry_after
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class OpenParserGatewayTimeoutError(OpenParserError):
|
|
108
|
+
def __init__(self, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
109
|
+
msg = _error_message(envelope) or "sync wait observed an indeterminate job"
|
|
110
|
+
super().__init__(msg, status=504, envelope=envelope)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class OpenParserServerError(OpenParserError):
|
|
114
|
+
def __init__(self, status: int, envelope: Optional[dict[str, Any]] = None) -> None:
|
|
115
|
+
msg = _error_message(envelope) or "internal server error"
|
|
116
|
+
super().__init__(msg, status=status, envelope=envelope)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OpenParserTimeoutError(OpenParserError):
|
|
120
|
+
def __init__(self, message: str = "operation timed out") -> None:
|
|
121
|
+
super().__init__(message, status=0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _error_message(envelope: Optional[dict[str, Any]]) -> Optional[str]:
|
|
125
|
+
if not envelope:
|
|
126
|
+
return None
|
|
127
|
+
message = envelope.get("message")
|
|
128
|
+
return message if isinstance(message, str) else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _normalize_error_body(payload: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
|
132
|
+
if not payload:
|
|
133
|
+
return None
|
|
134
|
+
error = payload.get("error")
|
|
135
|
+
if isinstance(error, dict):
|
|
136
|
+
return error
|
|
137
|
+
return payload
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def error_from_response(
|
|
141
|
+
status: int,
|
|
142
|
+
payload: Optional[dict[str, Any]],
|
|
143
|
+
retry_after: Optional[int] = None,
|
|
144
|
+
) -> OpenParserError:
|
|
145
|
+
envelope = _normalize_error_body(payload)
|
|
146
|
+
if status == 400:
|
|
147
|
+
return OpenParserValidationError(envelope)
|
|
148
|
+
if status == 401:
|
|
149
|
+
return OpenParserAuthError(envelope)
|
|
150
|
+
if status == 402:
|
|
151
|
+
return OpenParserPaymentRequiredError(envelope)
|
|
152
|
+
if status == 403:
|
|
153
|
+
return OpenParserForbiddenError(envelope)
|
|
154
|
+
if status == 404:
|
|
155
|
+
return OpenParserNotFoundError(envelope)
|
|
156
|
+
if status == 409:
|
|
157
|
+
return OpenParserConflictError(envelope)
|
|
158
|
+
if status == 413:
|
|
159
|
+
return OpenParserLimitExceededError(envelope)
|
|
160
|
+
if status == 415:
|
|
161
|
+
return OpenParserUnsupportedMediaError(envelope)
|
|
162
|
+
if status == 422:
|
|
163
|
+
return OpenParserUnprocessableError(envelope)
|
|
164
|
+
if status == 429:
|
|
165
|
+
return OpenParserRateLimitError(envelope, retry_after)
|
|
166
|
+
if status == 503:
|
|
167
|
+
return OpenParserServiceUnavailableError(envelope, retry_after)
|
|
168
|
+
if status == 504:
|
|
169
|
+
return OpenParserGatewayTimeoutError(envelope)
|
|
170
|
+
if status >= 500:
|
|
171
|
+
return OpenParserServerError(status, envelope)
|
|
172
|
+
return OpenParserError(f"unexpected status {status}", status=status, envelope=envelope)
|
openparser/py.typed
ADDED
|
File without changes
|