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.
Files changed (111) hide show
  1. retab-0.0.35.dist-info/METADATA +417 -0
  2. retab-0.0.35.dist-info/RECORD +111 -0
  3. retab-0.0.35.dist-info/WHEEL +5 -0
  4. retab-0.0.35.dist-info/top_level.txt +1 -0
  5. uiform/__init__.py +4 -0
  6. uiform/_resource.py +28 -0
  7. uiform/_utils/__init__.py +0 -0
  8. uiform/_utils/ai_models.py +100 -0
  9. uiform/_utils/benchmarking copy.py +588 -0
  10. uiform/_utils/benchmarking.py +485 -0
  11. uiform/_utils/chat.py +332 -0
  12. uiform/_utils/display.py +443 -0
  13. uiform/_utils/json_schema.py +2161 -0
  14. uiform/_utils/mime.py +168 -0
  15. uiform/_utils/responses.py +163 -0
  16. uiform/_utils/stream_context_managers.py +52 -0
  17. uiform/_utils/usage/__init__.py +0 -0
  18. uiform/_utils/usage/usage.py +300 -0
  19. uiform/client.py +701 -0
  20. uiform/py.typed +0 -0
  21. uiform/resources/__init__.py +0 -0
  22. uiform/resources/consensus/__init__.py +3 -0
  23. uiform/resources/consensus/client.py +114 -0
  24. uiform/resources/consensus/completions.py +252 -0
  25. uiform/resources/consensus/completions_stream.py +278 -0
  26. uiform/resources/consensus/responses.py +325 -0
  27. uiform/resources/consensus/responses_stream.py +373 -0
  28. uiform/resources/deployments/__init__.py +9 -0
  29. uiform/resources/deployments/client.py +78 -0
  30. uiform/resources/deployments/endpoints.py +322 -0
  31. uiform/resources/deployments/links.py +452 -0
  32. uiform/resources/deployments/logs.py +211 -0
  33. uiform/resources/deployments/mailboxes.py +496 -0
  34. uiform/resources/deployments/outlook.py +531 -0
  35. uiform/resources/deployments/tests.py +158 -0
  36. uiform/resources/documents/__init__.py +3 -0
  37. uiform/resources/documents/client.py +255 -0
  38. uiform/resources/documents/extractions.py +441 -0
  39. uiform/resources/evals.py +812 -0
  40. uiform/resources/files.py +24 -0
  41. uiform/resources/finetuning.py +62 -0
  42. uiform/resources/jsonlUtils.py +1046 -0
  43. uiform/resources/models.py +45 -0
  44. uiform/resources/openai_example.py +22 -0
  45. uiform/resources/processors/__init__.py +3 -0
  46. uiform/resources/processors/automations/__init__.py +9 -0
  47. uiform/resources/processors/automations/client.py +78 -0
  48. uiform/resources/processors/automations/endpoints.py +317 -0
  49. uiform/resources/processors/automations/links.py +356 -0
  50. uiform/resources/processors/automations/logs.py +211 -0
  51. uiform/resources/processors/automations/mailboxes.py +435 -0
  52. uiform/resources/processors/automations/outlook.py +444 -0
  53. uiform/resources/processors/automations/tests.py +158 -0
  54. uiform/resources/processors/client.py +474 -0
  55. uiform/resources/prompt_optimization.py +76 -0
  56. uiform/resources/schemas.py +369 -0
  57. uiform/resources/secrets/__init__.py +9 -0
  58. uiform/resources/secrets/client.py +20 -0
  59. uiform/resources/secrets/external_api_keys.py +109 -0
  60. uiform/resources/secrets/webhook.py +62 -0
  61. uiform/resources/usage.py +271 -0
  62. uiform/types/__init__.py +0 -0
  63. uiform/types/ai_models.py +645 -0
  64. uiform/types/automations/__init__.py +0 -0
  65. uiform/types/automations/cron.py +58 -0
  66. uiform/types/automations/endpoints.py +21 -0
  67. uiform/types/automations/links.py +28 -0
  68. uiform/types/automations/mailboxes.py +60 -0
  69. uiform/types/automations/outlook.py +68 -0
  70. uiform/types/automations/webhooks.py +21 -0
  71. uiform/types/chat.py +8 -0
  72. uiform/types/completions.py +93 -0
  73. uiform/types/consensus.py +10 -0
  74. uiform/types/db/__init__.py +0 -0
  75. uiform/types/db/annotations.py +24 -0
  76. uiform/types/db/files.py +36 -0
  77. uiform/types/deployments/__init__.py +0 -0
  78. uiform/types/deployments/cron.py +59 -0
  79. uiform/types/deployments/endpoints.py +28 -0
  80. uiform/types/deployments/links.py +36 -0
  81. uiform/types/deployments/mailboxes.py +67 -0
  82. uiform/types/deployments/outlook.py +76 -0
  83. uiform/types/deployments/webhooks.py +21 -0
  84. uiform/types/documents/__init__.py +0 -0
  85. uiform/types/documents/correct_orientation.py +13 -0
  86. uiform/types/documents/create_messages.py +226 -0
  87. uiform/types/documents/extractions.py +297 -0
  88. uiform/types/evals.py +207 -0
  89. uiform/types/events.py +76 -0
  90. uiform/types/extractions.py +85 -0
  91. uiform/types/jobs/__init__.py +0 -0
  92. uiform/types/jobs/base.py +150 -0
  93. uiform/types/jobs/batch_annotation.py +22 -0
  94. uiform/types/jobs/evaluation.py +133 -0
  95. uiform/types/jobs/finetune.py +6 -0
  96. uiform/types/jobs/prompt_optimization.py +41 -0
  97. uiform/types/jobs/webcrawl.py +6 -0
  98. uiform/types/logs.py +231 -0
  99. uiform/types/mime.py +257 -0
  100. uiform/types/modalities.py +68 -0
  101. uiform/types/pagination.py +6 -0
  102. uiform/types/schemas/__init__.py +0 -0
  103. uiform/types/schemas/enhance.py +53 -0
  104. uiform/types/schemas/evaluate.py +55 -0
  105. uiform/types/schemas/generate.py +32 -0
  106. uiform/types/schemas/layout.py +58 -0
  107. uiform/types/schemas/object.py +631 -0
  108. uiform/types/schemas/templates.py +107 -0
  109. uiform/types/secrets/__init__.py +0 -0
  110. uiform/types/secrets/external_api_keys.py +22 -0
  111. 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()