From b0512cf0eaa8a8a87b2f20cc92b0217486ec58d5 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 11 Aug 2026 23:04:11 +0530 Subject: [PATCH 01/16] refactor(client): issue requests through a generated transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client now builds its requests from a transport generated off the API's OpenAPI spec instead of assembling them by hand, and sends them over httpx. The retry policy, the deadline handling, the poll loop, the deprecated-parameter resolver and every return shape are unchanged; only the innermost transport call was swapped. Three things a naive swap would have broken, and what keeps them working: - Callers catch requests.ConnectionError and requests.Timeout by name. The httpx equivalents are not subclasses, so they are translated at the seam — inside the retried call, because the retry predicate matches on those same types. requests.ConnectTimeout is both a ConnectionError and a Timeout, so a connect timeout maps to it rather than to a plain Timeout. - The previous transport followed redirects; httpx does not by default. Without it a 30x from a proxy surfaces as "API error: empty response body". - The generated builders write every spec-declared parameter. Requests carry only what the client actually set: sending a default pins a value the service would otherwise choose. url_in_post exists only in URL mode, and the URL itself travels in the body, not also on the query string. Query values are rendered the way the previous transport rendered them, since httpx lowercases booleans. The generated tree is committed but never hand-edited — tools/gen_sdk.sh overwrites it wholesale from specs/llmwhisperer.json with a pinned generator, so fixes belong in client_v2.py or in the spec. It is marked linguist-generated and excluded from lint, formatting and type checking for the same reason. Testing: tests/unit/compat_test.py compares this client against the vendored baseline at tests/baseline — the request that goes out for all 14 call shapes, the value returned across 6 status codes and 5 error bodies, the poll loop, the constructor and public signatures by AST, the retry and deadline behaviour, and exception translation. 234 unit tests pass. A live round trip is still outstanding. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- .gitattributes | 1 + .gitignore | 1 + .pre-commit-config.yaml | 3 + pyproject.toml | 13 +- specs/llmwhisperer.json | 1151 +++++++++++++++++ src/unstract/llmwhisperer/client_v2.py | 256 ++-- .../llmwhisperer/sdk_llmwhisperer/__init__.py | 9 + .../sdk_llmwhisperer/api/__init__.py | 2 + .../sdk_llmwhisperer/api/account/__init__.py | 2 + .../api/account/test_connection.py | 125 ++ .../sdk_llmwhisperer/api/account/usage.py | 183 +++ .../api/account/usage_info.py | 123 ++ .../sdk_llmwhisperer/api/convert/__init__.py | 2 + .../api/convert/convert_to_pdf.py | 190 +++ .../api/convert/convert_xlsb_to_xlsx.py | 190 +++ .../sdk_llmwhisperer/api/insights/__init__.py | 2 + .../api/insights/document_insights.py | 265 ++++ .../insights/document_insights_retrieve.py | 157 +++ .../sdk_llmwhisperer/api/webhook/__init__.py | 2 + .../api/webhook/webhook_delete.py | 157 +++ .../api/webhook/webhook_get.py | 155 +++ .../api/webhook/webhook_post.py | 175 +++ .../api/webhook/webhook_put.py | 175 +++ .../sdk_llmwhisperer/api/whisper/__init__.py | 2 + .../sdk_llmwhisperer/api/whisper/detail.py | 153 +++ .../sdk_llmwhisperer/api/whisper/extract.py | 561 ++++++++ .../api/whisper/highlights.py | 185 +++ .../api/whisper/pdf_to_images.py | 215 +++ .../api/whisper/pdf_to_images_retrieve.py | 157 +++ .../api/whisper/pdf_to_images_status.py | 157 +++ .../sdk_llmwhisperer/api/whisper/retrieve.py | 168 +++ .../sdk_llmwhisperer/api/whisper/status.py | 153 +++ .../llmwhisperer/sdk_llmwhisperer/client.py | 269 ++++ .../llmwhisperer/sdk_llmwhisperer/errors.py | 17 + .../sdk_llmwhisperer/models/__init__.py | 50 + .../models/convert_to_pdf_response_200.py | 48 + .../convert_xlsb_to_xlsx_response_200.py | 48 + .../models/detail_response_200.py | 48 + .../models/document_insights_response_200.py | 48 + ...document_insights_retrieve_response_200.py | 48 + .../models/highlights_response_200.py | 48 + .../models/pdf_to_images_response_200.py | 48 + .../pdf_to_images_retrieve_response_200.py | 48 + .../pdf_to_images_status_response_200.py | 48 + .../models/test_connection_response_200.py | 48 + .../models/usage_info_response_200.py | 48 + .../models/usage_response_200.py | 48 + .../sdk_llmwhisperer/models/webhook_config.py | 78 ++ .../models/webhook_delete_response_200.py | 48 + .../models/webhook_get_response_200.py | 48 + .../models/webhook_post_response_200.py | 48 + .../models/webhook_put_response_200.py | 48 + .../models/whisper_accepted.py | 80 ++ .../sdk_llmwhisperer/models/whisper_result.py | 116 ++ ...whisper_result_confidence_metadata_item.py | 48 + .../models/whisper_result_metadata.py | 48 + .../sdk_llmwhisperer/models/whisper_status.py | 71 + .../llmwhisperer/sdk_llmwhisperer/types.py | 55 + tests/baseline/client_v2_pr34.py | 898 +++++++++++++ tests/unit/client_v2_test.py | 79 +- tests/unit/compat_test.py | 588 +++++++++ tools/gen_sdk.sh | 42 + tools/openapi-client.yaml | 3 + tools/refresh_baseline.sh | 24 + uv.lock | 65 +- 65 files changed, 8272 insertions(+), 117 deletions(-) create mode 100644 .gitattributes create mode 100644 specs/llmwhisperer.json create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/client.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/errors.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/detail_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_retrieve_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/highlights_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_status_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/test_connection_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_info_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_config.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_delete_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_get_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_put_response_200.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_accepted.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_confidence_metadata_item.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_metadata.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/types.py create mode 100644 tests/baseline/client_v2_pr34.py create mode 100644 tests/unit/compat_test.py create mode 100755 tools/gen_sdk.sh create mode 100644 tools/openapi-client.yaml create mode 100755 tools/refresh_baseline.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..382e90e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/unstract/llmwhisperer/sdk_llmwhisperer/** linguist-generated=true diff --git a/.gitignore b/.gitignore index 5e2bde3..536688b 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ cython_debug/ .pdm-python .python-version +.gen-venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fba8841..7513d16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,9 @@ default_language_version: python: python3.12 default_stages: - pre-commit +# Generated and vendored code is overwritten wholesale by its refresh script, so +# a fix applied here is lost on the next run. +exclude: "^(src/unstract/llmwhisperer/sdk_llmwhisperer/|tests/baseline/)" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 diff --git a/pyproject.toml b/pyproject.toml index 24a4ec5..1af5b1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,10 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] requires-python = ">=3.12" -dependencies = ["requests>=2", "tenacity>=8.0"] +# `requests` remains a dependency for its exception classes: callers catch +# ConnectionError and Timeout by name, and the httpx equivalents are not +# subclasses of them. +dependencies = ["httpx>=0.27", "attrs>=23.2", "requests>=2", "tenacity>=8.0"] [dependency-groups] test = [ @@ -86,6 +89,10 @@ exclude = [ "dist", "node_modules", "venv", + # Generated and vendored code is overwritten wholesale by its refresh + # script, so a lint finding there can never be fixed in place. + "src/unstract/llmwhisperer/sdk_llmwhisperer", + "tests/baseline", ] [tool.ruff.lint] @@ -172,4 +179,6 @@ ignore_missing_imports = true pretty = true show_column_numbers = true show_error_codes = true -exclude = ["venv", ".venv"] +# Generated and vendored code is overwritten wholesale by its refresh script, +# so a finding there can never be fixed in place. +exclude = ["venv", ".venv", "src/unstract/llmwhisperer/sdk_llmwhisperer/", "tests/baseline/"] diff --git a/specs/llmwhisperer.json b/specs/llmwhisperer.json new file mode 100644 index 0000000..d5ae488 --- /dev/null +++ b/specs/llmwhisperer.json @@ -0,0 +1,1151 @@ +{ + "components": { + "schemas": { + "WebhookConfig": { + "properties": { + "auth_token": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + }, + "webhook_name": { + "type": "string" + } + }, + "required": [ + "url", + "auth_token", + "webhook_name" + ], + "type": "object" + }, + "WhisperAccepted": { + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "whisper_hash": { + "type": "string" + } + }, + "type": "object" + }, + "WhisperResult": { + "properties": { + "confidence_metadata": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + "metadata": { + "additionalProperties": true, + "type": "object" + }, + "result_text": { + "type": "string" + }, + "webhook_metadata": { + "type": "string" + } + }, + "type": "object" + }, + "WhisperStatus": { + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "unstract_key": { + "in": "header", + "name": "unstract-key", + "type": "apiKey" + } + } + }, + "info": { + "title": "Unstract LLMWhisperer", + "version": "v2" + }, + "openapi": "3.0.3", + "paths": { + "/api/v2/convert-to-pdf": { + "post": { + "operationId": "convert_to_pdf", + "parameters": [ + { + "in": "query", + "name": "url", + "required": false, + "schema": { + "default": "", + "format": "uri", + "type": "string" + } + }, + { + "in": "query", + "name": "url_in_post", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Convert a document to PDF", + "tags": [ + "convert" + ] + } + }, + "/api/v2/convert-xlsb-to-xlsx": { + "post": { + "operationId": "convert_xlsb_to_xlsx", + "parameters": [ + { + "in": "query", + "name": "url", + "required": false, + "schema": { + "default": "", + "format": "uri", + "type": "string" + } + }, + { + "in": "query", + "name": "url_in_post", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Convert an XLSB workbook to XLSX", + "tags": [ + "convert" + ] + } + }, + "/api/v2/document-insights": { + "post": { + "operationId": "document_insights", + "parameters": [ + { + "in": "query", + "name": "file_name", + "required": false, + "schema": { + "default": "sample.pdf", + "type": "string" + } + }, + { + "in": "query", + "name": "pages_to_extract", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "tag", + "required": false, + "schema": { + "default": "default", + "type": "string" + } + }, + { + "in": "query", + "name": "url", + "required": false, + "schema": { + "default": "", + "format": "uri", + "type": "string" + } + }, + { + "in": "query", + "name": "url_in_post", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "use_webhook", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "webhook_metadata", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Run document insights over a file", + "tags": [ + "insights" + ] + } + }, + "/api/v2/document-insights-retrieve": { + "get": { + "operationId": "document_insights_retrieve", + "parameters": [ + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Retrieve document insights result", + "tags": [ + "insights" + ] + } + }, + "/api/v2/get-usage-info": { + "get": { + "operationId": "usage_info", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Subscription usage summary", + "tags": [ + "account" + ] + } + }, + "/api/v2/highlights": { + "get": { + "operationId": "highlights", + "parameters": [ + { + "in": "query", + "name": "extract_all_lines", + "required": false, + "schema": { + "default": "false", + "type": "string" + } + }, + { + "in": "query", + "name": "lines", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Line-level highlight geometry for an extraction", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/pdf-to-images": { + "post": { + "operationId": "pdf_to_images", + "parameters": [ + { + "in": "query", + "name": "file_name", + "required": false, + "schema": { + "default": "sample.pdf", + "type": "string" + } + }, + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "default": "png", + "type": "string" + } + }, + { + "in": "query", + "name": "tag", + "required": false, + "schema": { + "default": "default", + "type": "string" + } + }, + { + "in": "query", + "name": "url", + "required": false, + "schema": { + "default": "", + "format": "uri", + "type": "string" + } + }, + { + "in": "query", + "name": "url_in_post", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "pdf to images", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/pdf-to-images-retrieve": { + "get": { + "operationId": "pdf_to_images_retrieve", + "parameters": [ + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "pdf to images retrieve", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/pdf-to-images-status": { + "get": { + "operationId": "pdf_to_images_status", + "parameters": [ + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "pdf to images status", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/test-connection": { + "get": { + "operationId": "test_connection", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Verify credentials", + "tags": [ + "account" + ] + } + }, + "/api/v2/usage": { + "get": { + "operationId": "usage", + "parameters": [ + { + "in": "query", + "name": "from_date", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "tag", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "to_date", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Detailed usage statistics", + "tags": [ + "account" + ] + } + }, + "/api/v2/whisper": { + "post": { + "operationId": "extract", + "parameters": [ + { + "in": "query", + "name": "add_line_nos", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "allow_rotated_text", + "required": false, + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "in": "query", + "name": "checkbox_confidence_threshold", + "required": false, + "schema": { + "default": 0.3, + "type": "number" + } + }, + { + "in": "query", + "name": "derotate_threshold", + "required": false, + "schema": { + "default": 10.0, + "type": "number" + } + }, + { + "in": "query", + "name": "file_name", + "required": false, + "schema": { + "default": "sample.pdf", + "type": "string" + } + }, + { + "in": "query", + "name": "gaussian_blur_radius", + "required": false, + "schema": { + "default": 0, + "type": "number" + } + }, + { + "in": "query", + "name": "horizontal_stretch_factor", + "required": false, + "schema": { + "default": 1.0, + "type": "number" + } + }, + { + "in": "query", + "name": "ignore_vertical_text", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "include_line_confidence", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "lang", + "required": false, + "schema": { + "default": "eng", + "type": "string" + } + }, + { + "in": "query", + "name": "line_splitter_strategy", + "required": false, + "schema": { + "default": "left-priority", + "type": "string" + } + }, + { + "in": "query", + "name": "line_splitter_tolerance", + "required": false, + "schema": { + "default": 0.75, + "type": "number" + } + }, + { + "in": "query", + "name": "mark_horizontal_lines", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "mark_vertical_lines", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "median_filter_size", + "required": false, + "schema": { + "default": 0, + "type": "integer" + } + }, + { + "in": "query", + "name": "min_table_width", + "required": false, + "schema": { + "default": 0.0, + "type": "number" + } + }, + { + "in": "query", + "name": "mode", + "required": false, + "schema": { + "default": "form", + "type": "string" + } + }, + { + "in": "query", + "name": "output_mode", + "required": false, + "schema": { + "default": "layout_preserving", + "type": "string" + } + }, + { + "in": "query", + "name": "page_separator", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pages_to_extract", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "tag", + "required": false, + "schema": { + "default": "default", + "type": "string" + } + }, + { + "in": "query", + "name": "url", + "required": false, + "schema": { + "default": "", + "format": "uri", + "type": "string" + } + }, + { + "in": "query", + "name": "url_in_post", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "use_webhook", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "watermark_angle_threshold", + "required": false, + "schema": { + "default": 25.0, + "type": "number" + } + }, + { + "in": "query", + "name": "webhook_metadata", + "required": false, + "schema": { + "default": "", + "type": "string" + } + }, + { + "in": "query", + "name": "word_confidence_threshold", + "required": false, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WhisperAccepted" + } + } + }, + "description": "Accepted" + } + }, + "summary": "Submit a document for text extraction", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/whisper-detail": { + "get": { + "operationId": "detail", + "parameters": [ + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Metadata about a whisper job", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/whisper-manage-callback": { + "delete": { + "operationId": "webhook_delete", + "parameters": [ + { + "in": "query", + "name": "webhook_name", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Manage extraction webhooks", + "tags": [ + "webhook" + ] + }, + "get": { + "operationId": "webhook_get", + "parameters": [ + { + "in": "query", + "name": "webhook_name", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Manage extraction webhooks", + "tags": [ + "webhook" + ] + }, + "post": { + "operationId": "webhook_post", + "parameters": [ + { + "in": "query", + "name": "webhook_name", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Manage extraction webhooks", + "tags": [ + "webhook" + ] + }, + "put": { + "operationId": "webhook_put", + "parameters": [ + { + "in": "query", + "name": "webhook_name", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Manage extraction webhooks", + "tags": [ + "webhook" + ] + } + }, + "/api/v2/whisper-retrieve": { + "get": { + "operationId": "retrieve", + "parameters": [ + { + "in": "query", + "name": "text_only", + "required": false, + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WhisperResult" + } + }, + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + } + }, + "summary": "Retrieve extraction result (destructive \u2014 one shot)", + "tags": [ + "whisper" + ] + } + }, + "/api/v2/whisper-status": { + "get": { + "operationId": "status", + "parameters": [ + { + "in": "query", + "name": "whisper_hash", + "required": false, + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WhisperStatus" + } + } + }, + "description": "OK" + } + }, + "summary": "Poll extraction status", + "tags": [ + "whisper" + ] + } + } + }, + "security": [ + { + "unstract_key": [] + } + ], + "servers": [ + { + "url": "https://llmwhisperer-api.us-central.unstract.com" + } + ] +} diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index cf97f4b..edf7e02 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -23,14 +23,105 @@ import os import time import warnings +from types import ModuleType from typing import IO, Any +import httpx + +# `requests` remains a dependency for its exception classes. Callers catch +# ConnectionError and Timeout by name around these calls, and the httpx +# equivalents are not subclasses, so they are translated at the transport seam. import requests import tenacity from tenacity import retry_if_exception, stop_after_attempt, stop_after_delay, wait_exponential_jitter +from unstract.llmwhisperer.sdk_llmwhisperer.api.account import usage_info +from unstract.llmwhisperer.sdk_llmwhisperer.api.webhook import ( + webhook_delete, + webhook_get, + webhook_post, + webhook_put, +) +from unstract.llmwhisperer.sdk_llmwhisperer.api.whisper import detail, extract, highlights, retrieve, status +from unstract.llmwhisperer.sdk_llmwhisperer.models import WebhookConfig +from unstract.llmwhisperer.sdk_llmwhisperer.types import File BASE_URL_V2 = "https://llmwhisperer-api.us-central.unstract.com/api/v2" +#: The spec's paths are absolute from the service root, while ``base_url`` +#: already carries this prefix. Stripping it keeps the URL identical to the one +#: the client built by hand, for any ``base_url`` a caller configures. +_SPEC_PREFIX = "/api/v2" + +#: Query parameters each call sends. The generated builder writes every +#: spec-declared parameter, including ones this client has never sent, and +#: sending a default is not the same as omitting it: it pins a value the service +#: would otherwise choose. A new parameter must be added here to be sent at all. +_SEND_ONLY: dict[str, frozenset[str]] = { + "extract": frozenset( + { + "mode", + "output_mode", + "page_separator", + "pages_to_extract", + "median_filter_size", + "gaussian_blur_radius", + "line_splitter_tolerance", + "horizontal_stretch_factor", + "mark_vertical_lines", + "mark_horizontal_lines", + "line_splitter_strategy", + "add_line_nos", + "include_line_confidence", + "word_confidence_threshold", + "lang", + "tag", + "file_name", + "webhook_metadata", + "use_webhook", + # In URL mode the URL travels in the body; it is not also a query + # parameter, so `url` is deliberately absent here. + "url_in_post", + } + ), + "status": frozenset({"whisper_hash"}), + "detail": frozenset({"whisper_hash"}), + "retrieve": frozenset({"whisper_hash"}), + "highlights": frozenset({"whisper_hash", "lines", "extract_all_lines"}), + "usage_info": frozenset(), + "webhook_get": frozenset({"webhook_name"}), + "webhook_delete": frozenset({"webhook_name"}), + "webhook_post": frozenset(), + "webhook_put": frozenset(), +} + + +def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: + """Re-raise httpx transport failures as their ``requests`` equivalents. + + Callers document and catch the ``requests`` classes. Ordering matters: + ``TimeoutException`` must be checked before ``ConnectError``, and + ``TransportError`` is the catch-all that keeps a novel transport failure from + escaping untranslated. + """ + try: + return fn(*args, **kwargs) + except httpx.ConnectTimeout as e: + # requests.ConnectTimeout is both a ConnectionError and a Timeout; the + # plain Timeout httpx implies would stop matching half the callers. + raise requests.ConnectTimeout(str(e)) from e + except httpx.TimeoutException as e: + raise requests.Timeout(str(e)) from e + except httpx.ConnectError as e: + raise requests.ConnectionError(str(e)) from e + except httpx.TransportError as e: + raise requests.ConnectionError(str(e)) from e + + +def _wire_value(value: Any) -> Any: + """Render a query value the way ``requests`` did: httpx lowercases + bools.""" + return str(value) if isinstance(value, bool) else value + class LLMWhispererClientException(Exception): """Exception raised for errors in the LLMWhispererClient. @@ -70,7 +161,7 @@ class _RetryableHTTPError(Exception): """Internal exception wrapping an HTTP response with a retryable status code (429, 5xx).""" - def __init__(self, response: requests.Response) -> None: + def __init__(self, response: httpx.Response) -> None: self.response = response super().__init__(f"HTTP {response.status_code}") @@ -163,6 +254,47 @@ def __init__( self.retry_min_wait = retry_min_wait self.retry_max_wait = retry_max_wait + @property + def _transport(self) -> httpx.Client: + """The HTTP client, built on first use. + + ``follow_redirects`` is on because the previous transport followed them + by default; without it a 30x from a proxy or an http->https upgrade + surfaces as an empty response body. Timeouts are set per request. + """ + if getattr(self, "_transport_client", None) is None: + self._transport_client = httpx.Client( + headers=self.headers, + follow_redirects=True, + timeout=httpx.Timeout(None), + ) + return self._transport_client + + def _build_request( + self, module: ModuleType, send_only: frozenset[str] | None = None, **kwargs: Any + ) -> httpx.Request: + """Build a request from the generated builder for an operation. + + The generated code owns the URL, the parameter names and the body + encoding. What it must not own is which parameters go out: it writes + every spec-declared default, so the set is narrowed to what this client + actually sets. Its ``Content-Type`` is dropped too — the transport + derives the same one the previous client sent. + + ``send_only`` narrows further for a call whose parameter set varies with + its arguments; it must stay within the operation's declared set. + """ + declared = _SEND_ONLY[module.__name__.rsplit(".", 1)[-1]] + if send_only is None: + send_only = declared + elif not send_only <= declared: + raise LLMWhispererClientException(f"Undeclared parameters: {sorted(send_only - declared)}", 1) + built = module._get_kwargs(**kwargs) + params = {k: _wire_value(v) for k, v in built.pop("params", {}).items() if k in send_only} + built.pop("headers", None) + url = self.base_url + built.pop("url").removeprefix(_SPEC_PREFIX) + return self._transport.build_request(built.pop("method").upper(), url, params=params, **built) + @staticmethod def _is_retryable(exc: BaseException) -> bool: """Return True if the exception represents a transient/retryable @@ -198,13 +330,26 @@ def _retry_wait(self, retry_state: tenacity.RetryCallState) -> float: max=self.retry_max_wait, )(retry_state=retry_state) + def _send(self, request: httpx.Request, *, timeout: float, stream: bool = False) -> httpx.Response: + """Issue one request, translating transport failures on the way out. + + Translation happens here rather than around the retry loop, so + the retry predicate still sees the exception types it is + configured to retry. + """ + request.extensions = {**request.extensions, "timeout": httpx.Timeout(timeout).as_dict()} + response: httpx.Response = _translate_transport_errors(self._transport.send, request, stream=stream) + if stream: + _translate_transport_errors(response.read) + return response + def _send_request( self, - prepared: requests.PreparedRequest, + prepared: httpx.Request, timeout: int | None = None, stream: bool = False, deadline: float | None = None, - ) -> requests.Response: + ) -> httpx.Response: """Send an HTTP request with optional tenacity retry on transient errors. @@ -235,12 +380,10 @@ def _effective_timeout() -> int | float: return req_timeout if self.max_retries == 0: - s = requests.Session() - return s.send(prepared, timeout=_effective_timeout(), stream=stream) + return self._send(prepared, timeout=_effective_timeout(), stream=stream) - def _attempt() -> requests.Response: - s = requests.Session() - response = s.send(prepared, timeout=_effective_timeout(), stream=stream) + def _attempt() -> httpx.Response: + response = self._send(prepared, timeout=_effective_timeout(), stream=stream) if response.status_code == 429 or response.status_code >= 500: raise _RetryableHTTPError(response) return response @@ -277,10 +420,8 @@ def get_usage_info(self) -> Any: the error message and status code returned by the API. """ self.logger.debug("get_usage_info called") - url = f"{self.base_url}/get-usage-info" - self.logger.debug("url: %s", url) - req = requests.Request("GET", url, headers=self.headers) - prepared = req.prepare() + prepared = self._build_request(usage_info) + self.logger.debug("url: %s", prepared.url) response = self._send_request(prepared) if response.status_code != 200: err = json.loads(response.text) @@ -310,15 +451,13 @@ def get_highlight_data(self, whisper_hash: str, lines: str, extract_all_lines: b the error message and status code returned by the API. """ self.logger.debug("highlight called") - url = f"{self.base_url}/highlights" - params = { - "whisper_hash": whisper_hash, - "lines": lines, - "extract_all_lines": extract_all_lines, - } - self.logger.debug("url: %s", url) - req = requests.Request("GET", url, headers=self.headers, params=params) - prepared = req.prepare() + prepared = self._build_request( + highlights, + whisper_hash=whisper_hash, + lines=lines, + extract_all_lines=extract_all_lines, + ) + self.logger.debug("url: %s", prepared.url) response = self._send_request(prepared) if response.status_code != 200: err = json.loads(response.text) @@ -347,13 +486,10 @@ def whisper_detail(self, whisper_hash: str) -> Any: the error message and status code returned by the API. """ self.logger.debug("whisper_detail called") - url = f"{self.base_url}/whisper-detail" - params = {"whisper_hash": whisper_hash} - self.logger.debug("url: %s", url) + prepared = self._build_request(detail, whisper_hash=whisper_hash) + self.logger.debug("url: %s", prepared.url) self.logger.debug("whisper_hash: %s", whisper_hash) - req = requests.Request("GET", url, headers=self.headers, params=params) - prepared = req.prepare() response = self._send_request(prepared) if response.status_code != 200: if not (response.text or "").strip(): @@ -553,28 +689,18 @@ def whisper( if stream is not None: should_stream = True data = b"".join(stream) - req = requests.Request( - "POST", - api_url, - params=params, - headers=self.headers, - data=data, - ) - else: with open(file_path, "rb") as f: data = f.read() - req = requests.Request( - "POST", - api_url, - params=params, - headers=self.headers, - data=data, - ) else: + # The URL travels in the body, not on the query string; url_in_post + # is what tells the service to read it from there. params["url_in_post"] = True - req = requests.Request("POST", api_url, params=params, headers=self.headers, data=url) - prepared = req.prepare() + data = url.encode() + # The wire carries exactly the parameters assembled above — url_in_post + # only exists in URL mode, and the generated default would otherwise + # send it on every upload. + prepared = self._build_request(extract, frozenset(params), body=File(payload=data), **params) start_time = time.time() deadline = start_time + wait_timeout post_timeout = min(self.api_timeout, wait_timeout) @@ -676,11 +802,8 @@ def whisper_status(self, whisper_hash: str) -> Any: the error message and status code returned by the API. """ self.logger.debug("whisper_status called") - url = f"{self.base_url}/whisper-status" - params = {"whisper_hash": whisper_hash} - self.logger.debug("url: %s", url) - req = requests.Request("GET", url, headers=self.headers, params=params) - prepared = req.prepare() + prepared = self._build_request(status, whisper_hash=whisper_hash) + self.logger.debug("url: %s", prepared.url) response = self._send_request(prepared) if response.status_code != 200: if not (response.text or "").strip(): @@ -721,11 +844,8 @@ def whisper_retrieve(self, whisper_hash: str, encoding: str = "utf-8") -> Any: the error message and status code returned by the API. """ self.logger.debug("whisper_retrieve called") - url = f"{self.base_url}/whisper-retrieve" - params = {"whisper_hash": whisper_hash} - self.logger.debug("url: %s", url) - req = requests.Request("GET", url, headers=self.headers, params=params) - prepared = req.prepare() + prepared = self._build_request(retrieve, whisper_hash=whisper_hash) + self.logger.debug("url: %s", prepared.url) response = self._send_request(prepared) response.encoding = encoding if response.status_code != 200: @@ -758,14 +878,8 @@ def register_webhook(self, url: str, auth_token: str, webhook_name: str) -> Any: LLMWhispererClientException: If the API request fails, it raises an exception with the error message and status code returned by the API. """ - data = { - "url": url, - "auth_token": auth_token, - "webhook_name": webhook_name, - } - url = f"{self.base_url}/whisper-manage-callback" - req = requests.Request("POST", url, headers=self.headers, json=data) - prepared = req.prepare() + body = WebhookConfig(url=url, auth_token=auth_token, webhook_name=webhook_name) + prepared = self._build_request(webhook_post, body=body) response = self._send_request(prepared) if response.status_code != 201: err = json.loads(response.text) @@ -793,14 +907,8 @@ def update_webhook_details(self, webhook_name: str, url: str, auth_token: str) - LLMWhispererClientException: If the API request fails, it raises an exception with the error message and status code returned by the API. """ - data = { - "url": url, - "auth_token": auth_token, - "webhook_name": webhook_name, - } - url = f"{self.base_url}/whisper-manage-callback" - req = requests.Request("PUT", url, headers=self.headers, json=data) - prepared = req.prepare() + body = WebhookConfig(url=url, auth_token=auth_token, webhook_name=webhook_name) + prepared = self._build_request(webhook_put, body=body) response = self._send_request(prepared) if response.status_code != 200: err = json.loads(response.text) @@ -826,10 +934,7 @@ def get_webhook_details(self, webhook_name: str) -> Any: LLMWhispererClientException: If the API request fails, it raises an exception with the error message and status code returned by the API. """ - url = f"{self.base_url}/whisper-manage-callback" - params = {"webhook_name": webhook_name} - req = requests.Request("GET", url, headers=self.headers, params=params) - prepared = req.prepare() + prepared = self._build_request(webhook_get, webhook_name=webhook_name) response = self._send_request(prepared) if response.status_code != 200: err = json.loads(response.text) @@ -855,10 +960,7 @@ def delete_webhook(self, webhook_name: str) -> Any: LLMWhispererClientException: If the API request fails, it raises an exception with the error message and status code returned by the API. """ - url = f"{self.base_url}/whisper-manage-callback" - params = {"webhook_name": webhook_name} - req = requests.Request("DELETE", url, headers=self.headers, params=params) - prepared = req.prepare() + prepared = self._build_request(webhook_delete, webhook_name=webhook_name) response = self._send_request(prepared) if response.status_code != 200: err = json.loads(response.text) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/__init__.py new file mode 100644 index 0000000..fb34241 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/__init__.py @@ -0,0 +1,9 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""A client library for accessing Unstract LLMWhisperer""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/__init__.py new file mode 100644 index 0000000..7254a95 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains methods for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/__init__.py new file mode 100644 index 0000000..7e2ea57 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains endpoint functions for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py new file mode 100644 index 0000000..3297c2e --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py @@ -0,0 +1,125 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.test_connection_response_200 import TestConnectionResponse200 +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/test-connection", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> TestConnectionResponse200 | None: + if response.status_code == 200: + response_200 = TestConnectionResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[TestConnectionResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[TestConnectionResponse200]: + """Verify credentials + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[TestConnectionResponse200] + """ + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> TestConnectionResponse200 | None: + """Verify credentials + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + TestConnectionResponse200 + """ + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[TestConnectionResponse200]: + """Verify credentials + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[TestConnectionResponse200] + """ + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> TestConnectionResponse200 | None: + """Verify credentials + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + TestConnectionResponse200 + """ + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py new file mode 100644 index 0000000..38f34ff --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py @@ -0,0 +1,183 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.usage_response_200 import UsageResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + from_date: str | Unset = UNSET, + tag: str | Unset = UNSET, + to_date: str | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["from_date"] = from_date + + params["tag"] = tag + + params["to_date"] = to_date + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/usage", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UsageResponse200 | None: + if response.status_code == 200: + response_200 = UsageResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UsageResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + from_date: str | Unset = UNSET, + tag: str | Unset = UNSET, + to_date: str | Unset = UNSET, +) -> Response[UsageResponse200]: + """Detailed usage statistics + + Args: + from_date (str | Unset): + tag (str | Unset): + to_date (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UsageResponse200] + """ + kwargs = _get_kwargs( + from_date=from_date, + tag=tag, + to_date=to_date, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + from_date: str | Unset = UNSET, + tag: str | Unset = UNSET, + to_date: str | Unset = UNSET, +) -> UsageResponse200 | None: + """Detailed usage statistics + + Args: + from_date (str | Unset): + tag (str | Unset): + to_date (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UsageResponse200 + """ + return sync_detailed( + client=client, + from_date=from_date, + tag=tag, + to_date=to_date, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + from_date: str | Unset = UNSET, + tag: str | Unset = UNSET, + to_date: str | Unset = UNSET, +) -> Response[UsageResponse200]: + """Detailed usage statistics + + Args: + from_date (str | Unset): + tag (str | Unset): + to_date (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UsageResponse200] + """ + kwargs = _get_kwargs( + from_date=from_date, + tag=tag, + to_date=to_date, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + from_date: str | Unset = UNSET, + tag: str | Unset = UNSET, + to_date: str | Unset = UNSET, +) -> UsageResponse200 | None: + """Detailed usage statistics + + Args: + from_date (str | Unset): + tag (str | Unset): + to_date (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UsageResponse200 + """ + return ( + await asyncio_detailed( + client=client, + from_date=from_date, + tag=tag, + to_date=to_date, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py new file mode 100644 index 0000000..d806174 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py @@ -0,0 +1,123 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.usage_info_response_200 import UsageInfoResponse200 +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/get-usage-info", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UsageInfoResponse200 | None: + if response.status_code == 200: + response_200 = UsageInfoResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[UsageInfoResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[UsageInfoResponse200]: + """Subscription usage summary + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UsageInfoResponse200] + """ + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> UsageInfoResponse200 | None: + """Subscription usage summary + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UsageInfoResponse200 + """ + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[UsageInfoResponse200]: + """Subscription usage summary + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UsageInfoResponse200] + """ + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> UsageInfoResponse200 | None: + """Subscription usage summary + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UsageInfoResponse200 + """ + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/__init__.py new file mode 100644 index 0000000..7e2ea57 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains endpoint functions for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py new file mode 100644 index 0000000..33d5fab --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py @@ -0,0 +1,190 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.convert_to_pdf_response_200 import ConvertToPdfResponse200 +from ...types import UNSET, File, Response, Unset + + +def _get_kwargs( + *, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["url"] = url_query + + params["url_in_post"] = url_in_post + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/convert-to-pdf", + "params": params, + } + + _kwargs["content"] = body.payload + headers["Content-Type"] = "application/octet-stream" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ConvertToPdfResponse200 | None: + if response.status_code == 200: + response_200 = ConvertToPdfResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ConvertToPdfResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[ConvertToPdfResponse200]: + """Convert a document to PDF + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ConvertToPdfResponse200] + """ + kwargs = _get_kwargs( + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> ConvertToPdfResponse200 | None: + """Convert a document to PDF + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ConvertToPdfResponse200 + """ + return sync_detailed( + client=client, + body=body, + url_query=url_query, + url_in_post=url_in_post, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[ConvertToPdfResponse200]: + """Convert a document to PDF + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ConvertToPdfResponse200] + """ + kwargs = _get_kwargs( + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> ConvertToPdfResponse200 | None: + """Convert a document to PDF + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ConvertToPdfResponse200 + """ + return ( + await asyncio_detailed( + client=client, + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py new file mode 100644 index 0000000..441d863 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py @@ -0,0 +1,190 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.convert_xlsb_to_xlsx_response_200 import ConvertXlsbToXlsxResponse200 +from ...types import UNSET, File, Response, Unset + + +def _get_kwargs( + *, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["url"] = url_query + + params["url_in_post"] = url_in_post + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/convert-xlsb-to-xlsx", + "params": params, + } + + _kwargs["content"] = body.payload + headers["Content-Type"] = "application/octet-stream" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ConvertXlsbToXlsxResponse200 | None: + if response.status_code == 200: + response_200 = ConvertXlsbToXlsxResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ConvertXlsbToXlsxResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[ConvertXlsbToXlsxResponse200]: + """Convert an XLSB workbook to XLSX + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ConvertXlsbToXlsxResponse200] + """ + kwargs = _get_kwargs( + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> ConvertXlsbToXlsxResponse200 | None: + """Convert an XLSB workbook to XLSX + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ConvertXlsbToXlsxResponse200 + """ + return sync_detailed( + client=client, + body=body, + url_query=url_query, + url_in_post=url_in_post, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[ConvertXlsbToXlsxResponse200]: + """Convert an XLSB workbook to XLSX + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ConvertXlsbToXlsxResponse200] + """ + kwargs = _get_kwargs( + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: File, + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> ConvertXlsbToXlsxResponse200 | None: + """Convert an XLSB workbook to XLSX + + Args: + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ConvertXlsbToXlsxResponse200 + """ + return ( + await asyncio_detailed( + client=client, + body=body, + url_query=url_query, + url_in_post=url_in_post, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/__init__.py new file mode 100644 index 0000000..7e2ea57 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains endpoint functions for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py new file mode 100644 index 0000000..d8950ce --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py @@ -0,0 +1,265 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.document_insights_response_200 import DocumentInsightsResponse200 +from ...types import UNSET, File, Response, Unset + + +def _get_kwargs( + *, + body: File, + file_name: str | Unset = "sample.pdf", + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + webhook_metadata: str | Unset = "", +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["file_name"] = file_name + + params["pages_to_extract"] = pages_to_extract + + params["tag"] = tag + + params["url"] = url_query + + params["url_in_post"] = url_in_post + + params["use_webhook"] = use_webhook + + params["webhook_metadata"] = webhook_metadata + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/document-insights", + "params": params, + } + + _kwargs["content"] = body.payload + headers["Content-Type"] = "application/octet-stream" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DocumentInsightsResponse200 | None: + if response.status_code == 200: + response_200 = DocumentInsightsResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DocumentInsightsResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + file_name: str | Unset = "sample.pdf", + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + webhook_metadata: str | Unset = "", +) -> Response[DocumentInsightsResponse200]: + """Run document insights over a file + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + webhook_metadata (str | Unset): Default: ''. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocumentInsightsResponse200] + """ + kwargs = _get_kwargs( + body=body, + file_name=file_name, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + webhook_metadata=webhook_metadata, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: File, + file_name: str | Unset = "sample.pdf", + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + webhook_metadata: str | Unset = "", +) -> DocumentInsightsResponse200 | None: + """Run document insights over a file + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + webhook_metadata (str | Unset): Default: ''. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocumentInsightsResponse200 + """ + return sync_detailed( + client=client, + body=body, + file_name=file_name, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + webhook_metadata=webhook_metadata, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + file_name: str | Unset = "sample.pdf", + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + webhook_metadata: str | Unset = "", +) -> Response[DocumentInsightsResponse200]: + """Run document insights over a file + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + webhook_metadata (str | Unset): Default: ''. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocumentInsightsResponse200] + """ + kwargs = _get_kwargs( + body=body, + file_name=file_name, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + webhook_metadata=webhook_metadata, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: File, + file_name: str | Unset = "sample.pdf", + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + webhook_metadata: str | Unset = "", +) -> DocumentInsightsResponse200 | None: + """Run document insights over a file + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + webhook_metadata (str | Unset): Default: ''. + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocumentInsightsResponse200 + """ + return ( + await asyncio_detailed( + client=client, + body=body, + file_name=file_name, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + webhook_metadata=webhook_metadata, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py new file mode 100644 index 0000000..a6ba73b --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py @@ -0,0 +1,157 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.document_insights_retrieve_response_200 import DocumentInsightsRetrieveResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/document-insights-retrieve", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DocumentInsightsRetrieveResponse200 | None: + if response.status_code == 200: + response_200 = DocumentInsightsRetrieveResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DocumentInsightsRetrieveResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[DocumentInsightsRetrieveResponse200]: + """Retrieve document insights result + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocumentInsightsRetrieveResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> DocumentInsightsRetrieveResponse200 | None: + """Retrieve document insights result + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocumentInsightsRetrieveResponse200 + """ + return sync_detailed( + client=client, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[DocumentInsightsRetrieveResponse200]: + """Retrieve document insights result + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocumentInsightsRetrieveResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> DocumentInsightsRetrieveResponse200 | None: + """Retrieve document insights result + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocumentInsightsRetrieveResponse200 + """ + return ( + await asyncio_detailed( + client=client, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/__init__.py new file mode 100644 index 0000000..7e2ea57 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains endpoint functions for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py new file mode 100644 index 0000000..e4ff850 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py @@ -0,0 +1,157 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.webhook_delete_response_200 import WebhookDeleteResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + webhook_name: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["webhook_name"] = webhook_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/api/v2/whisper-manage-callback", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> WebhookDeleteResponse200 | None: + if response.status_code == 200: + response_200 = WebhookDeleteResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[WebhookDeleteResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> Response[WebhookDeleteResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookDeleteResponse200] + """ + kwargs = _get_kwargs( + webhook_name=webhook_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> WebhookDeleteResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookDeleteResponse200 + """ + return sync_detailed( + client=client, + webhook_name=webhook_name, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> Response[WebhookDeleteResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookDeleteResponse200] + """ + kwargs = _get_kwargs( + webhook_name=webhook_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> WebhookDeleteResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookDeleteResponse200 + """ + return ( + await asyncio_detailed( + client=client, + webhook_name=webhook_name, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py new file mode 100644 index 0000000..0a7525c --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py @@ -0,0 +1,155 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.webhook_get_response_200 import WebhookGetResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + webhook_name: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["webhook_name"] = webhook_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/whisper-manage-callback", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookGetResponse200 | None: + if response.status_code == 200: + response_200 = WebhookGetResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[WebhookGetResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> Response[WebhookGetResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookGetResponse200] + """ + kwargs = _get_kwargs( + webhook_name=webhook_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> WebhookGetResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookGetResponse200 + """ + return sync_detailed( + client=client, + webhook_name=webhook_name, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> Response[WebhookGetResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookGetResponse200] + """ + kwargs = _get_kwargs( + webhook_name=webhook_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + webhook_name: str | Unset = "", +) -> WebhookGetResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookGetResponse200 + """ + return ( + await asyncio_detailed( + client=client, + webhook_name=webhook_name, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py new file mode 100644 index 0000000..8674d4a --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py @@ -0,0 +1,175 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.webhook_config import WebhookConfig +from ...models.webhook_post_response_200 import WebhookPostResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["webhook_name"] = webhook_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/whisper-manage-callback", + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookPostResponse200 | None: + if response.status_code == 200: + response_200 = WebhookPostResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[WebhookPostResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> Response[WebhookPostResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookPostResponse200] + """ + kwargs = _get_kwargs( + body=body, + webhook_name=webhook_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> WebhookPostResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookPostResponse200 + """ + return sync_detailed( + client=client, + body=body, + webhook_name=webhook_name, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> Response[WebhookPostResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookPostResponse200] + """ + kwargs = _get_kwargs( + body=body, + webhook_name=webhook_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> WebhookPostResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookPostResponse200 + """ + return ( + await asyncio_detailed( + client=client, + body=body, + webhook_name=webhook_name, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py new file mode 100644 index 0000000..b131d80 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py @@ -0,0 +1,175 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.webhook_config import WebhookConfig +from ...models.webhook_put_response_200 import WebhookPutResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["webhook_name"] = webhook_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "put", + "url": "/api/v2/whisper-manage-callback", + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookPutResponse200 | None: + if response.status_code == 200: + response_200 = WebhookPutResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[WebhookPutResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> Response[WebhookPutResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookPutResponse200] + """ + kwargs = _get_kwargs( + body=body, + webhook_name=webhook_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> WebhookPutResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookPutResponse200 + """ + return sync_detailed( + client=client, + body=body, + webhook_name=webhook_name, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> Response[WebhookPutResponse200]: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WebhookPutResponse200] + """ + kwargs = _get_kwargs( + body=body, + webhook_name=webhook_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: WebhookConfig, + webhook_name: str | Unset = "", +) -> WebhookPutResponse200 | None: + """Manage extraction webhooks + + Args: + webhook_name (str | Unset): Default: ''. + body (WebhookConfig): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WebhookPutResponse200 + """ + return ( + await asyncio_detailed( + client=client, + body=body, + webhook_name=webhook_name, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/__init__.py new file mode 100644 index 0000000..7e2ea57 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/__init__.py @@ -0,0 +1,2 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains endpoint functions for accessing the API""" diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py new file mode 100644 index 0000000..bff096f --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py @@ -0,0 +1,153 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.detail_response_200 import DetailResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/whisper-detail", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> DetailResponse200 | None: + if response.status_code == 200: + response_200 = DetailResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[DetailResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[DetailResponse200]: + """Metadata about a whisper job + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DetailResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> DetailResponse200 | None: + """Metadata about a whisper job + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DetailResponse200 + """ + return sync_detailed( + client=client, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[DetailResponse200]: + """Metadata about a whisper job + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DetailResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> DetailResponse200 | None: + """Metadata about a whisper job + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DetailResponse200 + """ + return ( + await asyncio_detailed( + client=client, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py new file mode 100644 index 0000000..43dd576 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py @@ -0,0 +1,561 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.whisper_accepted import WhisperAccepted +from ...types import UNSET, File, Response, Unset + + +def _get_kwargs( + *, + body: File, + add_line_nos: bool | Unset = False, + allow_rotated_text: bool | Unset = True, + checkbox_confidence_threshold: float | Unset = 0.3, + derotate_threshold: float | Unset = 10.0, + file_name: str | Unset = "sample.pdf", + gaussian_blur_radius: float | Unset = 0.0, + horizontal_stretch_factor: float | Unset = 1.0, + ignore_vertical_text: bool | Unset = False, + include_line_confidence: bool | Unset = False, + lang: str | Unset = "eng", + line_splitter_strategy: str | Unset = "left-priority", + line_splitter_tolerance: float | Unset = 0.75, + mark_horizontal_lines: bool | Unset = False, + mark_vertical_lines: bool | Unset = False, + median_filter_size: int | Unset = 0, + min_table_width: float | Unset = 0.0, + mode: str | Unset = "form", + output_mode: str | Unset = "layout_preserving", + page_separator: str | Unset = UNSET, + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + watermark_angle_threshold: float | Unset = 25.0, + webhook_metadata: str | Unset = "", + word_confidence_threshold: float | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["add_line_nos"] = add_line_nos + + params["allow_rotated_text"] = allow_rotated_text + + params["checkbox_confidence_threshold"] = checkbox_confidence_threshold + + params["derotate_threshold"] = derotate_threshold + + params["file_name"] = file_name + + params["gaussian_blur_radius"] = gaussian_blur_radius + + params["horizontal_stretch_factor"] = horizontal_stretch_factor + + params["ignore_vertical_text"] = ignore_vertical_text + + params["include_line_confidence"] = include_line_confidence + + params["lang"] = lang + + params["line_splitter_strategy"] = line_splitter_strategy + + params["line_splitter_tolerance"] = line_splitter_tolerance + + params["mark_horizontal_lines"] = mark_horizontal_lines + + params["mark_vertical_lines"] = mark_vertical_lines + + params["median_filter_size"] = median_filter_size + + params["min_table_width"] = min_table_width + + params["mode"] = mode + + params["output_mode"] = output_mode + + params["page_separator"] = page_separator + + params["pages_to_extract"] = pages_to_extract + + params["tag"] = tag + + params["url"] = url_query + + params["url_in_post"] = url_in_post + + params["use_webhook"] = use_webhook + + params["watermark_angle_threshold"] = watermark_angle_threshold + + params["webhook_metadata"] = webhook_metadata + + params["word_confidence_threshold"] = word_confidence_threshold + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/whisper", + "params": params, + } + + _kwargs["content"] = body.payload + headers["Content-Type"] = "application/octet-stream" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperAccepted | None: + if response.status_code == 202: + response_202 = WhisperAccepted.from_dict(response.json()) + + return response_202 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperAccepted]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + add_line_nos: bool | Unset = False, + allow_rotated_text: bool | Unset = True, + checkbox_confidence_threshold: float | Unset = 0.3, + derotate_threshold: float | Unset = 10.0, + file_name: str | Unset = "sample.pdf", + gaussian_blur_radius: float | Unset = 0.0, + horizontal_stretch_factor: float | Unset = 1.0, + ignore_vertical_text: bool | Unset = False, + include_line_confidence: bool | Unset = False, + lang: str | Unset = "eng", + line_splitter_strategy: str | Unset = "left-priority", + line_splitter_tolerance: float | Unset = 0.75, + mark_horizontal_lines: bool | Unset = False, + mark_vertical_lines: bool | Unset = False, + median_filter_size: int | Unset = 0, + min_table_width: float | Unset = 0.0, + mode: str | Unset = "form", + output_mode: str | Unset = "layout_preserving", + page_separator: str | Unset = UNSET, + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + watermark_angle_threshold: float | Unset = 25.0, + webhook_metadata: str | Unset = "", + word_confidence_threshold: float | Unset = UNSET, +) -> Response[WhisperAccepted]: + """Submit a document for text extraction + + Args: + add_line_nos (bool | Unset): Default: False. + allow_rotated_text (bool | Unset): Default: True. + checkbox_confidence_threshold (float | Unset): Default: 0.3. + derotate_threshold (float | Unset): Default: 10.0. + file_name (str | Unset): Default: 'sample.pdf'. + gaussian_blur_radius (float | Unset): Default: 0.0. + horizontal_stretch_factor (float | Unset): Default: 1.0. + ignore_vertical_text (bool | Unset): Default: False. + include_line_confidence (bool | Unset): Default: False. + lang (str | Unset): Default: 'eng'. + line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_tolerance (float | Unset): Default: 0.75. + mark_horizontal_lines (bool | Unset): Default: False. + mark_vertical_lines (bool | Unset): Default: False. + median_filter_size (int | Unset): Default: 0. + min_table_width (float | Unset): Default: 0.0. + mode (str | Unset): Default: 'form'. + output_mode (str | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + watermark_angle_threshold (float | Unset): Default: 25.0. + webhook_metadata (str | Unset): Default: ''. + word_confidence_threshold (float | Unset): + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperAccepted] + """ + kwargs = _get_kwargs( + body=body, + add_line_nos=add_line_nos, + allow_rotated_text=allow_rotated_text, + checkbox_confidence_threshold=checkbox_confidence_threshold, + derotate_threshold=derotate_threshold, + file_name=file_name, + gaussian_blur_radius=gaussian_blur_radius, + horizontal_stretch_factor=horizontal_stretch_factor, + ignore_vertical_text=ignore_vertical_text, + include_line_confidence=include_line_confidence, + lang=lang, + line_splitter_strategy=line_splitter_strategy, + line_splitter_tolerance=line_splitter_tolerance, + mark_horizontal_lines=mark_horizontal_lines, + mark_vertical_lines=mark_vertical_lines, + median_filter_size=median_filter_size, + min_table_width=min_table_width, + mode=mode, + output_mode=output_mode, + page_separator=page_separator, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + watermark_angle_threshold=watermark_angle_threshold, + webhook_metadata=webhook_metadata, + word_confidence_threshold=word_confidence_threshold, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: File, + add_line_nos: bool | Unset = False, + allow_rotated_text: bool | Unset = True, + checkbox_confidence_threshold: float | Unset = 0.3, + derotate_threshold: float | Unset = 10.0, + file_name: str | Unset = "sample.pdf", + gaussian_blur_radius: float | Unset = 0.0, + horizontal_stretch_factor: float | Unset = 1.0, + ignore_vertical_text: bool | Unset = False, + include_line_confidence: bool | Unset = False, + lang: str | Unset = "eng", + line_splitter_strategy: str | Unset = "left-priority", + line_splitter_tolerance: float | Unset = 0.75, + mark_horizontal_lines: bool | Unset = False, + mark_vertical_lines: bool | Unset = False, + median_filter_size: int | Unset = 0, + min_table_width: float | Unset = 0.0, + mode: str | Unset = "form", + output_mode: str | Unset = "layout_preserving", + page_separator: str | Unset = UNSET, + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + watermark_angle_threshold: float | Unset = 25.0, + webhook_metadata: str | Unset = "", + word_confidence_threshold: float | Unset = UNSET, +) -> WhisperAccepted | None: + """Submit a document for text extraction + + Args: + add_line_nos (bool | Unset): Default: False. + allow_rotated_text (bool | Unset): Default: True. + checkbox_confidence_threshold (float | Unset): Default: 0.3. + derotate_threshold (float | Unset): Default: 10.0. + file_name (str | Unset): Default: 'sample.pdf'. + gaussian_blur_radius (float | Unset): Default: 0.0. + horizontal_stretch_factor (float | Unset): Default: 1.0. + ignore_vertical_text (bool | Unset): Default: False. + include_line_confidence (bool | Unset): Default: False. + lang (str | Unset): Default: 'eng'. + line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_tolerance (float | Unset): Default: 0.75. + mark_horizontal_lines (bool | Unset): Default: False. + mark_vertical_lines (bool | Unset): Default: False. + median_filter_size (int | Unset): Default: 0. + min_table_width (float | Unset): Default: 0.0. + mode (str | Unset): Default: 'form'. + output_mode (str | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + watermark_angle_threshold (float | Unset): Default: 25.0. + webhook_metadata (str | Unset): Default: ''. + word_confidence_threshold (float | Unset): + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperAccepted + """ + return sync_detailed( + client=client, + body=body, + add_line_nos=add_line_nos, + allow_rotated_text=allow_rotated_text, + checkbox_confidence_threshold=checkbox_confidence_threshold, + derotate_threshold=derotate_threshold, + file_name=file_name, + gaussian_blur_radius=gaussian_blur_radius, + horizontal_stretch_factor=horizontal_stretch_factor, + ignore_vertical_text=ignore_vertical_text, + include_line_confidence=include_line_confidence, + lang=lang, + line_splitter_strategy=line_splitter_strategy, + line_splitter_tolerance=line_splitter_tolerance, + mark_horizontal_lines=mark_horizontal_lines, + mark_vertical_lines=mark_vertical_lines, + median_filter_size=median_filter_size, + min_table_width=min_table_width, + mode=mode, + output_mode=output_mode, + page_separator=page_separator, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + watermark_angle_threshold=watermark_angle_threshold, + webhook_metadata=webhook_metadata, + word_confidence_threshold=word_confidence_threshold, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: File, + add_line_nos: bool | Unset = False, + allow_rotated_text: bool | Unset = True, + checkbox_confidence_threshold: float | Unset = 0.3, + derotate_threshold: float | Unset = 10.0, + file_name: str | Unset = "sample.pdf", + gaussian_blur_radius: float | Unset = 0.0, + horizontal_stretch_factor: float | Unset = 1.0, + ignore_vertical_text: bool | Unset = False, + include_line_confidence: bool | Unset = False, + lang: str | Unset = "eng", + line_splitter_strategy: str | Unset = "left-priority", + line_splitter_tolerance: float | Unset = 0.75, + mark_horizontal_lines: bool | Unset = False, + mark_vertical_lines: bool | Unset = False, + median_filter_size: int | Unset = 0, + min_table_width: float | Unset = 0.0, + mode: str | Unset = "form", + output_mode: str | Unset = "layout_preserving", + page_separator: str | Unset = UNSET, + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + watermark_angle_threshold: float | Unset = 25.0, + webhook_metadata: str | Unset = "", + word_confidence_threshold: float | Unset = UNSET, +) -> Response[WhisperAccepted]: + """Submit a document for text extraction + + Args: + add_line_nos (bool | Unset): Default: False. + allow_rotated_text (bool | Unset): Default: True. + checkbox_confidence_threshold (float | Unset): Default: 0.3. + derotate_threshold (float | Unset): Default: 10.0. + file_name (str | Unset): Default: 'sample.pdf'. + gaussian_blur_radius (float | Unset): Default: 0.0. + horizontal_stretch_factor (float | Unset): Default: 1.0. + ignore_vertical_text (bool | Unset): Default: False. + include_line_confidence (bool | Unset): Default: False. + lang (str | Unset): Default: 'eng'. + line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_tolerance (float | Unset): Default: 0.75. + mark_horizontal_lines (bool | Unset): Default: False. + mark_vertical_lines (bool | Unset): Default: False. + median_filter_size (int | Unset): Default: 0. + min_table_width (float | Unset): Default: 0.0. + mode (str | Unset): Default: 'form'. + output_mode (str | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + watermark_angle_threshold (float | Unset): Default: 25.0. + webhook_metadata (str | Unset): Default: ''. + word_confidence_threshold (float | Unset): + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperAccepted] + """ + kwargs = _get_kwargs( + body=body, + add_line_nos=add_line_nos, + allow_rotated_text=allow_rotated_text, + checkbox_confidence_threshold=checkbox_confidence_threshold, + derotate_threshold=derotate_threshold, + file_name=file_name, + gaussian_blur_radius=gaussian_blur_radius, + horizontal_stretch_factor=horizontal_stretch_factor, + ignore_vertical_text=ignore_vertical_text, + include_line_confidence=include_line_confidence, + lang=lang, + line_splitter_strategy=line_splitter_strategy, + line_splitter_tolerance=line_splitter_tolerance, + mark_horizontal_lines=mark_horizontal_lines, + mark_vertical_lines=mark_vertical_lines, + median_filter_size=median_filter_size, + min_table_width=min_table_width, + mode=mode, + output_mode=output_mode, + page_separator=page_separator, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + watermark_angle_threshold=watermark_angle_threshold, + webhook_metadata=webhook_metadata, + word_confidence_threshold=word_confidence_threshold, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: File, + add_line_nos: bool | Unset = False, + allow_rotated_text: bool | Unset = True, + checkbox_confidence_threshold: float | Unset = 0.3, + derotate_threshold: float | Unset = 10.0, + file_name: str | Unset = "sample.pdf", + gaussian_blur_radius: float | Unset = 0.0, + horizontal_stretch_factor: float | Unset = 1.0, + ignore_vertical_text: bool | Unset = False, + include_line_confidence: bool | Unset = False, + lang: str | Unset = "eng", + line_splitter_strategy: str | Unset = "left-priority", + line_splitter_tolerance: float | Unset = 0.75, + mark_horizontal_lines: bool | Unset = False, + mark_vertical_lines: bool | Unset = False, + median_filter_size: int | Unset = 0, + min_table_width: float | Unset = 0.0, + mode: str | Unset = "form", + output_mode: str | Unset = "layout_preserving", + page_separator: str | Unset = UNSET, + pages_to_extract: str | Unset = "", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, + use_webhook: str | Unset = "", + watermark_angle_threshold: float | Unset = 25.0, + webhook_metadata: str | Unset = "", + word_confidence_threshold: float | Unset = UNSET, +) -> WhisperAccepted | None: + """Submit a document for text extraction + + Args: + add_line_nos (bool | Unset): Default: False. + allow_rotated_text (bool | Unset): Default: True. + checkbox_confidence_threshold (float | Unset): Default: 0.3. + derotate_threshold (float | Unset): Default: 10.0. + file_name (str | Unset): Default: 'sample.pdf'. + gaussian_blur_radius (float | Unset): Default: 0.0. + horizontal_stretch_factor (float | Unset): Default: 1.0. + ignore_vertical_text (bool | Unset): Default: False. + include_line_confidence (bool | Unset): Default: False. + lang (str | Unset): Default: 'eng'. + line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_tolerance (float | Unset): Default: 0.75. + mark_horizontal_lines (bool | Unset): Default: False. + mark_vertical_lines (bool | Unset): Default: False. + median_filter_size (int | Unset): Default: 0. + min_table_width (float | Unset): Default: 0.0. + mode (str | Unset): Default: 'form'. + output_mode (str | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): + pages_to_extract (str | Unset): Default: ''. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + use_webhook (str | Unset): Default: ''. + watermark_angle_threshold (float | Unset): Default: 25.0. + webhook_metadata (str | Unset): Default: ''. + word_confidence_threshold (float | Unset): + body (File): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperAccepted + """ + return ( + await asyncio_detailed( + client=client, + body=body, + add_line_nos=add_line_nos, + allow_rotated_text=allow_rotated_text, + checkbox_confidence_threshold=checkbox_confidence_threshold, + derotate_threshold=derotate_threshold, + file_name=file_name, + gaussian_blur_radius=gaussian_blur_radius, + horizontal_stretch_factor=horizontal_stretch_factor, + ignore_vertical_text=ignore_vertical_text, + include_line_confidence=include_line_confidence, + lang=lang, + line_splitter_strategy=line_splitter_strategy, + line_splitter_tolerance=line_splitter_tolerance, + mark_horizontal_lines=mark_horizontal_lines, + mark_vertical_lines=mark_vertical_lines, + median_filter_size=median_filter_size, + min_table_width=min_table_width, + mode=mode, + output_mode=output_mode, + page_separator=page_separator, + pages_to_extract=pages_to_extract, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + use_webhook=use_webhook, + watermark_angle_threshold=watermark_angle_threshold, + webhook_metadata=webhook_metadata, + word_confidence_threshold=word_confidence_threshold, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py new file mode 100644 index 0000000..79b88c2 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py @@ -0,0 +1,185 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.highlights_response_200 import HighlightsResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + extract_all_lines: str | Unset = "false", + lines: str | Unset = "", + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["extract_all_lines"] = extract_all_lines + + params["lines"] = lines + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/highlights", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> HighlightsResponse200 | None: + if response.status_code == 200: + response_200 = HighlightsResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[HighlightsResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + extract_all_lines: str | Unset = "false", + lines: str | Unset = "", + whisper_hash: str | Unset = "", +) -> Response[HighlightsResponse200]: + """Line-level highlight geometry for an extraction + + Args: + extract_all_lines (str | Unset): Default: 'false'. + lines (str | Unset): Default: ''. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HighlightsResponse200] + """ + kwargs = _get_kwargs( + extract_all_lines=extract_all_lines, + lines=lines, + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + extract_all_lines: str | Unset = "false", + lines: str | Unset = "", + whisper_hash: str | Unset = "", +) -> HighlightsResponse200 | None: + """Line-level highlight geometry for an extraction + + Args: + extract_all_lines (str | Unset): Default: 'false'. + lines (str | Unset): Default: ''. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HighlightsResponse200 + """ + return sync_detailed( + client=client, + extract_all_lines=extract_all_lines, + lines=lines, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + extract_all_lines: str | Unset = "false", + lines: str | Unset = "", + whisper_hash: str | Unset = "", +) -> Response[HighlightsResponse200]: + """Line-level highlight geometry for an extraction + + Args: + extract_all_lines (str | Unset): Default: 'false'. + lines (str | Unset): Default: ''. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[HighlightsResponse200] + """ + kwargs = _get_kwargs( + extract_all_lines=extract_all_lines, + lines=lines, + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + extract_all_lines: str | Unset = "false", + lines: str | Unset = "", + whisper_hash: str | Unset = "", +) -> HighlightsResponse200 | None: + """Line-level highlight geometry for an extraction + + Args: + extract_all_lines (str | Unset): Default: 'false'. + lines (str | Unset): Default: ''. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + HighlightsResponse200 + """ + return ( + await asyncio_detailed( + client=client, + extract_all_lines=extract_all_lines, + lines=lines, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py new file mode 100644 index 0000000..0824603 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py @@ -0,0 +1,215 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.pdf_to_images_response_200 import PdfToImagesResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + file_name: str | Unset = "sample.pdf", + format_: str | Unset = "png", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["file_name"] = file_name + + params["format"] = format_ + + params["tag"] = tag + + params["url"] = url_query + + params["url_in_post"] = url_in_post + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/pdf-to-images", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PdfToImagesResponse200 | None: + if response.status_code == 200: + response_200 = PdfToImagesResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[PdfToImagesResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + file_name: str | Unset = "sample.pdf", + format_: str | Unset = "png", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[PdfToImagesResponse200]: + """Pdf to images + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + format_ (str | Unset): Default: 'png'. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesResponse200] + """ + kwargs = _get_kwargs( + file_name=file_name, + format_=format_, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + file_name: str | Unset = "sample.pdf", + format_: str | Unset = "png", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> PdfToImagesResponse200 | None: + """Pdf to images + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + format_ (str | Unset): Default: 'png'. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesResponse200 + """ + return sync_detailed( + client=client, + file_name=file_name, + format_=format_, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + file_name: str | Unset = "sample.pdf", + format_: str | Unset = "png", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> Response[PdfToImagesResponse200]: + """Pdf to images + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + format_ (str | Unset): Default: 'png'. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesResponse200] + """ + kwargs = _get_kwargs( + file_name=file_name, + format_=format_, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + file_name: str | Unset = "sample.pdf", + format_: str | Unset = "png", + tag: str | Unset = "default", + url_query: str | Unset = "", + url_in_post: bool | Unset = False, +) -> PdfToImagesResponse200 | None: + """Pdf to images + + Args: + file_name (str | Unset): Default: 'sample.pdf'. + format_ (str | Unset): Default: 'png'. + tag (str | Unset): Default: 'default'. + url_query (str | Unset): Default: ''. + url_in_post (bool | Unset): Default: False. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesResponse200 + """ + return ( + await asyncio_detailed( + client=client, + file_name=file_name, + format_=format_, + tag=tag, + url_query=url_query, + url_in_post=url_in_post, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py new file mode 100644 index 0000000..32260d6 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py @@ -0,0 +1,157 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.pdf_to_images_retrieve_response_200 import PdfToImagesRetrieveResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/pdf-to-images-retrieve", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> PdfToImagesRetrieveResponse200 | None: + if response.status_code == 200: + response_200 = PdfToImagesRetrieveResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[PdfToImagesRetrieveResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[PdfToImagesRetrieveResponse200]: + """Pdf to images retrieve + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesRetrieveResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> PdfToImagesRetrieveResponse200 | None: + """Pdf to images retrieve + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesRetrieveResponse200 + """ + return sync_detailed( + client=client, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[PdfToImagesRetrieveResponse200]: + """Pdf to images retrieve + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesRetrieveResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> PdfToImagesRetrieveResponse200 | None: + """Pdf to images retrieve + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesRetrieveResponse200 + """ + return ( + await asyncio_detailed( + client=client, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py new file mode 100644 index 0000000..8cad5f7 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py @@ -0,0 +1,157 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.pdf_to_images_status_response_200 import PdfToImagesStatusResponse200 +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/pdf-to-images-status", + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> PdfToImagesStatusResponse200 | None: + if response.status_code == 200: + response_200 = PdfToImagesStatusResponse200.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[PdfToImagesStatusResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[PdfToImagesStatusResponse200]: + """Pdf to images status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesStatusResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> PdfToImagesStatusResponse200 | None: + """Pdf to images status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesStatusResponse200 + """ + return sync_detailed( + client=client, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[PdfToImagesStatusResponse200]: + """Pdf to images status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PdfToImagesStatusResponse200] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> PdfToImagesStatusResponse200 | None: + """Pdf to images status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PdfToImagesStatusResponse200 + """ + return ( + await asyncio_detailed( + client=client, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py new file mode 100644 index 0000000..a8177e5 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py @@ -0,0 +1,168 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.whisper_result import WhisperResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + text_only: bool | Unset = False, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["text_only"] = text_only + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/whisper-retrieve", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperResult | None: + if response.status_code == 200: + response_200 = WhisperResult.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + text_only: bool | Unset = False, + whisper_hash: str | Unset = "", +) -> Response[WhisperResult]: + """Retrieve extraction result (destructive — one shot) + + Args: + text_only (bool | Unset): Default: False. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperResult] + """ + kwargs = _get_kwargs( + text_only=text_only, + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + text_only: bool | Unset = False, + whisper_hash: str | Unset = "", +) -> WhisperResult | None: + """Retrieve extraction result (destructive — one shot) + + Args: + text_only (bool | Unset): Default: False. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperResult + """ + return sync_detailed( + client=client, + text_only=text_only, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + text_only: bool | Unset = False, + whisper_hash: str | Unset = "", +) -> Response[WhisperResult]: + """Retrieve extraction result (destructive — one shot) + + Args: + text_only (bool | Unset): Default: False. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperResult] + """ + kwargs = _get_kwargs( + text_only=text_only, + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + text_only: bool | Unset = False, + whisper_hash: str | Unset = "", +) -> WhisperResult | None: + """Retrieve extraction result (destructive — one shot) + + Args: + text_only (bool | Unset): Default: False. + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperResult + """ + return ( + await asyncio_detailed( + client=client, + text_only=text_only, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py new file mode 100644 index 0000000..554b70b --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py @@ -0,0 +1,153 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.whisper_status import WhisperStatus +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + whisper_hash: str | Unset = "", +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["whisper_hash"] = whisper_hash + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/whisper-status", + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperStatus | None: + if response.status_code == 200: + response_200 = WhisperStatus.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperStatus]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[WhisperStatus]: + """Poll extraction status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperStatus] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> WhisperStatus | None: + """Poll extraction status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperStatus + """ + return sync_detailed( + client=client, + whisper_hash=whisper_hash, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> Response[WhisperStatus]: + """Poll extraction status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[WhisperStatus] + """ + kwargs = _get_kwargs( + whisper_hash=whisper_hash, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + whisper_hash: str | Unset = "", +) -> WhisperStatus | None: + """Poll extraction status + + Args: + whisper_hash (str | Unset): Default: ''. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + WhisperStatus + """ + return ( + await asyncio_detailed( + client=client, + whisper_hash=whisper_hash, + ) + ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/client.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/client.py new file mode 100644 index 0000000..51022a6 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/client.py @@ -0,0 +1,269 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +import ssl +from typing import Any + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: object, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/errors.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/errors.py new file mode 100644 index 0000000..1984f61 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/errors.py @@ -0,0 +1,17 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py new file mode 100644 index 0000000..0465011 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py @@ -0,0 +1,50 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains all the data models used in inputs/outputs""" + +from .convert_to_pdf_response_200 import ConvertToPdfResponse200 +from .convert_xlsb_to_xlsx_response_200 import ConvertXlsbToXlsxResponse200 +from .detail_response_200 import DetailResponse200 +from .document_insights_response_200 import DocumentInsightsResponse200 +from .document_insights_retrieve_response_200 import DocumentInsightsRetrieveResponse200 +from .highlights_response_200 import HighlightsResponse200 +from .pdf_to_images_response_200 import PdfToImagesResponse200 +from .pdf_to_images_retrieve_response_200 import PdfToImagesRetrieveResponse200 +from .pdf_to_images_status_response_200 import PdfToImagesStatusResponse200 +from .test_connection_response_200 import TestConnectionResponse200 +from .usage_info_response_200 import UsageInfoResponse200 +from .usage_response_200 import UsageResponse200 +from .webhook_config import WebhookConfig +from .webhook_delete_response_200 import WebhookDeleteResponse200 +from .webhook_get_response_200 import WebhookGetResponse200 +from .webhook_post_response_200 import WebhookPostResponse200 +from .webhook_put_response_200 import WebhookPutResponse200 +from .whisper_accepted import WhisperAccepted +from .whisper_result import WhisperResult +from .whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem +from .whisper_result_metadata import WhisperResultMetadata +from .whisper_status import WhisperStatus + +__all__ = ( + "ConvertToPdfResponse200", + "ConvertXlsbToXlsxResponse200", + "DetailResponse200", + "DocumentInsightsResponse200", + "DocumentInsightsRetrieveResponse200", + "HighlightsResponse200", + "PdfToImagesResponse200", + "PdfToImagesRetrieveResponse200", + "PdfToImagesStatusResponse200", + "TestConnectionResponse200", + "UsageInfoResponse200", + "UsageResponse200", + "WebhookConfig", + "WebhookDeleteResponse200", + "WebhookGetResponse200", + "WebhookPostResponse200", + "WebhookPutResponse200", + "WhisperAccepted", + "WhisperResult", + "WhisperResultConfidenceMetadataItem", + "WhisperResultMetadata", + "WhisperStatus", +) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py new file mode 100644 index 0000000..2f09307 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ConvertToPdfResponse200") + + +@_attrs_define +class ConvertToPdfResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + convert_to_pdf_response_200 = cls() + + convert_to_pdf_response_200.additional_properties = d + return convert_to_pdf_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py new file mode 100644 index 0000000..0211b28 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ConvertXlsbToXlsxResponse200") + + +@_attrs_define +class ConvertXlsbToXlsxResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + convert_xlsb_to_xlsx_response_200 = cls() + + convert_xlsb_to_xlsx_response_200.additional_properties = d + return convert_xlsb_to_xlsx_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/detail_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/detail_response_200.py new file mode 100644 index 0000000..72b9d37 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/detail_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DetailResponse200") + + +@_attrs_define +class DetailResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + detail_response_200 = cls() + + detail_response_200.additional_properties = d + return detail_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py new file mode 100644 index 0000000..a0c02ed --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DocumentInsightsResponse200") + + +@_attrs_define +class DocumentInsightsResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + document_insights_response_200 = cls() + + document_insights_response_200.additional_properties = d + return document_insights_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_retrieve_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_retrieve_response_200.py new file mode 100644 index 0000000..42004a2 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_retrieve_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DocumentInsightsRetrieveResponse200") + + +@_attrs_define +class DocumentInsightsRetrieveResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + document_insights_retrieve_response_200 = cls() + + document_insights_retrieve_response_200.additional_properties = d + return document_insights_retrieve_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/highlights_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/highlights_response_200.py new file mode 100644 index 0000000..11ebec8 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/highlights_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="HighlightsResponse200") + + +@_attrs_define +class HighlightsResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + highlights_response_200 = cls() + + highlights_response_200.additional_properties = d + return highlights_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py new file mode 100644 index 0000000..39626d3 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PdfToImagesResponse200") + + +@_attrs_define +class PdfToImagesResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + pdf_to_images_response_200 = cls() + + pdf_to_images_response_200.additional_properties = d + return pdf_to_images_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py new file mode 100644 index 0000000..270e203 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PdfToImagesRetrieveResponse200") + + +@_attrs_define +class PdfToImagesRetrieveResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + pdf_to_images_retrieve_response_200 = cls() + + pdf_to_images_retrieve_response_200.additional_properties = d + return pdf_to_images_retrieve_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_status_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_status_response_200.py new file mode 100644 index 0000000..d21c47b --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_status_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PdfToImagesStatusResponse200") + + +@_attrs_define +class PdfToImagesStatusResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + pdf_to_images_status_response_200 = cls() + + pdf_to_images_status_response_200.additional_properties = d + return pdf_to_images_status_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/test_connection_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/test_connection_response_200.py new file mode 100644 index 0000000..54d9b65 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/test_connection_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="TestConnectionResponse200") + + +@_attrs_define +class TestConnectionResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + test_connection_response_200 = cls() + + test_connection_response_200.additional_properties = d + return test_connection_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_info_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_info_response_200.py new file mode 100644 index 0000000..1890ac5 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_info_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UsageInfoResponse200") + + +@_attrs_define +class UsageInfoResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + usage_info_response_200 = cls() + + usage_info_response_200.additional_properties = d + return usage_info_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_response_200.py new file mode 100644 index 0000000..232d6fa --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/usage_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UsageResponse200") + + +@_attrs_define +class UsageResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + usage_response_200 = cls() + + usage_response_200.additional_properties = d + return usage_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_config.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_config.py new file mode 100644 index 0000000..e9458ca --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_config.py @@ -0,0 +1,78 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WebhookConfig") + + +@_attrs_define +class WebhookConfig: + """ + Attributes: + auth_token (str): + url (str): + webhook_name (str): + """ + + auth_token: str + url: str + webhook_name: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + auth_token = self.auth_token + + url = self.url + + webhook_name = self.webhook_name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "auth_token": auth_token, + "url": url, + "webhook_name": webhook_name, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + auth_token = d.pop("auth_token") + + url = d.pop("url") + + webhook_name = d.pop("webhook_name") + + webhook_config = cls( + auth_token=auth_token, + url=url, + webhook_name=webhook_name, + ) + + webhook_config.additional_properties = d + return webhook_config + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_delete_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_delete_response_200.py new file mode 100644 index 0000000..276303e --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_delete_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WebhookDeleteResponse200") + + +@_attrs_define +class WebhookDeleteResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + webhook_delete_response_200 = cls() + + webhook_delete_response_200.additional_properties = d + return webhook_delete_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_get_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_get_response_200.py new file mode 100644 index 0000000..8aba08c --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_get_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WebhookGetResponse200") + + +@_attrs_define +class WebhookGetResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + webhook_get_response_200 = cls() + + webhook_get_response_200.additional_properties = d + return webhook_get_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py new file mode 100644 index 0000000..171dc1d --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WebhookPostResponse200") + + +@_attrs_define +class WebhookPostResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + webhook_post_response_200 = cls() + + webhook_post_response_200.additional_properties = d + return webhook_post_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_put_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_put_response_200.py new file mode 100644 index 0000000..24891ee --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_put_response_200.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WebhookPutResponse200") + + +@_attrs_define +class WebhookPutResponse200: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + webhook_put_response_200 = cls() + + webhook_put_response_200.additional_properties = d + return webhook_put_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_accepted.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_accepted.py new file mode 100644 index 0000000..d8d4073 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_accepted.py @@ -0,0 +1,80 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="WhisperAccepted") + + +@_attrs_define +class WhisperAccepted: + """ + Attributes: + message (str | Unset): + status (str | Unset): + whisper_hash (str | Unset): + """ + + message: str | Unset = UNSET + status: str | Unset = UNSET + whisper_hash: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + message = self.message + + status = self.status + + whisper_hash = self.whisper_hash + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if message is not UNSET: + field_dict["message"] = message + if status is not UNSET: + field_dict["status"] = status + if whisper_hash is not UNSET: + field_dict["whisper_hash"] = whisper_hash + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + message = d.pop("message", UNSET) + + status = d.pop("status", UNSET) + + whisper_hash = d.pop("whisper_hash", UNSET) + + whisper_accepted = cls( + message=message, + status=status, + whisper_hash=whisper_hash, + ) + + whisper_accepted.additional_properties = d + return whisper_accepted + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py new file mode 100644 index 0000000..12d6445 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py @@ -0,0 +1,116 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem + from ..models.whisper_result_metadata import WhisperResultMetadata + + +T = TypeVar("T", bound="WhisperResult") + + +@_attrs_define +class WhisperResult: + """ + Attributes: + confidence_metadata (list[WhisperResultConfidenceMetadataItem] | Unset): + metadata (WhisperResultMetadata | Unset): + result_text (str | Unset): + webhook_metadata (str | Unset): + """ + + confidence_metadata: list[WhisperResultConfidenceMetadataItem] | Unset = UNSET + metadata: WhisperResultMetadata | Unset = UNSET + result_text: str | Unset = UNSET + webhook_metadata: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + confidence_metadata: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.confidence_metadata, Unset): + confidence_metadata = [] + for confidence_metadata_item_data in self.confidence_metadata: + confidence_metadata_item = confidence_metadata_item_data.to_dict() + confidence_metadata.append(confidence_metadata_item) + + metadata: dict[str, Any] | Unset = UNSET + if not isinstance(self.metadata, Unset): + metadata = self.metadata.to_dict() + + result_text = self.result_text + + webhook_metadata = self.webhook_metadata + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if confidence_metadata is not UNSET: + field_dict["confidence_metadata"] = confidence_metadata + if metadata is not UNSET: + field_dict["metadata"] = metadata + if result_text is not UNSET: + field_dict["result_text"] = result_text + if webhook_metadata is not UNSET: + field_dict["webhook_metadata"] = webhook_metadata + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem + from ..models.whisper_result_metadata import WhisperResultMetadata + + d = dict(src_dict) + _confidence_metadata = d.pop("confidence_metadata", UNSET) + confidence_metadata: list[WhisperResultConfidenceMetadataItem] | Unset = UNSET + if _confidence_metadata is not UNSET: + confidence_metadata = [] + for confidence_metadata_item_data in _confidence_metadata: + confidence_metadata_item = WhisperResultConfidenceMetadataItem.from_dict(confidence_metadata_item_data) + + confidence_metadata.append(confidence_metadata_item) + + _metadata = d.pop("metadata", UNSET) + metadata: WhisperResultMetadata | Unset + if isinstance(_metadata, Unset): + metadata = UNSET + else: + metadata = WhisperResultMetadata.from_dict(_metadata) + + result_text = d.pop("result_text", UNSET) + + webhook_metadata = d.pop("webhook_metadata", UNSET) + + whisper_result = cls( + confidence_metadata=confidence_metadata, + metadata=metadata, + result_text=result_text, + webhook_metadata=webhook_metadata, + ) + + whisper_result.additional_properties = d + return whisper_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_confidence_metadata_item.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_confidence_metadata_item.py new file mode 100644 index 0000000..50805b3 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_confidence_metadata_item.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WhisperResultConfidenceMetadataItem") + + +@_attrs_define +class WhisperResultConfidenceMetadataItem: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + whisper_result_confidence_metadata_item = cls() + + whisper_result_confidence_metadata_item.additional_properties = d + return whisper_result_confidence_metadata_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_metadata.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_metadata.py new file mode 100644 index 0000000..0b0dc7f --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_metadata.py @@ -0,0 +1,48 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WhisperResultMetadata") + + +@_attrs_define +class WhisperResultMetadata: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + whisper_result_metadata = cls() + + whisper_result_metadata.additional_properties = d + return whisper_result_metadata + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py new file mode 100644 index 0000000..7a92c6b --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py @@ -0,0 +1,71 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Self, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="WhisperStatus") + + +@_attrs_define +class WhisperStatus: + """ + Attributes: + message (str | Unset): + status (str | Unset): + """ + + message: str | Unset = UNSET + status: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + message = self.message + + status = self.status + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if message is not UNSET: + field_dict["message"] = message + if status is not UNSET: + field_dict["status"] = status + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + d = dict(src_dict) + message = d.pop("message", UNSET) + + status = d.pop("status", UNSET) + + whisper_status = cls( + message=message, + status=status, + ) + + whisper_status.additional_properties = d + return whisper_status + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/types.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/types.py new file mode 100644 index 0000000..68a0e3f --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/types.py @@ -0,0 +1,55 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/tests/baseline/client_v2_pr34.py b/tests/baseline/client_v2_pr34.py new file mode 100644 index 0000000..5c563c0 --- /dev/null +++ b/tests/baseline/client_v2_pr34.py @@ -0,0 +1,898 @@ +# Vendored from llm-whisperer-python-client 0e9fda3 (PR #34 head), the parity baseline. +# DO NOT EDIT. Refresh with tools/refresh_baseline.sh when the baseline moves. +"""This module provides a Python client for interacting with the LLMWhisperer +API. + +Note: This is for the LLMWhisperer API v2.x + +Prepare documents for LLM consumption +LLMs are powerful, but their output is as good as the input you provide. +LLMWhisperer is a technology that presents data from complex documents +(different designs and formats) to LLMs in a way that they can best understand. + +LLMWhisperer is available as an API that can be integrated into your existing +systems to preprocess your documents before they are fed into LLMs. It can handle +a variety of document types, including PDFs, images, and scanned documents. + +This client simplifies the process of making requests to the API and handling the responses. + +Classes: + LLMWhispererClientException: Exception raised for errors in the LLMWhispererClient. +""" + +import json +import logging +import os +import time +import warnings +from typing import IO, Any + +import requests +import tenacity +from tenacity import retry_if_exception, stop_after_attempt, stop_after_delay, wait_exponential_jitter + +BASE_URL_V2 = "https://llmwhisperer-api.us-central.unstract.com/api/v2" + + +class LLMWhispererClientException(Exception): + """Exception raised for errors in the LLMWhispererClient. + + Attributes: + message (str): Explanation of the error. + status_code (int): HTTP status code returned by the LLMWhisperer API. + + Args: + message (str): Explanation of the error. + status_code (int, optional): HTTP status code returned by the LLMWhisperer API. Defaults to None. + """ + + def __init__(self, value: str, status_code: int | None = None) -> None: + """Initialize the LLMWhispererClientException. + + Args: + value: The error message or value. + status_code: The HTTP status code returned by the LLMWhisperer API. + """ + self.value = value + self.status_code = status_code + + def __str__(self) -> str: + """Return string representation of the exception. + + Returns: + String representation of the error value. + """ + return repr(self.value) + + def error_message(self) -> str: + return self.value + + +class _RetryableHTTPError(Exception): + """Internal exception wrapping an HTTP response with a retryable status + code (429, 5xx).""" + + def __init__(self, response: requests.Response) -> None: + self.response = response + super().__init__(f"HTTP {response.status_code}") + + +class LLMWhispererClientV2: + """A client for interacting with the LLMWhisperer API. + + Note: This is for the LLMWhisperer API v2.x + + This client uses the requests library to make HTTP requests to the + LLMWhisperer API. It also includes a logger for tracking the + client's activities and errors. + """ + + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + logger = logging.getLogger(__name__) + log_stream_handler = logging.StreamHandler() + log_stream_handler.setFormatter(formatter) + logger.addHandler(log_stream_handler) + + api_key: str = "" + base_url: str = "" + api_timeout: int = 120 + + def __init__( + self, + base_url: str = "", + api_key: str = "", + logging_level: str = "", + custom_headers: dict[str, str] | None = None, + max_retries: int = 3, + retry_min_wait: float = 1.0, + retry_max_wait: float = 60.0, + ) -> None: + """Initializes the LLMWhispererClient with the given parameters. + + Args: + base_url (str, optional): The base URL for the LLMWhisperer API. Defaults to "". + If the base_url is not provided, the client will use + the value of the LLMWHISPERER_BASE_URL_V2 environment + variable,or the default value. + api_key (str, optional): The API key for the LLMWhisperer API. Defaults to "". + If the api_key is not provided, the client will use the + value of the LLMWHISPERER_API_KEY environment variable. + logging_level (str, optional): The logging level for the client. Can be "DEBUG", + "INFO", "WARNING" or "ERROR". Defaults to the + value of the LLMWHISPERER_LOGGING_LEVEL + environment variable, or "DEBUG" if the + environment variable is not set. + custom_headers (Optional[Dict[str, str]], optional): Custom headers to add to + every request. These will + be merged with default + headers, with custom + headers taking precedence. + Defaults to None. + max_retries (int, optional): Maximum number of retry attempts for transient + HTTP errors. Set to 0 to disable retries. + Defaults to 3. + retry_min_wait (float, optional): Minimum backoff wait in seconds. Defaults to 1.0. + retry_max_wait (float, optional): Maximum backoff wait in seconds. Defaults to 60.0. + """ + if logging_level == "": + logging_level = os.getenv("LLMWHISPERER_LOGGING_LEVEL", "DEBUG") + if logging_level == "DEBUG": + self.logger.setLevel(logging.DEBUG) + elif logging_level == "INFO": + self.logger.setLevel(logging.INFO) + elif logging_level == "WARNING": + self.logger.setLevel(logging.WARNING) + elif logging_level == "ERROR": + self.logger.setLevel(logging.ERROR) + self.logger.setLevel(logging_level) + self.logger.debug("logging_level set to %s", logging_level) + if base_url == "": + self.base_url = os.getenv("LLMWHISPERER_BASE_URL_V2", BASE_URL_V2) + else: + self.base_url = base_url + self.logger.debug("base_url set to %s", self.base_url) + + if api_key == "": + self.api_key = os.getenv("LLMWHISPERER_API_KEY", "") + else: + self.api_key = api_key + + self.headers = {"unstract-key": self.api_key} + if custom_headers: + self.headers.update(custom_headers) + + self.max_retries = max_retries + self.retry_min_wait = retry_min_wait + self.retry_max_wait = retry_max_wait + + @staticmethod + def _is_retryable(exc: BaseException) -> bool: + """Return True if the exception represents a transient/retryable + error.""" + if isinstance(exc, requests.ConnectionError | requests.Timeout): + return True + if isinstance(exc, _RetryableHTTPError): + return bool(exc.response.status_code == 429 or exc.response.status_code >= 500) + return False + + def _log_retry(self, retry_state: tenacity.RetryCallState) -> None: + """Log a warning before each retry sleep.""" + exc = retry_state.outcome.exception() if retry_state.outcome else None + attempt = retry_state.attempt_number + if isinstance(exc, _RetryableHTTPError): + self.logger.warning("Retry attempt %d: HTTP %d", attempt, exc.response.status_code) + elif isinstance(exc, requests.ConnectionError | requests.Timeout): + self.logger.warning("Retry attempt %d: %s", attempt, type(exc).__name__) + + def _retry_wait(self, retry_state: tenacity.RetryCallState) -> float: + """Compute wait time, respecting Retry-After header on 429 + responses.""" + exc = retry_state.outcome.exception() if retry_state.outcome else None + if isinstance(exc, _RetryableHTTPError) and exc.response.status_code == 429: + retry_after = exc.response.headers.get("Retry-After") + if retry_after is not None: + try: + return float(retry_after) + except (ValueError, TypeError): + pass + return wait_exponential_jitter( + initial=self.retry_min_wait, + max=self.retry_max_wait, + )(retry_state=retry_state) + + def _send_request( + self, + prepared: requests.PreparedRequest, + timeout: int | None = None, + stream: bool = False, + deadline: float | None = None, + ) -> requests.Response: + """Send an HTTP request with optional tenacity retry on transient + errors. + + Args: + prepared: The prepared request to send. + timeout: Request timeout in seconds. Defaults to self.api_timeout. + stream: Whether to stream the response. Defaults to False. + deadline: Absolute time (time.time()) by which all attempts must finish. + When set, each attempt's HTTP timeout is capped to the remaining time + and retries stop once the deadline is exceeded. Defaults to None + (no deadline). + + Returns: + The HTTP response. + + Raises: + requests.ConnectionError: If connection fails after all retries. + requests.Timeout: If request times out after all retries. + """ + if timeout is None: + timeout = self.api_timeout + req_timeout: int = timeout + + def _effective_timeout() -> int | float: + if deadline is not None: + remaining = max(0.1, deadline - time.time()) + return min(req_timeout, remaining) + return req_timeout + + if self.max_retries == 0: + s = requests.Session() + return s.send(prepared, timeout=_effective_timeout(), stream=stream) + + def _attempt() -> requests.Response: + s = requests.Session() + response = s.send(prepared, timeout=_effective_timeout(), stream=stream) + if response.status_code == 429 or response.status_code >= 500: + raise _RetryableHTTPError(response) + return response + + stop_condition: tenacity.stop.stop_base = stop_after_attempt(self.max_retries + 1) + if deadline is not None: + max_duration = max(0, deadline - time.time()) + stop_condition = stop_condition | stop_after_delay(max_duration) + + retrying = tenacity.Retrying( + retry=retry_if_exception(self._is_retryable), + stop=stop_condition, + wait=self._retry_wait, + before_sleep=self._log_retry, + reraise=True, + ) + try: + return retrying(_attempt) + except _RetryableHTTPError as e: + return e.response + + def get_usage_info(self) -> Any: + """Retrieves the usage information of the LLMWhisperer API. + + This method sends a GET request to the '/get-usage-info' endpoint of the LLMWhisperer API. + The response is a JSON object containing the usage information. + Refer to https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_usage_api + + Returns: + Dict[Any, Any]: A dictionary containing the usage information. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + self.logger.debug("get_usage_info called") + url = f"{self.base_url}/get-usage-info" + self.logger.debug("url: %s", url) + req = requests.Request("GET", url, headers=self.headers) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def get_highlight_data(self, whisper_hash: str, lines: str, extract_all_lines: bool = False) -> Any: + """Retrieves the highlight information of the LLMWhisperer API. + + This method sends a GET request to the '/highlights' endpoint of the LLMWhisperer API. + The response is a JSON object containing the usage information. + Refer to https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_usage_api + + Args: + whisper_hash (str): The hash of the whisper operation. + lines (str): Define which lines metadata to retrieve. + You can specify which lines metadata to retrieve with this parameter. + Example 1-5,7,21- will retrieve lines metadata 1,2,3,4,5,7,21,22,23,24... + till the last line meta data. + + Returns: + Dict[Any, Any]: A dictionary containing the highlight information. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + self.logger.debug("highlight called") + url = f"{self.base_url}/highlights" + params = { + "whisper_hash": whisper_hash, + "lines": lines, + "extract_all_lines": extract_all_lines, + } + self.logger.debug("url: %s", url) + req = requests.Request("GET", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def whisper_detail(self, whisper_hash: str) -> Any: + """Retrieves the details of a text extraction process. + + This method sends a GET request to the '/whisper-detail' endpoint of the LLMWhisperer API. + The response is a JSON object containing metadata about the extraction job. + Refer to https://docs.unstract.com/llmwhisperer/llm_whisperer/apis/llm_whisperer_text_extraction_detail_api + + Args: + whisper_hash (str): The identifier returned when starting the extraction process. + + Returns: + Dict[Any, Any]: A dictionary containing the extraction details including + completed_at, mode, processed_pages, processing_started_at, + processing_time_in_seconds, requested_pages, tag, total_pages, + upload_file_size_in_kb, and whisper_hash. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + self.logger.debug("whisper_detail called") + url = f"{self.base_url}/whisper-detail" + params = {"whisper_hash": whisper_hash} + self.logger.debug("url: %s", url) + self.logger.debug("whisper_hash: %s", whisper_hash) + + req = requests.Request("GET", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + if not (response.text or "").strip(): + raise LLMWhispererClientException("API error: empty response body", response.status_code) + try: + err = json.loads(response.text) + except json.JSONDecodeError as e: + response_preview = response.text[:500] + "..." if len(response.text) > 500 else response.text + raise LLMWhispererClientException( + f"API error: non-JSON response - {response_preview}", response.status_code + ) from e + raise LLMWhispererClientException(err, response.status_code) + return json.loads(response.text) + + def _resolve_deprecated_param( + self, + name: str, + value: str | None, + deprecated_name: str, + deprecated_value: str | None, + default: str, + *, + forward: bool, + ) -> str: + """Resolves a renamed parameter, warning when the old name is used. + + Args: + name: The supported parameter name. + value: Value passed under the supported name, None when unset. + deprecated_name: The deprecated parameter name. + deprecated_value: Value passed under the deprecated name, None when unset. + default: Value to use when neither name is passed. + forward: Whether the deprecated value is honoured. False for parameters the + service never received, where applying the value now would silently + change extraction output. + + Returns: + The resolved value. + + Raises: + LLMWhispererClientException: If both names are passed. + """ + if deprecated_value is None: + return default if value is None else value + if value is not None: + raise LLMWhispererClientException( + f"Cannot pass both '{deprecated_name}' and '{name}', use '{name}' only", + 1, + ) + message = f"'{deprecated_name}' is deprecated and will be removed in a future release, use '{name}' instead" + if not forward: + message += f". The value passed is ignored: '{deprecated_name}' never reached the service" + self.logger.warning(message) + warnings.warn(message, DeprecationWarning, stacklevel=3) + return deprecated_value if forward else default + + def whisper( + self, + file_path: str = "", + stream: IO[bytes] | None = None, + url: str = "", + mode: str = "form", + output_mode: str = "layout_preserving", + page_seperator: str | None = None, + pages_to_extract: str = "", + median_filter_size: int = 0, + gaussian_blur_radius: int = 0, + line_splitter_tolerance: float = 0.4, + horizontal_stretch_factor: float = 1.0, + mark_vertical_lines: bool = False, + mark_horizontal_lines: bool = False, + line_spitter_strategy: str | None = None, + add_line_nos: bool = False, + include_line_confidence: bool = False, + word_confidence_threshold: float = 0.3, + lang: str = "eng", + tag: str = "default", + filename: str | None = None, + webhook_metadata: str = "", + use_webhook: str = "", + wait_for_completion: bool = False, + wait_timeout: int = 180, + encoding: str = "utf-8", + page_separator: str | None = None, + line_splitter_strategy: str | None = None, + file_name: str | None = None, + ) -> Any: + """Sends a request to the LLMWhisperer API to process a document. + Refer to https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_api. + + Args: + file_path (str, optional): The path to the file to be processed. Defaults to "". + stream (IO[bytes], optional): A stream of bytes to be processed. Defaults to None. + url (str, optional): The URL of the file to be processed. Defaults to "". + mode (str, optional): The processing mode. Can be "high_quality", "form", "low_cost", "native_text" + or "table". Defaults to "high_quality". + output_mode (str, optional): The output mode. Can be "layout_preserving" or "text". + Defaults to "layout_preserving". + page_seperator (str, optional): Deprecated misspelling of page_separator, still + honoured. Defaults to None. + pages_to_extract (str, optional): The pages to extract. Defaults to "". + median_filter_size (int, optional): The size of the median filter. Defaults to 0. + gaussian_blur_radius (int, optional): The radius of the Gaussian blur. Defaults to 0. + line_splitter_tolerance (float, optional): The line splitter tolerance. Defaults to 0.4. + horizontal_stretch_factor (float, optional): The horizontal stretch factor. Defaults to 1.0. + mark_vertical_lines (bool, optional): Whether to mark vertical lines. Defaults to False. + mark_horizontal_lines (bool, optional): Whether to mark horizontal lines. Defaults to False. + line_spitter_strategy (str, optional): Deprecated misspelling of + line_splitter_strategy. The value is ignored, since it was never sent under a + name the service reads. Defaults to None. + add_line_nos (bool, optional): Adds line numbers to the extracted text and saves line metadata, + which can be queried later using the highlights API. + include_line_confidence (bool, optional): Adds line confidence to the line metadata returned by + the highlights API. Requires add_line_nos to be enabled. Defaults to False. + word_confidence_threshold (float, optional): The minimum OCR confidence score a word must have to be + included in the extracted text. Accepts a value in the range [0.0, 1.0], where higher values are + stricter. Any word whose confidence value falls below the configured threshold is ignored and + excluded from the final output. This parameter works only with "form", "high_quality" and "table" + modes. Defaults to 0.3. + lang (str, optional): The language of the document. Defaults to "eng". + tag (str, optional): The tag for the document. Defaults to "default". + filename (str, optional): Deprecated name for file_name, still honoured. + Defaults to None. + webhook_metadata (str, optional): The webhook metadata. This data will be passed to the webhook if + webhooks are used Defaults to "". + use_webhook (str, optional): Webhook name to call. Defaults to "". If not provided, then + no webhook will be called. + wait_for_completion (bool, optional): Whether to wait for the whisper operation to complete. + Defaults to False. + wait_timeout (int, optional): The number of seconds to wait for the whisper operation to complete. + Defaults to 180. + encoding (str): The character encoding to use for processing the text. Defaults to "utf-8". + page_separator (str, optional): The page separator. Defaults to "<<<". + line_splitter_strategy (str, optional): The line splitter strategy. + Defaults to "left-priority". + file_name (str, optional): The name of the file to store in reports. Defaults to "". + + Returns: + Dict[Any, Any]: The response from the API as a dictionary. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + Also raised when a parameter is passed under both its + deprecated and its supported name. + """ + self.logger.debug("whisper called") + page_separator = self._resolve_deprecated_param( + "page_separator", page_separator, "page_seperator", page_seperator, "<<<", forward=True + ) + line_splitter_strategy = self._resolve_deprecated_param( + "line_splitter_strategy", + line_splitter_strategy, + "line_spitter_strategy", + line_spitter_strategy, + "left-priority", + forward=False, + ) + file_name = self._resolve_deprecated_param("file_name", file_name, "filename", filename, "", forward=True) + api_url = f"{self.base_url}/whisper" + params = { + "mode": mode, + "output_mode": output_mode, + "page_separator": page_separator, + "pages_to_extract": pages_to_extract, + "median_filter_size": median_filter_size, + "gaussian_blur_radius": gaussian_blur_radius, + "line_splitter_tolerance": line_splitter_tolerance, + "horizontal_stretch_factor": horizontal_stretch_factor, + "mark_vertical_lines": mark_vertical_lines, + "mark_horizontal_lines": mark_horizontal_lines, + "line_splitter_strategy": line_splitter_strategy, + "add_line_nos": add_line_nos, + "include_line_confidence": include_line_confidence, + "word_confidence_threshold": word_confidence_threshold, + "lang": lang, + "tag": tag, + "file_name": file_name, + "webhook_metadata": webhook_metadata, + "use_webhook": use_webhook, + } + + self.logger.debug("api_url: %s", api_url) + self.logger.debug("params: %s", params) + + if use_webhook != "" and wait_for_completion: + raise LLMWhispererClientException("Cannot wait for completion when using webhook", 1) + + if url == "" and file_path == "" and stream is None: + raise LLMWhispererClientException( + "Either url, stream or file_path must be provided", + 1, + ) + + should_stream = False + if url == "": + if stream is not None: + should_stream = True + data = b"".join(stream) + req = requests.Request( + "POST", + api_url, + params=params, + headers=self.headers, + data=data, + ) + + else: + with open(file_path, "rb") as f: + data = f.read() + req = requests.Request( + "POST", + api_url, + params=params, + headers=self.headers, + data=data, + ) + else: + params["url_in_post"] = True + req = requests.Request("POST", api_url, params=params, headers=self.headers, data=url) + prepared = req.prepare() + start_time = time.time() + deadline = start_time + wait_timeout + post_timeout = min(self.api_timeout, wait_timeout) + response = self._send_request(prepared, timeout=post_timeout, stream=should_stream, deadline=deadline) + response.encoding = encoding + if response.status_code not in (200, 202): + try: + message = json.loads(response.text) + if not isinstance(message, dict): + message = {"message": str(message)} + except (json.JSONDecodeError, ValueError): + message = {"message": response.text} + message["status_code"] = response.status_code + message["extraction"] = {} + raise LLMWhispererClientException(message) + if response.status_code == 202: + try: + message = json.loads(response.text) + if not isinstance(message, dict): + message = {"message": str(message)} + except (json.JSONDecodeError, ValueError): + message = {"message": response.text} + message["status_code"] = response.status_code + message["extraction"] = {} + if not wait_for_completion: + return message + whisper_hash = message["whisper_hash"] + while time.time() - start_time < wait_timeout: + status = self.whisper_status(whisper_hash=whisper_hash) + if status["status_code"] != 200: + message["status_code"] = -1 + message["message"] = "Whisper client operation failed" + message["extraction"] = {} + return message + if status["status"] == "accepted": + self.logger.debug(f"Whisper-hash:{whisper_hash} | STATUS: {status['status']}...") + if status["status"] == "processing": + self.logger.debug(f"Whisper-hash:{whisper_hash} | STATUS: processing...") + + elif status["status"] == "error": + self.logger.debug(f"Whisper-hash:{whisper_hash} | STATUS: failed...") + self.logger.error(f"Whisper-hash:{whisper_hash} | STATUS: failed with {status['message']}") + message["status_code"] = -1 + message["message"] = status["message"] + message["status"] = "error" + message["extraction"] = {} + return message + elif "error" in status["status"]: + # for backward compatabity + self.logger.debug(f"Whisper-hash:{whisper_hash} | STATUS: failed...") + self.logger.error(f"Whisper-hash:{whisper_hash} | STATUS: failed with {status['status']}") + message["status_code"] = -1 + message["message"] = status["status"] + message["status"] = "error" + message["extraction"] = {} + return message + elif status["status"] == "processed": + self.logger.debug(f"Whisper-hash:{whisper_hash} | STATUS: processed!") + resultx = self.whisper_retrieve(whisper_hash=whisper_hash) + if resultx["status_code"] == 200: + message["status_code"] = 200 + message["message"] = "Whisper operation completed" + message["status"] = "processed" + message["extraction"] = resultx["extraction"] + else: + message["status_code"] = -1 + message["message"] = "Whisper client operation failed" + message["extraction"] = {} + return message + time.sleep(5) + message["status_code"] = -1 + message["message"] = "Whisper client operation timed out" + message["extraction"] = {} + return message + + # Will not reach here if status code is 202 + message = json.loads(response.text) + message["status_code"] = response.status_code + return message + + def whisper_status(self, whisper_hash: str) -> Any: + """Retrieves the status of the whisper operation from the LLMWhisperer + API. + + This method sends a GET request to the '/whisper-status' endpoint of the LLMWhisperer API. + The response is a JSON object containing the status of the whisper operation. + + Refer https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_status_api + + Args: + whisper_hash (str): The hash of the whisper (returned by whisper method) + + Returns: + dict: A dictionary containing the status of the whisper operation. The keys in the + dictionary include 'status_code' and the status details. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + self.logger.debug("whisper_status called") + url = f"{self.base_url}/whisper-status" + params = {"whisper_hash": whisper_hash} + self.logger.debug("url: %s", url) + req = requests.Request("GET", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + if not (response.text or "").strip(): + self.logger.error(f"API error - empty response body, status code: {response.status_code}") + raise LLMWhispererClientException("API error: empty response body", response.status_code) + try: + err = json.loads(response.text) + except json.JSONDecodeError as e: + # Truncate response text if too long to avoid log pollution + response_preview = response.text[:500] + "..." if len(response.text) > 500 else response.text + self.logger.error(f"API error - JSON decode failed: {e}; Response preview: {response_preview!r}") + raise LLMWhispererClientException( + f"API error: non-JSON response - {response_preview}", response.status_code + ) from e + raise LLMWhispererClientException(err, response.status_code) + message = json.loads(response.text) + message["status_code"] = response.status_code + return message + + def whisper_retrieve(self, whisper_hash: str, encoding: str = "utf-8") -> Any: + """Retrieves the result of the whisper operation from the LLMWhisperer + API. + + This method sends a GET request to the '/whisper-retrieve' endpoint of the LLMWhisperer API. + The response is a JSON object containing the result of the whisper operation. + + Refer to https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_retrieve_api + + Args: + whisper_hash (str): The hash of the whisper operation. + encoding (str): The character encoding to use for processing the text. Defaults to "utf-8". + + Returns: + dict: A dictionary containing the status code and the extracted text from the whisper operation. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + self.logger.debug("whisper_retrieve called") + url = f"{self.base_url}/whisper-retrieve" + params = {"whisper_hash": whisper_hash} + self.logger.debug("url: %s", url) + req = requests.Request("GET", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + response.encoding = encoding + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + + return { + "status_code": response.status_code, + "extraction": json.loads(response.text), + } + + def register_webhook(self, url: str, auth_token: str, webhook_name: str) -> Any: + """Registers a webhook with the LLMWhisperer API. + + This method sends a POST request to the '/whisper-manage-callback' endpoint of the LLMWhisperer API. + The response is a JSON object containing the status of the webhook registration. + + Refer to https://docs.unstract.com/llm_whisperer/apis/ + + Args: + url (str): The URL of the webhook. + auth_token (str): The authentication token for the webhook. + webhook_name (str): The name of the webhook. + + Returns: + Any: A dictionary containing the status code and the response from the API. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + data = { + "url": url, + "auth_token": auth_token, + "webhook_name": webhook_name, + } + url = f"{self.base_url}/whisper-manage-callback" + req = requests.Request("POST", url, headers=self.headers, json=data) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 201: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def update_webhook_details(self, webhook_name: str, url: str, auth_token: str) -> Any: + """Updates the details of a webhook from the LLMWhisperer API. + + This method sends a PUT request to the '/whisper-manage-callback' endpoint of the LLMWhisperer API. + The response is a JSON object containing the status of the webhook update. + + Refer to https://docs.unstract.com/llm_whisperer/apis/ + + Args: + webhook_name (str): The name of the webhook. + url (str): The URL of the webhook. + auth_token (str): The authentication token for the webhook. + + Returns: + dict: A dictionary containing the status code and the response from the API. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + data = { + "url": url, + "auth_token": auth_token, + "webhook_name": webhook_name, + } + url = f"{self.base_url}/whisper-manage-callback" + req = requests.Request("PUT", url, headers=self.headers, json=data) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def get_webhook_details(self, webhook_name: str) -> Any: + """Retrieves the details of a webhook from the LLMWhisperer API. + + This method sends a GET request to the '/whisper-manage-callback' endpoint of the LLMWhisperer API. + The response is a JSON object containing the details of the webhook. + + Refer to https://docs.unstract.com/llm_whisperer/apis/ + + Args: + webhook_name (str): The name of the webhook. + + Returns: + dict: A dictionary containing the status code and the response from the API. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + url = f"{self.base_url}/whisper-manage-callback" + params = {"webhook_name": webhook_name} + req = requests.Request("GET", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def delete_webhook(self, webhook_name: str) -> Any: + """Deletes a webhook from the LLMWhisperer API. + + This method sends a DELETE request to the '/whisper-manage-callback' endpoint of the LLMWhisperer API. + The response is a JSON object containing the status of the webhook deletion. + + Refer to https://docs.unstract.com/llm_whisperer/apis/ + + Args: + webhook_name (str): The name of the webhook. + + Returns: + dict: A dictionary containing the status code and the response from the API. + + Raises: + LLMWhispererClientException: If the API request fails, it raises an exception with + the error message and status code returned by the API. + """ + url = f"{self.base_url}/whisper-manage-callback" + params = {"webhook_name": webhook_name} + req = requests.Request("DELETE", url, headers=self.headers, params=params) + prepared = req.prepare() + response = self._send_request(prepared) + if response.status_code != 200: + err = json.loads(response.text) + err["status_code"] = response.status_code + raise LLMWhispererClientException(err) + return json.loads(response.text) + + def get_highlight_rect( + self, + line_metadata: list[int], + target_width: int, + target_height: int, + ) -> tuple[int, int, int, int, int]: + """Given the line metadata and the line number, this function returns + the bounding box of the line in the format (page,x1,y1,x2,y2). + + Args: + line_metadata (list[int]): The line metadata returned by the LLMWhisperer API. + target_width (int): The width of your target image/page in UI. + target_height (int): The height of your target image/page in UI. + + Returns: + tuple: The bounding box of the line in the format (page,x1,y1,x2,y2) + """ + page = line_metadata[0] + x1 = 0 + y1 = line_metadata[1] - line_metadata[2] + x2 = target_width + y2 = line_metadata[1] + original_height = line_metadata[3] + + y1 = int((float(y1) / float(original_height)) * float(target_height)) + y2 = int((float(y2) / float(original_height)) * float(target_height)) + + return (page, x1, y1, x2, y2) diff --git a/tests/unit/client_v2_test.py b/tests/unit/client_v2_test.py index 9f7a7fa..bbb4f59 100644 --- a/tests/unit/client_v2_test.py +++ b/tests/unit/client_v2_test.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock from urllib.parse import parse_qs, urlparse +import httpx import pytest import requests from pytest_mock import MockerFixture @@ -15,7 +16,7 @@ def test_register_webhook(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 201 mock_response.text = '{"message": "Webhook registered successfully"}' # noqa: E501 @@ -28,7 +29,7 @@ def test_register_webhook(mocker: MockerFixture, client_v2: LLMWhispererClientV2 def test_get_webhook_details(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 200 mock_response.text = '{"status": "success", "webhook_details": {"url": "http://test-webhook.com/callback"}}' # noqa: E501 @@ -42,7 +43,7 @@ def test_get_webhook_details(mocker: MockerFixture, client_v2: LLMWhispererClien def test_whisper_detail_success(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper_detail returns extraction details on success.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 200 mock_response.text = ( @@ -65,7 +66,7 @@ def test_whisper_detail_success(mocker: MockerFixture, client_v2: LLMWhispererCl def test_whisper_detail_not_found(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper_detail raises exception when record is not found.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 400 mock_response.text = '{"message": "Record not found"}' @@ -83,7 +84,7 @@ def test_whisper_detail_not_found(mocker: MockerFixture, client_v2: LLMWhisperer def test_whisper_json_string_response_error(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper method handles JSON string responses correctly for error cases.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 400 mock_response.text = '"Error message as JSON string"' @@ -102,7 +103,7 @@ def test_whisper_json_string_response_error(mocker: MockerFixture, client_v2: LL def test_whisper_json_string_response_202(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper method handles JSON string responses correctly for 202 status.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 202 mock_response.text = '"Processing in progress"' @@ -119,7 +120,7 @@ def test_whisper_json_string_response_202(mocker: MockerFixture, client_v2: LLMW def test_whisper_invalid_json_response_error(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper method handles invalid JSON responses correctly for error cases.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "Invalid JSON response" @@ -138,7 +139,7 @@ def test_whisper_invalid_json_response_error(mocker: MockerFixture, client_v2: L def test_whisper_invalid_json_response_202(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Test whisper method handles invalid JSON responses correctly for 202 status.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_response = MagicMock() mock_response.status_code = 202 mock_response.text = "Invalid JSON response" @@ -155,20 +156,20 @@ def test_whisper_invalid_json_response_202(mocker: MockerFixture, client_v2: LLM def test_whisper_default_word_confidence_threshold(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Whisper() sends the default word_confidence_threshold when not specified.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_send.return_value = _mock_response(200, '{"status_code": 200, "extraction": {"text": "ok"}}') client_v2.whisper(url="https://example.com/test.pdf", wait_for_completion=False) prepared_request = mock_send.call_args[0][0] - query = parse_qs(urlparse(prepared_request.url).query) + query = parse_qs(urlparse(str(prepared_request.url)).query) assert query["word_confidence_threshold"] == ["0.3"] def test_whisper_custom_word_confidence_threshold(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: """Whisper() forwards a custom word_confidence_threshold as a request param.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_send.return_value = _mock_response(200, '{"status_code": 200, "extraction": {"text": "ok"}}') client_v2.whisper( @@ -178,7 +179,7 @@ def test_whisper_custom_word_confidence_threshold(mocker: MockerFixture, client_ ) prepared_request = mock_send.call_args[0][0] - query = parse_qs(urlparse(prepared_request.url).query) + query = parse_qs(urlparse(str(prepared_request.url)).query) assert query["word_confidence_threshold"] == ["0.75"] @@ -188,12 +189,12 @@ def test_whisper_custom_word_confidence_threshold(mocker: MockerFixture, client_ def _whisper_query(mocker: MockerFixture, client_v2: LLMWhispererClientV2, **kwargs: object) -> dict[str, list[str]]: """Calls whisper() with a mocked transport and returns the query params sent.""" - mock_send = mocker.patch("requests.Session.send") + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send") mock_send.return_value = _mock_response(200, '{"status_code": 200, "extraction": {"text": "ok"}}') client_v2.whisper(url="https://example.com/test.pdf", wait_for_completion=False, **kwargs) - return parse_qs(urlparse(mock_send.call_args[0][0].url).query) + return parse_qs(urlparse(str(mock_send.call_args[0][0].url)).query) def test_whisper_sends_corrected_param_names(mocker: MockerFixture, client_v2: LLMWhispererClientV2) -> None: @@ -302,8 +303,9 @@ def _mock_response(status_code: int = 200, text: str = '{"status": "ok"}') -> Ma def test_retry_on_connection_error(mocker: MockerFixture, retry_client: LLMWhispererClientV2) -> None: """ConnectionError triggers retry, succeeds on 3rd attempt.""" success_resp = _mock_response(200, '{"pages_processed": 1}') - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=[ requests.ConnectionError("connection refused"), requests.ConnectionError("connection refused"), @@ -320,8 +322,9 @@ def test_retry_on_connection_error(mocker: MockerFixture, retry_client: LLMWhisp def test_retry_on_timeout(mocker: MockerFixture, retry_client: LLMWhispererClientV2) -> None: """Timeout triggers retry, succeeds on 2nd attempt.""" success_resp = _mock_response(200, '{"pages_processed": 1}') - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=[ requests.Timeout("request timed out"), success_resp, @@ -338,8 +341,9 @@ def test_retry_on_429(mocker: MockerFixture, retry_client: LLMWhispererClientV2) """HTTP 429 triggers retry, succeeds on 2nd attempt.""" rate_limit_resp = _mock_response(429, '{"error": "rate limited"}') success_resp = _mock_response(200, '{"pages_processed": 1}') - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=[rate_limit_resp, success_resp], ) @@ -353,8 +357,9 @@ def test_retry_on_500(mocker: MockerFixture, retry_client: LLMWhispererClientV2) """HTTP 500 triggers retry, succeeds on 2nd attempt.""" server_err_resp = _mock_response(500, '{"error": "internal server error"}') success_resp = _mock_response(200, '{"pages_processed": 1}') - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=[server_err_resp, success_resp], ) @@ -367,7 +372,7 @@ def test_retry_on_500(mocker: MockerFixture, retry_client: LLMWhispererClientV2) def test_no_retry_on_400(mocker: MockerFixture, retry_client: LLMWhispererClientV2) -> None: """HTTP 400 does NOT retry (client error).""" bad_request_resp = _mock_response(400, '{"error": "bad request"}') - mock_send = mocker.patch("requests.Session.send", return_value=bad_request_resp) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=bad_request_resp) with pytest.raises(LLMWhispererClientException): retry_client.get_usage_info() @@ -378,7 +383,7 @@ def test_no_retry_on_400(mocker: MockerFixture, retry_client: LLMWhispererClient def test_no_retry_on_401(mocker: MockerFixture, retry_client: LLMWhispererClientV2) -> None: """HTTP 401 does NOT retry (auth error).""" unauth_resp = _mock_response(401, '{"error": "unauthorized"}') - mock_send = mocker.patch("requests.Session.send", return_value=unauth_resp) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=unauth_resp) with pytest.raises(LLMWhispererClientException): retry_client.get_usage_info() @@ -388,8 +393,9 @@ def test_no_retry_on_401(mocker: MockerFixture, retry_client: LLMWhispererClient def test_retries_exhausted_raises(mocker: MockerFixture, retry_client: LLMWhispererClientV2) -> None: """After all retries exhausted on ConnectionError, raises the exception.""" - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=requests.ConnectionError("connection refused"), ) @@ -404,7 +410,7 @@ def test_retries_exhausted_500_returns_response(mocker: MockerFixture, retry_cli """After all retries exhausted on 500, returns the error response (caller raises exception).""" server_err_resp = _mock_response(500, '{"error": "internal server error"}') - mock_send = mocker.patch("requests.Session.send", return_value=server_err_resp) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=server_err_resp) with pytest.raises(LLMWhispererClientException): retry_client.get_usage_info() @@ -415,8 +421,9 @@ def test_retries_exhausted_500_returns_response(mocker: MockerFixture, retry_cli def test_retry_disabled(mocker: MockerFixture, no_retry_client: LLMWhispererClientV2) -> None: """max_retries=0 means single attempt only, no retry on failure.""" - mock_send = mocker.patch( - "requests.Session.send", + mock_send = mocker.patch.object( + LLMWhispererClientV2, + "_send", side_effect=requests.ConnectionError("connection refused"), ) @@ -443,7 +450,7 @@ def test_whisper_post_uses_min_of_api_timeout_and_wait_timeout( # api_timeout defaults to 120, wait_timeout will be 180 # So POST timeout should be min(120, 180) = 120 mock_response = _mock_response(200, '{"status_code": 200, "extraction": {"text": "ok"}}') - mock_send = mocker.patch("requests.Session.send", return_value=mock_response) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=mock_response) client.whisper(url="https://example.com/test.pdf", wait_timeout=180) @@ -466,7 +473,7 @@ def test_whisper_post_uses_wait_timeout_when_smaller( max_retries=0, ) mock_response = _mock_response(200, '{"status_code": 200, "extraction": {"text": "ok"}}') - mock_send = mocker.patch("requests.Session.send", return_value=mock_response) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=mock_response) client.whisper(url="https://example.com/test.pdf", wait_timeout=10) @@ -485,12 +492,11 @@ def test_send_request_deadline_caps_timeout(mocker: MockerFixture) -> None: max_retries=0, ) mock_response = _mock_response(200) - mock_send = mocker.patch("requests.Session.send", return_value=mock_response) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=mock_response) # Set deadline 2 seconds from now, but request timeout=300 deadline = time.time() + 2.0 - req = requests.Request("GET", "http://localhost/test", headers={}) - prepared = req.prepare() + prepared = httpx.Request("GET", "http://localhost/test") client._send_request(prepared, timeout=300, deadline=deadline) @@ -513,13 +519,12 @@ def test_send_request_deadline_stops_retries(mocker: MockerFixture) -> None: ) server_err_resp = _mock_response(500, '{"error": "internal server error"}') - mock_send = mocker.patch("requests.Session.send", return_value=server_err_resp) + mock_send = mocker.patch.object(LLMWhispererClientV2, "_send", return_value=server_err_resp) # Deadline is 0.3s from now — with 0.1-0.2s waits between retries, # only a few attempts should fit before the deadline expires deadline = time.time() + 0.3 - req = requests.Request("GET", "http://localhost/test", headers={}) - prepared = req.prepare() + prepared = httpx.Request("GET", "http://localhost/test") response = client._send_request(prepared, timeout=1, deadline=deadline) diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py new file mode 100644 index 0000000..cec8558 --- /dev/null +++ b/tests/unit/compat_test.py @@ -0,0 +1,588 @@ +"""Parity tests against the published client. + +The transport underneath ``LLMWhispererClientV2`` changed; its published +behaviour must not. These tests pin the seams where that could silently break: +the constructor and method signatures, what goes out on the wire, which +exceptions come back out, and what each method returns — the last two by running +the published client side by side over the same responses. + +The baseline is vendored under ``tests/baseline`` rather than imported from an +installed distribution, so the comparison is against a fixed published client +instead of whatever the working tree currently says. +""" + +import ast +import importlib.util +import inspect +import io +import json +import time +from collections.abc import Callable +from pathlib import Path +from types import ModuleType +from typing import Any +from unittest.mock import MagicMock, patch +from urllib.parse import parse_qs, urlparse + +import httpx +import pytest +import requests +from unstract.llmwhisperer.client_v2 import ( + _SEND_ONLY, + LLMWhispererClientException, + LLMWhispererClientV2, +) + +BASELINE_REF = "0e9fda3" +BASELINE_PATH = Path(__file__).parents[1] / "baseline" / "client_v2_pr34.py" +SPEC_PATH = Path(__file__).parents[2] / "specs" / "llmwhisperer.json" + +Call = Callable[[Any, str], Any] + +BASE_URL = "https://x.test/api/v2" + +# Operations the spec declares that this client does not expose. The published +# client exposes none of them either, so the facade is at parity — the spec +# surface is simply wider than the hand-written one. Listing them here keeps the +# coverage check honest instead of passing on whatever happens to be wrapped. +UNWRAPPED_OPERATIONS = frozenset( + { + "usage", + "test_connection", + "pdf_to_images", + "pdf_to_images_status", + "pdf_to_images_retrieve", + "document_insights", + "document_insights_retrieve", + "convert_to_pdf", + "convert_xlsb_to_xlsx", + } +) + + +def _load_baseline() -> ModuleType: + """Import the vendored published client under its own module name.""" + spec = importlib.util.spec_from_file_location("baseline_client_v2", BASELINE_PATH) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +baseline = _load_baseline() + + +def _client(**kwargs: Any) -> LLMWhispererClientV2: + kwargs.setdefault("base_url", BASE_URL) + kwargs.setdefault("api_key", "test-key") + kwargs.setdefault("logging_level", "ERROR") + kwargs.setdefault("max_retries", 0) + return LLMWhispererClientV2(**kwargs) + + +def _baseline_client(**kwargs: Any) -> Any: + kwargs.setdefault("base_url", BASE_URL) + kwargs.setdefault("api_key", "test-key") + kwargs.setdefault("logging_level", "ERROR") + kwargs.setdefault("max_retries", 0) + return baseline.LLMWhispererClientV2(**kwargs) + + +def _mock_response(status_code: int = 200, text: str = '{"ok": true}') -> MagicMock: + """A response both transports can be fed, so only the client differs.""" + response = MagicMock() + response.status_code = status_code + response.text = text + response.headers = {} + return response + + +@pytest.fixture +def sample_file(tmp_path: Path) -> str: + path = tmp_path / "sample.pdf" + path.write_bytes(b"PDFBYTES") + return str(path) + + +# -------------------------------------------------------------------------- +# What goes out on the wire, compared against the published client +# -------------------------------------------------------------------------- + +# Every call the client can make, as (name, callable). Each runs against both +# clients with the same mocked response, and the two requests must agree. +CALLS = { + "usage_info": (lambda c, f: c.get_usage_info(), 200), + "whisper_status": (lambda c, f: c.whisper_status("hash-1"), 200), + "whisper_retrieve": (lambda c, f: c.whisper_retrieve("hash-1"), 200), + "whisper_detail": (lambda c, f: c.whisper_detail("hash-1"), 200), + "highlights": (lambda c, f: c.get_highlight_data("hash-1", "1-5"), 200), + "highlights_all_lines": (lambda c, f: c.get_highlight_data("hash-1", "-1", True), 200), + "webhook_get": (lambda c, f: c.get_webhook_details("wh"), 200), + "webhook_delete": (lambda c, f: c.delete_webhook("wh"), 200), + "webhook_put": (lambda c, f: c.update_webhook_details("wh", "http://cb", "tok"), 200), + "webhook_post": (lambda c, f: c.register_webhook("http://cb", "tok", "wh"), 201), + "whisper_file": (lambda c, f: c.whisper(file_path=f, wait_for_completion=False), 200), + "whisper_stream": (lambda c, f: c.whisper(stream=io.BytesIO(b"STREAM"), wait_for_completion=False), 200), + "whisper_url": (lambda c, f: c.whisper(url="https://e.test/a.pdf", wait_for_completion=False), 200), + "whisper_every_param": ( + lambda c, f: c.whisper( + file_path=f, + wait_for_completion=False, + mode="high_quality", + output_mode="text", + page_separator="---", + pages_to_extract="1-2", + median_filter_size=2, + gaussian_blur_radius=1, + line_splitter_tolerance=0.9, + horizontal_stretch_factor=1.5, + mark_vertical_lines=True, + mark_horizontal_lines=True, + line_splitter_strategy="mid-priority", + add_line_nos=True, + include_line_confidence=True, + word_confidence_threshold=0.75, + lang="deu", + tag="t", + file_name="invoice.pdf", + webhook_metadata="meta", + use_webhook="wh", + ), + 200, + ), +} + +_WHISPER_OK = '{"status_code": 200, "extraction": {"result_text": "ok"}}' + + +def _sent(call: Call, status_code: int, sample_file: str) -> tuple[httpx.Request, Any]: + """Run the call against both clients and return the two requests.""" + text = _WHISPER_OK + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(status_code, text)) as ours: + call(_client(), sample_file) + with patch("requests.Session.send", return_value=_mock_response(status_code, text)) as theirs: + call(_baseline_client(), sample_file) + return ours.call_args[0][0], theirs.call_args[0][0] + + +@pytest.mark.parametrize("name", list(CALLS)) +def test_request_matches_the_published_client(name: str, sample_file: str) -> None: + """Method, path, query and body must be identical. + + Query-parameter *order* is not compared: httpx sorts what the + previous transport emitted in insertion order, and order carries no + meaning. + """ + call, status_code = CALLS[name] + ours, theirs = _sent(call, status_code, sample_file) + + ours_url, theirs_url = urlparse(str(ours.url)), urlparse(theirs.url) + assert ours.method.upper() == theirs.method.upper() + assert (ours_url.scheme, ours_url.netloc, ours_url.path) == ( + theirs_url.scheme, + theirs_url.netloc, + theirs_url.path, + ) + assert parse_qs(ours_url.query) == parse_qs(theirs_url.query) + + their_body = theirs.body.encode() if isinstance(theirs.body, str) else theirs.body + if ours.headers.get("content-type") == "application/json": + # Both send the same object; the encoders differ on separators and key + # order, neither of which a JSON reader can observe. + assert json.loads(ours.read()) == json.loads(their_body) + else: + assert ours.read() == (their_body or b"") + + +def test_the_auth_header_is_unchanged(sample_file: str) -> None: + ours, theirs = _sent(*CALLS["usage_info"], sample_file) + assert ours.headers["unstract-key"] == theirs.headers["unstract-key"] == "test-key" + + +def test_custom_headers_still_reach_the_request() -> None: + client = _client(custom_headers={"x-trace": "abc", "unstract-key": "override"}) + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response()) as send: + client.get_usage_info() + request = send.call_args[0][0] + assert request.headers["x-trace"] == "abc" + assert request.headers["unstract-key"] == "override" + + +def test_url_mode_does_not_put_the_url_on_the_query_string() -> None: + """The URL travels in the body. + + The spec declares a `url` parameter, and + sending it as well would be a parameter the published client never sent. + """ + client = _client() + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(200, _WHISPER_OK)) as send: + client.whisper(url="https://e.test/a.pdf", wait_for_completion=False) + request = send.call_args[0][0] + assert "url" not in parse_qs(urlparse(str(request.url)).query) + assert request.read() == b"https://e.test/a.pdf" + + +def test_upload_mode_does_not_send_url_in_post(sample_file: str) -> None: + """`url_in_post` only exists in URL mode; its generated default would + otherwise ride along on every upload.""" + client = _client() + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(200, _WHISPER_OK)) as send: + client.whisper(file_path=sample_file, wait_for_completion=False) + assert "url_in_post" not in parse_qs(urlparse(str(send.call_args[0][0].url)).query) + + +def test_booleans_are_sent_the_way_the_previous_transport_sent_them() -> None: + """Httpx renders bools lowercase; the previous transport used + `str(bool)`.""" + client = _client() + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(200, _WHISPER_OK)) as send: + client.whisper(url="https://e.test/a.pdf", wait_for_completion=False, add_line_nos=True) + query = parse_qs(urlparse(str(send.call_args[0][0].url)).query) + assert query["add_line_nos"] == ["True"] + assert query["url_in_post"] == ["True"] + assert query["mark_vertical_lines"] == ["False"] + + +def test_send_only_covers_every_parameter_whisper_builds(sample_file: str) -> None: + """The guard is only as good as its declared set; this pins the two + together so a new whisper parameter cannot reach the wire undeclared.""" + client = _client() + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(200, _WHISPER_OK)) as send: + client.whisper(url="https://e.test/a.pdf", wait_for_completion=False) + query = set(parse_qs(urlparse(str(send.call_args[0][0].url)).query)) + assert query <= _SEND_ONLY["extract"] + + +def test_undeclared_parameters_are_refused() -> None: + from unstract.llmwhisperer.sdk_llmwhisperer.api.whisper import status as status_module + + client = _client() + with pytest.raises(LLMWhispererClientException): + client._build_request(status_module, frozenset({"whisper_hash", "made_up"}), whisper_hash="h") + + +def test_no_operation_sends_a_spec_default_the_client_never_set() -> None: + """The generated builder writes every declared parameter; each call must + narrow that to what it asked for.""" + from unstract.llmwhisperer.sdk_llmwhisperer.api.whisper import extract, retrieve + + assert "text_only" in inspect.signature(retrieve._get_kwargs).parameters + assert "text_only" not in _SEND_ONLY["retrieve"] + declared = set(inspect.signature(extract._get_kwargs).parameters) - {"body"} + assert declared - _SEND_ONLY["extract"] - {"url_query"} + + +# -------------------------------------------------------------------------- +# Return values and exceptions, compared against the published client +# -------------------------------------------------------------------------- + +ERROR_BODIES = [ + ("json_error", '{"message": "denied"}'), + ("nested_json", '{"message": {"detail": "denied"}}'), + ("json_string", '"denied"'), + ("html", "gateway"), + ("empty", ""), +] + + +def _outcome(call: Call, client: Any, sample_file: str, status_code: int, text: str) -> tuple[Any, ...]: + """Normalise a call into a comparable value: returned, or raised.""" + with ( + patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(status_code, text)), + patch("requests.Session.send", return_value=_mock_response(status_code, text)), + ): + try: + return ("return", call(client, sample_file)) + except Exception as exc: # noqa: BLE001 - the comparison is the point + return ("raise", type(exc).__name__, getattr(exc, "status_code", None), str(exc)[:160]) + + +@pytest.mark.parametrize("status_code", [200, 201, 400, 401, 404, 500]) +@pytest.mark.parametrize("name", list(CALLS)) +def test_return_value_matches_the_published_client(name: str, status_code: int, sample_file: str) -> None: + call = CALLS[name][0] + ours = _outcome(call, _client(), sample_file, status_code, '{"message": "x", "status": "processed"}') + theirs = _outcome(call, _baseline_client(), sample_file, status_code, '{"message": "x", "status": "processed"}') + assert ours == theirs + + +@pytest.mark.parametrize(("body_name", "text"), ERROR_BODIES, ids=[b[0] for b in ERROR_BODIES]) +@pytest.mark.parametrize("name", list(CALLS)) +def test_error_handling_matches_the_published_client(name: str, body_name: str, text: str, sample_file: str) -> None: + """Including the published client's own rough edges: several methods parse + an error body unguarded and leak ``JSONDecodeError``. + + A drop-in inherits the contract, bugs included. + """ + call = CALLS[name][0] + ours = _outcome(call, _client(), sample_file, 500, text) + theirs = _outcome(call, _baseline_client(), sample_file, 500, text) + assert ours == theirs + + +def test_whisper_poll_loop_matches_the_published_client(sample_file: str) -> None: + """wait_for_completion drives status and retrieve; the assembled result + dict must be identical.""" + accepted = '{"whisper_hash": "h-1", "message": "queued"}' + processed = '{"status": "processed", "message": "done"}' + extraction = '{"result_text": "hello"}' + + def run(client: Any, patch_target: Any) -> Any: + responses = [ + _mock_response(202, accepted), + _mock_response(200, processed), + _mock_response(200, extraction), + ] + with patch_target(side_effect=responses): + return client.whisper(file_path=sample_file, wait_for_completion=True, wait_timeout=30) + + ours = run(_client(), lambda **kw: patch.object(LLMWhispererClientV2, "_send", **kw)) + theirs = run(_baseline_client(), lambda **kw: patch("requests.Session.send", **kw)) + assert ours == theirs + assert ours["extraction"] == {"result_text": "hello"} + + +# -------------------------------------------------------------------------- +# Transport behaviour +# -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("raised", "expected"), + [ + (httpx.ConnectTimeout("connect timed out"), requests.ConnectTimeout), + (httpx.ReadTimeout("read timed out"), requests.Timeout), + (httpx.WriteTimeout("write timed out"), requests.Timeout), + (httpx.PoolTimeout("pool timed out"), requests.Timeout), + (httpx.ConnectError("refused"), requests.ConnectionError), + (httpx.ReadError("reset"), requests.ConnectionError), + (httpx.WriteError("broken pipe"), requests.ConnectionError), + (httpx.ProtocolError("bad framing"), requests.ConnectionError), + (httpx.ProxyError("proxy exploded"), requests.ConnectionError), + ], +) +def test_transport_errors_are_translated(raised: Exception, expected: type[Exception]) -> None: + """Callers catch the ``requests`` classes by name; httpx's are not + subclasses of them.""" + client = _client() + with patch.object(client._transport, "send", side_effect=raised): + with pytest.raises(expected): + client.get_usage_info() + + +def test_a_connect_timeout_is_still_a_connection_error() -> None: + """``requests.ConnectTimeout`` is both a ``ConnectionError`` and a + ``Timeout``. + + Mapping it to a plain ``Timeout`` would stop matching every + caller that catches the connection family. + """ + client = _client() + with patch.object(client._transport, "send", side_effect=httpx.ConnectTimeout("connect timed out")): + with pytest.raises(requests.ConnectionError): + client.get_usage_info() + + +def test_translated_errors_keep_the_original_cause() -> None: + client = _client() + original = httpx.ConnectError("refused") + with patch.object(client._transport, "send", side_effect=original): + with pytest.raises(requests.ConnectionError) as excinfo: + client.get_usage_info() + assert excinfo.value.__cause__ is original + + +def test_transport_failures_are_still_retried() -> None: + """Retry matches on the ``requests`` types, so translation has to happen + inside the retried call or transport-error retry silently stops working.""" + client = _client(max_retries=2, retry_min_wait=0.001, retry_max_wait=0.002) + with patch.object(client._transport, "send", side_effect=httpx.ConnectError("refused")) as send: + with pytest.raises(requests.ConnectionError): + client.get_usage_info() + assert send.call_count == 3 + + +def test_redirects_are_followed() -> None: + """The previous transport followed them by default. + + Without this a 30x surfaces as 'API error: empty response body'. + """ + assert _client()._transport.follow_redirects is True + + +def test_the_request_timeout_reaches_the_transport() -> None: + """Unlike the other client's ``api_timeout``, this one is a real socket + timeout and must not be dropped on the way down.""" + client = _client() + with patch.object(client._transport, "send", return_value=_mock_response()) as send: + client.get_usage_info() + request = send.call_args[0][0] + assert request.extensions["timeout"]["read"] == client.api_timeout + + +def test_the_deadline_still_caps_each_attempt() -> None: + client = _client() + request = httpx.Request("GET", "http://localhost/test") + with patch.object(client._transport, "send", return_value=_mock_response()) as send: + client._send_request(request, timeout=300, deadline=time.time() + 2.0) + assert send.call_args[0][0].extensions["timeout"]["read"] <= 3.0 + + +def test_the_deadline_still_stops_retries() -> None: + client = _client(max_retries=10, retry_min_wait=0.1, retry_max_wait=0.2) + request = httpx.Request("GET", "http://localhost/test") + with patch.object(client._transport, "send", return_value=_mock_response(500, '{"e": 1}')) as send: + response = client._send_request(request, timeout=1, deadline=time.time() + 0.3) + assert response.status_code == 500 + assert send.call_count < 11 + + +def test_encoding_is_applied_to_a_real_response(sample_file: str) -> None: + """``whisper`` and ``whisper_retrieve`` set the response encoding before + reading text; on a real response that has to still be legal.""" + body = '{"result_text": "café"}'.encode("latin-1") + client = _client() + with patch.object(LLMWhispererClientV2, "_send", return_value=httpx.Response(200, content=body)): + result = client.whisper_retrieve("hash-1", encoding="latin-1") + assert result["extraction"] == {"result_text": "café"} + + +# -------------------------------------------------------------------------- +# Construction and surface +# -------------------------------------------------------------------------- + + +def _baseline_class_node() -> ast.ClassDef: + tree = ast.parse(BASELINE_PATH.read_text()) + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "LLMWhispererClientV2": + return node + raise AssertionError("LLMWhispererClientV2 not found in the baseline") + + +def _baseline_method(name: str) -> ast.FunctionDef: + for node in _baseline_class_node().body: + if isinstance(node, ast.FunctionDef) and node.name == name: + return node + raise AssertionError(f"{name} not found in the baseline") + + +def _params(node: ast.FunctionDef) -> list[tuple[str, object]]: + args = node.args.args[1:] + node.args.kwonlyargs + defaults = [None] * (len(node.args.args[1:]) - len(node.args.defaults)) + [ + ast.literal_eval(d) for d in node.args.defaults + ] + defaults += [ast.literal_eval(d) if d is not None else None for d in node.args.kw_defaults] + return list(zip([a.arg for a in args], defaults, strict=False)) + + +def _live_params(func: Any) -> list[tuple[str, object]]: + return [ + (name, None if p.default is inspect.Parameter.empty else p.default) + for name, p in inspect.signature(func).parameters.items() + if name not in ("self", "cls") + ] + + +def test_constructor_is_unchanged() -> None: + """Names, order and defaults all matter: callers pass some positionally.""" + assert _live_params(LLMWhispererClientV2.__init__) == _params(_baseline_method("__init__")) + + +def test_public_methods_are_unchanged() -> None: + methods = { + node.name: node + for node in _baseline_class_node().body + if isinstance(node, ast.FunctionDef) and not node.name.startswith("_") + } + assert len(methods) == 11, "the published surface is 11 public methods" + + for name, node in methods.items(): + live = getattr(LLMWhispererClientV2, name, None) + assert live is not None, f"{name} disappeared from the client" + assert _live_params(live) == _params(node), name + + +def test_the_deprecated_parameter_resolver_is_unchanged() -> None: + """Which renames forward and which stay dead is a service-side fact, not a + style choice: applying a dead one would change extraction output. + """ + assert _live_params(LLMWhispererClientV2._resolve_deprecated_param) == _params( + _baseline_method("_resolve_deprecated_param") + ) + assert _body_dump(_baseline_method("_resolve_deprecated_param")) == _body_dump( + _method_node(LLMWhispererClientV2, "_resolve_deprecated_param") + ) + + +def _method_node(cls: type, name: str) -> ast.FunctionDef: + source = inspect.getsource(getattr(cls, name)) + node = ast.parse(inspect.cleandoc(source)).body[0] + assert isinstance(node, ast.FunctionDef) + return node + + +def _body_dump(node: ast.FunctionDef) -> str: + """The statements, minus the docstring — re-indenting changes its text.""" + body = node.body[1:] if isinstance(node.body[0], ast.Expr) else node.body + return ast.dump(ast.Module(body=body, type_ignores=[])) + + +def test_get_highlight_rect_is_unchanged() -> None: + """Pure geometry, no request: it should survive the port verbatim.""" + assert _body_dump(_baseline_method("get_highlight_rect")) == _body_dump( + _method_node(LLMWhispererClientV2, "get_highlight_rect") + ) + + +def test_class_attributes_are_unchanged() -> None: + for node in _baseline_class_node().body: + target, assigned = None, None + if isinstance(node, ast.AnnAssign) and node.value is not None: + target, assigned = getattr(node.target, "id", None), node.value + elif isinstance(node, ast.Assign) and len(node.targets) == 1: + target, assigned = getattr(node.targets[0], "id", None), node.value + if target is None or assigned is None: + continue + try: + value = ast.literal_eval(assigned) + except ValueError: + continue # logger and friends: identity, not value + assert getattr(LLMWhispererClientV2, target) == value, target + + +def test_retry_policy_attributes_are_unchanged() -> None: + """Retry is ungenerated and untested by a signature sweep, and has drifted + before.""" + ours, theirs = _client(max_retries=3), _baseline_client(max_retries=3) + for attribute in ("max_retries", "retry_min_wait", "retry_max_wait", "api_timeout", "base_url", "headers"): + assert getattr(ours, attribute) == getattr(theirs, attribute), attribute + assert LLMWhispererClientV2._is_retryable(requests.ConnectionError()) is True + assert LLMWhispererClientV2._is_retryable(requests.Timeout()) is True + assert LLMWhispererClientV2._is_retryable(ValueError()) is False + + +def test_defaults_match_a_default_constructed_published_client(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LLMWHISPERER_BASE_URL_V2", raising=False) + monkeypatch.setenv("LLMWHISPERER_API_KEY", "env-key") + ours = LLMWhispererClientV2(logging_level="ERROR") + theirs = baseline.LLMWhispererClientV2(logging_level="ERROR") + for attribute in ("base_url", "api_key", "api_timeout", "headers", "max_retries"): + assert getattr(ours, attribute) == getattr(theirs, attribute), attribute + + +def test_every_wrapped_operation_is_covered() -> None: + """A new spec operation shows up here as a failure, not as silence.""" + spec = json.loads(SPEC_PATH.read_text()) + declared = { + operation["operationId"] + for path in spec["paths"].values() + for method, operation in path.items() + if method in {"get", "post", "put", "patch", "delete"} + } + assert declared - UNWRAPPED_OPERATIONS == set(_SEND_ONLY) + + +def test_the_baseline_is_pinned() -> None: + assert BASELINE_REF in BASELINE_PATH.read_text(encoding="utf-8").splitlines()[0] + assert "DO NOT EDIT" in BASELINE_PATH.read_text(encoding="utf-8") diff --git a/tools/gen_sdk.sh b/tools/gen_sdk.sh new file mode 100755 index 0000000..8ce6e9a --- /dev/null +++ b/tools/gen_sdk.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Regenerate the transport layer from the committed OpenAPI spec. +# +# The generated tree is committed but NEVER hand-edited: regeneration overwrites +# it wholesale, so a fix applied there is lost on the next run. Fixes belong in +# the facade (client_v2.py) or upstream in the spec. +# +# ./tools/gen_sdk.sh && git diff --stat src/unstract/llmwhisperer/sdk_llmwhisperer +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENV="$REPO/.gen-venv" +OUT="src/unstract/llmwhisperer/sdk_llmwhisperer" +# Pinned: unpinned, a generator upgrade and a spec change produce the same diff, +# and the drift gate can no longer tell them apart. +GENERATOR="openapi-python-client==0.29.0" + +if [ ! -x "$VENV/bin/openapi-python-client" ]; then + uv venv "$VENV" + uv pip install --python "$VENV/bin/python" "$GENERATOR" +fi + +want="${GENERATOR#*==}" +have="$("$VENV/bin/openapi-python-client" --version | awk '{print $NF}')" +if [ "$have" != "$want" ]; then + echo "generator is $have, expected $want — reinstalling" >&2 + uv pip install --python "$VENV/bin/python" "$GENERATOR" +fi + +rm -rf "${REPO:?}/$OUT" +(cd "$REPO" && "$VENV/bin/openapi-python-client" generate \ + --path "$REPO/specs/llmwhisperer.json" --output-path "$REPO/$OUT" \ + --config "$REPO/tools/openapi-client.yaml" --overwrite --meta none) + +# Stamp every file, so the rule survives contact with a reader who arrived via +# grep rather than via this script. +header='# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT.' +find "$REPO/$OUT" -name '*.py' -print0 | while IFS= read -r -d '' f; do + printf '%s\n%s\n' "$header" "$(cat "$f")" > "$f.tmp" && mv "$f.tmp" "$f" +done + +echo "generated $OUT ($(find "$REPO/$OUT" -name '*.py' | wc -l) files)" diff --git a/tools/openapi-client.yaml b/tools/openapi-client.yaml new file mode 100644 index 0000000..d2196b9 --- /dev/null +++ b/tools/openapi-client.yaml @@ -0,0 +1,3 @@ +# openapi-python-client config. Kept minimal on purpose: every knob here is +# maintenance surface, and post-processing the generated code is a kill criterion. +literal_enums: true diff --git a/tools/refresh_baseline.sh b/tools/refresh_baseline.sh new file mode 100755 index 0000000..c7e75cd --- /dev/null +++ b/tools/refresh_baseline.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Refresh the vendored parity baseline in tests/baseline/. +# +# The compat suite compares this client against a fixed published one, not +# against the working tree — a baseline that moves with local edits measures +# nothing. It is vendored rather than resolved at test time so the suite stays +# offline, and refreshing it is a deliberate act with a reviewable diff. +# +# ./tools/refresh_baseline.sh 0e9fda3 pr34 +set -euo pipefail + +REF="${1:?usage: refresh_baseline.sh }" +SLUG="${2:?usage: refresh_baseline.sh }" +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$REPO/tests/baseline/client_v2_$SLUG.py" + +{ + echo "# Vendored from llm-whisperer-python-client $REF, the parity baseline." + echo "# DO NOT EDIT. Refresh with tools/refresh_baseline.sh when the baseline moves." + git -C "$REPO" show "$REF:src/unstract/llmwhisperer/client_v2.py" +} > "$OUT" + +echo "wrote $OUT" +echo "update BASELINE_REF in tests/test_compat.py to match" diff --git a/uv.lock b/uv.lock index 6ed9922..d6c5729 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,29 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -164,6 +186,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.10" @@ -220,6 +279,8 @@ wheels = [ name = "llmwhisperer-client" source = { editable = "." } dependencies = [ + { name = "attrs" }, + { name = "httpx" }, { name = "requests" }, { name = "tenacity" }, ] @@ -245,6 +306,8 @@ test = [ [package.metadata] requires-dist = [ + { name = "attrs", specifier = ">=23.2" }, + { name = "httpx", specifier = ">=0.27" }, { name = "requests", specifier = ">=2" }, { name = "tenacity", specifier = ">=8.0" }, ] From bb586c49346595cf2afc8ed0d0ccb96bef6c65dd Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 12:58:39 +0530 Subject: [PATCH 02/16] fix(client): raise ReadTimeout, not a bare Timeout, on a read timeout httpx.ReadTimeout was landing in the TimeoutException catch-all and coming back out as requests.Timeout. Callers that catch requests.ReadTimeout by name stopped matching. The translation table test used pytest.raises, which is subclass-tolerant and passed either way; it now asserts the exact class. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- src/unstract/llmwhisperer/client_v2.py | 2 ++ tests/unit/compat_test.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index edf7e02..e39ac18 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -109,6 +109,8 @@ def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: # requests.ConnectTimeout is both a ConnectionError and a Timeout; the # plain Timeout httpx implies would stop matching half the callers. raise requests.ConnectTimeout(str(e)) from e + except httpx.ReadTimeout as e: + raise requests.ReadTimeout(str(e)) from e except httpx.TimeoutException as e: raise requests.Timeout(str(e)) from e except httpx.ConnectError as e: diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index cec8558..d4b5a99 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -351,7 +351,7 @@ def run(client: Any, patch_target: Any) -> Any: ("raised", "expected"), [ (httpx.ConnectTimeout("connect timed out"), requests.ConnectTimeout), - (httpx.ReadTimeout("read timed out"), requests.Timeout), + (httpx.ReadTimeout("read timed out"), requests.ReadTimeout), (httpx.WriteTimeout("write timed out"), requests.Timeout), (httpx.PoolTimeout("pool timed out"), requests.Timeout), (httpx.ConnectError("refused"), requests.ConnectionError), @@ -363,11 +363,17 @@ def run(client: Any, patch_target: Any) -> Any: ) def test_transport_errors_are_translated(raised: Exception, expected: type[Exception]) -> None: """Callers catch the ``requests`` classes by name; httpx's are not - subclasses of them.""" + subclasses of them. + + The exact class matters, not just the family: a caller that catches + ``ReadTimeout`` sees nothing if a broader ``Timeout`` is raised in its + place. + """ client = _client() with patch.object(client._transport, "send", side_effect=raised): - with pytest.raises(expected): + with pytest.raises(expected) as caught: client.get_usage_info() + assert type(caught.value) is expected def test_a_connect_timeout_is_still_a_connection_error() -> None: From 02485e1e108b854f5379f4b64aa129e071952022 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 14:09:58 +0530 Subject: [PATCH 03/16] feat(client): accept the six extraction parameters the service added The service takes six OCR parameters this client has no argument for -- allow_rotated_text, watermark_angle_threshold, ignore_vertical_text, derotate_threshold, checkbox_confidence_threshold and min_table_width -- so a caller who needs one cannot reach it at all. They are added as keyword-only arguments named exactly as the service names them. Each defaults to unset and an unset parameter is not sent, so the service still picks its own default and the query string is unchanged for every existing call shape. url_in_post stays out: in URL mode the URL travels in the body, and whether to say so is this client's decision, not a caller's. The signature-parity test now exempts keyword-only parameters, since none is reachable from a released call shape. --- src/unstract/llmwhisperer/client_v2.py | 47 +++++++++++++++++++++++++- tests/unit/compat_test.py | 46 ++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index e39ac18..e92d0c0 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -43,7 +43,7 @@ ) from unstract.llmwhisperer.sdk_llmwhisperer.api.whisper import detail, extract, highlights, retrieve, status from unstract.llmwhisperer.sdk_llmwhisperer.models import WebhookConfig -from unstract.llmwhisperer.sdk_llmwhisperer.types import File +from unstract.llmwhisperer.sdk_llmwhisperer.types import UNSET, File, Unset BASE_URL_V2 = "https://llmwhisperer-api.us-central.unstract.com/api/v2" @@ -78,6 +78,12 @@ "file_name", "webhook_metadata", "use_webhook", + "allow_rotated_text", + "watermark_angle_threshold", + "ignore_vertical_text", + "derotate_threshold", + "checkbox_confidence_threshold", + "min_table_width", # In URL mode the URL travels in the body; it is not also a query # parameter, so `url` is deliberately absent here. "url_in_post", @@ -578,6 +584,13 @@ def whisper( page_separator: str | None = None, line_splitter_strategy: str | None = None, file_name: str | None = None, + *, + allow_rotated_text: bool | Unset = UNSET, + watermark_angle_threshold: float | Unset = UNSET, + ignore_vertical_text: bool | Unset = UNSET, + derotate_threshold: float | Unset = UNSET, + checkbox_confidence_threshold: float | Unset = UNSET, + min_table_width: float | Unset = UNSET, ) -> Any: """Sends a request to the LLMWhisperer API to process a document. Refer to https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_api. @@ -628,6 +641,21 @@ def whisper( line_splitter_strategy (str, optional): The line splitter strategy. Defaults to "left-priority". file_name (str, optional): The name of the file to store in reports. Defaults to "". + allow_rotated_text (bool, optional): Whether to keep words whose own orientation is + rotated. With this off, a word angled further than watermark_angle_threshold is + treated as a watermark and excluded. Defaults to True. + watermark_angle_threshold (float, optional): The angle in degrees beyond which a + rotated word counts as a watermark. Only applies when allow_rotated_text is off. + Defaults to 25.0. + ignore_vertical_text (bool, optional): Whether to drop vertically oriented text + instead of extracting it. Defaults to False. + derotate_threshold (float, optional): The page rotation in degrees beyond which the + page is straightened and re-read. Defaults to 10.0. + checkbox_confidence_threshold (float, optional): The minimum confidence a detected + checkbox mark must have to be reported as marked. Accepts a value in the range + [0.0, 1.0]. Defaults to 0.3. + min_table_width (float, optional): The minimum width a table must span, as a + fraction of the page width, to be extracted as a table. Defaults to 0. Returns: Dict[Any, Any]: The response from the API as a dictionary. @@ -673,6 +701,23 @@ def whisper( "webhook_metadata": webhook_metadata, "use_webhook": use_webhook, } + # Only what the caller asked for. These have no default here on purpose: + # sending one pins a value the service would otherwise choose, and the + # two diverge the moment the service's own default moves. + params.update( + { + name: value + for name, value in ( + ("allow_rotated_text", allow_rotated_text), + ("watermark_angle_threshold", watermark_angle_threshold), + ("ignore_vertical_text", ignore_vertical_text), + ("derotate_threshold", derotate_threshold), + ("checkbox_confidence_threshold", checkbox_confidence_threshold), + ("min_table_width", min_table_width), + ) + if not isinstance(value, Unset) + } + ) self.logger.debug("api_url: %s", api_url) self.logger.debug("params: %s", params) diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index d4b5a99..7ba7dda 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -253,6 +253,35 @@ def test_send_only_covers_every_parameter_whisper_builds(sample_file: str) -> No assert query <= _SEND_ONLY["extract"] +_ADDED_PARAMS = { + "allow_rotated_text": (False, "False"), + "watermark_angle_threshold": (0.0, "0.0"), + "ignore_vertical_text": (True, "True"), + "derotate_threshold": (0, "0"), + "checkbox_confidence_threshold": (0.0, "0.0"), + "min_table_width": (0.5, "0.5"), +} + + +def _whisper_query(client: Any, **kwargs: Any) -> dict[str, list[str]]: + with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response(200, _WHISPER_OK)) as send: + client.whisper(url="https://e.test/a.pdf", wait_for_completion=False, **kwargs) + return parse_qs(urlparse(str(send.call_args[0][0].url)).query) + + +def test_an_unrequested_parameter_is_not_sent() -> None: + """Sending one pins a value the service would otherwise choose, and the two + diverge the moment the service's own default moves.""" + assert not set(_whisper_query(_client())) & set(_ADDED_PARAMS) + + +@pytest.mark.parametrize(("name", "value", "expected"), [(n, v, e) for n, (v, e) in _ADDED_PARAMS.items()]) +def test_a_requested_parameter_is_sent(name: str, value: Any, expected: str) -> None: + """Every value here is falsy or off: a truthiness filter would drop them and + hand the decision back to the service without saying so.""" + assert _whisper_query(_client(), **{name: value})[name] == [expected] + + def test_undeclared_parameters_are_refused() -> None: from unstract.llmwhisperer.sdk_llmwhisperer.api.whisper import status as status_module @@ -269,7 +298,10 @@ def test_no_operation_sends_a_spec_default_the_client_never_set() -> None: assert "text_only" in inspect.signature(retrieve._get_kwargs).parameters assert "text_only" not in _SEND_ONLY["retrieve"] declared = set(inspect.signature(extract._get_kwargs).parameters) - {"body"} - assert declared - _SEND_ONLY["extract"] - {"url_query"} + # In URL mode the URL travels in the body, so the query parameter the + # generator writes for it is the one extract must never send. + assert declared - _SEND_ONLY["extract"] == {"url_query"} + assert _SEND_ONLY["extract"] <= declared # -------------------------------------------------------------------------- @@ -482,11 +514,17 @@ def _params(node: ast.FunctionDef) -> list[tuple[str, object]]: return list(zip([a.arg for a in args], defaults, strict=False)) -def _live_params(func: Any) -> list[tuple[str, object]]: +def _live_params(func: Any, *, keyword_only: bool = True) -> list[tuple[str, object]]: + """Parameters in order, with their defaults. + + ``keyword_only=False`` drops keyword-only parameters, for the comparisons + where a keyword-only addition is allowed: none is reachable from a released + call shape, so adding one leaves every existing call intact. + """ return [ (name, None if p.default is inspect.Parameter.empty else p.default) for name, p in inspect.signature(func).parameters.items() - if name not in ("self", "cls") + if name not in ("self", "cls") and (keyword_only or p.kind is not inspect.Parameter.KEYWORD_ONLY) ] @@ -506,7 +544,7 @@ def test_public_methods_are_unchanged() -> None: for name, node in methods.items(): live = getattr(LLMWhispererClientV2, name, None) assert live is not None, f"{name} disappeared from the client" - assert _live_params(live) == _params(node), name + assert _live_params(live, keyword_only=False) == _params(node), name def test_the_deprecated_parameter_resolver_is_unchanged() -> None: From 174a8e981aa1c40734791534e1594647299588a8 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 20:43:06 +0530 Subject: [PATCH 04/16] chore(sdk): regenerate from the remediated spec The spec now carries what the walk could not infer: which parameters are required, the closed sets the service validates against, the error body it returns, and the binary media types three endpoints answer with. Two of those broke generation quietly. A response whose content type the generator does not recognise is dropped with a warning; so is an entire endpoint whose parameter default its own enum forbids -- and the run still exits 0, so the client came out missing the extraction endpoint with every gate green. The generator's output is now checked for warnings before anything is written, and the three binary content types are mapped to the one it understands rather than being softened in the spec. The unwrapped-operation list is checked against the spec before being subtracted from it: an entry excusing an operation the spec no longer declares would otherwise keep passing forever. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- specs/llmwhisperer.json | 1002 +++++++++++++++-- .../api/account/test_connection.py | 41 +- .../sdk_llmwhisperer/api/account/usage.py | 63 +- .../api/account/usage_info.py | 43 +- .../api/convert/convert_to_pdf.py | 103 +- .../api/convert/convert_xlsb_to_xlsx.py | 103 +- .../api/{whisper => convert}/pdf_to_images.py | 113 +- .../pdf_to_images_retrieve.py | 77 +- .../pdf_to_images_status.py | 69 +- .../api/insights/document_insights.py | 157 ++- .../insights/document_insights_retrieve.py | 69 +- .../api/webhook/webhook_delete.py | 61 +- .../api/webhook/webhook_get.py | 63 +- .../api/webhook/webhook_post.py | 73 +- .../api/webhook/webhook_put.py | 65 +- .../sdk_llmwhisperer/api/whisper/detail.py | 65 +- .../sdk_llmwhisperer/api/whisper/extract.py | 231 ++-- .../api/whisper/highlights.py | 112 +- .../sdk_llmwhisperer/api/whisper/retrieve.py | 61 +- .../sdk_llmwhisperer/api/whisper/status.py | 63 +- .../sdk_llmwhisperer/models/__init__.py | 28 +- ...onvert_to_pdf_response_200.py => error.py} | 26 +- .../models/extract_line_splitter_strategy.py | 16 + .../sdk_llmwhisperer/models/extract_mode.py | 23 + .../models/extract_output_mode.py | 17 + .../pdf_to_images_retrieve_response_200.py | 48 - ...se_200.py => webhook_post_response_201.py} | 10 +- .../sdk_llmwhisperer/models/whisper_result.py | 41 + ...y => whisper_result_line_metadata_item.py} | 10 +- ....py => whisper_result_whisper_metadata.py} | 10 +- .../sdk_llmwhisperer/models/whisper_status.py | 29 +- ...e_200.py => whisper_status_detail_item.py} | 10 +- tests/unit/compat_test.py | 4 + tools/gen_sdk.sh | 12 +- tools/openapi-client.yaml | 9 + 35 files changed, 2176 insertions(+), 751 deletions(-) rename src/unstract/llmwhisperer/sdk_llmwhisperer/api/{whisper => convert}/pdf_to_images.py (65%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/api/{whisper => convert}/pdf_to_images_retrieve.py (65%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/api/{whisper => convert}/pdf_to_images_status.py (69%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/models/{convert_to_pdf_response_200.py => error.py} (70%) create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_line_splitter_strategy.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_mode.py create mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_output_mode.py delete mode 100644 src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py rename src/unstract/llmwhisperer/sdk_llmwhisperer/models/{webhook_post_response_200.py => webhook_post_response_201.py} (83%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/models/{convert_xlsb_to_xlsx_response_200.py => whisper_result_line_metadata_item.py} (81%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/models/{document_insights_response_200.py => whisper_result_whisper_metadata.py} (82%) rename src/unstract/llmwhisperer/sdk_llmwhisperer/models/{pdf_to_images_response_200.py => whisper_status_detail_item.py} (83%) diff --git a/specs/llmwhisperer.json b/specs/llmwhisperer.json index d5ae488..4b07fe2 100644 --- a/specs/llmwhisperer.json +++ b/specs/llmwhisperer.json @@ -1,6 +1,14 @@ { "components": { "schemas": { + "Error": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + }, "WebhookConfig": { "properties": { "auth_token": { @@ -44,6 +52,13 @@ }, "type": "array" }, + "line_metadata": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, "metadata": { "additionalProperties": true, "type": "object" @@ -53,12 +68,23 @@ }, "webhook_metadata": { "type": "string" + }, + "whisper_metadata": { + "additionalProperties": true, + "type": "object" } }, "type": "object" }, "WhisperStatus": { "properties": { + "detail": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, "message": { "type": "string" }, @@ -87,13 +113,20 @@ "post": { "operationId": "convert_to_pdf", "parameters": [ + { + "in": "query", + "name": "mode", + "required": false, + "schema": { + "default": "form", + "type": "string" + } + }, { "in": "query", "name": "url", "required": false, "schema": { - "default": "", - "format": "uri", "type": "string" } }, @@ -116,19 +149,59 @@ } } }, - "required": true + "required": false }, "responses": { "200": { "content": { - "application/json": { + "application/pdf": { "schema": { - "additionalProperties": true, - "type": "object" + "format": "binary", + "type": "string" } } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Convert a document to PDF", @@ -141,13 +214,20 @@ "post": { "operationId": "convert_xlsb_to_xlsx", "parameters": [ + { + "in": "query", + "name": "mode", + "required": false, + "schema": { + "default": "form", + "type": "string" + } + }, { "in": "query", "name": "url", "required": false, "schema": { - "default": "", - "format": "uri", "type": "string" } }, @@ -170,19 +250,59 @@ } } }, - "required": true + "required": false }, "responses": { "200": { "content": { - "application/json": { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { "schema": { - "additionalProperties": true, - "type": "object" + "format": "binary", + "type": "string" } } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Convert an XLSB workbook to XLSX", @@ -204,12 +324,20 @@ "type": "string" } }, + { + "in": "query", + "name": "mode", + "required": false, + "schema": { + "default": "form", + "type": "string" + } + }, { "in": "query", "name": "pages_to_extract", "required": false, "schema": { - "default": "", "type": "string" } }, @@ -227,8 +355,6 @@ "name": "url", "required": false, "schema": { - "default": "", - "format": "uri", "type": "string" } }, @@ -246,7 +372,6 @@ "name": "use_webhook", "required": false, "schema": { - "default": "", "type": "string" } }, @@ -255,7 +380,6 @@ "name": "webhook_metadata", "required": false, "schema": { - "default": "", "type": "string" } } @@ -269,19 +393,58 @@ } } }, - "required": true + "required": false }, "responses": { - "200": { + "202": { "content": { "application/json": { "schema": { - "additionalProperties": true, - "type": "object" + "$ref": "#/components/schemas/WhisperAccepted" } } }, - "description": "OK" + "description": "Accepted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Run document insights over a file", @@ -297,9 +460,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -315,9 +477,49 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, - "summary": "Retrieve document insights result", + "summary": "Retrieve document insights result (destructive \u2014 one shot)", "tags": [ "insights" ] @@ -338,6 +540,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Subscription usage summary", @@ -355,25 +597,33 @@ "name": "extract_all_lines", "required": false, "schema": { - "default": "false", - "type": "string" + "default": false, + "type": "boolean" } }, { + "description": "Line numbers or ranges, e.g. `1-5,9`. Not required when `extract_all_lines=true`.", "in": "query", "name": "lines", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "mode", "required": false, "schema": { - "default": "", + "default": "form", "type": "string" } }, { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -389,6 +639,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Line-level highlight geometry for an extraction", @@ -419,6 +709,15 @@ "type": "string" } }, + { + "in": "query", + "name": "mode", + "required": false, + "schema": { + "default": "form", + "type": "string" + } + }, { "in": "query", "name": "tag", @@ -433,8 +732,6 @@ "name": "url", "required": false, "schema": { - "default": "", - "format": "uri", "type": "string" } }, @@ -448,22 +745,72 @@ } } ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": false + }, "responses": { - "200": { + "202": { "content": { "application/json": { "schema": { - "additionalProperties": true, - "type": "object" + "$ref": "#/components/schemas/WhisperAccepted" } } }, - "description": "OK" + "description": "Accepted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, - "summary": "pdf to images", + "summary": "Render a PDF's pages as images", "tags": [ - "whisper" + "convert" ] } }, @@ -474,9 +821,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -484,19 +830,59 @@ "responses": { "200": { "content": { - "application/json": { + "application/zip": { "schema": { - "additionalProperties": true, - "type": "object" + "format": "binary", + "type": "string" } } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, - "summary": "pdf to images retrieve", + "summary": "Retrieve rendered images as a zip (destructive \u2014 one shot)", "tags": [ - "whisper" + "convert" ] } }, @@ -507,9 +893,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -525,11 +910,51 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, - "summary": "pdf to images status", + "summary": "Poll PDF-to-images status", "tags": [ - "whisper" + "convert" ] } }, @@ -548,6 +973,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Verify credentials", @@ -571,7 +1036,7 @@ { "in": "query", "name": "tag", - "required": false, + "required": true, "schema": { "type": "string" } @@ -595,7 +1060,47 @@ } } }, - "description": "OK" + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Detailed usage statistics", @@ -704,6 +1209,11 @@ "required": false, "schema": { "default": "left-priority", + "enum": [ + "left-priority", + "mid-priority", + "right-priority" + ], "type": "string" } }, @@ -758,6 +1268,16 @@ "required": false, "schema": { "default": "form", + "enum": [ + "document_insights", + "excel", + "form", + "high_quality", + "low_cost", + "native_text", + "pdf_to_images", + "table" + ], "type": "string" } }, @@ -767,6 +1287,12 @@ "required": false, "schema": { "default": "layout_preserving", + "enum": [ + "dump-text", + "layout_preserving", + "line-printer", + "text" + ], "type": "string" } }, @@ -775,6 +1301,7 @@ "name": "page_separator", "required": false, "schema": { + "default": "<<<", "type": "string" } }, @@ -783,7 +1310,6 @@ "name": "pages_to_extract", "required": false, "schema": { - "default": "", "type": "string" } }, @@ -797,16 +1323,17 @@ } }, { + "description": "Fetch the document from this URL instead of sending a body.", "in": "query", "name": "url", "required": false, "schema": { - "default": "", "format": "uri", "type": "string" } }, { + "description": "Read the URL to fetch from the request body.", "in": "query", "name": "url_in_post", "required": false, @@ -820,7 +1347,6 @@ "name": "use_webhook", "required": false, "schema": { - "default": "", "type": "string" } }, @@ -838,7 +1364,6 @@ "name": "webhook_metadata", "required": false, "schema": { - "default": "", "type": "string" } }, @@ -860,7 +1385,7 @@ } } }, - "required": true + "required": false }, "responses": { "202": { @@ -872,6 +1397,46 @@ } }, "description": "Accepted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Submit a document for text extraction", @@ -887,9 +1452,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -905,6 +1469,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Metadata about a whisper job", @@ -920,9 +1524,8 @@ { "in": "query", "name": "webhook_name", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -938,6 +1541,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Manage extraction webhooks", @@ -951,9 +1594,8 @@ { "in": "query", "name": "webhook_name", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -969,6 +1611,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Manage extraction webhooks", @@ -978,17 +1660,7 @@ }, "post": { "operationId": "webhook_post", - "parameters": [ - { - "in": "query", - "name": "webhook_name", - "required": false, - "schema": { - "default": "", - "type": "string" - } - } - ], + "parameters": [], "requestBody": { "content": { "application/json": { @@ -1000,7 +1672,7 @@ "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { @@ -1009,7 +1681,47 @@ } } }, - "description": "OK" + "description": "Created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Manage extraction webhooks", @@ -1019,17 +1731,7 @@ }, "put": { "operationId": "webhook_put", - "parameters": [ - { - "in": "query", - "name": "webhook_name", - "required": false, - "schema": { - "default": "", - "type": "string" - } - } - ], + "parameters": [], "requestBody": { "content": { "application/json": { @@ -1051,6 +1753,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Manage extraction webhooks", @@ -1075,9 +1817,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -1097,6 +1838,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Retrieve extraction result (destructive \u2014 one shot)", @@ -1112,9 +1893,8 @@ { "in": "query", "name": "whisper_hash", - "required": false, + "required": true, "schema": { - "default": "", "type": "string" } } @@ -1129,6 +1909,46 @@ } }, "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The request was rejected -- see `message`." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The API key is missing or not valid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The key is valid but not entitled to this operation." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "No such resource." } }, "summary": "Poll extraction status", @@ -1145,7 +1965,7 @@ ], "servers": [ { - "url": "https://llmwhisperer-api.us-central.unstract.com" + "url": "https://llmwhisperer-api.unstract.com" } ] } diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py index 3297c2e..4f93e88 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/test_connection.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.test_connection_response_200 import TestConnectionResponse200 from ...types import Response @@ -22,12 +23,32 @@ def _get_kwargs() -> dict[str, Any]: def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> TestConnectionResponse200 | None: +) -> Error | TestConnectionResponse200 | None: if response.status_code == 200: response_200 = TestConnectionResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -36,7 +57,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[TestConnectionResponse200]: +) -> Response[Error | TestConnectionResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -48,7 +69,7 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, -) -> Response[TestConnectionResponse200]: +) -> Response[Error | TestConnectionResponse200]: """Verify credentials Raises: @@ -56,7 +77,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[TestConnectionResponse200] + Response[Error | TestConnectionResponse200] """ kwargs = _get_kwargs() @@ -70,7 +91,7 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, -) -> TestConnectionResponse200 | None: +) -> Error | TestConnectionResponse200 | None: """Verify credentials Raises: @@ -78,7 +99,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - TestConnectionResponse200 + Error | TestConnectionResponse200 """ return sync_detailed( client=client, @@ -88,7 +109,7 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, -) -> Response[TestConnectionResponse200]: +) -> Response[Error | TestConnectionResponse200]: """Verify credentials Raises: @@ -96,7 +117,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[TestConnectionResponse200] + Response[Error | TestConnectionResponse200] """ kwargs = _get_kwargs() @@ -108,7 +129,7 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, -) -> TestConnectionResponse200 | None: +) -> Error | TestConnectionResponse200 | None: """Verify credentials Raises: @@ -116,7 +137,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - TestConnectionResponse200 + Error | TestConnectionResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py index 38f34ff..7b129b0 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.usage_response_200 import UsageResponse200 from ...types import UNSET, Response, Unset @@ -13,7 +14,7 @@ def _get_kwargs( *, from_date: str | Unset = UNSET, - tag: str | Unset = UNSET, + tag: str, to_date: str | Unset = UNSET, ) -> dict[str, Any]: @@ -36,19 +37,43 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UsageResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | UsageResponse200 | None: if response.status_code == 200: response_200 = UsageResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UsageResponse200]: +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Error | UsageResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -61,14 +86,14 @@ def sync_detailed( *, client: AuthenticatedClient | Client, from_date: str | Unset = UNSET, - tag: str | Unset = UNSET, + tag: str, to_date: str | Unset = UNSET, -) -> Response[UsageResponse200]: +) -> Response[Error | UsageResponse200]: """Detailed usage statistics Args: from_date (str | Unset): - tag (str | Unset): + tag (str): to_date (str | Unset): Raises: @@ -76,7 +101,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[UsageResponse200] + Response[Error | UsageResponse200] """ kwargs = _get_kwargs( from_date=from_date, @@ -95,14 +120,14 @@ def sync( *, client: AuthenticatedClient | Client, from_date: str | Unset = UNSET, - tag: str | Unset = UNSET, + tag: str, to_date: str | Unset = UNSET, -) -> UsageResponse200 | None: +) -> Error | UsageResponse200 | None: """Detailed usage statistics Args: from_date (str | Unset): - tag (str | Unset): + tag (str): to_date (str | Unset): Raises: @@ -110,7 +135,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - UsageResponse200 + Error | UsageResponse200 """ return sync_detailed( client=client, @@ -124,14 +149,14 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, from_date: str | Unset = UNSET, - tag: str | Unset = UNSET, + tag: str, to_date: str | Unset = UNSET, -) -> Response[UsageResponse200]: +) -> Response[Error | UsageResponse200]: """Detailed usage statistics Args: from_date (str | Unset): - tag (str | Unset): + tag (str): to_date (str | Unset): Raises: @@ -139,7 +164,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[UsageResponse200] + Response[Error | UsageResponse200] """ kwargs = _get_kwargs( from_date=from_date, @@ -156,14 +181,14 @@ async def asyncio( *, client: AuthenticatedClient | Client, from_date: str | Unset = UNSET, - tag: str | Unset = UNSET, + tag: str, to_date: str | Unset = UNSET, -) -> UsageResponse200 | None: +) -> Error | UsageResponse200 | None: """Detailed usage statistics Args: from_date (str | Unset): - tag (str | Unset): + tag (str): to_date (str | Unset): Raises: @@ -171,7 +196,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - UsageResponse200 + Error | UsageResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py index d806174..75e8e39 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/account/usage_info.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.usage_info_response_200 import UsageInfoResponse200 from ...types import Response @@ -20,12 +21,34 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UsageInfoResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | UsageInfoResponse200 | None: if response.status_code == 200: response_200 = UsageInfoResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -34,7 +57,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[UsageInfoResponse200]: +) -> Response[Error | UsageInfoResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -46,7 +69,7 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, -) -> Response[UsageInfoResponse200]: +) -> Response[Error | UsageInfoResponse200]: """Subscription usage summary Raises: @@ -54,7 +77,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[UsageInfoResponse200] + Response[Error | UsageInfoResponse200] """ kwargs = _get_kwargs() @@ -68,7 +91,7 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, -) -> UsageInfoResponse200 | None: +) -> Error | UsageInfoResponse200 | None: """Subscription usage summary Raises: @@ -76,7 +99,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - UsageInfoResponse200 + Error | UsageInfoResponse200 """ return sync_detailed( client=client, @@ -86,7 +109,7 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, -) -> Response[UsageInfoResponse200]: +) -> Response[Error | UsageInfoResponse200]: """Subscription usage summary Raises: @@ -94,7 +117,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[UsageInfoResponse200] + Response[Error | UsageInfoResponse200] """ kwargs = _get_kwargs() @@ -106,7 +129,7 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, -) -> UsageInfoResponse200 | None: +) -> Error | UsageInfoResponse200 | None: """Subscription usage summary Raises: @@ -114,7 +137,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - UsageInfoResponse200 + Error | UsageInfoResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py index 33d5fab..334eb18 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_to_pdf.py @@ -1,25 +1,29 @@ # Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. from http import HTTPStatus +from io import BytesIO from typing import Any import httpx from ... import errors from ...client import AuthenticatedClient, Client -from ...models.convert_to_pdf_response_200 import ConvertToPdfResponse200 +from ...models.error import Error from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, ) -> dict[str, Any]: headers: dict[str, Any] = {} params: dict[str, Any] = {} + params["mode"] = mode + params["url"] = url_query params["url_in_post"] = url_in_post @@ -32,30 +36,47 @@ def _get_kwargs( "params": params, } - _kwargs["content"] = body.payload + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ConvertToPdfResponse200 | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | File | None: if response.status_code == 200: - response_200 = ConvertToPdfResponse200.from_dict(response.json()) + response_200 = File(payload=BytesIO(response.content)) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ConvertToPdfResponse200]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Error | File]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -67,26 +88,29 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[ConvertToPdfResponse200]: +) -> Response[Error | File]: """Convert a document to PDF Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ConvertToPdfResponse200] + Response[Error | File] """ kwargs = _get_kwargs( body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) @@ -101,27 +125,30 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> ConvertToPdfResponse200 | None: +) -> Error | File | None: """Convert a document to PDF Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ConvertToPdfResponse200 + Error | File """ return sync_detailed( client=client, body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ).parsed @@ -130,26 +157,29 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[ConvertToPdfResponse200]: +) -> Response[Error | File]: """Convert a document to PDF Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ConvertToPdfResponse200] + Response[Error | File] """ kwargs = _get_kwargs( body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) @@ -162,28 +192,31 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> ConvertToPdfResponse200 | None: +) -> Error | File | None: """Convert a document to PDF Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ConvertToPdfResponse200 + Error | File """ return ( await asyncio_detailed( client=client, body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py index 441d863..c3ec4bc 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/convert_xlsb_to_xlsx.py @@ -1,25 +1,29 @@ # Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. from http import HTTPStatus +from io import BytesIO from typing import Any import httpx from ... import errors from ...client import AuthenticatedClient, Client -from ...models.convert_xlsb_to_xlsx_response_200 import ConvertXlsbToXlsxResponse200 +from ...models.error import Error from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, ) -> dict[str, Any]: headers: dict[str, Any] = {} params: dict[str, Any] = {} + params["mode"] = mode + params["url"] = url_query params["url_in_post"] = url_in_post @@ -32,30 +36,47 @@ def _get_kwargs( "params": params, } - _kwargs["content"] = body.payload + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ConvertXlsbToXlsxResponse200 | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | File | None: if response.status_code == 200: - response_200 = ConvertXlsbToXlsxResponse200.from_dict(response.json()) + response_200 = File(payload=BytesIO(response.content)) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ConvertXlsbToXlsxResponse200]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Error | File]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -67,26 +88,29 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[ConvertXlsbToXlsxResponse200]: +) -> Response[Error | File]: """Convert an XLSB workbook to XLSX Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ConvertXlsbToXlsxResponse200] + Response[Error | File] """ kwargs = _get_kwargs( body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) @@ -101,27 +125,30 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> ConvertXlsbToXlsxResponse200 | None: +) -> Error | File | None: """Convert an XLSB workbook to XLSX Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ConvertXlsbToXlsxResponse200 + Error | File """ return sync_detailed( client=client, body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ).parsed @@ -130,26 +157,29 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[ConvertXlsbToXlsxResponse200]: +) -> Response[Error | File]: """Convert an XLSB workbook to XLSX Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ConvertXlsbToXlsxResponse200] + Response[Error | File] """ kwargs = _get_kwargs( body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) @@ -162,28 +192,31 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - body: File, - url_query: str | Unset = "", + body: File | Unset = UNSET, + mode: str | Unset = "form", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> ConvertXlsbToXlsxResponse200 | None: +) -> Error | File | None: """Convert an XLSB workbook to XLSX Args: - url_query (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ConvertXlsbToXlsxResponse200 + Error | File """ return ( await asyncio_detailed( client=client, body=body, + mode=mode, url_query=url_query, url_in_post=url_in_post, ) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images.py similarity index 65% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images.py index 0824603..51a8047 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images.py @@ -6,18 +6,22 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.pdf_to_images_response_200 import PdfToImagesResponse200 -from ...types import UNSET, Response, Unset +from ...models.error import Error +from ...models.whisper_accepted import WhisperAccepted +from ...types import UNSET, File, Response, Unset def _get_kwargs( *, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", format_: str | Unset = "png", + mode: str | Unset = "form", tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, ) -> dict[str, Any]: + headers: dict[str, Any] = {} params: dict[str, Any] = {} @@ -25,6 +29,8 @@ def _get_kwargs( params["format"] = format_ + params["mode"] = mode + params["tag"] = tag params["url"] = url_query @@ -39,14 +45,41 @@ def _get_kwargs( "params": params, } + if not isinstance(body, Unset): + _kwargs["content"] = body.payload + headers["Content-Type"] = "application/octet-stream" + + _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PdfToImagesResponse200 | None: - if response.status_code == 200: - response_200 = PdfToImagesResponse200.from_dict(response.json()) +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | WhisperAccepted | None: + if response.status_code == 202: + response_202 = WhisperAccepted.from_dict(response.json()) + + return response_202 + + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) - return response_200 + return response_404 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) @@ -56,7 +89,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[PdfToImagesResponse200]: +) -> Response[Error | WhisperAccepted]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -68,31 +101,37 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", format_: str | Unset = "png", + mode: str | Unset = "form", tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[PdfToImagesResponse200]: - """Pdf to images +) -> Response[Error | WhisperAccepted]: + """Render a PDF's pages as images Args: file_name (str | Unset): Default: 'sample.pdf'. format_ (str | Unset): Default: 'png'. + mode (str | Unset): Default: 'form'. tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesResponse200] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( + body=body, file_name=file_name, format_=format_, + mode=mode, tag=tag, url_query=url_query, url_in_post=url_in_post, @@ -108,32 +147,38 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", format_: str | Unset = "png", + mode: str | Unset = "form", tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> PdfToImagesResponse200 | None: - """Pdf to images +) -> Error | WhisperAccepted | None: + """Render a PDF's pages as images Args: file_name (str | Unset): Default: 'sample.pdf'. format_ (str | Unset): Default: 'png'. + mode (str | Unset): Default: 'form'. tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesResponse200 + Error | WhisperAccepted """ return sync_detailed( client=client, + body=body, file_name=file_name, format_=format_, + mode=mode, tag=tag, url_query=url_query, url_in_post=url_in_post, @@ -143,31 +188,37 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", format_: str | Unset = "png", + mode: str | Unset = "form", tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> Response[PdfToImagesResponse200]: - """Pdf to images +) -> Response[Error | WhisperAccepted]: + """Render a PDF's pages as images Args: file_name (str | Unset): Default: 'sample.pdf'. format_ (str | Unset): Default: 'png'. + mode (str | Unset): Default: 'form'. tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesResponse200] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( + body=body, file_name=file_name, format_=format_, + mode=mode, tag=tag, url_query=url_query, url_in_post=url_in_post, @@ -181,33 +232,39 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", format_: str | Unset = "png", + mode: str | Unset = "form", tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, -) -> PdfToImagesResponse200 | None: - """Pdf to images +) -> Error | WhisperAccepted | None: + """Render a PDF's pages as images Args: file_name (str | Unset): Default: 'sample.pdf'. format_ (str | Unset): Default: 'png'. + mode (str | Unset): Default: 'form'. tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesResponse200 + Error | WhisperAccepted """ return ( await asyncio_detailed( client=client, + body=body, file_name=file_name, format_=format_, + mode=mode, tag=tag, url_query=url_query, url_in_post=url_in_post, diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_retrieve.py similarity index 65% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_retrieve.py index 32260d6..dc9ec1d 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_retrieve.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_retrieve.py @@ -1,18 +1,19 @@ # Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. from http import HTTPStatus +from io import BytesIO from typing import Any import httpx from ... import errors from ...client import AuthenticatedClient, Client -from ...models.pdf_to_images_retrieve_response_200 import PdfToImagesRetrieveResponse200 -from ...types import UNSET, Response, Unset +from ...models.error import Error +from ...types import UNSET, File, Response def _get_kwargs( *, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -30,23 +31,39 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> PdfToImagesRetrieveResponse200 | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | File | None: if response.status_code == 200: - response_200 = PdfToImagesRetrieveResponse200.from_dict(response.json()) + response_200 = File(payload=BytesIO(response.content)) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[PdfToImagesRetrieveResponse200]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Error | File]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,19 +75,19 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[PdfToImagesRetrieveResponse200]: - """Pdf to images retrieve + whisper_hash: str, +) -> Response[Error | File]: + """Retrieve rendered images as a zip (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesRetrieveResponse200] + Response[Error | File] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -86,19 +103,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> PdfToImagesRetrieveResponse200 | None: - """Pdf to images retrieve + whisper_hash: str, +) -> Error | File | None: + """Retrieve rendered images as a zip (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesRetrieveResponse200 + Error | File """ return sync_detailed( client=client, @@ -109,19 +126,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[PdfToImagesRetrieveResponse200]: - """Pdf to images retrieve + whisper_hash: str, +) -> Response[Error | File]: + """Retrieve rendered images as a zip (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesRetrieveResponse200] + Response[Error | File] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -135,19 +152,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> PdfToImagesRetrieveResponse200 | None: - """Pdf to images retrieve + whisper_hash: str, +) -> Error | File | None: + """Retrieve rendered images as a zip (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesRetrieveResponse200 + Error | File """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_status.py similarity index 69% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_status.py index 8cad5f7..0664810 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/pdf_to_images_status.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/convert/pdf_to_images_status.py @@ -6,13 +6,14 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.pdf_to_images_status_response_200 import PdfToImagesStatusResponse200 -from ...types import UNSET, Response, Unset +from ...types import UNSET, Response def _get_kwargs( *, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -32,12 +33,32 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> PdfToImagesStatusResponse200 | None: +) -> Error | PdfToImagesStatusResponse200 | None: if response.status_code == 200: response_200 = PdfToImagesStatusResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,7 +67,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[PdfToImagesStatusResponse200]: +) -> Response[Error | PdfToImagesStatusResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,19 +79,19 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[PdfToImagesStatusResponse200]: - """Pdf to images status + whisper_hash: str, +) -> Response[Error | PdfToImagesStatusResponse200]: + """Poll PDF-to-images status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesStatusResponse200] + Response[Error | PdfToImagesStatusResponse200] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -86,19 +107,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> PdfToImagesStatusResponse200 | None: - """Pdf to images status + whisper_hash: str, +) -> Error | PdfToImagesStatusResponse200 | None: + """Poll PDF-to-images status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesStatusResponse200 + Error | PdfToImagesStatusResponse200 """ return sync_detailed( client=client, @@ -109,19 +130,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[PdfToImagesStatusResponse200]: - """Pdf to images status + whisper_hash: str, +) -> Response[Error | PdfToImagesStatusResponse200]: + """Poll PDF-to-images status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[PdfToImagesStatusResponse200] + Response[Error | PdfToImagesStatusResponse200] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -135,19 +156,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> PdfToImagesStatusResponse200 | None: - """Pdf to images status + whisper_hash: str, +) -> Error | PdfToImagesStatusResponse200 | None: + """Poll PDF-to-images status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - PdfToImagesStatusResponse200 + Error | PdfToImagesStatusResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py index d8950ce..e914b42 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights.py @@ -6,20 +6,22 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.document_insights_response_200 import DocumentInsightsResponse200 +from ...models.error import Error +from ...models.whisper_accepted import WhisperAccepted from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: File, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", - pages_to_extract: str | Unset = "", + mode: str | Unset = "form", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", - webhook_metadata: str | Unset = "", + use_webhook: str | Unset = UNSET, + webhook_metadata: str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -27,6 +29,8 @@ def _get_kwargs( params["file_name"] = file_name + params["mode"] = mode + params["pages_to_extract"] = pages_to_extract params["tag"] = tag @@ -47,7 +51,8 @@ def _get_kwargs( "params": params, } - _kwargs["content"] = body.payload + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers @@ -56,11 +61,31 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> DocumentInsightsResponse200 | None: - if response.status_code == 200: - response_200 = DocumentInsightsResponse200.from_dict(response.json()) +) -> Error | WhisperAccepted | None: + if response.status_code == 202: + response_202 = WhisperAccepted.from_dict(response.json()) + + return response_202 + + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) - return response_200 + return response_404 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) @@ -70,7 +95,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[DocumentInsightsResponse200]: +) -> Response[Error | WhisperAccepted]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -82,37 +107,40 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", - pages_to_extract: str | Unset = "", + mode: str | Unset = "form", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", - webhook_metadata: str | Unset = "", -) -> Response[DocumentInsightsResponse200]: + use_webhook: str | Unset = UNSET, + webhook_metadata: str | Unset = UNSET, +) -> Response[Error | WhisperAccepted]: """Run document insights over a file Args: file_name (str | Unset): Default: 'sample.pdf'. - pages_to_extract (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. - webhook_metadata (str | Unset): Default: ''. - body (File): + use_webhook (str | Unset): + webhook_metadata (str | Unset): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DocumentInsightsResponse200] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( body=body, file_name=file_name, + mode=mode, pages_to_extract=pages_to_extract, tag=tag, url_query=url_query, @@ -131,38 +159,41 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", - pages_to_extract: str | Unset = "", + mode: str | Unset = "form", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", - webhook_metadata: str | Unset = "", -) -> DocumentInsightsResponse200 | None: + use_webhook: str | Unset = UNSET, + webhook_metadata: str | Unset = UNSET, +) -> Error | WhisperAccepted | None: """Run document insights over a file Args: file_name (str | Unset): Default: 'sample.pdf'. - pages_to_extract (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. - webhook_metadata (str | Unset): Default: ''. - body (File): + use_webhook (str | Unset): + webhook_metadata (str | Unset): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DocumentInsightsResponse200 + Error | WhisperAccepted """ return sync_detailed( client=client, body=body, file_name=file_name, + mode=mode, pages_to_extract=pages_to_extract, tag=tag, url_query=url_query, @@ -175,37 +206,40 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", - pages_to_extract: str | Unset = "", + mode: str | Unset = "form", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", - webhook_metadata: str | Unset = "", -) -> Response[DocumentInsightsResponse200]: + use_webhook: str | Unset = UNSET, + webhook_metadata: str | Unset = UNSET, +) -> Response[Error | WhisperAccepted]: """Run document insights over a file Args: file_name (str | Unset): Default: 'sample.pdf'. - pages_to_extract (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. - webhook_metadata (str | Unset): Default: ''. - body (File): + use_webhook (str | Unset): + webhook_metadata (str | Unset): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DocumentInsightsResponse200] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( body=body, file_name=file_name, + mode=mode, pages_to_extract=pages_to_extract, tag=tag, url_query=url_query, @@ -222,39 +256,42 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, file_name: str | Unset = "sample.pdf", - pages_to_extract: str | Unset = "", + mode: str | Unset = "form", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", - webhook_metadata: str | Unset = "", -) -> DocumentInsightsResponse200 | None: + use_webhook: str | Unset = UNSET, + webhook_metadata: str | Unset = UNSET, +) -> Error | WhisperAccepted | None: """Run document insights over a file Args: file_name (str | Unset): Default: 'sample.pdf'. - pages_to_extract (str | Unset): Default: ''. + mode (str | Unset): Default: 'form'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. - webhook_metadata (str | Unset): Default: ''. - body (File): + use_webhook (str | Unset): + webhook_metadata (str | Unset): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DocumentInsightsResponse200 + Error | WhisperAccepted """ return ( await asyncio_detailed( client=client, body=body, file_name=file_name, + mode=mode, pages_to_extract=pages_to_extract, tag=tag, url_query=url_query, diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py index a6ba73b..20baf37 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/insights/document_insights_retrieve.py @@ -7,12 +7,13 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.document_insights_retrieve_response_200 import DocumentInsightsRetrieveResponse200 -from ...types import UNSET, Response, Unset +from ...models.error import Error +from ...types import UNSET, Response def _get_kwargs( *, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -32,12 +33,32 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> DocumentInsightsRetrieveResponse200 | None: +) -> DocumentInsightsRetrieveResponse200 | Error | None: if response.status_code == 200: response_200 = DocumentInsightsRetrieveResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,7 +67,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[DocumentInsightsRetrieveResponse200]: +) -> Response[DocumentInsightsRetrieveResponse200 | Error]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,19 +79,19 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[DocumentInsightsRetrieveResponse200]: - """Retrieve document insights result + whisper_hash: str, +) -> Response[DocumentInsightsRetrieveResponse200 | Error]: + """Retrieve document insights result (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DocumentInsightsRetrieveResponse200] + Response[DocumentInsightsRetrieveResponse200 | Error] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -86,19 +107,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> DocumentInsightsRetrieveResponse200 | None: - """Retrieve document insights result + whisper_hash: str, +) -> DocumentInsightsRetrieveResponse200 | Error | None: + """Retrieve document insights result (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DocumentInsightsRetrieveResponse200 + DocumentInsightsRetrieveResponse200 | Error """ return sync_detailed( client=client, @@ -109,19 +130,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[DocumentInsightsRetrieveResponse200]: - """Retrieve document insights result + whisper_hash: str, +) -> Response[DocumentInsightsRetrieveResponse200 | Error]: + """Retrieve document insights result (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DocumentInsightsRetrieveResponse200] + Response[DocumentInsightsRetrieveResponse200 | Error] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -135,19 +156,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> DocumentInsightsRetrieveResponse200 | None: - """Retrieve document insights result + whisper_hash: str, +) -> DocumentInsightsRetrieveResponse200 | Error | None: + """Retrieve document insights result (destructive — one shot) Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DocumentInsightsRetrieveResponse200 + DocumentInsightsRetrieveResponse200 | Error """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py index e4ff850..bf3f7dd 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_delete.py @@ -6,13 +6,14 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.webhook_delete_response_200 import WebhookDeleteResponse200 -from ...types import UNSET, Response, Unset +from ...types import UNSET, Response def _get_kwargs( *, - webhook_name: str | Unset = "", + webhook_name: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -32,12 +33,32 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> WebhookDeleteResponse200 | None: +) -> Error | WebhookDeleteResponse200 | None: if response.status_code == 200: response_200 = WebhookDeleteResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,7 +67,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[WebhookDeleteResponse200]: +) -> Response[Error | WebhookDeleteResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,19 +79,19 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> Response[WebhookDeleteResponse200]: + webhook_name: str, +) -> Response[Error | WebhookDeleteResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookDeleteResponse200] + Response[Error | WebhookDeleteResponse200] """ kwargs = _get_kwargs( webhook_name=webhook_name, @@ -86,19 +107,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> WebhookDeleteResponse200 | None: + webhook_name: str, +) -> Error | WebhookDeleteResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookDeleteResponse200 + Error | WebhookDeleteResponse200 """ return sync_detailed( client=client, @@ -109,19 +130,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> Response[WebhookDeleteResponse200]: + webhook_name: str, +) -> Response[Error | WebhookDeleteResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookDeleteResponse200] + Response[Error | WebhookDeleteResponse200] """ kwargs = _get_kwargs( webhook_name=webhook_name, @@ -135,19 +156,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> WebhookDeleteResponse200 | None: + webhook_name: str, +) -> Error | WebhookDeleteResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookDeleteResponse200 + Error | WebhookDeleteResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py index 0a7525c..093cf6d 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_get.py @@ -6,13 +6,14 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.webhook_get_response_200 import WebhookGetResponse200 -from ...types import UNSET, Response, Unset +from ...types import UNSET, Response def _get_kwargs( *, - webhook_name: str | Unset = "", + webhook_name: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -30,12 +31,34 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookGetResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | WebhookGetResponse200 | None: if response.status_code == 200: response_200 = WebhookGetResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -44,7 +67,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[WebhookGetResponse200]: +) -> Response[Error | WebhookGetResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -56,19 +79,19 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> Response[WebhookGetResponse200]: + webhook_name: str, +) -> Response[Error | WebhookGetResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookGetResponse200] + Response[Error | WebhookGetResponse200] """ kwargs = _get_kwargs( webhook_name=webhook_name, @@ -84,19 +107,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> WebhookGetResponse200 | None: + webhook_name: str, +) -> Error | WebhookGetResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookGetResponse200 + Error | WebhookGetResponse200 """ return sync_detailed( client=client, @@ -107,19 +130,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> Response[WebhookGetResponse200]: + webhook_name: str, +) -> Response[Error | WebhookGetResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookGetResponse200] + Response[Error | WebhookGetResponse200] """ kwargs = _get_kwargs( webhook_name=webhook_name, @@ -133,19 +156,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - webhook_name: str | Unset = "", -) -> WebhookGetResponse200 | None: + webhook_name: str, +) -> Error | WebhookGetResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. + webhook_name (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookGetResponse200 + Error | WebhookGetResponse200 """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py index 8674d4a..4556683 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_post.py @@ -6,28 +6,21 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.webhook_config import WebhookConfig -from ...models.webhook_post_response_200 import WebhookPostResponse200 -from ...types import UNSET, Response, Unset +from ...models.webhook_post_response_201 import WebhookPostResponse201 +from ...types import Response def _get_kwargs( *, body: WebhookConfig, - webhook_name: str | Unset = "", ) -> dict[str, Any]: headers: dict[str, Any] = {} - params: dict[str, Any] = {} - - params["webhook_name"] = webhook_name - - params = {k: v for k, v in params.items() if v is not UNSET and v is not None} - _kwargs: dict[str, Any] = { "method": "post", "url": "/api/v2/whisper-manage-callback", - "params": params, } _kwargs["json"] = body.to_dict() @@ -38,11 +31,33 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookPostResponse200 | None: - if response.status_code == 200: - response_200 = WebhookPostResponse200.from_dict(response.json()) +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | WebhookPostResponse201 | None: + if response.status_code == 201: + response_201 = WebhookPostResponse201.from_dict(response.json()) + + return response_201 + + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) - return response_200 + return response_404 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) @@ -52,7 +67,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[WebhookPostResponse200]: +) -> Response[Error | WebhookPostResponse201]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -65,12 +80,10 @@ def sync_detailed( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> Response[WebhookPostResponse200]: +) -> Response[Error | WebhookPostResponse201]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -78,11 +91,10 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookPostResponse200] + Response[Error | WebhookPostResponse201] """ kwargs = _get_kwargs( body=body, - webhook_name=webhook_name, ) response = client.get_httpx_client().request( @@ -96,12 +108,10 @@ def sync( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> WebhookPostResponse200 | None: +) -> Error | WebhookPostResponse201 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -109,12 +119,11 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookPostResponse200 + Error | WebhookPostResponse201 """ return sync_detailed( client=client, body=body, - webhook_name=webhook_name, ).parsed @@ -122,12 +131,10 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> Response[WebhookPostResponse200]: +) -> Response[Error | WebhookPostResponse201]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -135,11 +142,10 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookPostResponse200] + Response[Error | WebhookPostResponse201] """ kwargs = _get_kwargs( body=body, - webhook_name=webhook_name, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -151,12 +157,10 @@ async def asyncio( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> WebhookPostResponse200 | None: +) -> Error | WebhookPostResponse201 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -164,12 +168,11 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookPostResponse200 + Error | WebhookPostResponse201 """ return ( await asyncio_detailed( client=client, body=body, - webhook_name=webhook_name, ) ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py index b131d80..f843dcc 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/webhook/webhook_put.py @@ -6,28 +6,21 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.webhook_config import WebhookConfig from ...models.webhook_put_response_200 import WebhookPutResponse200 -from ...types import UNSET, Response, Unset +from ...types import Response def _get_kwargs( *, body: WebhookConfig, - webhook_name: str | Unset = "", ) -> dict[str, Any]: headers: dict[str, Any] = {} - params: dict[str, Any] = {} - - params["webhook_name"] = webhook_name - - params = {k: v for k, v in params.items() if v is not UNSET and v is not None} - _kwargs: dict[str, Any] = { "method": "put", "url": "/api/v2/whisper-manage-callback", - "params": params, } _kwargs["json"] = body.to_dict() @@ -38,12 +31,34 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WebhookPutResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | WebhookPutResponse200 | None: if response.status_code == 200: response_200 = WebhookPutResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -52,7 +67,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[WebhookPutResponse200]: +) -> Response[Error | WebhookPutResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -65,12 +80,10 @@ def sync_detailed( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> Response[WebhookPutResponse200]: +) -> Response[Error | WebhookPutResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -78,11 +91,10 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookPutResponse200] + Response[Error | WebhookPutResponse200] """ kwargs = _get_kwargs( body=body, - webhook_name=webhook_name, ) response = client.get_httpx_client().request( @@ -96,12 +108,10 @@ def sync( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> WebhookPutResponse200 | None: +) -> Error | WebhookPutResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -109,12 +119,11 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookPutResponse200 + Error | WebhookPutResponse200 """ return sync_detailed( client=client, body=body, - webhook_name=webhook_name, ).parsed @@ -122,12 +131,10 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> Response[WebhookPutResponse200]: +) -> Response[Error | WebhookPutResponse200]: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -135,11 +142,10 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WebhookPutResponse200] + Response[Error | WebhookPutResponse200] """ kwargs = _get_kwargs( body=body, - webhook_name=webhook_name, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -151,12 +157,10 @@ async def asyncio( *, client: AuthenticatedClient | Client, body: WebhookConfig, - webhook_name: str | Unset = "", -) -> WebhookPutResponse200 | None: +) -> Error | WebhookPutResponse200 | None: """Manage extraction webhooks Args: - webhook_name (str | Unset): Default: ''. body (WebhookConfig): Raises: @@ -164,12 +168,11 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WebhookPutResponse200 + Error | WebhookPutResponse200 """ return ( await asyncio_detailed( client=client, body=body, - webhook_name=webhook_name, ) ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py index bff096f..8cec0b6 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/detail.py @@ -7,12 +7,13 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.detail_response_200 import DetailResponse200 -from ...types import UNSET, Response, Unset +from ...models.error import Error +from ...types import UNSET, Response def _get_kwargs( *, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -30,19 +31,43 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> DetailResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DetailResponse200 | Error | None: if response.status_code == 200: response_200 = DetailResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[DetailResponse200]: +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DetailResponse200 | Error]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -54,19 +79,19 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[DetailResponse200]: + whisper_hash: str, +) -> Response[DetailResponse200 | Error]: """Metadata about a whisper job Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DetailResponse200] + Response[DetailResponse200 | Error] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -82,19 +107,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> DetailResponse200 | None: + whisper_hash: str, +) -> DetailResponse200 | Error | None: """Metadata about a whisper job Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DetailResponse200 + DetailResponse200 | Error """ return sync_detailed( client=client, @@ -105,19 +130,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[DetailResponse200]: + whisper_hash: str, +) -> Response[DetailResponse200 | Error]: """Metadata about a whisper job Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[DetailResponse200] + Response[DetailResponse200 | Error] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -131,19 +156,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> DetailResponse200 | None: + whisper_hash: str, +) -> DetailResponse200 | Error | None: """Metadata about a whisper job Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - DetailResponse200 + DetailResponse200 | Error """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py index 43dd576..ebad4ca 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/extract.py @@ -6,13 +6,17 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.extract_line_splitter_strategy import ExtractLineSplitterStrategy +from ...models.extract_mode import ExtractMode +from ...models.extract_output_mode import ExtractOutputMode from ...models.whisper_accepted import WhisperAccepted from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: File, + body: File | Unset = UNSET, add_line_nos: bool | Unset = False, allow_rotated_text: bool | Unset = True, checkbox_confidence_threshold: float | Unset = 0.3, @@ -23,22 +27,22 @@ def _get_kwargs( ignore_vertical_text: bool | Unset = False, include_line_confidence: bool | Unset = False, lang: str | Unset = "eng", - line_splitter_strategy: str | Unset = "left-priority", + line_splitter_strategy: ExtractLineSplitterStrategy | Unset = "left-priority", line_splitter_tolerance: float | Unset = 0.75, mark_horizontal_lines: bool | Unset = False, mark_vertical_lines: bool | Unset = False, median_filter_size: int | Unset = 0, min_table_width: float | Unset = 0.0, - mode: str | Unset = "form", - output_mode: str | Unset = "layout_preserving", - page_separator: str | Unset = UNSET, - pages_to_extract: str | Unset = "", + mode: ExtractMode | Unset = "form", + output_mode: ExtractOutputMode | Unset = "layout_preserving", + page_separator: str | Unset = "<<<", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", + use_webhook: str | Unset = UNSET, watermark_angle_threshold: float | Unset = 25.0, - webhook_metadata: str | Unset = "", + webhook_metadata: str | Unset = UNSET, word_confidence_threshold: float | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -65,7 +69,11 @@ def _get_kwargs( params["lang"] = lang - params["line_splitter_strategy"] = line_splitter_strategy + json_line_splitter_strategy: str | Unset = UNSET + if not isinstance(line_splitter_strategy, Unset): + json_line_splitter_strategy = line_splitter_strategy + + params["line_splitter_strategy"] = json_line_splitter_strategy params["line_splitter_tolerance"] = line_splitter_tolerance @@ -77,9 +85,17 @@ def _get_kwargs( params["min_table_width"] = min_table_width - params["mode"] = mode + json_mode: str | Unset = UNSET + if not isinstance(mode, Unset): + json_mode = mode + + params["mode"] = json_mode + + json_output_mode: str | Unset = UNSET + if not isinstance(output_mode, Unset): + json_output_mode = output_mode - params["output_mode"] = output_mode + params["output_mode"] = json_output_mode params["page_separator"] = page_separator @@ -107,26 +123,51 @@ def _get_kwargs( "params": params, } - _kwargs["content"] = body.payload + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperAccepted | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | WhisperAccepted | None: if response.status_code == 202: response_202 = WhisperAccepted.from_dict(response.json()) return response_202 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperAccepted]: +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Error | WhisperAccepted]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -138,7 +179,7 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, add_line_nos: bool | Unset = False, allow_rotated_text: bool | Unset = True, checkbox_confidence_threshold: float | Unset = 0.3, @@ -149,24 +190,24 @@ def sync_detailed( ignore_vertical_text: bool | Unset = False, include_line_confidence: bool | Unset = False, lang: str | Unset = "eng", - line_splitter_strategy: str | Unset = "left-priority", + line_splitter_strategy: ExtractLineSplitterStrategy | Unset = "left-priority", line_splitter_tolerance: float | Unset = 0.75, mark_horizontal_lines: bool | Unset = False, mark_vertical_lines: bool | Unset = False, median_filter_size: int | Unset = 0, min_table_width: float | Unset = 0.0, - mode: str | Unset = "form", - output_mode: str | Unset = "layout_preserving", - page_separator: str | Unset = UNSET, - pages_to_extract: str | Unset = "", + mode: ExtractMode | Unset = "form", + output_mode: ExtractOutputMode | Unset = "layout_preserving", + page_separator: str | Unset = "<<<", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", + use_webhook: str | Unset = UNSET, watermark_angle_threshold: float | Unset = 25.0, - webhook_metadata: str | Unset = "", + webhook_metadata: str | Unset = UNSET, word_confidence_threshold: float | Unset = UNSET, -) -> Response[WhisperAccepted]: +) -> Response[Error | WhisperAccepted]: """Submit a document for text extraction Args: @@ -180,31 +221,31 @@ def sync_detailed( ignore_vertical_text (bool | Unset): Default: False. include_line_confidence (bool | Unset): Default: False. lang (str | Unset): Default: 'eng'. - line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_strategy (ExtractLineSplitterStrategy | Unset): Default: 'left-priority'. line_splitter_tolerance (float | Unset): Default: 0.75. mark_horizontal_lines (bool | Unset): Default: False. mark_vertical_lines (bool | Unset): Default: False. median_filter_size (int | Unset): Default: 0. min_table_width (float | Unset): Default: 0.0. - mode (str | Unset): Default: 'form'. - output_mode (str | Unset): Default: 'layout_preserving'. - page_separator (str | Unset): - pages_to_extract (str | Unset): Default: ''. + mode (ExtractMode | Unset): Default: 'form'. + output_mode (ExtractOutputMode | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): Default: '<<<'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. + use_webhook (str | Unset): watermark_angle_threshold (float | Unset): Default: 25.0. - webhook_metadata (str | Unset): Default: ''. + webhook_metadata (str | Unset): word_confidence_threshold (float | Unset): - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperAccepted] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( body=body, @@ -247,7 +288,7 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, add_line_nos: bool | Unset = False, allow_rotated_text: bool | Unset = True, checkbox_confidence_threshold: float | Unset = 0.3, @@ -258,24 +299,24 @@ def sync( ignore_vertical_text: bool | Unset = False, include_line_confidence: bool | Unset = False, lang: str | Unset = "eng", - line_splitter_strategy: str | Unset = "left-priority", + line_splitter_strategy: ExtractLineSplitterStrategy | Unset = "left-priority", line_splitter_tolerance: float | Unset = 0.75, mark_horizontal_lines: bool | Unset = False, mark_vertical_lines: bool | Unset = False, median_filter_size: int | Unset = 0, min_table_width: float | Unset = 0.0, - mode: str | Unset = "form", - output_mode: str | Unset = "layout_preserving", - page_separator: str | Unset = UNSET, - pages_to_extract: str | Unset = "", + mode: ExtractMode | Unset = "form", + output_mode: ExtractOutputMode | Unset = "layout_preserving", + page_separator: str | Unset = "<<<", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", + use_webhook: str | Unset = UNSET, watermark_angle_threshold: float | Unset = 25.0, - webhook_metadata: str | Unset = "", + webhook_metadata: str | Unset = UNSET, word_confidence_threshold: float | Unset = UNSET, -) -> WhisperAccepted | None: +) -> Error | WhisperAccepted | None: """Submit a document for text extraction Args: @@ -289,31 +330,31 @@ def sync( ignore_vertical_text (bool | Unset): Default: False. include_line_confidence (bool | Unset): Default: False. lang (str | Unset): Default: 'eng'. - line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_strategy (ExtractLineSplitterStrategy | Unset): Default: 'left-priority'. line_splitter_tolerance (float | Unset): Default: 0.75. mark_horizontal_lines (bool | Unset): Default: False. mark_vertical_lines (bool | Unset): Default: False. median_filter_size (int | Unset): Default: 0. min_table_width (float | Unset): Default: 0.0. - mode (str | Unset): Default: 'form'. - output_mode (str | Unset): Default: 'layout_preserving'. - page_separator (str | Unset): - pages_to_extract (str | Unset): Default: ''. + mode (ExtractMode | Unset): Default: 'form'. + output_mode (ExtractOutputMode | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): Default: '<<<'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. + use_webhook (str | Unset): watermark_angle_threshold (float | Unset): Default: 25.0. - webhook_metadata (str | Unset): Default: ''. + webhook_metadata (str | Unset): word_confidence_threshold (float | Unset): - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperAccepted + Error | WhisperAccepted """ return sync_detailed( client=client, @@ -351,7 +392,7 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, add_line_nos: bool | Unset = False, allow_rotated_text: bool | Unset = True, checkbox_confidence_threshold: float | Unset = 0.3, @@ -362,24 +403,24 @@ async def asyncio_detailed( ignore_vertical_text: bool | Unset = False, include_line_confidence: bool | Unset = False, lang: str | Unset = "eng", - line_splitter_strategy: str | Unset = "left-priority", + line_splitter_strategy: ExtractLineSplitterStrategy | Unset = "left-priority", line_splitter_tolerance: float | Unset = 0.75, mark_horizontal_lines: bool | Unset = False, mark_vertical_lines: bool | Unset = False, median_filter_size: int | Unset = 0, min_table_width: float | Unset = 0.0, - mode: str | Unset = "form", - output_mode: str | Unset = "layout_preserving", - page_separator: str | Unset = UNSET, - pages_to_extract: str | Unset = "", + mode: ExtractMode | Unset = "form", + output_mode: ExtractOutputMode | Unset = "layout_preserving", + page_separator: str | Unset = "<<<", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", + use_webhook: str | Unset = UNSET, watermark_angle_threshold: float | Unset = 25.0, - webhook_metadata: str | Unset = "", + webhook_metadata: str | Unset = UNSET, word_confidence_threshold: float | Unset = UNSET, -) -> Response[WhisperAccepted]: +) -> Response[Error | WhisperAccepted]: """Submit a document for text extraction Args: @@ -393,31 +434,31 @@ async def asyncio_detailed( ignore_vertical_text (bool | Unset): Default: False. include_line_confidence (bool | Unset): Default: False. lang (str | Unset): Default: 'eng'. - line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_strategy (ExtractLineSplitterStrategy | Unset): Default: 'left-priority'. line_splitter_tolerance (float | Unset): Default: 0.75. mark_horizontal_lines (bool | Unset): Default: False. mark_vertical_lines (bool | Unset): Default: False. median_filter_size (int | Unset): Default: 0. min_table_width (float | Unset): Default: 0.0. - mode (str | Unset): Default: 'form'. - output_mode (str | Unset): Default: 'layout_preserving'. - page_separator (str | Unset): - pages_to_extract (str | Unset): Default: ''. + mode (ExtractMode | Unset): Default: 'form'. + output_mode (ExtractOutputMode | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): Default: '<<<'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. + use_webhook (str | Unset): watermark_angle_threshold (float | Unset): Default: 25.0. - webhook_metadata (str | Unset): Default: ''. + webhook_metadata (str | Unset): word_confidence_threshold (float | Unset): - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperAccepted] + Response[Error | WhisperAccepted] """ kwargs = _get_kwargs( body=body, @@ -458,7 +499,7 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - body: File, + body: File | Unset = UNSET, add_line_nos: bool | Unset = False, allow_rotated_text: bool | Unset = True, checkbox_confidence_threshold: float | Unset = 0.3, @@ -469,24 +510,24 @@ async def asyncio( ignore_vertical_text: bool | Unset = False, include_line_confidence: bool | Unset = False, lang: str | Unset = "eng", - line_splitter_strategy: str | Unset = "left-priority", + line_splitter_strategy: ExtractLineSplitterStrategy | Unset = "left-priority", line_splitter_tolerance: float | Unset = 0.75, mark_horizontal_lines: bool | Unset = False, mark_vertical_lines: bool | Unset = False, median_filter_size: int | Unset = 0, min_table_width: float | Unset = 0.0, - mode: str | Unset = "form", - output_mode: str | Unset = "layout_preserving", - page_separator: str | Unset = UNSET, - pages_to_extract: str | Unset = "", + mode: ExtractMode | Unset = "form", + output_mode: ExtractOutputMode | Unset = "layout_preserving", + page_separator: str | Unset = "<<<", + pages_to_extract: str | Unset = UNSET, tag: str | Unset = "default", - url_query: str | Unset = "", + url_query: str | Unset = UNSET, url_in_post: bool | Unset = False, - use_webhook: str | Unset = "", + use_webhook: str | Unset = UNSET, watermark_angle_threshold: float | Unset = 25.0, - webhook_metadata: str | Unset = "", + webhook_metadata: str | Unset = UNSET, word_confidence_threshold: float | Unset = UNSET, -) -> WhisperAccepted | None: +) -> Error | WhisperAccepted | None: """Submit a document for text extraction Args: @@ -500,31 +541,31 @@ async def asyncio( ignore_vertical_text (bool | Unset): Default: False. include_line_confidence (bool | Unset): Default: False. lang (str | Unset): Default: 'eng'. - line_splitter_strategy (str | Unset): Default: 'left-priority'. + line_splitter_strategy (ExtractLineSplitterStrategy | Unset): Default: 'left-priority'. line_splitter_tolerance (float | Unset): Default: 0.75. mark_horizontal_lines (bool | Unset): Default: False. mark_vertical_lines (bool | Unset): Default: False. median_filter_size (int | Unset): Default: 0. min_table_width (float | Unset): Default: 0.0. - mode (str | Unset): Default: 'form'. - output_mode (str | Unset): Default: 'layout_preserving'. - page_separator (str | Unset): - pages_to_extract (str | Unset): Default: ''. + mode (ExtractMode | Unset): Default: 'form'. + output_mode (ExtractOutputMode | Unset): Default: 'layout_preserving'. + page_separator (str | Unset): Default: '<<<'. + pages_to_extract (str | Unset): tag (str | Unset): Default: 'default'. - url_query (str | Unset): Default: ''. + url_query (str | Unset): url_in_post (bool | Unset): Default: False. - use_webhook (str | Unset): Default: ''. + use_webhook (str | Unset): watermark_angle_threshold (float | Unset): Default: 25.0. - webhook_metadata (str | Unset): Default: ''. + webhook_metadata (str | Unset): word_confidence_threshold (float | Unset): - body (File): + body (File | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperAccepted + Error | WhisperAccepted """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py index 79b88c2..89480cf 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/highlights.py @@ -6,15 +6,17 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.highlights_response_200 import HighlightsResponse200 from ...types import UNSET, Response, Unset def _get_kwargs( *, - extract_all_lines: str | Unset = "false", - lines: str | Unset = "", - whisper_hash: str | Unset = "", + extract_all_lines: bool | Unset = False, + lines: str, + mode: str | Unset = "form", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -23,6 +25,8 @@ def _get_kwargs( params["lines"] = lines + params["mode"] = mode + params["whisper_hash"] = whisper_hash params = {k: v for k, v in params.items() if v is not UNSET and v is not None} @@ -36,12 +40,34 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> HighlightsResponse200 | None: +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Error | HighlightsResponse200 | None: if response.status_code == 200: response_200 = HighlightsResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -50,7 +76,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[HighlightsResponse200]: +) -> Response[Error | HighlightsResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -62,27 +88,30 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - extract_all_lines: str | Unset = "false", - lines: str | Unset = "", - whisper_hash: str | Unset = "", -) -> Response[HighlightsResponse200]: + extract_all_lines: bool | Unset = False, + lines: str, + mode: str | Unset = "form", + whisper_hash: str, +) -> Response[Error | HighlightsResponse200]: """Line-level highlight geometry for an extraction Args: - extract_all_lines (str | Unset): Default: 'false'. - lines (str | Unset): Default: ''. - whisper_hash (str | Unset): Default: ''. + extract_all_lines (bool | Unset): Default: False. + lines (str): + mode (str | Unset): Default: 'form'. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[HighlightsResponse200] + Response[Error | HighlightsResponse200] """ kwargs = _get_kwargs( extract_all_lines=extract_all_lines, lines=lines, + mode=mode, whisper_hash=whisper_hash, ) @@ -96,28 +125,31 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - extract_all_lines: str | Unset = "false", - lines: str | Unset = "", - whisper_hash: str | Unset = "", -) -> HighlightsResponse200 | None: + extract_all_lines: bool | Unset = False, + lines: str, + mode: str | Unset = "form", + whisper_hash: str, +) -> Error | HighlightsResponse200 | None: """Line-level highlight geometry for an extraction Args: - extract_all_lines (str | Unset): Default: 'false'. - lines (str | Unset): Default: ''. - whisper_hash (str | Unset): Default: ''. + extract_all_lines (bool | Unset): Default: False. + lines (str): + mode (str | Unset): Default: 'form'. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - HighlightsResponse200 + Error | HighlightsResponse200 """ return sync_detailed( client=client, extract_all_lines=extract_all_lines, lines=lines, + mode=mode, whisper_hash=whisper_hash, ).parsed @@ -125,27 +157,30 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - extract_all_lines: str | Unset = "false", - lines: str | Unset = "", - whisper_hash: str | Unset = "", -) -> Response[HighlightsResponse200]: + extract_all_lines: bool | Unset = False, + lines: str, + mode: str | Unset = "form", + whisper_hash: str, +) -> Response[Error | HighlightsResponse200]: """Line-level highlight geometry for an extraction Args: - extract_all_lines (str | Unset): Default: 'false'. - lines (str | Unset): Default: ''. - whisper_hash (str | Unset): Default: ''. + extract_all_lines (bool | Unset): Default: False. + lines (str): + mode (str | Unset): Default: 'form'. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[HighlightsResponse200] + Response[Error | HighlightsResponse200] """ kwargs = _get_kwargs( extract_all_lines=extract_all_lines, lines=lines, + mode=mode, whisper_hash=whisper_hash, ) @@ -157,29 +192,32 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - extract_all_lines: str | Unset = "false", - lines: str | Unset = "", - whisper_hash: str | Unset = "", -) -> HighlightsResponse200 | None: + extract_all_lines: bool | Unset = False, + lines: str, + mode: str | Unset = "form", + whisper_hash: str, +) -> Error | HighlightsResponse200 | None: """Line-level highlight geometry for an extraction Args: - extract_all_lines (str | Unset): Default: 'false'. - lines (str | Unset): Default: ''. - whisper_hash (str | Unset): Default: ''. + extract_all_lines (bool | Unset): Default: False. + lines (str): + mode (str | Unset): Default: 'form'. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - HighlightsResponse200 + Error | HighlightsResponse200 """ return ( await asyncio_detailed( client=client, extract_all_lines=extract_all_lines, lines=lines, + mode=mode, whisper_hash=whisper_hash, ) ).parsed diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py index a8177e5..99d198b 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/retrieve.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.whisper_result import WhisperResult from ...types import UNSET, Response, Unset @@ -13,7 +14,7 @@ def _get_kwargs( *, text_only: bool | Unset = False, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -33,19 +34,41 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperResult | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | WhisperResult | None: if response.status_code == 200: response_200 = WhisperResult.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperResult]: +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Error | WhisperResult]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,20 +81,20 @@ def sync_detailed( *, client: AuthenticatedClient | Client, text_only: bool | Unset = False, - whisper_hash: str | Unset = "", -) -> Response[WhisperResult]: + whisper_hash: str, +) -> Response[Error | WhisperResult]: """Retrieve extraction result (destructive — one shot) Args: text_only (bool | Unset): Default: False. - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperResult] + Response[Error | WhisperResult] """ kwargs = _get_kwargs( text_only=text_only, @@ -89,20 +112,20 @@ def sync( *, client: AuthenticatedClient | Client, text_only: bool | Unset = False, - whisper_hash: str | Unset = "", -) -> WhisperResult | None: + whisper_hash: str, +) -> Error | WhisperResult | None: """Retrieve extraction result (destructive — one shot) Args: text_only (bool | Unset): Default: False. - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperResult + Error | WhisperResult """ return sync_detailed( client=client, @@ -115,20 +138,20 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, text_only: bool | Unset = False, - whisper_hash: str | Unset = "", -) -> Response[WhisperResult]: + whisper_hash: str, +) -> Response[Error | WhisperResult]: """Retrieve extraction result (destructive — one shot) Args: text_only (bool | Unset): Default: False. - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperResult] + Response[Error | WhisperResult] """ kwargs = _get_kwargs( text_only=text_only, @@ -144,20 +167,20 @@ async def asyncio( *, client: AuthenticatedClient | Client, text_only: bool | Unset = False, - whisper_hash: str | Unset = "", -) -> WhisperResult | None: + whisper_hash: str, +) -> Error | WhisperResult | None: """Retrieve extraction result (destructive — one shot) Args: text_only (bool | Unset): Default: False. - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperResult + Error | WhisperResult """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py index 554b70b..f1eaa61 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/api/whisper/status.py @@ -6,13 +6,14 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.error import Error from ...models.whisper_status import WhisperStatus -from ...types import UNSET, Response, Unset +from ...types import UNSET, Response def _get_kwargs( *, - whisper_hash: str | Unset = "", + whisper_hash: str, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -30,19 +31,41 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> WhisperStatus | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | WhisperStatus | None: if response.status_code == 200: response_200 = WhisperStatus.from_dict(response.json()) return response_200 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = Error.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[WhisperStatus]: +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Error | WhisperStatus]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -54,19 +77,19 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[WhisperStatus]: + whisper_hash: str, +) -> Response[Error | WhisperStatus]: """Poll extraction status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperStatus] + Response[Error | WhisperStatus] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -82,19 +105,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> WhisperStatus | None: + whisper_hash: str, +) -> Error | WhisperStatus | None: """Poll extraction status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperStatus + Error | WhisperStatus """ return sync_detailed( client=client, @@ -105,19 +128,19 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> Response[WhisperStatus]: + whisper_hash: str, +) -> Response[Error | WhisperStatus]: """Poll extraction status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[WhisperStatus] + Response[Error | WhisperStatus] """ kwargs = _get_kwargs( whisper_hash=whisper_hash, @@ -131,19 +154,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - whisper_hash: str | Unset = "", -) -> WhisperStatus | None: + whisper_hash: str, +) -> Error | WhisperStatus | None: """Poll extraction status Args: - whisper_hash (str | Unset): Default: ''. + whisper_hash (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - WhisperStatus + Error | WhisperStatus """ return ( await asyncio_detailed( diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py index 0465011..4135b36 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/__init__.py @@ -1,14 +1,13 @@ # Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. """Contains all the data models used in inputs/outputs""" -from .convert_to_pdf_response_200 import ConvertToPdfResponse200 -from .convert_xlsb_to_xlsx_response_200 import ConvertXlsbToXlsxResponse200 from .detail_response_200 import DetailResponse200 -from .document_insights_response_200 import DocumentInsightsResponse200 from .document_insights_retrieve_response_200 import DocumentInsightsRetrieveResponse200 +from .error import Error +from .extract_line_splitter_strategy import ExtractLineSplitterStrategy +from .extract_mode import ExtractMode +from .extract_output_mode import ExtractOutputMode from .highlights_response_200 import HighlightsResponse200 -from .pdf_to_images_response_200 import PdfToImagesResponse200 -from .pdf_to_images_retrieve_response_200 import PdfToImagesRetrieveResponse200 from .pdf_to_images_status_response_200 import PdfToImagesStatusResponse200 from .test_connection_response_200 import TestConnectionResponse200 from .usage_info_response_200 import UsageInfoResponse200 @@ -16,23 +15,25 @@ from .webhook_config import WebhookConfig from .webhook_delete_response_200 import WebhookDeleteResponse200 from .webhook_get_response_200 import WebhookGetResponse200 -from .webhook_post_response_200 import WebhookPostResponse200 +from .webhook_post_response_201 import WebhookPostResponse201 from .webhook_put_response_200 import WebhookPutResponse200 from .whisper_accepted import WhisperAccepted from .whisper_result import WhisperResult from .whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem +from .whisper_result_line_metadata_item import WhisperResultLineMetadataItem from .whisper_result_metadata import WhisperResultMetadata +from .whisper_result_whisper_metadata import WhisperResultWhisperMetadata from .whisper_status import WhisperStatus +from .whisper_status_detail_item import WhisperStatusDetailItem __all__ = ( - "ConvertToPdfResponse200", - "ConvertXlsbToXlsxResponse200", "DetailResponse200", - "DocumentInsightsResponse200", "DocumentInsightsRetrieveResponse200", + "Error", + "ExtractLineSplitterStrategy", + "ExtractMode", + "ExtractOutputMode", "HighlightsResponse200", - "PdfToImagesResponse200", - "PdfToImagesRetrieveResponse200", "PdfToImagesStatusResponse200", "TestConnectionResponse200", "UsageInfoResponse200", @@ -40,11 +41,14 @@ "WebhookConfig", "WebhookDeleteResponse200", "WebhookGetResponse200", - "WebhookPostResponse200", + "WebhookPostResponse201", "WebhookPutResponse200", "WhisperAccepted", "WhisperResult", "WhisperResultConfidenceMetadataItem", + "WhisperResultLineMetadataItem", "WhisperResultMetadata", + "WhisperResultWhisperMetadata", "WhisperStatus", + "WhisperStatusDetailItem", ) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/error.py similarity index 70% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/models/error.py index 2f09307..bdd2e8b 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_to_pdf_response_200.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/error.py @@ -7,29 +7,43 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="ConvertToPdfResponse200") +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Error") @_attrs_define -class ConvertToPdfResponse200: - """ """ +class Error: + """ + Attributes: + message (str | Unset): + """ + message: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + message = self.message field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) + field_dict.update({}) + if message is not UNSET: + field_dict["message"] = message return field_dict @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) - convert_to_pdf_response_200 = cls() + message = d.pop("message", UNSET) + + error = cls( + message=message, + ) - convert_to_pdf_response_200.additional_properties = d - return convert_to_pdf_response_200 + error.additional_properties = d + return error @property def additional_keys(self) -> list[str]: diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_line_splitter_strategy.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_line_splitter_strategy.py new file mode 100644 index 0000000..c62c229 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_line_splitter_strategy.py @@ -0,0 +1,16 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from typing import Literal + +ExtractLineSplitterStrategy = Literal["left-priority", "mid-priority", "right-priority"] + +EXTRACT_LINE_SPLITTER_STRATEGY_VALUES: set[ExtractLineSplitterStrategy] = { + "left-priority", + "mid-priority", + "right-priority", +} + + +def check_extract_line_splitter_strategy(value: str) -> ExtractLineSplitterStrategy: + if value in EXTRACT_LINE_SPLITTER_STRATEGY_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {EXTRACT_LINE_SPLITTER_STRATEGY_VALUES!r}") diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_mode.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_mode.py new file mode 100644 index 0000000..c6a22a2 --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_mode.py @@ -0,0 +1,23 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from typing import Literal + +ExtractMode = Literal[ + "document_insights", "excel", "form", "high_quality", "low_cost", "native_text", "pdf_to_images", "table" +] + +EXTRACT_MODE_VALUES: set[ExtractMode] = { + "document_insights", + "excel", + "form", + "high_quality", + "low_cost", + "native_text", + "pdf_to_images", + "table", +} + + +def check_extract_mode(value: str) -> ExtractMode: + if value in EXTRACT_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {EXTRACT_MODE_VALUES!r}") diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_output_mode.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_output_mode.py new file mode 100644 index 0000000..87757bc --- /dev/null +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/extract_output_mode.py @@ -0,0 +1,17 @@ +# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. +from typing import Literal + +ExtractOutputMode = Literal["dump-text", "layout_preserving", "line-printer", "text"] + +EXTRACT_OUTPUT_MODE_VALUES: set[ExtractOutputMode] = { + "dump-text", + "layout_preserving", + "line-printer", + "text", +} + + +def check_extract_output_mode(value: str) -> ExtractOutputMode: + if value in EXTRACT_OUTPUT_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {EXTRACT_OUTPUT_MODE_VALUES!r}") diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py deleted file mode 100644 index 270e203..0000000 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_retrieve_response_200.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by tools/gen_sdk.sh from specs/llmwhisperer.json. DO NOT EDIT. -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, Self, TypeVar - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -T = TypeVar("T", bound="PdfToImagesRetrieveResponse200") - - -@_attrs_define -class PdfToImagesRetrieveResponse200: - """ """ - - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - def to_dict(self) -> dict[str, Any]: - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - - return field_dict - - @classmethod - def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: - d = dict(src_dict) - pdf_to_images_retrieve_response_200 = cls() - - pdf_to_images_retrieve_response_200.additional_properties = d - return pdf_to_images_retrieve_response_200 - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_201.py similarity index 83% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_201.py index 171dc1d..1c70203 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_200.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/webhook_post_response_201.py @@ -7,11 +7,11 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="WebhookPostResponse200") +T = TypeVar("T", bound="WebhookPostResponse201") @_attrs_define -class WebhookPostResponse200: +class WebhookPostResponse201: """ """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -26,10 +26,10 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) - webhook_post_response_200 = cls() + webhook_post_response_201 = cls() - webhook_post_response_200.additional_properties = d - return webhook_post_response_200 + webhook_post_response_201.additional_properties = d + return webhook_post_response_201 @property def additional_keys(self) -> list[str]: diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py index 12d6445..66128da 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result.py @@ -11,7 +11,9 @@ if TYPE_CHECKING: from ..models.whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem + from ..models.whisper_result_line_metadata_item import WhisperResultLineMetadataItem from ..models.whisper_result_metadata import WhisperResultMetadata + from ..models.whisper_result_whisper_metadata import WhisperResultWhisperMetadata T = TypeVar("T", bound="WhisperResult") @@ -22,15 +24,19 @@ class WhisperResult: """ Attributes: confidence_metadata (list[WhisperResultConfidenceMetadataItem] | Unset): + line_metadata (list[WhisperResultLineMetadataItem] | Unset): metadata (WhisperResultMetadata | Unset): result_text (str | Unset): webhook_metadata (str | Unset): + whisper_metadata (WhisperResultWhisperMetadata | Unset): """ confidence_metadata: list[WhisperResultConfidenceMetadataItem] | Unset = UNSET + line_metadata: list[WhisperResultLineMetadataItem] | Unset = UNSET metadata: WhisperResultMetadata | Unset = UNSET result_text: str | Unset = UNSET webhook_metadata: str | Unset = UNSET + whisper_metadata: WhisperResultWhisperMetadata | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -41,6 +47,13 @@ def to_dict(self) -> dict[str, Any]: confidence_metadata_item = confidence_metadata_item_data.to_dict() confidence_metadata.append(confidence_metadata_item) + line_metadata: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.line_metadata, Unset): + line_metadata = [] + for line_metadata_item_data in self.line_metadata: + line_metadata_item = line_metadata_item_data.to_dict() + line_metadata.append(line_metadata_item) + metadata: dict[str, Any] | Unset = UNSET if not isinstance(self.metadata, Unset): metadata = self.metadata.to_dict() @@ -49,24 +62,34 @@ def to_dict(self) -> dict[str, Any]: webhook_metadata = self.webhook_metadata + whisper_metadata: dict[str, Any] | Unset = UNSET + if not isinstance(self.whisper_metadata, Unset): + whisper_metadata = self.whisper_metadata.to_dict() + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) if confidence_metadata is not UNSET: field_dict["confidence_metadata"] = confidence_metadata + if line_metadata is not UNSET: + field_dict["line_metadata"] = line_metadata if metadata is not UNSET: field_dict["metadata"] = metadata if result_text is not UNSET: field_dict["result_text"] = result_text if webhook_metadata is not UNSET: field_dict["webhook_metadata"] = webhook_metadata + if whisper_metadata is not UNSET: + field_dict["whisper_metadata"] = whisper_metadata return field_dict @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: from ..models.whisper_result_confidence_metadata_item import WhisperResultConfidenceMetadataItem + from ..models.whisper_result_line_metadata_item import WhisperResultLineMetadataItem from ..models.whisper_result_metadata import WhisperResultMetadata + from ..models.whisper_result_whisper_metadata import WhisperResultWhisperMetadata d = dict(src_dict) _confidence_metadata = d.pop("confidence_metadata", UNSET) @@ -78,6 +101,15 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: confidence_metadata.append(confidence_metadata_item) + _line_metadata = d.pop("line_metadata", UNSET) + line_metadata: list[WhisperResultLineMetadataItem] | Unset = UNSET + if _line_metadata is not UNSET: + line_metadata = [] + for line_metadata_item_data in _line_metadata: + line_metadata_item = WhisperResultLineMetadataItem.from_dict(line_metadata_item_data) + + line_metadata.append(line_metadata_item) + _metadata = d.pop("metadata", UNSET) metadata: WhisperResultMetadata | Unset if isinstance(_metadata, Unset): @@ -89,11 +121,20 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: webhook_metadata = d.pop("webhook_metadata", UNSET) + _whisper_metadata = d.pop("whisper_metadata", UNSET) + whisper_metadata: WhisperResultWhisperMetadata | Unset + if isinstance(_whisper_metadata, Unset): + whisper_metadata = UNSET + else: + whisper_metadata = WhisperResultWhisperMetadata.from_dict(_whisper_metadata) + whisper_result = cls( confidence_metadata=confidence_metadata, + line_metadata=line_metadata, metadata=metadata, result_text=result_text, webhook_metadata=webhook_metadata, + whisper_metadata=whisper_metadata, ) whisper_result.additional_properties = d diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_line_metadata_item.py similarity index 81% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_line_metadata_item.py index 0211b28..2e65a1e 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/convert_xlsb_to_xlsx_response_200.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_line_metadata_item.py @@ -7,11 +7,11 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="ConvertXlsbToXlsxResponse200") +T = TypeVar("T", bound="WhisperResultLineMetadataItem") @_attrs_define -class ConvertXlsbToXlsxResponse200: +class WhisperResultLineMetadataItem: """ """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -26,10 +26,10 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) - convert_xlsb_to_xlsx_response_200 = cls() + whisper_result_line_metadata_item = cls() - convert_xlsb_to_xlsx_response_200.additional_properties = d - return convert_xlsb_to_xlsx_response_200 + whisper_result_line_metadata_item.additional_properties = d + return whisper_result_line_metadata_item @property def additional_keys(self) -> list[str]: diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_whisper_metadata.py similarity index 82% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_whisper_metadata.py index a0c02ed..a7f1712 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/document_insights_response_200.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_result_whisper_metadata.py @@ -7,11 +7,11 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="DocumentInsightsResponse200") +T = TypeVar("T", bound="WhisperResultWhisperMetadata") @_attrs_define -class DocumentInsightsResponse200: +class WhisperResultWhisperMetadata: """ """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -26,10 +26,10 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) - document_insights_response_200 = cls() + whisper_result_whisper_metadata = cls() - document_insights_response_200.additional_properties = d - return document_insights_response_200 + whisper_result_whisper_metadata.additional_properties = d + return whisper_result_whisper_metadata @property def additional_keys(self) -> list[str]: diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py index 7a92c6b..b701e82 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status.py @@ -2,13 +2,17 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Self, TypeVar +from typing import TYPE_CHECKING, Any, Self, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.whisper_status_detail_item import WhisperStatusDetailItem + + T = TypeVar("T", bound="WhisperStatus") @@ -16,15 +20,24 @@ class WhisperStatus: """ Attributes: + detail (list[WhisperStatusDetailItem] | Unset): message (str | Unset): status (str | Unset): """ + detail: list[WhisperStatusDetailItem] | Unset = UNSET message: str | Unset = UNSET status: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + detail: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.detail, Unset): + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() + detail.append(detail_item) + message = self.message status = self.status @@ -32,6 +45,8 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) + if detail is not UNSET: + field_dict["detail"] = detail if message is not UNSET: field_dict["message"] = message if status is not UNSET: @@ -41,12 +56,24 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.whisper_status_detail_item import WhisperStatusDetailItem + d = dict(src_dict) + _detail = d.pop("detail", UNSET) + detail: list[WhisperStatusDetailItem] | Unset = UNSET + if _detail is not UNSET: + detail = [] + for detail_item_data in _detail: + detail_item = WhisperStatusDetailItem.from_dict(detail_item_data) + + detail.append(detail_item) + message = d.pop("message", UNSET) status = d.pop("status", UNSET) whisper_status = cls( + detail=detail, message=message, status=status, ) diff --git a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status_detail_item.py similarity index 83% rename from src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py rename to src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status_detail_item.py index 39626d3..420f295 100644 --- a/src/unstract/llmwhisperer/sdk_llmwhisperer/models/pdf_to_images_response_200.py +++ b/src/unstract/llmwhisperer/sdk_llmwhisperer/models/whisper_status_detail_item.py @@ -7,11 +7,11 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="PdfToImagesResponse200") +T = TypeVar("T", bound="WhisperStatusDetailItem") @_attrs_define -class PdfToImagesResponse200: +class WhisperStatusDetailItem: """ """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -26,10 +26,10 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) - pdf_to_images_response_200 = cls() + whisper_status_detail_item = cls() - pdf_to_images_response_200.additional_properties = d - return pdf_to_images_response_200 + whisper_status_detail_item.additional_properties = d + return whisper_status_detail_item @property def additional_keys(self) -> list[str]: diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index 7ba7dda..848418a 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -624,6 +624,10 @@ def test_every_wrapped_operation_is_covered() -> None: for method, operation in path.items() if method in {"get", "post", "put", "patch", "delete"} } + # Checked before the subtraction: an entry excusing an operation the spec no + # longer declares keeps this passing forever, and nothing about a green run + # says the list is still describing anything. + assert UNWRAPPED_OPERATIONS <= declared assert declared - UNWRAPPED_OPERATIONS == set(_SEND_ONLY) diff --git a/tools/gen_sdk.sh b/tools/gen_sdk.sh index 8ce6e9a..889eea1 100755 --- a/tools/gen_sdk.sh +++ b/tools/gen_sdk.sh @@ -28,9 +28,19 @@ if [ "$have" != "$want" ]; then fi rm -rf "${REPO:?}/$OUT" +log="$(mktemp)" +trap 'rm -f "$log"' EXIT (cd "$REPO" && "$VENV/bin/openapi-python-client" generate \ --path "$REPO/specs/llmwhisperer.json" --output-path "$REPO/$OUT" \ - --config "$REPO/tools/openapi-client.yaml" --overwrite --meta none) + --config "$REPO/tools/openapi-client.yaml" --overwrite --meta none) 2>&1 | tee "$log" + +# The generator downgrades a schema it cannot parse to a warning, drops +# the endpoint or model it belongs to, writes the rest and exits 0. The +# result is a client missing an operation and a spec that still looks fine. +if grep -qi warning "$log"; then + echo "the generator reported a problem above and still exited 0; whatever it could not parse is missing from the output" >&2 + exit 1 +fi # Stamp every file, so the rule survives contact with a reader who arrived via # grep rather than via this script. diff --git a/tools/openapi-client.yaml b/tools/openapi-client.yaml index d2196b9..f9741b8 100644 --- a/tools/openapi-client.yaml +++ b/tools/openapi-client.yaml @@ -1,3 +1,12 @@ # openapi-python-client config. Kept minimal on purpose: every knob here is # maintenance surface, and post-processing the generated code is a kill criterion. literal_enums: true + +# The service returns these as bytes. The generator knows how to hand back a +# binary body but only recognises a handful of content types by name, and a +# response it cannot parse is dropped rather than reported. Mapping them here +# keeps the spec truthful about what comes back. +content_type_overrides: + application/pdf: application/octet-stream + application/zip: application/octet-stream + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: application/octet-stream From 8457ee5eafa8097da7b21e4d73a8dcdf99a15f0a Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 20:43:48 +0530 Subject: [PATCH 05/16] docs: note the service version a custom page separator needs The parameter was renamed server-side, and a service older than v2.64.2 reads only the previous spelling: the separator silently falls back to the default instead of failing, which is the kind of thing a caller finds in the output rather than in an error. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7249953..135ef2d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ This package provides **LLMWhispererClientV2**, the client for LLMWhisperer API Documentation is available [here](https://docs.unstract.com/llmwhisperer/). +### Service version note + +A custom `page_separator` needs LLMWhisperer **v2.64.2 or later**. The query +parameter was renamed in that release; an older service reads only the previous +spelling, so it falls back to the default `<<<` separator and reports no error. +Check the service version before relying on a custom separator against a +self-hosted deployment. + ## Running Tests Install test dependencies and run all tests: From 13fd71eceef03b80f6ea4a5abd82e0cad2a418fb Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 21:12:05 +0530 Subject: [PATCH 06/16] fix(client): restore the transport's own request headers Two unrelated drifts under the same seam. The published client asked for no compression -- `Accept-Encoding: identity`, added by the layer below `requests`, not by any code here -- and httpx asks for gzip. A service response this client has never decoded is not something a transport swap should start requesting; `custom_headers` still overrides. Three httpx failures also reached callers as httpx classes, which nothing downstream catches: a redirect loop, an undecodable body, and any future RequestError that is not a TransportError. Two more mapped to a class the published client never raised for them, since requests had no write or pool timeout. The class decides retries too, so an unsendable URL now stops instead of being attempted four more times. Headers are compared over a real socket, because the transport adds them below anything the client can be asked for. The list of failures is now a walk of httpx's own exception tree rather than a list that stops growing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- src/unstract/llmwhisperer/client_v2.py | 34 ++++-- tests/unit/compat_test.py | 137 ++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index e92d0c0..05ed9e2 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -101,13 +101,21 @@ } +#: Headers the previous transport put on the wire without being asked, which +#: httpx spells differently. Only `Accept-Encoding` is load-bearing: the +#: previous one asked for no compression, so a service that gzips its response +#: was never exercised against this client. Overridable via `custom_headers`. +_TRANSPORT_HEADERS = {"Accept-Encoding": "identity"} + + def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: """Re-raise httpx transport failures as their ``requests`` equivalents. - Callers document and catch the ``requests`` classes. Ordering matters: - ``TimeoutException`` must be checked before ``ConnectError``, and - ``TransportError`` is the catch-all that keeps a novel transport failure from - escaping untranslated. + Callers document and catch the ``requests`` classes, and the retry policy + keys off them too, so the class chosen here decides whether a failure is + retried. Every branch is ordered before the base class it derives from, and + ``RequestError`` is the catch-all that keeps a novel failure from escaping + untranslated. """ try: return fn(*args, **kwargs) @@ -117,11 +125,25 @@ def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: raise requests.ConnectTimeout(str(e)) from e except httpx.ReadTimeout as e: raise requests.ReadTimeout(str(e)) from e + except (httpx.WriteTimeout, httpx.PoolTimeout) as e: + # Neither had a Timeout equivalent: a send that failed and a pool that + # could not hand out a connection both surfaced as ConnectionError. + raise requests.ConnectionError(str(e)) from e except httpx.TimeoutException as e: raise requests.Timeout(str(e)) from e + except httpx.UnsupportedProtocol as e: + # A URL rejected before any socket is opened. Deliberately not a + # ConnectionError: retrying a malformed URL cannot start working. + raise requests.exceptions.MissingSchema(str(e)) from e + except httpx.ProxyError as e: + raise requests.exceptions.ProxyError(str(e)) from e except httpx.ConnectError as e: raise requests.ConnectionError(str(e)) from e - except httpx.TransportError as e: + except httpx.TooManyRedirects as e: + raise requests.TooManyRedirects(str(e)) from e + except httpx.DecodingError as e: + raise requests.exceptions.ContentDecodingError(str(e)) from e + except httpx.RequestError as e: raise requests.ConnectionError(str(e)) from e @@ -272,7 +294,7 @@ def _transport(self) -> httpx.Client: """ if getattr(self, "_transport_client", None) is None: self._transport_client = httpx.Client( - headers=self.headers, + headers={**_TRANSPORT_HEADERS, **self.headers}, follow_redirects=True, timeout=httpx.Timeout(None), ) diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index 848418a..5341dca 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -16,6 +16,8 @@ import inspect import io import json +import socket +import threading import time from collections.abc import Callable from pathlib import Path @@ -199,6 +201,87 @@ def test_the_auth_header_is_unchanged(sample_file: str) -> None: assert ours.headers["unstract-key"] == theirs.headers["unstract-key"] == "test-key" +def _wire_heads(*calls: Callable[[str], Any]) -> list[dict[str, str]]: + """Run each call against a loopback server and return its request headers. + + Below the client, the transport adds headers of its own -- and drops none + of them into any object the client can be asked for. A socket is the only + place both clients can be compared on what they actually send. One server + serves every call, so the `Host` header is the same for all of them. + """ + heads: list[bytes] = [] + server = socket.socket() + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("127.0.0.1", 0)) + server.listen(len(calls)) + + def serve() -> None: + for _ in calls: + conn, _address = server.accept() + data = b"" + while b"\r\n\r\n" not in data: + chunk = conn.recv(65536) + if not chunk: + break + data += chunk + heads.append(data.split(b"\r\n\r\n")[0]) + body = b'{"ok":true}' + conn.sendall( + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" + b"Content-Length: %d\r\n\r\n%s" % (len(body), body) + ) + conn.close() + + thread = threading.Thread(target=serve, daemon=True) + thread.start() + try: + url = f"http://127.0.0.1:{server.getsockname()[1]}/api/v2" + for call in calls: + call(url) + finally: + thread.join(timeout=10) + server.close() + + return [ + { + name.lower(): value.strip() + for name, _, value in ( + line.partition(":") for line in head.decode().split("\r\n")[1:] + ) + } + for head in heads + ] + + +def test_wire_headers_match_the_published_client() -> None: + """`Accept-Encoding` is the load-bearing one: the published client asked + for no compression, so a response this client has never seen decoded is + not something a transport swap should start requesting.""" + ours, theirs = _wire_heads( + lambda url: _client(base_url=url).get_usage_info(), + lambda url: _baseline_client(base_url=url).get_usage_info(), + ) + + assert theirs["accept-encoding"] == "identity" + assert {name: ours[name] for name in theirs if name != "user-agent"} == { + name: value for name, value in theirs.items() if name != "user-agent" + } + # The two httpx adds mean what their absence meant: `*/*` is the default + # Accept and keep-alive the default in HTTP/1.1. It also names itself, + # which is the one value that changes. + assert ours.keys() - theirs.keys() == {"accept", "connection"} + assert ours["user-agent"].startswith("python-httpx/") + + +def test_custom_headers_override_the_transport_defaults() -> None: + (ours,) = _wire_heads( + lambda url: _client( + base_url=url, custom_headers={"Accept-Encoding": "gzip"} + ).get_usage_info() + ) + assert ours["accept-encoding"] == "gzip" + + def test_custom_headers_still_reach_the_request() -> None: client = _client(custom_headers={"x-trace": "abc", "unstract-key": "override"}) with patch.object(LLMWhispererClientV2, "_send", return_value=_mock_response()) as send: @@ -384,13 +467,18 @@ def run(client: Any, patch_target: Any) -> Any: [ (httpx.ConnectTimeout("connect timed out"), requests.ConnectTimeout), (httpx.ReadTimeout("read timed out"), requests.ReadTimeout), - (httpx.WriteTimeout("write timed out"), requests.Timeout), - (httpx.PoolTimeout("pool timed out"), requests.Timeout), + # Neither had a Timeout equivalent: a send that failed and a pool that + # could not hand out a connection both surfaced as ConnectionError. + (httpx.WriteTimeout("write timed out"), requests.ConnectionError), + (httpx.PoolTimeout("pool timed out"), requests.ConnectionError), (httpx.ConnectError("refused"), requests.ConnectionError), (httpx.ReadError("reset"), requests.ConnectionError), (httpx.WriteError("broken pipe"), requests.ConnectionError), (httpx.ProtocolError("bad framing"), requests.ConnectionError), - (httpx.ProxyError("proxy exploded"), requests.ConnectionError), + (httpx.ProxyError("proxy exploded"), requests.exceptions.ProxyError), + (httpx.UnsupportedProtocol("no scheme"), requests.exceptions.MissingSchema), + (httpx.TooManyRedirects("looping"), requests.TooManyRedirects), + (httpx.DecodingError("bad gzip"), requests.exceptions.ContentDecodingError), ], ) def test_transport_errors_are_translated(raised: Exception, expected: type[Exception]) -> None: @@ -408,6 +496,49 @@ def test_transport_errors_are_translated(raised: Exception, expected: type[Excep assert type(caught.value) is expected +def _httpx_request_errors() -> list[type[Exception]]: + """Every httpx request failure, discovered rather than listed. + + A hand-written list is exactly as complete as it was the day it was + written; this one grows when httpx does. + """ + found, stack = [], [httpx.RequestError] + while stack: + cls = stack.pop() + found.append(cls) + stack.extend(cls.__subclasses__()) + return sorted(found, key=lambda cls: cls.__name__) + + +@pytest.mark.parametrize("cls", _httpx_request_errors(), ids=lambda cls: cls.__name__) +def test_no_httpx_failure_escapes_untranslated(cls: type[Exception]) -> None: + """An httpx class reaching a caller is a class no caller catches.""" + client = _client() + with patch.object(client._transport, "send", side_effect=cls("boom")): + with pytest.raises(requests.RequestException): + client.get_usage_info() + + +@pytest.mark.parametrize( + ("raised", "retried"), + [ + (httpx.PoolTimeout("pool timed out"), True), + (httpx.ProxyError("proxy exploded"), True), + # Retrying these cannot start working: the URL stays malformed, the + # redirect chain stays a loop, the body stays undecodable. + (httpx.UnsupportedProtocol("no scheme"), False), + (httpx.TooManyRedirects("looping"), False), + (httpx.DecodingError("bad gzip"), False), + ], +) +def test_translation_decides_what_gets_retried(raised: Exception, retried: bool) -> None: + client = _client(max_retries=2, retry_min_wait=0, retry_max_wait=0) + with patch.object(client._transport, "send", side_effect=raised) as send: + with pytest.raises(requests.RequestException): + client.get_usage_info() + assert (send.call_count > 1) is retried + + def test_a_connect_timeout_is_still_a_connection_error() -> None: """``requests.ConnectTimeout`` is both a ``ConnectionError`` and a ``Timeout``. From 47072cca4af4038cd7a4313e4fbd476178a3dd61 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 21:14:22 +0530 Subject: [PATCH 07/16] ci: fail when the committed SDK is not what the spec generates The generated tree is committed, so an edit inside it reviews like any other change and then vanishes on the next regeneration -- as does a spec change nobody ran the generator over. Regenerating in CI and diffing is what notices either one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- .github/workflows/ci_test.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ci_test.yaml b/.github/workflows/ci_test.yaml index c6490a6..7226ab1 100644 --- a/.github/workflows/ci_test.yaml +++ b/.github/workflows/ci_test.yaml @@ -9,6 +9,29 @@ on: branches: [main] jobs: + sdk-drift: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.6.14" + python-version: 3.12.9 + + # The generated tree is committed, so an edit to it reviews like any + # other change and then disappears on the next regeneration. Same for a + # spec change that never had the generator run over it. + - name: Regenerate from the committed spec + run: ./tools/gen_sdk.sh + + - name: Fail if the committed SDK is not what the spec generates + run: git diff --exit-code -- src/unstract/llmwhisperer/sdk_llmwhisperer + test: if: github.event.pull_request.draft == false runs-on: ubuntu-latest From 45664b22bedec1321c11afb0d55a5fb23cf827b0 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 21:50:23 +0530 Subject: [PATCH 08/16] test: compare against the released client, pinned by digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The baseline was a pre-release commit pinned by a version string in its own header comment, which an edit to the file can rewrite as easily as the code below it. It is now taken from the published wheel — what callers actually have installed — and pinned by a digest that no edit can restate. --- .../{client_v2_pr34.py => client_v2_2_8_0.py} | 4 +-- tests/unit/compat_test.py | 14 +++++---- tools/refresh_baseline.sh | 29 ++++++++++++------- 3 files changed, 29 insertions(+), 18 deletions(-) rename tests/baseline/{client_v2_pr34.py => client_v2_2_8_0.py} (99%) diff --git a/tests/baseline/client_v2_pr34.py b/tests/baseline/client_v2_2_8_0.py similarity index 99% rename from tests/baseline/client_v2_pr34.py rename to tests/baseline/client_v2_2_8_0.py index 5c563c0..b1e27d6 100644 --- a/tests/baseline/client_v2_pr34.py +++ b/tests/baseline/client_v2_2_8_0.py @@ -1,5 +1,5 @@ -# Vendored from llm-whisperer-python-client 0e9fda3 (PR #34 head), the parity baseline. -# DO NOT EDIT. Refresh with tools/refresh_baseline.sh when the baseline moves. +# Vendored from the released llmwhisperer-client 2.8.0 wheel on PyPI. DO NOT EDIT. +# Refresh with tools/refresh_baseline.sh when the parity baseline is intentionally moved. """This module provides a Python client for interacting with the LLMWhisperer API. diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index 5341dca..3f51c4b 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -12,6 +12,7 @@ """ import ast +import hashlib import importlib.util import inspect import io @@ -35,8 +36,9 @@ LLMWhispererClientV2, ) -BASELINE_REF = "0e9fda3" -BASELINE_PATH = Path(__file__).parents[1] / "baseline" / "client_v2_pr34.py" +BASELINE_VERSION = "2.8.0" +BASELINE_PATH = Path(__file__).parents[1] / "baseline" / "client_v2_2_8_0.py" +BASELINE_SHA256 = "0c5c60d6c5bd6bab61ac9889b55764f692818badf0a8015f9bbfca022db75d0c" SPEC_PATH = Path(__file__).parents[2] / "specs" / "llmwhisperer.json" Call = Callable[[Any, str], Any] @@ -762,6 +764,8 @@ def test_every_wrapped_operation_is_covered() -> None: assert declared - UNWRAPPED_OPERATIONS == set(_SEND_ONLY) -def test_the_baseline_is_pinned() -> None: - assert BASELINE_REF in BASELINE_PATH.read_text(encoding="utf-8").splitlines()[0] - assert "DO NOT EDIT" in BASELINE_PATH.read_text(encoding="utf-8") +def test_the_baseline_is_the_released_client_unmodified() -> None: + # A digest, not a version string in a comment: an edited baseline can claim + # any provenance it likes, and every parity test here would still pass. + assert BASELINE_PATH.name == f"client_v2_{BASELINE_VERSION.replace('.', '_')}.py" + assert hashlib.sha256(BASELINE_PATH.read_bytes()).hexdigest() == BASELINE_SHA256 diff --git a/tools/refresh_baseline.sh b/tools/refresh_baseline.sh index c7e75cd..9ee2efa 100755 --- a/tools/refresh_baseline.sh +++ b/tools/refresh_baseline.sh @@ -1,24 +1,31 @@ #!/usr/bin/env bash # Refresh the vendored parity baseline in tests/baseline/. # -# The compat suite compares this client against a fixed published one, not +# The compat suite compares this client against the last RELEASED one, not # against the working tree — a baseline that moves with local edits measures -# nothing. It is vendored rather than resolved at test time so the suite stays -# offline, and refreshing it is a deliberate act with a reviewable diff. +# nothing. It is taken from the published wheel rather than from a git ref +# because the wheel is what callers actually have installed, and it is vendored +# rather than downloaded at test time so the suite stays offline. # -# ./tools/refresh_baseline.sh 0e9fda3 pr34 +# ./tools/refresh_baseline.sh 2.8.0 set -euo pipefail -REF="${1:?usage: refresh_baseline.sh }" -SLUG="${2:?usage: refresh_baseline.sh }" +VERSION="${1:?usage: refresh_baseline.sh }" REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -OUT="$REPO/tests/baseline/client_v2_$SLUG.py" +SLUG="${VERSION//./_}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +(cd "$WORK" && pip download "llmwhisperer-client==$VERSION" --no-deps -q && unzip -o -q ./*.whl -d x) +OUT="$REPO/tests/baseline/client_v2_$SLUG.py" { - echo "# Vendored from llm-whisperer-python-client $REF, the parity baseline." - echo "# DO NOT EDIT. Refresh with tools/refresh_baseline.sh when the baseline moves." - git -C "$REPO" show "$REF:src/unstract/llmwhisperer/client_v2.py" + echo "# Vendored from the released llmwhisperer-client $VERSION wheel on PyPI. DO NOT EDIT." + echo "# Refresh with tools/refresh_baseline.sh when the parity baseline is intentionally moved." + cat "$WORK/x/unstract/llmwhisperer/client_v2.py" } > "$OUT" echo "wrote $OUT" -echo "update BASELINE_REF in tests/test_compat.py to match" +echo "in tests/unit/compat_test.py set:" +echo " BASELINE_VERSION = \"$VERSION\"" +echo " BASELINE_SHA256 = \"$(sha256sum "$OUT" | cut -d' ' -f1)\"" From e663ada138c8e2fc53584f13b72f6c533899b086 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 21:50:23 +0530 Subject: [PATCH 09/16] build: bound httpx to the series the transport is generated against The generated transport is written against one httpx minor series; an upgrade needs a regeneration and a test run, not a resolver decision taken at install time in someone else's environment. --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1af5b1a..920f199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,10 @@ requires-python = ">=3.12" # `requests` remains a dependency for its exception classes: callers catch # ConnectionError and Timeout by name, and the httpx equivalents are not # subclasses of them. -dependencies = ["httpx>=0.27", "attrs>=23.2", "requests>=2", "tenacity>=8.0"] +# httpx is upper-bounded because the generated transport is written against +# one minor series: a bump has to be regenerated and re-tested, not resolved +# into. +dependencies = ["httpx>=0.27,<0.29", "attrs>=23.2", "requests>=2", "tenacity>=8.0"] [dependency-groups] test = [ From 54d376bad408e29ab12c66748161692040d5576e Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 21:52:33 +0530 Subject: [PATCH 10/16] fix(client): treat an explicit None as unset on the optional OCR params A query string carries no null, so a caller passing None got the literal string "None" sent as the value. These are overrides the service defaults when absent, and absent is what None asks for. --- src/unstract/llmwhisperer/client_v2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index 05ed9e2..3e9aa7c 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -725,7 +725,9 @@ def whisper( } # Only what the caller asked for. These have no default here on purpose: # sending one pins a value the service would otherwise choose, and the - # two diverge the moment the service's own default moves. + # two diverge the moment the service's own default moves. ``None`` is + # dropped with ``UNSET``: a query string carries no null, so it would go + # out as the literal string "None". params.update( { name: value @@ -737,7 +739,7 @@ def whisper( ("checkbox_confidence_threshold", checkbox_confidence_threshold), ("min_table_width", min_table_width), ) - if not isinstance(value, Unset) + if not isinstance(value, Unset) and value is not None } ) From 6c942a2b3d0bfa9a510dce2694d798dd32ee81d4 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 22:02:43 +0530 Subject: [PATCH 11/16] chore: make the lint and type checks pass They did not, and had not for some time. Three things were in the way: - ruff and docformatter disagreed about where a multi-line docstring's closing quotes belong, so each run flipped every docstring back and pre-commit could never converge. D209 is now off; docformatter decides. - the pinned hook ran ruff 0.3.4 while the dev group installed 0.11.9, and the two disagree on import order. Both are pinned to one version now. - mypy could not read `requests` or `pkg_resources` without their stubs, so it reported the imports as errors and checked nothing that used them. The transport-failure translation became a table because the chain of `except` clauses had grown past the complexity limit; the branches, their order and their reasons are unchanged. `Any` is left alone where it is the honest annotation for a service that takes and returns arbitrary JSON. --- .pre-commit-config.yaml | 4 +- pyproject.toml | 18 +++++++- src/unstract/llmwhisperer/client_v2.py | 60 +++++++++++++------------- tests/conftest.py | 1 + tests/integration/client_v2_test.py | 51 +++++++++++----------- tests/integration/conftest.py | 1 + tests/unit/client_v2_test.py | 1 + tests/unit/compat_test.py | 24 +++++------ tests/utils_test.py | 1 + uv.lock | 29 ++++++++++++- 10 files changed, 120 insertions(+), 70 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7513d16..5514e72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,9 @@ repos: - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + # Kept in step with the pinned ruff in the dev group: two versions disagree + # on import order and docstrings, and each undoes the other's fixes. + rev: v0.11.9 hooks: - id: ruff args: [--fix] diff --git a/pyproject.toml b/pyproject.toml index 920f199..ec2de8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,10 +39,14 @@ dev = [ "mypy~=1.2.0", "pre-commit~=3.3.1", "yamllint>=1.35.1", - "ruff<1.0.0,>=0.2.2", + "ruff==0.11.9", "pytest>=8.0.1", "pycln>=2.5.0", "poethepoet>=0.34.0", + # Stub-only packages. Without them mypy reports the import itself as an + # error and cannot check any use of what it names. + "types-requests>=2.32", + "types-setuptools>=75", ] [tool.poe.tasks] @@ -116,6 +120,11 @@ select = [ fixable = ["ALL"] ignore = [ "D205", + # docformatter pulls a closing quote back onto the last line and ruff moves + # it off again, so with both enabled every multi-line docstring flips on + # every run and pre-commit never converges. docformatter wins here because + # it is the one that also rewraps. + "D209", "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D102", # Missing docstring in public method @@ -128,6 +137,13 @@ ignore = [ "N818", ] +[tool.ruff.lint.per-file-ignores] +# The client mirrors a service that takes and returns arbitrary JSON, and its +# published signatures say so. Narrowing these annotations would describe an API +# that is not the one callers have. +"src/unstract/llmwhisperer/client_v2.py" = ["ANN401"] +"tests/**" = ["ANN401"] + [tool.ruff.format] quote-style = "double" indent-style = "space" diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index 3e9aa7c..48c2f30 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -34,6 +34,7 @@ import requests import tenacity from tenacity import retry_if_exception, stop_after_attempt, stop_after_delay, wait_exponential_jitter + from unstract.llmwhisperer.sdk_llmwhisperer.api.account import usage_info from unstract.llmwhisperer.sdk_llmwhisperer.api.webhook import ( webhook_delete, @@ -108,43 +109,44 @@ _TRANSPORT_HEADERS = {"Accept-Encoding": "identity"} +#: httpx failure -> the ``requests`` class callers catch. First match wins, so a +#: subclass has to precede the base it derives from, and ``RequestError`` last is +#: what keeps a novel httpx failure from escaping untranslated. +_TRANSLATIONS: tuple[tuple[type[httpx.RequestError], type[Exception]], ...] = ( + # requests.ConnectTimeout is both a ConnectionError and a Timeout; the plain + # Timeout httpx implies would stop matching half the callers. + (httpx.ConnectTimeout, requests.ConnectTimeout), + (httpx.ReadTimeout, requests.ReadTimeout), + # Neither had a Timeout equivalent: a send that failed and a pool that could + # not hand out a connection both surfaced as ConnectionError. + (httpx.WriteTimeout, requests.ConnectionError), + (httpx.PoolTimeout, requests.ConnectionError), + (httpx.TimeoutException, requests.Timeout), + # A URL rejected before any socket is opened. Deliberately not a + # ConnectionError: retrying a malformed URL cannot start working. + (httpx.UnsupportedProtocol, requests.exceptions.MissingSchema), + (httpx.ProxyError, requests.exceptions.ProxyError), + (httpx.ConnectError, requests.ConnectionError), + (httpx.TooManyRedirects, requests.TooManyRedirects), + (httpx.DecodingError, requests.exceptions.ContentDecodingError), + (httpx.RequestError, requests.ConnectionError), +) + + def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: """Re-raise httpx transport failures as their ``requests`` equivalents. Callers document and catch the ``requests`` classes, and the retry policy keys off them too, so the class chosen here decides whether a failure is - retried. Every branch is ordered before the base class it derives from, and - ``RequestError`` is the catch-all that keeps a novel failure from escaping - untranslated. + retried. """ try: return fn(*args, **kwargs) - except httpx.ConnectTimeout as e: - # requests.ConnectTimeout is both a ConnectionError and a Timeout; the - # plain Timeout httpx implies would stop matching half the callers. - raise requests.ConnectTimeout(str(e)) from e - except httpx.ReadTimeout as e: - raise requests.ReadTimeout(str(e)) from e - except (httpx.WriteTimeout, httpx.PoolTimeout) as e: - # Neither had a Timeout equivalent: a send that failed and a pool that - # could not hand out a connection both surfaced as ConnectionError. - raise requests.ConnectionError(str(e)) from e - except httpx.TimeoutException as e: - raise requests.Timeout(str(e)) from e - except httpx.UnsupportedProtocol as e: - # A URL rejected before any socket is opened. Deliberately not a - # ConnectionError: retrying a malformed URL cannot start working. - raise requests.exceptions.MissingSchema(str(e)) from e - except httpx.ProxyError as e: - raise requests.exceptions.ProxyError(str(e)) from e - except httpx.ConnectError as e: - raise requests.ConnectionError(str(e)) from e - except httpx.TooManyRedirects as e: - raise requests.TooManyRedirects(str(e)) from e - except httpx.DecodingError as e: - raise requests.exceptions.ContentDecodingError(str(e)) from e except httpx.RequestError as e: - raise requests.ConnectionError(str(e)) from e + for failure, equivalent in _TRANSLATIONS: + if isinstance(e, failure): + raise equivalent(str(e)) from e + raise def _wire_value(value: Any) -> Any: @@ -576,7 +578,7 @@ def _resolve_deprecated_param( warnings.warn(message, DeprecationWarning, stacklevel=3) return deprecated_value if forward else default - def whisper( + def whisper( # noqa: C901 self, file_path: str = "", stream: IO[bytes] | None = None, diff --git a/tests/conftest.py b/tests/conftest.py index 8eea76e..0ac4a6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import os import pytest + from unstract.llmwhisperer.client_v2 import LLMWhispererClientV2 diff --git a/tests/integration/client_v2_test.py b/tests/integration/client_v2_test.py index 2c2e863..61fdd90 100644 --- a/tests/integration/client_v2_test.py +++ b/tests/integration/client_v2_test.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest + from unstract.llmwhisperer.client_v2 import ( LLMWhispererClientException, LLMWhispererClientV2, @@ -37,9 +38,9 @@ def test_get_usage_info(client_v2: LLMWhispererClientV2) -> None: "today_page_count", "current_page_count_table", ] - assert set(expected_keys).issubset( - usage_info.keys() - ), f"usage_info is missing expected keys: {set(expected_keys) - set(usage_info.keys())}" + assert set(expected_keys).issubset(usage_info.keys()), ( + f"usage_info is missing expected keys: {set(expected_keys) - set(usage_info.keys())}" + ) @pytest.mark.parametrize( @@ -70,7 +71,7 @@ def test_whisper_v2( file_path=file_path, wait_for_completion=True, ) - logger.debug(f"Result for '{output_mode}', '{mode}', " f"'{input_file}: {whisper_result}") + logger.debug(f"Result for '{output_mode}', '{mode}', '{input_file}: {whisper_result}") exp_basename = f"{Path(input_file).stem}.{mode}.{output_mode}.txt" exp_file = os.path.join(data_dir, "expected", exp_basename) @@ -165,7 +166,7 @@ def test_whisper_v2_url_in_post( ) -> None: usage_before = client_v2.get_usage_info() whisper_result = client_v2.whisper(mode=mode, output_mode=output_mode, url=url, wait_for_completion=True) - logger.debug(f"Result for '{output_mode}', '{mode}', " f"'{input_file}: {whisper_result}") + logger.debug(f"Result for '{output_mode}', '{mode}', '{input_file}: {whisper_result}") exp_basename = f"{Path(input_file).stem}.{mode}.{output_mode}.txt" exp_file = os.path.join(data_dir, "expected", exp_basename) @@ -243,7 +244,8 @@ def test_webhook(client_v2: LLMWhispererClientV2, url: str, token: str, webhook_ def test_whisper_detail(client_v2: LLMWhispererClientV2, data_dir: str) -> None: - """Test whisper_detail returns extraction metadata after a whisper operation.""" + """Test whisper_detail returns extraction metadata after a whisper + operation.""" file_path = os.path.join(data_dir, "credit_card.pdf") whisper_result = client_v2.whisper( mode="native_text", @@ -269,9 +271,9 @@ def test_whisper_detail(client_v2: LLMWhispererClientV2, data_dir: str) -> None: "upload_file_size_in_kb", "whisper_hash", ] - assert set(expected_keys).issubset( - detail.keys() - ), f"whisper_detail is missing expected keys: {set(expected_keys) - set(detail.keys())}" + assert set(expected_keys).issubset(detail.keys()), ( + f"whisper_detail is missing expected keys: {set(expected_keys) - set(detail.keys())}" + ) assert detail["mode"] == "native_text" assert detail["processed_pages"] > 0 assert detail["total_pages"] > 0 @@ -287,10 +289,9 @@ def test_whisper_detail_not_found(client_v2: LLMWhispererClientV2) -> None: assert "message" in error -def test_whisper_line_splitter_strategy_reaches_service( - client_v2: LLMWhispererClientV2, data_dir: str -) -> None: - """An unknown strategy is rejected, which only happens if the param arrives.""" +def test_whisper_line_splitter_strategy_reaches_service(client_v2: LLMWhispererClientV2, data_dir: str) -> None: + """An unknown strategy is rejected, which only happens if the param + arrives.""" file_path = os.path.join(data_dir, "credit_card.pdf") with pytest.raises(LLMWhispererClientException) as exc_info: @@ -344,19 +345,19 @@ def assert_extracted_text(file_path: str, whisper_result: dict, mode: str, outpu def verify_usage(before_extract: dict, after_extract: dict, page_count: int, mode: str = "form") -> None: all_modes = ["form", "high_quality", "low_cost", "native_text"] all_modes.remove(mode) - assert ( - after_extract["today_page_count"] == before_extract["today_page_count"] + page_count - ), "today_page_count calculation is wrong" - assert ( - after_extract["current_page_count"] == before_extract["current_page_count"] + page_count - ), "current_page_count calculation is wrong" + assert after_extract["today_page_count"] == before_extract["today_page_count"] + page_count, ( + "today_page_count calculation is wrong" + ) + assert after_extract["current_page_count"] == before_extract["current_page_count"] + page_count, ( + "current_page_count calculation is wrong" + ) if after_extract["overage_page_count"] > 0: - assert ( - after_extract["overage_page_count"] == before_extract["overage_page_count"] + page_count - ), "overage_page_count calculation is wrong" - assert ( - after_extract[f"current_page_count_{mode}"] == before_extract[f"current_page_count_{mode}"] + page_count - ), f"{mode} mode calculation is wrong" + assert after_extract["overage_page_count"] == before_extract["overage_page_count"] + page_count, ( + "overage_page_count calculation is wrong" + ) + assert after_extract[f"current_page_count_{mode}"] == before_extract[f"current_page_count_{mode}"] + page_count, ( + f"{mode} mode calculation is wrong" + ) for i in range(len(all_modes)): assert ( after_extract[f"current_page_count_{all_modes[i]}"] == before_extract[f"current_page_count_{all_modes[i]}"] diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d7b5c5d..56fa811 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,4 +1,5 @@ import pytest + from unstract.llmwhisperer.client_v2 import LLMWhispererClientV2 diff --git a/tests/unit/client_v2_test.py b/tests/unit/client_v2_test.py index bbb4f59..45c41f5 100644 --- a/tests/unit/client_v2_test.py +++ b/tests/unit/client_v2_test.py @@ -6,6 +6,7 @@ import pytest import requests from pytest_mock import MockerFixture + from unstract.llmwhisperer.client_v2 import LLMWhispererClientException, LLMWhispererClientV2 WEBHOOK_URL = "http://test-webhook.com/callback" diff --git a/tests/unit/compat_test.py b/tests/unit/compat_test.py index 3f51c4b..4c56f0f 100644 --- a/tests/unit/compat_test.py +++ b/tests/unit/compat_test.py @@ -30,6 +30,7 @@ import httpx import pytest import requests + from unstract.llmwhisperer.client_v2 import ( _SEND_ONLY, LLMWhispererClientException, @@ -229,8 +230,7 @@ def serve() -> None: heads.append(data.split(b"\r\n\r\n")[0]) body = b'{"ok":true}' conn.sendall( - b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" - b"Content-Length: %d\r\n\r\n%s" % (len(body), body) + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" % (len(body), body) ) conn.close() @@ -247,9 +247,7 @@ def serve() -> None: return [ { name.lower(): value.strip() - for name, _, value in ( - line.partition(":") for line in head.decode().split("\r\n")[1:] - ) + for name, _, value in (line.partition(":") for line in head.decode().split("\r\n")[1:]) } for head in heads ] @@ -257,8 +255,8 @@ def serve() -> None: def test_wire_headers_match_the_published_client() -> None: """`Accept-Encoding` is the load-bearing one: the published client asked - for no compression, so a response this client has never seen decoded is - not something a transport swap should start requesting.""" + for no compression, so a response this client has never seen decoded is not + something a transport swap should start requesting.""" ours, theirs = _wire_heads( lambda url: _client(base_url=url).get_usage_info(), lambda url: _baseline_client(base_url=url).get_usage_info(), @@ -277,9 +275,7 @@ def test_wire_headers_match_the_published_client() -> None: def test_custom_headers_override_the_transport_defaults() -> None: (ours,) = _wire_heads( - lambda url: _client( - base_url=url, custom_headers={"Accept-Encoding": "gzip"} - ).get_usage_info() + lambda url: _client(base_url=url, custom_headers={"Accept-Encoding": "gzip"}).get_usage_info() ) assert ours["accept-encoding"] == "gzip" @@ -363,7 +359,8 @@ def test_an_unrequested_parameter_is_not_sent() -> None: @pytest.mark.parametrize(("name", "value", "expected"), [(n, v, e) for n, (v, e) in _ADDED_PARAMS.items()]) def test_a_requested_parameter_is_sent(name: str, value: Any, expected: str) -> None: """Every value here is falsy or off: a truthiness filter would drop them and - hand the decision back to the service without saying so.""" + hand the decision back to the service without saying so. + """ assert _whisper_query(_client(), **{name: value})[name] == [expected] @@ -512,7 +509,10 @@ def _httpx_request_errors() -> list[type[Exception]]: return sorted(found, key=lambda cls: cls.__name__) -@pytest.mark.parametrize("cls", _httpx_request_errors(), ids=lambda cls: cls.__name__) +_REQUEST_ERRORS = _httpx_request_errors() + + +@pytest.mark.parametrize("cls", _REQUEST_ERRORS, ids=[cls.__name__ for cls in _REQUEST_ERRORS]) def test_no_httpx_failure_escapes_untranslated(cls: type[Exception]) -> None: """An httpx class reaching a caller is a class no caller catches.""" client = _client() diff --git a/tests/utils_test.py b/tests/utils_test.py index 5968f76..fd482f1 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,4 +1,5 @@ import pytest + from unstract.llmwhisperer.utils import LLMWhispererUtils diff --git a/uv.lock b/uv.lock index d6c5729..caec324 100644 --- a/uv.lock +++ b/uv.lock @@ -294,6 +294,8 @@ dev = [ { name = "pycln" }, { name = "pytest" }, { name = "ruff" }, + { name = "types-requests" }, + { name = "types-setuptools" }, { name = "yamllint" }, ] test = [ @@ -307,7 +309,7 @@ test = [ [package.metadata] requires-dist = [ { name = "attrs", specifier = ">=23.2" }, - { name = "httpx", specifier = ">=0.27" }, + { name = "httpx", specifier = ">=0.27,<0.29" }, { name = "requests", specifier = ">=2" }, { name = "tenacity", specifier = ">=8.0" }, ] @@ -320,7 +322,9 @@ dev = [ { name = "pre-commit", specifier = "~=3.3.1" }, { name = "pycln", specifier = ">=2.5.0" }, { name = "pytest", specifier = ">=8.0.1" }, - { name = "ruff", specifier = ">=0.2.2,<1.0.0" }, + { name = "ruff", specifier = "==0.11.9" }, + { name = "types-requests", specifier = ">=2.32" }, + { name = "types-setuptools", specifier = ">=75" }, { name = "yamllint", specifier = ">=1.35.1" }, ] test = [ @@ -799,6 +803,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" }, ] +[[package]] +name = "types-requests" +version = "2.33.0.20260712" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, +] + +[[package]] +name = "types-setuptools" +version = "84.0.0.20260812" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/cd/3b2a3362a526f91c33f785a291462b2ec448ae531101c62372fc30a21f53/types_setuptools-84.0.0.20260812.tar.gz", hash = "sha256:09bedc248ebbb7a232c9419dfcdca329706e61bf2aa5743e9424d027f1d956b4", size = 46545, upload-time = "2026-08-12T03:52:42.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e5/3a41cab4066465593facd660b2683c017f3d88d3297cdd26db0677b8e479/types_setuptools-84.0.0.20260812-py3-none-any.whl", hash = "sha256:799c08e4bc6a288e8a0b538afb5f5ff320a08927ba8dbedb30c74ee3ba5d867b", size = 70320, upload-time = "2026-08-12T03:52:41.154Z" }, +] + [[package]] name = "typing-extensions" version = "4.13.2" From 717f69266fe51605585ae35fa550ff98cc43d479 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 22:20:33 +0530 Subject: [PATCH 12/16] chore(spec): refresh the vendored spec for the published server list The spec advertised one region-neutral URL that does not resolve; it now lists the two regions that serve the API. Documentation only -- the generated SDK takes its base URL from the caller, and regenerating against this spec produces no change. --- specs/llmwhisperer.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specs/llmwhisperer.json b/specs/llmwhisperer.json index 4b07fe2..8cc8dfe 100644 --- a/specs/llmwhisperer.json +++ b/specs/llmwhisperer.json @@ -104,6 +104,7 @@ } }, "info": { + "description": "The hosted regions are listed under `servers`; a self-hosted deployment serves the same API from its own URL, which every client takes as a configuration option.", "title": "Unstract LLMWhisperer", "version": "v2" }, @@ -1965,7 +1966,12 @@ ], "servers": [ { - "url": "https://llmwhisperer-api.unstract.com" + "description": "US region (the default of the published clients).", + "url": "https://llmwhisperer-api.us-central.unstract.com" + }, + { + "description": "EU region.", + "url": "https://llmwhisperer-api.eu-west.unstract.com" } ] } From ef5e5af854f2e986456d977698ef913f2eb8ca8c Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 22:20:33 +0530 Subject: [PATCH 13/16] docs: say which of the service's operations this client wraps The committed spec covers the whole service while the client wraps part of it, and nothing said so: a reader comparing the two had no way to tell a deliberate omission from a gap. Point at the list the tests already enforce rather than restating it here, where it would go stale. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 135ef2d..0db015e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ This package provides **LLMWhispererClientV2**, the client for LLMWhisperer API Documentation is available [here](https://docs.unstract.com/llmwhisperer/). +### Covered surface + +`specs/llmwhisperer.json` describes the whole service, because it is generated +from the service's own source. This client wraps a subset of it, unchanged from +what it has always wrapped. The operations it deliberately does not expose are +listed as `UNWRAPPED_OPERATIONS` in `tests/unit/compat_test.py`, which fails if +the two disagree — so that list, not this paragraph, is what to read. + ### Service version note A custom `page_separator` needs LLMWhisperer **v2.64.2 or later**. The query From 94fef826b0e0e8387f14fa33a0a19b4d2976b192 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 22:29:12 +0530 Subject: [PATCH 14/16] docs: state the header rationale without reference to a prior state A comment that describes what the code used to do stops being checkable once that state is gone. --- src/unstract/llmwhisperer/client_v2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index 48c2f30..3f981e2 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -102,10 +102,10 @@ } -#: Headers the previous transport put on the wire without being asked, which -#: httpx spells differently. Only `Accept-Encoding` is load-bearing: the -#: previous one asked for no compression, so a service that gzips its response -#: was never exercised against this client. Overridable via `custom_headers`. +#: Headers the released client put on the wire without being asked, which httpx +#: spells differently. `Accept-Encoding` is load-bearing: it asks for no +#: compression, and a service that gzips its response has never been exercised +#: against this client. Overridable via `custom_headers`. _TRANSPORT_HEADERS = {"Accept-Encoding": "identity"} From 7f64caf5893370e0d472c50df3df39ef198fb37b Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 12 Aug 2026 22:56:45 +0530 Subject: [PATCH 15/16] fix: translate InvalidURL, and let the security hooks read every file The formatter exclusions were global, so detect-private-key and gitleaks skipped the generated tree and the vendored baseline. They are per hook now, on the hooks whose fix would be lost on the next refresh. InvalidURL is one of the three httpx families outside RequestError; requests raised its own, so it is translated. The docstring names the other two as propagating. The drift gate also sees a newly created file now. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- .github/workflows/ci_test.yaml | 6 +++++- .pre-commit-config.yaml | 15 ++++++++++++--- src/unstract/llmwhisperer/client_v2.py | 10 ++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci_test.yaml b/.github/workflows/ci_test.yaml index 7226ab1..04894ef 100644 --- a/.github/workflows/ci_test.yaml +++ b/.github/workflows/ci_test.yaml @@ -29,8 +29,12 @@ jobs: - name: Regenerate from the committed spec run: ./tools/gen_sdk.sh + # `git add -N` first: a diff alone cannot see a file the generator has + # newly created, which is exactly what a spec growing an endpoint does. - name: Fail if the committed SDK is not what the spec generates - run: git diff --exit-code -- src/unstract/llmwhisperer/sdk_llmwhisperer + run: | + git add -N -- src/unstract/llmwhisperer/sdk_llmwhisperer + git diff --exit-code -- src/unstract/llmwhisperer/sdk_llmwhisperer test: if: github.event.pull_request.draft == false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5514e72..94754b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,23 +2,26 @@ default_language_version: python: python3.12 default_stages: - pre-commit -# Generated and vendored code is overwritten wholesale by its refresh script, so -# a fix applied here is lost on the next run. -exclude: "^(src/unstract/llmwhisperer/sdk_llmwhisperer/|tests/baseline/)" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: + # Excluded per hook rather than globally: a formatter's fix to generated + # or vendored code is lost on the next refresh, but the security hooks + # below have to read every file in the tree. - id: trailing-whitespace + exclude: &generated "^(src/unstract/llmwhisperer/sdk_llmwhisperer/|tests/baseline/)" exclude_types: - "markdown" - id: end-of-file-fixer + exclude: *generated - id: check-yaml args: [--unsafe] - id: check-added-large-files args: ["--maxkb=10240"] - id: check-case-conflict - id: check-docstring-first + exclude: *generated - id: check-ast - id: check-json exclude: ".vscode/launch.json" @@ -32,6 +35,7 @@ repos: - id: destroyed-symlinks - id: forbid-new-submodules - id: mixed-line-ending + exclude: *generated - repo: https://github.com/astral-sh/ruff-pre-commit # Kept in step with the pinned ruff in the dev group: two versions disagree @@ -40,7 +44,9 @@ repos: hooks: - id: ruff args: [--fix] + exclude: *generated - id: ruff-format + exclude: *generated - repo: https://github.com/hadialqattan/pycln rev: v2.4.0 @@ -48,18 +54,21 @@ repos: - id: pycln entry: uv run pycln args: [--config=pyproject.toml] + exclude: *generated - repo: https://github.com/pycqa/docformatter rev: v1.7.5 hooks: - id: docformatter language: python + exclude: *generated - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade entry: pyupgrade --py39-plus --keep-runtime-typing + exclude: *generated types: - python diff --git a/src/unstract/llmwhisperer/client_v2.py b/src/unstract/llmwhisperer/client_v2.py index 3f981e2..62a126f 100644 --- a/src/unstract/llmwhisperer/client_v2.py +++ b/src/unstract/llmwhisperer/client_v2.py @@ -112,7 +112,7 @@ #: httpx failure -> the ``requests`` class callers catch. First match wins, so a #: subclass has to precede the base it derives from, and ``RequestError`` last is #: what keeps a novel httpx failure from escaping untranslated. -_TRANSLATIONS: tuple[tuple[type[httpx.RequestError], type[Exception]], ...] = ( +_TRANSLATIONS: tuple[tuple[type[Exception], type[Exception]], ...] = ( # requests.ConnectTimeout is both a ConnectionError and a Timeout; the plain # Timeout httpx implies would stop matching half the callers. (httpx.ConnectTimeout, requests.ConnectTimeout), @@ -129,6 +129,7 @@ (httpx.ConnectError, requests.ConnectionError), (httpx.TooManyRedirects, requests.TooManyRedirects), (httpx.DecodingError, requests.exceptions.ContentDecodingError), + (httpx.InvalidURL, requests.exceptions.InvalidURL), (httpx.RequestError, requests.ConnectionError), ) @@ -139,10 +140,15 @@ def _translate_transport_errors(fn: Any, *args: Any, **kwargs: Any) -> Any: Callers document and catch the ``requests`` classes, and the retry policy keys off them too, so the class chosen here decides whether a failure is retried. + + ``RequestError`` is the catch-all for the transport subtree, which is where + a novel failure appears. httpx puts three families outside it: ``InvalidURL``, + translated here because ``requests`` raised its own, and ``StreamError`` and + ``CookieConflict``, which propagate as themselves. """ try: return fn(*args, **kwargs) - except httpx.RequestError as e: + except (httpx.RequestError, httpx.InvalidURL) as e: for failure, equivalent in _TRANSLATIONS: if isinstance(e, failure): raise equivalent(str(e)) from e From b7ca8977c0624ffc9d3fcf69b8c469ffae12520a Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Thu, 13 Aug 2026 17:43:05 +0530 Subject: [PATCH 16/16] fix: give the generator its own venv on PATH It shells out to ruff for post-processing. Finding none, it warns and exits 0, and the warning gate reports that as a spec it could not parse -- a clean regeneration on a runner without a global ruff failed with a message pointing at the wrong thing entirely. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- tools/gen_sdk.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/gen_sdk.sh b/tools/gen_sdk.sh index 889eea1..9442b28 100755 --- a/tools/gen_sdk.sh +++ b/tools/gen_sdk.sh @@ -10,6 +10,10 @@ set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" VENV="$REPO/.gen-venv" +# The generator shells out to ruff for its own post-processing. Without this it +# finds whatever ruff the caller happens to have, or none, and reports the miss +# as a warning -- which the gate below reads as an unparsable spec. +export PATH="$VENV/bin:$PATH" OUT="src/unstract/llmwhisperer/sdk_llmwhisperer" # Pinned: unpinned, a generator upgrade and a spec change produce the same diff, # and the drift gate can no longer tell them apart.