retab 0.0.35__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.
- retab-0.0.35.dist-info/METADATA +417 -0
- retab-0.0.35.dist-info/RECORD +111 -0
- retab-0.0.35.dist-info/WHEEL +5 -0
- retab-0.0.35.dist-info/top_level.txt +1 -0
- uiform/__init__.py +4 -0
- uiform/_resource.py +28 -0
- uiform/_utils/__init__.py +0 -0
- uiform/_utils/ai_models.py +100 -0
- uiform/_utils/benchmarking copy.py +588 -0
- uiform/_utils/benchmarking.py +485 -0
- uiform/_utils/chat.py +332 -0
- uiform/_utils/display.py +443 -0
- uiform/_utils/json_schema.py +2161 -0
- uiform/_utils/mime.py +168 -0
- uiform/_utils/responses.py +163 -0
- uiform/_utils/stream_context_managers.py +52 -0
- uiform/_utils/usage/__init__.py +0 -0
- uiform/_utils/usage/usage.py +300 -0
- uiform/client.py +701 -0
- uiform/py.typed +0 -0
- uiform/resources/__init__.py +0 -0
- uiform/resources/consensus/__init__.py +3 -0
- uiform/resources/consensus/client.py +114 -0
- uiform/resources/consensus/completions.py +252 -0
- uiform/resources/consensus/completions_stream.py +278 -0
- uiform/resources/consensus/responses.py +325 -0
- uiform/resources/consensus/responses_stream.py +373 -0
- uiform/resources/deployments/__init__.py +9 -0
- uiform/resources/deployments/client.py +78 -0
- uiform/resources/deployments/endpoints.py +322 -0
- uiform/resources/deployments/links.py +452 -0
- uiform/resources/deployments/logs.py +211 -0
- uiform/resources/deployments/mailboxes.py +496 -0
- uiform/resources/deployments/outlook.py +531 -0
- uiform/resources/deployments/tests.py +158 -0
- uiform/resources/documents/__init__.py +3 -0
- uiform/resources/documents/client.py +255 -0
- uiform/resources/documents/extractions.py +441 -0
- uiform/resources/evals.py +812 -0
- uiform/resources/files.py +24 -0
- uiform/resources/finetuning.py +62 -0
- uiform/resources/jsonlUtils.py +1046 -0
- uiform/resources/models.py +45 -0
- uiform/resources/openai_example.py +22 -0
- uiform/resources/processors/__init__.py +3 -0
- uiform/resources/processors/automations/__init__.py +9 -0
- uiform/resources/processors/automations/client.py +78 -0
- uiform/resources/processors/automations/endpoints.py +317 -0
- uiform/resources/processors/automations/links.py +356 -0
- uiform/resources/processors/automations/logs.py +211 -0
- uiform/resources/processors/automations/mailboxes.py +435 -0
- uiform/resources/processors/automations/outlook.py +444 -0
- uiform/resources/processors/automations/tests.py +158 -0
- uiform/resources/processors/client.py +474 -0
- uiform/resources/prompt_optimization.py +76 -0
- uiform/resources/schemas.py +369 -0
- uiform/resources/secrets/__init__.py +9 -0
- uiform/resources/secrets/client.py +20 -0
- uiform/resources/secrets/external_api_keys.py +109 -0
- uiform/resources/secrets/webhook.py +62 -0
- uiform/resources/usage.py +271 -0
- uiform/types/__init__.py +0 -0
- uiform/types/ai_models.py +645 -0
- uiform/types/automations/__init__.py +0 -0
- uiform/types/automations/cron.py +58 -0
- uiform/types/automations/endpoints.py +21 -0
- uiform/types/automations/links.py +28 -0
- uiform/types/automations/mailboxes.py +60 -0
- uiform/types/automations/outlook.py +68 -0
- uiform/types/automations/webhooks.py +21 -0
- uiform/types/chat.py +8 -0
- uiform/types/completions.py +93 -0
- uiform/types/consensus.py +10 -0
- uiform/types/db/__init__.py +0 -0
- uiform/types/db/annotations.py +24 -0
- uiform/types/db/files.py +36 -0
- uiform/types/deployments/__init__.py +0 -0
- uiform/types/deployments/cron.py +59 -0
- uiform/types/deployments/endpoints.py +28 -0
- uiform/types/deployments/links.py +36 -0
- uiform/types/deployments/mailboxes.py +67 -0
- uiform/types/deployments/outlook.py +76 -0
- uiform/types/deployments/webhooks.py +21 -0
- uiform/types/documents/__init__.py +0 -0
- uiform/types/documents/correct_orientation.py +13 -0
- uiform/types/documents/create_messages.py +226 -0
- uiform/types/documents/extractions.py +297 -0
- uiform/types/evals.py +207 -0
- uiform/types/events.py +76 -0
- uiform/types/extractions.py +85 -0
- uiform/types/jobs/__init__.py +0 -0
- uiform/types/jobs/base.py +150 -0
- uiform/types/jobs/batch_annotation.py +22 -0
- uiform/types/jobs/evaluation.py +133 -0
- uiform/types/jobs/finetune.py +6 -0
- uiform/types/jobs/prompt_optimization.py +41 -0
- uiform/types/jobs/webcrawl.py +6 -0
- uiform/types/logs.py +231 -0
- uiform/types/mime.py +257 -0
- uiform/types/modalities.py +68 -0
- uiform/types/pagination.py +6 -0
- uiform/types/schemas/__init__.py +0 -0
- uiform/types/schemas/enhance.py +53 -0
- uiform/types/schemas/evaluate.py +55 -0
- uiform/types/schemas/generate.py +32 -0
- uiform/types/schemas/layout.py +58 -0
- uiform/types/schemas/object.py +631 -0
- uiform/types/schemas/templates.py +107 -0
- uiform/types/secrets/__init__.py +0 -0
- uiform/types/secrets/external_api_keys.py +22 -0
- uiform/types/standards.py +39 -0
uiform/client.py
ADDED
@@ -0,0 +1,701 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import Any, AsyncIterator, BinaryIO, Iterator, List, Optional
|
5
|
+
|
6
|
+
import backoff
|
7
|
+
import backoff.types
|
8
|
+
import httpx
|
9
|
+
from pydantic_core import PydanticUndefined
|
10
|
+
|
11
|
+
from .resources import processors, consensus, documents, evals, files, finetuning, models, schemas, secrets, usage
|
12
|
+
from .types.standards import PreparedRequest
|
13
|
+
import truststore
|
14
|
+
|
15
|
+
|
16
|
+
class MaxRetriesExceeded(Exception):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
def raise_max_tries_exceeded(details: backoff.types.Details) -> None:
|
21
|
+
exception = details.get("exception")
|
22
|
+
tries = details["tries"]
|
23
|
+
if isinstance(exception, BaseException):
|
24
|
+
raise Exception(f"Max tries exceeded after {tries} tries.") from exception
|
25
|
+
else:
|
26
|
+
raise Exception(f"Max tries exceeded after {tries} tries.")
|
27
|
+
|
28
|
+
|
29
|
+
class BaseUiForm:
|
30
|
+
"""Base class for UiForm clients that handles authentication and configuration.
|
31
|
+
|
32
|
+
This class provides core functionality for API authentication, configuration, and common HTTP operations
|
33
|
+
used by both synchronous and asynchronous clients.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
api_key (str, optional): UiForm API key. If not provided, will look for UIFORM_API_KEY env variable.
|
37
|
+
base_url (str, optional): Base URL for API requests. Defaults to https://api.uiform.com
|
38
|
+
timeout (float): Request timeout in seconds. Defaults to 240.0
|
39
|
+
max_retries (int): Maximum number of retries for failed requests. Defaults to 3
|
40
|
+
openai_api_key (str, optional): OpenAI API key. Will look for OPENAI_API_KEY env variable if not provided
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
ValueError: If no API key is provided through arguments or environment variables
|
44
|
+
"""
|
45
|
+
|
46
|
+
# claude_api_key (str, optional): Claude API key. Will look for CLAUDE_API_KEY env variable if not provided
|
47
|
+
# xai_api_key (str, optional): XAI API key. Will look for XAI_API_KEY env variable if not provided
|
48
|
+
# gemini_api_key (str, optional): Gemini API key. Will look for GEMINI_API_KEY env variable if not provided
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
api_key: Optional[str] = None,
|
53
|
+
base_url: Optional[str] = None,
|
54
|
+
timeout: float = 240.0,
|
55
|
+
max_retries: int = 3,
|
56
|
+
openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
57
|
+
gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
58
|
+
# claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
59
|
+
xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
60
|
+
) -> None:
|
61
|
+
if api_key is None:
|
62
|
+
api_key = os.environ.get("UIFORM_API_KEY")
|
63
|
+
|
64
|
+
if api_key is None:
|
65
|
+
raise ValueError(
|
66
|
+
"No API key provided. You can create an API key at https://uiform.com\n"
|
67
|
+
"Then either pass it to the client (api_key='your-key') or set the UIFORM_API_KEY environment variable"
|
68
|
+
)
|
69
|
+
|
70
|
+
if base_url is None:
|
71
|
+
base_url = os.environ.get("UIFORM_API_BASE_URL", "https://api.uiform.com")
|
72
|
+
|
73
|
+
|
74
|
+
truststore.inject_into_ssl()
|
75
|
+
self.api_key = api_key
|
76
|
+
self.base_url = base_url.rstrip("/")
|
77
|
+
self.timeout = timeout
|
78
|
+
self.max_retries = max_retries
|
79
|
+
self.headers = {
|
80
|
+
"Api-Key": self.api_key,
|
81
|
+
"Content-Type": "application/json",
|
82
|
+
}
|
83
|
+
|
84
|
+
# Only check environment variables if the value is PydanticUndefined
|
85
|
+
if openai_api_key is PydanticUndefined:
|
86
|
+
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
87
|
+
|
88
|
+
# if claude_api_key is PydanticUndefined:
|
89
|
+
# claude_api_key = os.environ.get("CLAUDE_API_KEY")
|
90
|
+
|
91
|
+
# if xai_api_key is PydanticUndefined:
|
92
|
+
# xai_api_key = os.environ.get("XAI_API_KEY")
|
93
|
+
|
94
|
+
if gemini_api_key is PydanticUndefined:
|
95
|
+
gemini_api_key = os.environ.get("GEMINI_API_KEY")
|
96
|
+
|
97
|
+
# Only add headers if the values are actual strings (not None or PydanticUndefined)
|
98
|
+
if openai_api_key and openai_api_key is not PydanticUndefined:
|
99
|
+
self.headers["OpenAI-Api-Key"] = openai_api_key
|
100
|
+
|
101
|
+
# if claude_api_key and claude_api_key is not PydanticUndefined:
|
102
|
+
# self.headers["Anthropic-Api-Key"] = claude_api_key
|
103
|
+
|
104
|
+
if xai_api_key and xai_api_key is not PydanticUndefined:
|
105
|
+
self.headers["XAI-Api-Key"] = xai_api_key
|
106
|
+
|
107
|
+
if gemini_api_key and gemini_api_key is not PydanticUndefined:
|
108
|
+
self.headers["Gemini-Api-Key"] = gemini_api_key
|
109
|
+
|
110
|
+
def _prepare_url(self, endpoint: str) -> str:
|
111
|
+
return f"{self.base_url}/{endpoint.lstrip('/')}"
|
112
|
+
|
113
|
+
def _validate_response(self, response_object: httpx.Response) -> None:
|
114
|
+
if response_object.status_code >= 500:
|
115
|
+
response_object.raise_for_status()
|
116
|
+
elif response_object.status_code == 422:
|
117
|
+
raise RuntimeError(f"Validation error (422): {response_object.text}")
|
118
|
+
elif not response_object.is_success:
|
119
|
+
raise RuntimeError(f"Request failed ({response_object.status_code}): {response_object.text}")
|
120
|
+
|
121
|
+
def _get_headers(self, idempotency_key: str | None = None) -> dict[str, Any]:
|
122
|
+
headers = self.headers.copy()
|
123
|
+
if idempotency_key:
|
124
|
+
headers["Idempotency-Key"] = idempotency_key
|
125
|
+
return headers
|
126
|
+
|
127
|
+
def _parse_response(self, response: httpx.Response) -> Any:
|
128
|
+
"""Parse response based on content-type.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Any: Parsed JSON object for JSON responses, raw text string for text responses
|
132
|
+
"""
|
133
|
+
content_type = response.headers.get("content-type", "")
|
134
|
+
|
135
|
+
# Check if it's a JSON response
|
136
|
+
if "application/json" in content_type or "application/stream+json" in content_type:
|
137
|
+
return response.json()
|
138
|
+
# Check if it's a text response
|
139
|
+
elif "text/plain" in content_type or "text/" in content_type:
|
140
|
+
return response.text
|
141
|
+
else:
|
142
|
+
# Default to JSON parsing for backwards compatibility
|
143
|
+
try:
|
144
|
+
return response.json()
|
145
|
+
except Exception:
|
146
|
+
# If JSON parsing fails, return as text
|
147
|
+
return response.text
|
148
|
+
|
149
|
+
|
150
|
+
class UiForm(BaseUiForm):
|
151
|
+
"""Synchronous client for interacting with the UiForm API.
|
152
|
+
|
153
|
+
This client provides synchronous access to all UiForm API resources including files, fine-tuning,
|
154
|
+
prompt optimization, documents, models, datasets, and schemas.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
api_key (str, optional): UiForm API key. If not provided, will look for UIFORM_API_KEY env variable.
|
158
|
+
base_url (str, optional): Base URL for API requests. Defaults to https://api.uiform.com
|
159
|
+
timeout (float): Request timeout in seconds. Defaults to 240.0
|
160
|
+
max_retries (int): Maximum number of retries for failed requests. Defaults to 3
|
161
|
+
openai_api_key (str, optional): OpenAI API key. Will look for OPENAI_API_KEY env variable if not provided
|
162
|
+
claude_api_key (str, optional): Claude API key. Will look for CLAUDE_API_KEY env variable if not provided
|
163
|
+
xai_api_key (str, optional): XAI API key. Will look for XAI_API_KEY env variable if not provided
|
164
|
+
gemini_api_key (str, optional): Gemini API key. Will look for GEMINI_API_KEY env variable if not provided
|
165
|
+
|
166
|
+
Attributes:
|
167
|
+
files: Access to file operations
|
168
|
+
fine_tuning: Access to model fine-tuning operations
|
169
|
+
prompt_optimization: Access to prompt optimization operations
|
170
|
+
documents: Access to document operations
|
171
|
+
models: Access to model operations
|
172
|
+
datasets: Access to dataset operations
|
173
|
+
schemas: Access to schema operations
|
174
|
+
responses: Access to responses API (OpenAI Responses API compatible interface)
|
175
|
+
"""
|
176
|
+
|
177
|
+
def __init__(
|
178
|
+
self,
|
179
|
+
api_key: Optional[str] = None,
|
180
|
+
base_url: Optional[str] = None,
|
181
|
+
timeout: float = 240.0,
|
182
|
+
max_retries: int = 3,
|
183
|
+
openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
184
|
+
gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
185
|
+
# claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
186
|
+
# xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
187
|
+
) -> None:
|
188
|
+
super().__init__(
|
189
|
+
api_key=api_key,
|
190
|
+
base_url=base_url,
|
191
|
+
timeout=timeout,
|
192
|
+
max_retries=max_retries,
|
193
|
+
openai_api_key=openai_api_key,
|
194
|
+
gemini_api_key=gemini_api_key,
|
195
|
+
# claude_api_key=claude_api_key,
|
196
|
+
# xai_api_key=xai_api_key,
|
197
|
+
)
|
198
|
+
|
199
|
+
self.client = httpx.Client(timeout=self.timeout)
|
200
|
+
self.evals = evals.Evals(client=self)
|
201
|
+
self.files = files.Files(client=self)
|
202
|
+
self.fine_tuning = finetuning.FineTuning(client=self)
|
203
|
+
# self.prompt_optimization = prompt_optimization.PromptOptimization(client=self)
|
204
|
+
self.documents = documents.Documents(client=self)
|
205
|
+
self.models = models.Models(client=self)
|
206
|
+
self.schemas = schemas.Schemas(client=self)
|
207
|
+
self.processors = processors.Processors(client=self)
|
208
|
+
self.secrets = secrets.Secrets(client=self)
|
209
|
+
self.usage = usage.Usage(client=self)
|
210
|
+
self.consensus = consensus.Consensus(client=self)
|
211
|
+
|
212
|
+
def _request(
|
213
|
+
self,
|
214
|
+
method: str,
|
215
|
+
endpoint: str,
|
216
|
+
data: Optional[dict[str, Any]] = None,
|
217
|
+
params: Optional[dict[str, Any]] = None,
|
218
|
+
form_data: Optional[dict[str, Any]] = None,
|
219
|
+
files: Optional[dict[str, Any] | list] = None,
|
220
|
+
idempotency_key: str | None = None,
|
221
|
+
raise_for_status: bool = False,
|
222
|
+
) -> Any:
|
223
|
+
"""Makes a synchronous HTTP request to the API.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
method (str): HTTP method (GET, POST, etc.)
|
227
|
+
endpoint (str): API endpoint path
|
228
|
+
data (Optional[dict]): Request payload (JSON)
|
229
|
+
params (Optional[dict]): Query parameters
|
230
|
+
form_data (Optional[dict]): Form data for multipart/form-data requests
|
231
|
+
files (Optional[dict]): Files for multipart/form-data requests
|
232
|
+
idempotency_key (str, optional): Idempotency key for request
|
233
|
+
raise_for_status (bool): Whether to raise on HTTP errors
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
Any: Parsed JSON response or raw text string depending on response content-type
|
237
|
+
|
238
|
+
Raises:
|
239
|
+
RuntimeError: If request fails after max retries or validation error occurs
|
240
|
+
"""
|
241
|
+
|
242
|
+
def raw_request() -> Any:
|
243
|
+
# Prepare request kwargs
|
244
|
+
request_kwargs = {
|
245
|
+
"method": method,
|
246
|
+
"url": self._prepare_url(endpoint),
|
247
|
+
"params": params,
|
248
|
+
"headers": self._get_headers(idempotency_key),
|
249
|
+
}
|
250
|
+
|
251
|
+
# Handle different content types
|
252
|
+
if files or form_data:
|
253
|
+
# For multipart/form-data requests
|
254
|
+
if form_data:
|
255
|
+
request_kwargs["data"] = form_data
|
256
|
+
if files:
|
257
|
+
request_kwargs["files"] = files
|
258
|
+
# Remove Content-Type header to let httpx set it automatically for multipart
|
259
|
+
headers = request_kwargs["headers"].copy()
|
260
|
+
headers.pop("Content-Type", None)
|
261
|
+
request_kwargs["headers"] = headers
|
262
|
+
elif data:
|
263
|
+
# For JSON requests
|
264
|
+
request_kwargs["json"] = data
|
265
|
+
|
266
|
+
response = self.client.request(**request_kwargs)
|
267
|
+
self._validate_response(response)
|
268
|
+
return self._parse_response(response)
|
269
|
+
|
270
|
+
@backoff.on_exception(backoff.expo, httpx.HTTPStatusError, max_tries=self.max_retries + 1, on_giveup=raise_max_tries_exceeded)
|
271
|
+
def wrapped_request() -> Any:
|
272
|
+
return raw_request()
|
273
|
+
|
274
|
+
if raise_for_status:
|
275
|
+
# If raise_for_status is True, we want to raise an exception if the request fails, not retry...
|
276
|
+
return raw_request()
|
277
|
+
else:
|
278
|
+
return wrapped_request()
|
279
|
+
|
280
|
+
def _request_stream(
|
281
|
+
self,
|
282
|
+
method: str,
|
283
|
+
endpoint: str,
|
284
|
+
data: Optional[dict[str, Any]] = None,
|
285
|
+
params: Optional[dict[str, Any]] = None,
|
286
|
+
form_data: Optional[dict[str, Any]] = None,
|
287
|
+
files: Optional[dict[str, Any] | list] = None,
|
288
|
+
idempotency_key: str | None = None,
|
289
|
+
raise_for_status: bool = False,
|
290
|
+
) -> Iterator[Any]:
|
291
|
+
"""Makes a streaming synchronous HTTP request to the API.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
method (str): HTTP method (GET, POST, etc.)
|
295
|
+
endpoint (str): API endpoint path
|
296
|
+
data (Optional[dict]): Request payload (JSON)
|
297
|
+
params (Optional[dict]): Query parameters
|
298
|
+
form_data (Optional[dict]): Form data for multipart/form-data requests
|
299
|
+
files (Optional[dict]): Files for multipart/form-data requests
|
300
|
+
idempotency_key (str, optional): Idempotency key for request
|
301
|
+
raise_for_status (bool): Whether to raise on HTTP errors
|
302
|
+
Returns:
|
303
|
+
Iterator[Any]: Generator yielding parsed JSON objects or raw text strings from the stream
|
304
|
+
|
305
|
+
Raises:
|
306
|
+
RuntimeError: If request fails after max retries or validation error occurs
|
307
|
+
"""
|
308
|
+
|
309
|
+
def raw_request() -> Iterator[Any]:
|
310
|
+
# Prepare request kwargs
|
311
|
+
stream_kwargs = {
|
312
|
+
"method": method,
|
313
|
+
"url": self._prepare_url(endpoint),
|
314
|
+
"params": params,
|
315
|
+
"headers": self._get_headers(idempotency_key),
|
316
|
+
}
|
317
|
+
|
318
|
+
# Handle different content types
|
319
|
+
if files or form_data:
|
320
|
+
# For multipart/form-data requests
|
321
|
+
if form_data:
|
322
|
+
stream_kwargs["data"] = form_data
|
323
|
+
if files:
|
324
|
+
stream_kwargs["files"] = files
|
325
|
+
# Remove Content-Type header to let httpx set it automatically for multipart
|
326
|
+
headers = stream_kwargs["headers"].copy()
|
327
|
+
headers.pop("Content-Type", None)
|
328
|
+
stream_kwargs["headers"] = headers
|
329
|
+
elif data:
|
330
|
+
# For JSON requests
|
331
|
+
stream_kwargs["json"] = data
|
332
|
+
|
333
|
+
with self.client.stream(**stream_kwargs) as response_ctx_manager:
|
334
|
+
self._validate_response(response_ctx_manager)
|
335
|
+
|
336
|
+
content_type = response_ctx_manager.headers.get("content-type", "")
|
337
|
+
is_json_stream = "application/json" in content_type or "application/stream+json" in content_type
|
338
|
+
is_text_stream = "text/plain" in content_type or ("text/" in content_type and not is_json_stream)
|
339
|
+
|
340
|
+
for chunk in response_ctx_manager.iter_lines():
|
341
|
+
if not chunk:
|
342
|
+
continue
|
343
|
+
|
344
|
+
if is_json_stream:
|
345
|
+
try:
|
346
|
+
yield json.loads(chunk)
|
347
|
+
except Exception:
|
348
|
+
pass
|
349
|
+
elif is_text_stream:
|
350
|
+
yield chunk
|
351
|
+
else:
|
352
|
+
# Default behavior: try JSON first, fall back to text
|
353
|
+
try:
|
354
|
+
yield json.loads(chunk)
|
355
|
+
except Exception:
|
356
|
+
yield chunk
|
357
|
+
|
358
|
+
@backoff.on_exception(backoff.expo, httpx.HTTPStatusError, max_tries=self.max_retries + 1, on_giveup=raise_max_tries_exceeded)
|
359
|
+
def wrapped_request() -> Iterator[Any]:
|
360
|
+
for item in raw_request():
|
361
|
+
yield item
|
362
|
+
|
363
|
+
iterator_ = raw_request() if raise_for_status else wrapped_request()
|
364
|
+
|
365
|
+
for item in iterator_:
|
366
|
+
yield item
|
367
|
+
|
368
|
+
# Simplified request methods using standard PreparedRequest object
|
369
|
+
def _prepared_request(self, request: PreparedRequest) -> Any:
|
370
|
+
return self._request(
|
371
|
+
method=request.method,
|
372
|
+
endpoint=request.url,
|
373
|
+
data=request.data,
|
374
|
+
params=request.params,
|
375
|
+
form_data=request.form_data,
|
376
|
+
files=request.files,
|
377
|
+
idempotency_key=request.idempotency_key,
|
378
|
+
raise_for_status=request.raise_for_status
|
379
|
+
)
|
380
|
+
|
381
|
+
def _prepared_request_stream(self, request: PreparedRequest) -> Iterator[Any]:
|
382
|
+
for item in self._request_stream(
|
383
|
+
method=request.method,
|
384
|
+
endpoint=request.url,
|
385
|
+
data=request.data,
|
386
|
+
params=request.params,
|
387
|
+
form_data=request.form_data,
|
388
|
+
files=request.files,
|
389
|
+
idempotency_key=request.idempotency_key,
|
390
|
+
raise_for_status=request.raise_for_status
|
391
|
+
):
|
392
|
+
yield item
|
393
|
+
|
394
|
+
def close(self) -> None:
|
395
|
+
"""Closes the HTTP client session."""
|
396
|
+
self.client.close()
|
397
|
+
|
398
|
+
def __enter__(self) -> "UiForm":
|
399
|
+
"""Context manager entry point.
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
UiForm: The client instance
|
403
|
+
"""
|
404
|
+
return self
|
405
|
+
|
406
|
+
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
407
|
+
"""Context manager exit point that ensures the client is properly closed.
|
408
|
+
|
409
|
+
Args:
|
410
|
+
exc_type: The type of the exception that was raised, if any
|
411
|
+
exc_value: The instance of the exception that was raised, if any
|
412
|
+
traceback: The traceback of the exception that was raised, if any
|
413
|
+
"""
|
414
|
+
self.close()
|
415
|
+
|
416
|
+
|
417
|
+
class AsyncUiForm(BaseUiForm):
|
418
|
+
"""Asynchronous client for interacting with the UiForm API.
|
419
|
+
|
420
|
+
This client provides asynchronous access to all UiForm API resources including files, fine-tuning,
|
421
|
+
prompt optimization, documents, models, datasets, and schemas.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
api_key (str, optional): UiForm API key. If not provided, will look for UIFORM_API_KEY env variable.
|
425
|
+
base_url (str, optional): Base URL for API requests. Defaults to https://api.uiform.com
|
426
|
+
timeout (float): Request timeout in seconds. Defaults to 240.0
|
427
|
+
max_retries (int): Maximum number of retries for failed requests. Defaults to 3
|
428
|
+
openai_api_key (str, optional): OpenAI API key. Will look for OPENAI_API_KEY env variable if not provided
|
429
|
+
claude_api_key (str, optional): Claude API key. Will look for CLAUDE_API_KEY env variable if not provided
|
430
|
+
xai_api_key (str, optional): XAI API key. Will look for XAI_API_KEY env variable if not provided
|
431
|
+
gemini_api_key (str, optional): Gemini API key. Will look for GEMINI_API_KEY env variable if not provided
|
432
|
+
|
433
|
+
Attributes:
|
434
|
+
files: Access to asynchronous file operations
|
435
|
+
fine_tuning: Access to asynchronous model fine-tuning operations
|
436
|
+
prompt_optimization: Access to asynchronous prompt optimization operations
|
437
|
+
documents: Access to asynchronous document operations
|
438
|
+
models: Access to asynchronous model operations
|
439
|
+
datasets: Access to asynchronous dataset operations
|
440
|
+
schemas: Access to asynchronous schema operations
|
441
|
+
responses: Access to responses API (OpenAI Responses API compatible interface)
|
442
|
+
"""
|
443
|
+
|
444
|
+
def __init__(
|
445
|
+
self,
|
446
|
+
api_key: Optional[str] = None,
|
447
|
+
base_url: Optional[str] = None,
|
448
|
+
timeout: float = 240.0,
|
449
|
+
max_retries: int = 3,
|
450
|
+
openai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
451
|
+
gemini_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
452
|
+
# claude_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
453
|
+
# xai_api_key: Optional[str] = PydanticUndefined, # type: ignore[assignment]
|
454
|
+
) -> None:
|
455
|
+
super().__init__(
|
456
|
+
api_key=api_key,
|
457
|
+
base_url=base_url,
|
458
|
+
timeout=timeout,
|
459
|
+
max_retries=max_retries,
|
460
|
+
openai_api_key=openai_api_key,
|
461
|
+
gemini_api_key=gemini_api_key,
|
462
|
+
# claude_api_key=claude_api_key,
|
463
|
+
# xai_api_key=xai_api_key,
|
464
|
+
)
|
465
|
+
|
466
|
+
self.client = httpx.AsyncClient(timeout=self.timeout)
|
467
|
+
|
468
|
+
self.evals = evals.AsyncEvals(client=self)
|
469
|
+
self.files = files.AsyncFiles(client=self)
|
470
|
+
self.fine_tuning = finetuning.AsyncFineTuning(client=self)
|
471
|
+
# self.prompt_optimization = prompt_optimization.AsyncPromptOptimization(client=self)
|
472
|
+
self.documents = documents.AsyncDocuments(client=self)
|
473
|
+
self.models = models.AsyncModels(client=self)
|
474
|
+
self.schemas = schemas.AsyncSchemas(client=self)
|
475
|
+
self.processors = processors.AsyncProcessors(client=self)
|
476
|
+
self.secrets = secrets.AsyncSecrets(client=self)
|
477
|
+
self.usage = usage.AsyncUsage(client=self)
|
478
|
+
self.consensus = consensus.AsyncConsensus(client=self)
|
479
|
+
|
480
|
+
def _parse_response(self, response: httpx.Response) -> Any:
|
481
|
+
"""Parse response based on content-type.
|
482
|
+
|
483
|
+
Returns:
|
484
|
+
Any: Parsed JSON object for JSON responses, raw text string for text responses
|
485
|
+
"""
|
486
|
+
content_type = response.headers.get("content-type", "")
|
487
|
+
|
488
|
+
# Check if it's a JSON response
|
489
|
+
if "application/json" in content_type or "application/stream+json" in content_type:
|
490
|
+
return response.json()
|
491
|
+
# Check if it's a text response
|
492
|
+
elif "text/plain" in content_type or "text/" in content_type:
|
493
|
+
return response.text
|
494
|
+
else:
|
495
|
+
# Default to JSON parsing for backwards compatibility
|
496
|
+
try:
|
497
|
+
return response.json()
|
498
|
+
except Exception:
|
499
|
+
# If JSON parsing fails, return as text
|
500
|
+
return response.text
|
501
|
+
|
502
|
+
async def _request(
|
503
|
+
self,
|
504
|
+
method: str,
|
505
|
+
endpoint: str,
|
506
|
+
data: Optional[dict[str, Any]] = None,
|
507
|
+
params: Optional[dict[str, Any]] = None,
|
508
|
+
form_data: Optional[dict[str, Any]] = None,
|
509
|
+
files: Optional[dict[str, Any] | list] = None,
|
510
|
+
idempotency_key: str | None = None,
|
511
|
+
raise_for_status: bool = False,
|
512
|
+
) -> Any:
|
513
|
+
"""Makes an asynchronous HTTP request to the API.
|
514
|
+
|
515
|
+
Args:
|
516
|
+
method (str): HTTP method (GET, POST, etc.)
|
517
|
+
endpoint (str): API endpoint path
|
518
|
+
data (Optional[dict]): Request payload (JSON)
|
519
|
+
params (Optional[dict]): Query parameters
|
520
|
+
form_data (Optional[dict]): Form data for multipart/form-data requests
|
521
|
+
files (Optional[dict]): Files for multipart/form-data requests
|
522
|
+
idempotency_key (str, optional): Idempotency key for request
|
523
|
+
raise_for_status (bool): Whether to raise on HTTP errors
|
524
|
+
Returns:
|
525
|
+
Any: Parsed JSON response or raw text string depending on response content-type
|
526
|
+
|
527
|
+
Raises:
|
528
|
+
RuntimeError: If request fails after max retries or validation error occurs
|
529
|
+
"""
|
530
|
+
|
531
|
+
async def raw_request() -> Any:
|
532
|
+
# Prepare request kwargs
|
533
|
+
request_kwargs = {
|
534
|
+
"method": method,
|
535
|
+
"url": self._prepare_url(endpoint),
|
536
|
+
"params": params,
|
537
|
+
"headers": self._get_headers(idempotency_key),
|
538
|
+
}
|
539
|
+
|
540
|
+
# Handle different content types
|
541
|
+
if files or form_data:
|
542
|
+
# For multipart/form-data requests
|
543
|
+
if form_data:
|
544
|
+
request_kwargs["data"] = form_data
|
545
|
+
if files:
|
546
|
+
request_kwargs["files"] = files
|
547
|
+
# Remove Content-Type header to let httpx set it automatically for multipart
|
548
|
+
headers = request_kwargs["headers"].copy()
|
549
|
+
headers.pop("Content-Type", None)
|
550
|
+
request_kwargs["headers"] = headers
|
551
|
+
elif data:
|
552
|
+
# For JSON requests
|
553
|
+
request_kwargs["json"] = data
|
554
|
+
|
555
|
+
response = await self.client.request(**request_kwargs)
|
556
|
+
self._validate_response(response)
|
557
|
+
return self._parse_response(response)
|
558
|
+
|
559
|
+
@backoff.on_exception(backoff.expo, httpx.HTTPStatusError, max_tries=self.max_retries + 1, on_giveup=raise_max_tries_exceeded)
|
560
|
+
async def wrapped_request() -> Any:
|
561
|
+
return await raw_request()
|
562
|
+
|
563
|
+
if raise_for_status:
|
564
|
+
return await raw_request()
|
565
|
+
else:
|
566
|
+
return await wrapped_request()
|
567
|
+
|
568
|
+
async def _request_stream(
|
569
|
+
self,
|
570
|
+
method: str,
|
571
|
+
endpoint: str,
|
572
|
+
data: Optional[dict[str, Any]] = None,
|
573
|
+
params: Optional[dict[str, Any]] = None,
|
574
|
+
form_data: Optional[dict[str, Any]] = None,
|
575
|
+
files: Optional[dict[str, Any] | list] = None,
|
576
|
+
idempotency_key: str | None = None,
|
577
|
+
raise_for_status: bool = False,
|
578
|
+
) -> AsyncIterator[Any]:
|
579
|
+
"""Makes a streaming asynchronous HTTP request to the API.
|
580
|
+
|
581
|
+
Args:
|
582
|
+
method (str): HTTP method (GET, POST, etc.)
|
583
|
+
endpoint (str): API endpoint path
|
584
|
+
data (Optional[dict]): Request payload (JSON)
|
585
|
+
params (Optional[dict]): Query parameters
|
586
|
+
form_data (Optional[dict]): Form data for multipart/form-data requests
|
587
|
+
files (Optional[dict]): Files for multipart/form-data requests
|
588
|
+
idempotency_key (str, optional): Idempotency key for request
|
589
|
+
raise_for_status (bool): Whether to raise on HTTP errors
|
590
|
+
Returns:
|
591
|
+
AsyncIterator[Any]: Async generator yielding parsed JSON objects or raw text strings from the stream
|
592
|
+
|
593
|
+
Raises:
|
594
|
+
RuntimeError: If request fails after max retries or validation error occurs
|
595
|
+
"""
|
596
|
+
|
597
|
+
async def raw_request() -> AsyncIterator[Any]:
|
598
|
+
# Prepare request kwargs
|
599
|
+
stream_kwargs = {
|
600
|
+
"method": method,
|
601
|
+
"url": self._prepare_url(endpoint),
|
602
|
+
"params": params,
|
603
|
+
"headers": self._get_headers(idempotency_key),
|
604
|
+
}
|
605
|
+
|
606
|
+
# Handle different content types
|
607
|
+
if files or form_data:
|
608
|
+
# For multipart/form-data requests
|
609
|
+
if form_data:
|
610
|
+
stream_kwargs["data"] = form_data
|
611
|
+
if files:
|
612
|
+
stream_kwargs["files"] = files
|
613
|
+
# Remove Content-Type header to let httpx set it automatically for multipart
|
614
|
+
headers = stream_kwargs["headers"].copy()
|
615
|
+
headers.pop("Content-Type", None)
|
616
|
+
stream_kwargs["headers"] = headers
|
617
|
+
elif data:
|
618
|
+
# For JSON requests
|
619
|
+
stream_kwargs["json"] = data
|
620
|
+
|
621
|
+
async with self.client.stream(**stream_kwargs) as response_ctx_manager:
|
622
|
+
self._validate_response(response_ctx_manager)
|
623
|
+
|
624
|
+
content_type = response_ctx_manager.headers.get("content-type", "")
|
625
|
+
is_json_stream = "application/json" in content_type or "application/stream+json" in content_type
|
626
|
+
is_text_stream = "text/plain" in content_type or ("text/" in content_type and not is_json_stream)
|
627
|
+
|
628
|
+
async for chunk in response_ctx_manager.aiter_lines():
|
629
|
+
if not chunk:
|
630
|
+
continue
|
631
|
+
|
632
|
+
if is_json_stream:
|
633
|
+
try:
|
634
|
+
yield json.loads(chunk)
|
635
|
+
except Exception:
|
636
|
+
pass
|
637
|
+
elif is_text_stream:
|
638
|
+
yield chunk
|
639
|
+
else:
|
640
|
+
# Default behavior: try JSON first, fall back to text
|
641
|
+
try:
|
642
|
+
yield json.loads(chunk)
|
643
|
+
except Exception:
|
644
|
+
yield chunk
|
645
|
+
|
646
|
+
@backoff.on_exception(backoff.expo, httpx.HTTPStatusError, max_tries=self.max_retries + 1, on_giveup=raise_max_tries_exceeded)
|
647
|
+
async def wrapped_request() -> AsyncIterator[Any]:
|
648
|
+
async for item in raw_request():
|
649
|
+
yield item
|
650
|
+
|
651
|
+
async_iterator_ = raw_request() if raise_for_status else wrapped_request()
|
652
|
+
|
653
|
+
async for item in async_iterator_:
|
654
|
+
yield item
|
655
|
+
|
656
|
+
async def _prepared_request(self, request: PreparedRequest) -> Any:
|
657
|
+
return await self._request(
|
658
|
+
method=request.method,
|
659
|
+
endpoint=request.url,
|
660
|
+
data=request.data,
|
661
|
+
params=request.params,
|
662
|
+
form_data=request.form_data,
|
663
|
+
files=request.files,
|
664
|
+
idempotency_key=request.idempotency_key,
|
665
|
+
raise_for_status=request.raise_for_status
|
666
|
+
)
|
667
|
+
|
668
|
+
async def _prepared_request_stream(self, request: PreparedRequest) -> AsyncIterator[Any]:
|
669
|
+
async for item in self._request_stream(
|
670
|
+
method=request.method,
|
671
|
+
endpoint=request.url,
|
672
|
+
data=request.data,
|
673
|
+
params=request.params,
|
674
|
+
form_data=request.form_data,
|
675
|
+
files=request.files,
|
676
|
+
idempotency_key=request.idempotency_key,
|
677
|
+
raise_for_status=request.raise_for_status
|
678
|
+
):
|
679
|
+
yield item
|
680
|
+
|
681
|
+
async def close(self) -> None:
|
682
|
+
"""Closes the async HTTP client session."""
|
683
|
+
await self.client.aclose()
|
684
|
+
|
685
|
+
async def __aenter__(self) -> "AsyncUiForm":
|
686
|
+
"""Async context manager entry point.
|
687
|
+
|
688
|
+
Returns:
|
689
|
+
AsyncUiForm: The async client instance
|
690
|
+
"""
|
691
|
+
return self
|
692
|
+
|
693
|
+
async def __aexit__(self, exc_type: type, exc_value: BaseException, traceback: TracebackType) -> None:
|
694
|
+
"""Async context manager exit point that ensures the client is properly closed.
|
695
|
+
|
696
|
+
Args:
|
697
|
+
exc_type: The type of the exception that was raised, if any
|
698
|
+
exc_value: The instance of the exception that was raised, if any
|
699
|
+
traceback: The traceback of the exception that was raised, if any
|
700
|
+
"""
|
701
|
+
await self.close()
|