Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b0512cf
refactor(client): issue requests through a generated transport
chandrasekharan-zipstack Aug 11, 2026
bb586c4
fix(client): raise ReadTimeout, not a bare Timeout, on a read timeout
chandrasekharan-zipstack Aug 12, 2026
02485e1
feat(client): accept the six extraction parameters the service added
chandrasekharan-zipstack Aug 12, 2026
174a8e9
chore(sdk): regenerate from the remediated spec
chandrasekharan-zipstack Aug 12, 2026
8457ee5
docs: note the service version a custom page separator needs
chandrasekharan-zipstack Aug 12, 2026
13fd71e
fix(client): restore the transport's own request headers
chandrasekharan-zipstack Aug 12, 2026
47072cc
ci: fail when the committed SDK is not what the spec generates
chandrasekharan-zipstack Aug 12, 2026
45664b2
test: compare against the released client, pinned by digest
chandrasekharan-zipstack Aug 12, 2026
e663ada
build: bound httpx to the series the transport is generated against
chandrasekharan-zipstack Aug 12, 2026
54d376b
fix(client): treat an explicit None as unset on the optional OCR params
chandrasekharan-zipstack Aug 12, 2026
6c942a2
chore: make the lint and type checks pass
chandrasekharan-zipstack Aug 12, 2026
717f692
chore(spec): refresh the vendored spec for the published server list
chandrasekharan-zipstack Aug 12, 2026
ef5e5af
docs: say which of the service's operations this client wraps
chandrasekharan-zipstack Aug 12, 2026
94fef82
docs: state the header rationale without reference to a prior state
chandrasekharan-zipstack Aug 12, 2026
7f64caf
fix: translate InvalidURL, and let the security hooks read every file
chandrasekharan-zipstack Aug 12, 2026
b7ca897
fix: give the generator its own venv on PATH
chandrasekharan-zipstack Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/unstract/llmwhisperer/sdk_llmwhisperer/** linguist-generated=true
27 changes: 27 additions & 0 deletions .github/workflows/ci_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ 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

# `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 add -N -- src/unstract/llmwhisperer/sdk_llmwhisperer
git diff --exit-code -- src/unstract/llmwhisperer/sdk_llmwhisperer

test:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ cython_debug/

.pdm-python
.python-version
.gen-venv
16 changes: 15 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ 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"
Expand All @@ -29,32 +35,40 @@ repos:
- id: destroyed-symlinks
- id: forbid-new-submodules
- id: mixed-line-ending
exclude: *generated

- 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]
exclude: *generated
- id: ruff-format
exclude: *generated

- repo: https://github.com/hadialqattan/pycln
rev: v2.4.0
hooks:
- 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

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ 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
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:
Expand Down
34 changes: 31 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ 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.
# 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 = [
Expand All @@ -33,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]
Expand Down Expand Up @@ -86,6 +96,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]
Expand All @@ -106,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
Expand All @@ -118,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"
Expand Down Expand Up @@ -172,4 +198,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/"]
Loading