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.
Files changed (173) hide show
  1. openparser/__init__.py +54 -0
  2. openparser/_files.py +43 -0
  3. openparser/_generated/__init__.py +8 -0
  4. openparser/_generated/api/__init__.py +1 -0
  5. openparser/_generated/api/extract/__init__.py +1 -0
  6. openparser/_generated/api/extract/extract_async.py +280 -0
  7. openparser/_generated/api/extract/extract_batch.py +284 -0
  8. openparser/_generated/api/extract/extract_sync.py +315 -0
  9. openparser/_generated/api/extract/suggest_schema.py +260 -0
  10. openparser/_generated/api/files/__init__.py +1 -0
  11. openparser/_generated/api/files/create_file.py +237 -0
  12. openparser/_generated/api/files/delete_file.py +200 -0
  13. openparser/_generated/api/files/get_file.py +200 -0
  14. openparser/_generated/api/files/get_file_content.py +198 -0
  15. openparser/_generated/api/jobs/__init__.py +1 -0
  16. openparser/_generated/api/jobs/get_job.py +261 -0
  17. openparser/_generated/api/jobs/get_job_result.py +305 -0
  18. openparser/_generated/api/jobs/get_job_source.py +233 -0
  19. openparser/_generated/api/jobs/list_jobs.py +340 -0
  20. openparser/_generated/api/models/__init__.py +1 -0
  21. openparser/_generated/api/models/list_llm_models.py +288 -0
  22. openparser/_generated/api/models/list_ocr_models.py +184 -0
  23. openparser/_generated/api/parse/__init__.py +1 -0
  24. openparser/_generated/api/parse/parse_async.py +316 -0
  25. openparser/_generated/api/parse/parse_batch.py +280 -0
  26. openparser/_generated/api/parse/parse_sync.py +369 -0
  27. openparser/_generated/api/pipelines/__init__.py +1 -0
  28. openparser/_generated/api/pipelines/create_extraction_pipeline.py +242 -0
  29. openparser/_generated/api/pipelines/delete_extraction_pipeline.py +212 -0
  30. openparser/_generated/api/pipelines/get_extraction_pipeline.py +212 -0
  31. openparser/_generated/api/pipelines/list_extraction_pipelines.py +180 -0
  32. openparser/_generated/api/pipelines/update_extraction_pipeline.py +258 -0
  33. openparser/_generated/client.py +270 -0
  34. openparser/_generated/errors.py +14 -0
  35. openparser/_generated/models/__init__.py +261 -0
  36. openparser/_generated/models/batch_child_page.py +97 -0
  37. openparser/_generated/models/batch_child_summary.py +195 -0
  38. openparser/_generated/models/batch_job_accepted.py +139 -0
  39. openparser/_generated/models/batch_job_accepted_operation.py +8 -0
  40. openparser/_generated/models/batch_summary_counts.py +97 -0
  41. openparser/_generated/models/bounding_box.py +82 -0
  42. openparser/_generated/models/chunk_provenance_span.py +97 -0
  43. openparser/_generated/models/content_kind.py +10 -0
  44. openparser/_generated/models/create_extraction_pipeline_request.py +177 -0
  45. openparser/_generated/models/create_extraction_pipeline_request_grounding.py +8 -0
  46. openparser/_generated/models/create_extraction_pipeline_request_llm_options.py +84 -0
  47. openparser/_generated/models/create_extraction_pipeline_request_llm_options_reasoning_effort_type_1.py +13 -0
  48. openparser/_generated/models/create_extraction_pipeline_request_ocr_options.py +78 -0
  49. openparser/_generated/models/create_extraction_pipeline_request_schema.py +68 -0
  50. openparser/_generated/models/create_file_body.py +78 -0
  51. openparser/_generated/models/delete_extraction_pipeline_response.py +66 -0
  52. openparser/_generated/models/delete_file_response.py +57 -0
  53. openparser/_generated/models/error_body.py +106 -0
  54. openparser/_generated/models/error_body_details.py +65 -0
  55. openparser/_generated/models/error_response.py +65 -0
  56. openparser/_generated/models/extract_async_body.py +136 -0
  57. openparser/_generated/models/extract_batch_body.py +143 -0
  58. openparser/_generated/models/extract_batch_item.py +207 -0
  59. openparser/_generated/models/extract_batch_item_grounding.py +8 -0
  60. openparser/_generated/models/extract_batch_item_llm_options.py +84 -0
  61. openparser/_generated/models/extract_batch_item_llm_options_reasoning_effort_type_1.py +13 -0
  62. openparser/_generated/models/extract_batch_item_ocr_options.py +78 -0
  63. openparser/_generated/models/extract_batch_item_schema.py +65 -0
  64. openparser/_generated/models/extract_batch_request.py +99 -0
  65. openparser/_generated/models/extract_batch_request_items_item.py +201 -0
  66. openparser/_generated/models/extract_batch_request_items_item_grounding.py +8 -0
  67. openparser/_generated/models/extract_batch_request_items_item_llm_options.py +84 -0
  68. openparser/_generated/models/extract_batch_request_items_item_llm_options_reasoning_effort_type_1.py +13 -0
  69. openparser/_generated/models/extract_batch_request_items_item_ocr_options.py +78 -0
  70. openparser/_generated/models/extract_batch_request_items_item_schema.py +65 -0
  71. openparser/_generated/models/extract_batch_request_output_format.py +8 -0
  72. openparser/_generated/models/extract_request.py +226 -0
  73. openparser/_generated/models/extract_request_grounding.py +8 -0
  74. openparser/_generated/models/extract_request_llm_options.py +84 -0
  75. openparser/_generated/models/extract_request_llm_options_reasoning_effort_type_1.py +13 -0
  76. openparser/_generated/models/extract_request_ocr_options.py +78 -0
  77. openparser/_generated/models/extract_request_output_format.py +8 -0
  78. openparser/_generated/models/extract_request_schema.py +65 -0
  79. openparser/_generated/models/extract_sync_body.py +136 -0
  80. openparser/_generated/models/extraction_attempt.py +122 -0
  81. openparser/_generated/models/extraction_attempt_kind.py +8 -0
  82. openparser/_generated/models/extraction_attempt_status.py +9 -0
  83. openparser/_generated/models/extraction_chunk.py +184 -0
  84. openparser/_generated/models/extraction_citation.py +169 -0
  85. openparser/_generated/models/extraction_citation_granularity.py +8 -0
  86. openparser/_generated/models/extraction_citation_source_type.py +16 -0
  87. openparser/_generated/models/extraction_grounding_field.py +102 -0
  88. openparser/_generated/models/extraction_grounding_mode.py +8 -0
  89. openparser/_generated/models/extraction_grounding_result.py +87 -0
  90. openparser/_generated/models/extraction_pipeline.py +201 -0
  91. openparser/_generated/models/extraction_pipeline_list_response.py +76 -0
  92. openparser/_generated/models/extraction_pipeline_llm_options.py +84 -0
  93. openparser/_generated/models/extraction_pipeline_llm_options_reasoning_effort_type_0_type_1.py +13 -0
  94. openparser/_generated/models/extraction_pipeline_ocr_options.py +73 -0
  95. openparser/_generated/models/extraction_pipeline_schema.py +67 -0
  96. openparser/_generated/models/extraction_terminal_result.py +206 -0
  97. openparser/_generated/models/extraction_terminal_result_reasoning_effort_type_0.py +13 -0
  98. openparser/_generated/models/extraction_usage_totals.py +80 -0
  99. openparser/_generated/models/job.py +437 -0
  100. openparser/_generated/models/job_accepted.py +131 -0
  101. openparser/_generated/models/job_accepted_operation.py +8 -0
  102. openparser/_generated/models/job_extraction_schema.py +65 -0
  103. openparser/_generated/models/job_failure.py +98 -0
  104. openparser/_generated/models/job_failure_details.py +65 -0
  105. openparser/_generated/models/job_list_response.py +91 -0
  106. openparser/_generated/models/job_operation.py +10 -0
  107. openparser/_generated/models/job_progress.py +68 -0
  108. openparser/_generated/models/job_related_extractions_item.py +145 -0
  109. openparser/_generated/models/job_status.py +11 -0
  110. openparser/_generated/models/job_summary.py +264 -0
  111. openparser/_generated/models/json_schema_object.py +66 -0
  112. openparser/_generated/models/list_llm_models_mode.py +8 -0
  113. openparser/_generated/models/ocr_llm_model_catalog_entry.py +257 -0
  114. openparser/_generated/models/ocr_llm_model_catalog_entry_pricing.py +76 -0
  115. openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0.py +134 -0
  116. openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0_default_effort_type_0.py +13 -0
  117. openparser/_generated/models/ocr_llm_model_catalog_entry_reasoning_type_0_supported_efforts_type_0_item.py +13 -0
  118. openparser/_generated/models/ocr_llm_model_catalog_entry_recommendation.py +8 -0
  119. openparser/_generated/models/ocr_llm_models_response.py +150 -0
  120. openparser/_generated/models/ocr_llm_models_response_mode.py +8 -0
  121. openparser/_generated/models/ocr_model.py +7 -0
  122. openparser/_generated/models/ocr_model_catalog_entry.py +131 -0
  123. openparser/_generated/models/ocr_model_catalog_entry_availability.py +9 -0
  124. openparser/_generated/models/ocr_model_catalog_entry_capabilities.py +97 -0
  125. openparser/_generated/models/ocr_model_catalog_entry_capabilities_options.py +73 -0
  126. openparser/_generated/models/ocr_model_catalog_entry_option_defaults.py +73 -0
  127. openparser/_generated/models/ocr_model_catalog_entry_pricing.py +68 -0
  128. openparser/_generated/models/ocr_models_response.py +76 -0
  129. openparser/_generated/models/ocr_output_format.py +8 -0
  130. openparser/_generated/models/paddle_raw_profile.py +76 -0
  131. openparser/_generated/models/paddle_raw_profile_options.py +115 -0
  132. openparser/_generated/models/page_block.py +332 -0
  133. openparser/_generated/models/page_block_kind.py +9 -0
  134. openparser/_generated/models/page_block_polygon_type_0_item.py +65 -0
  135. openparser/_generated/models/parse_async_body.py +133 -0
  136. openparser/_generated/models/parse_batch_body.py +143 -0
  137. openparser/_generated/models/parse_batch_item.py +109 -0
  138. openparser/_generated/models/parse_batch_item_ocr_options.py +78 -0
  139. openparser/_generated/models/parse_batch_request.py +100 -0
  140. openparser/_generated/models/parse_batch_request_items_item.py +108 -0
  141. openparser/_generated/models/parse_batch_request_items_item_ocr_options.py +78 -0
  142. openparser/_generated/models/parse_batch_request_output_format.py +8 -0
  143. openparser/_generated/models/parse_request.py +112 -0
  144. openparser/_generated/models/parse_request_ocr_options.py +78 -0
  145. openparser/_generated/models/parse_request_output_format.py +8 -0
  146. openparser/_generated/models/parse_sync_body.py +133 -0
  147. openparser/_generated/models/parsed_document.py +187 -0
  148. openparser/_generated/models/public_file.py +117 -0
  149. openparser/_generated/models/raw_parse_result.py +110 -0
  150. openparser/_generated/models/raw_parse_result_result.py +65 -0
  151. openparser/_generated/models/region.py +224 -0
  152. openparser/_generated/models/region_content.py +155 -0
  153. openparser/_generated/models/region_polygon_type_0_item.py +65 -0
  154. openparser/_generated/models/region_type.py +16 -0
  155. openparser/_generated/models/suggest_schema_request.py +69 -0
  156. openparser/_generated/models/suggest_schema_response.py +74 -0
  157. openparser/_generated/models/suggest_schema_response_schema.py +66 -0
  158. openparser/_generated/models/update_extraction_pipeline_request.py +227 -0
  159. openparser/_generated/models/update_extraction_pipeline_request_grounding.py +8 -0
  160. openparser/_generated/models/update_extraction_pipeline_request_llm_options_type_0.py +84 -0
  161. openparser/_generated/models/update_extraction_pipeline_request_llm_options_type_0_reasoning_effort_type_1.py +13 -0
  162. openparser/_generated/models/update_extraction_pipeline_request_ocr_options_type_0.py +78 -0
  163. openparser/_generated/models/update_extraction_pipeline_request_schema.py +67 -0
  164. openparser/_generated/py.typed +1 -0
  165. openparser/_generated/types.py +53 -0
  166. openparser/_telemetry.py +34 -0
  167. openparser/client.py +541 -0
  168. openparser/errors.py +172 -0
  169. openparser/py.typed +0 -0
  170. openparser_sdk-0.0.1.dist-info/METADATA +178 -0
  171. openparser_sdk-0.0.1.dist-info/RECORD +173 -0
  172. openparser_sdk-0.0.1.dist-info/WHEEL +4 -0
  173. 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