From d808fc240639c43902ebc4b5b23a227791746e75 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Fri, 18 Sep 2026 16:32:16 +0530 Subject: [PATCH 1/3] chore: sync to unstract-client 1.7.1 and llmwhisperer-client 2.9.1 Both services withdrew options from their published specs, and the clients released against those specs no longer accept them. The vendored specs are now the ones those clients were generated from, so the CLI stops offering: - whisper extract: --derotate-threshold, --min-table-width, --ignore-vertical-text; --mode no longer takes excel or document_insights; --output-mode narrows to layout_preserving|text - docstudio deployment run: --use-file-history The flag snapshot and the tests that pin --mode's choices follow the specs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- pyproject.toml | 4 +- src/unstract_cli/specs/docstudio.json | 13 +++++-- src/unstract_cli/specs/llmwhisperer.json | 13 +++---- src/unstract_cli/specs/provenance.json | 12 +++--- tests/derived_flags.json | 48 ------------------------ tests/test_discover.py | 2 - tests/test_params.py | 2 +- uv.lock | 16 ++++---- 8 files changed, 32 insertions(+), 78 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1bfbbd0..daa96f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,8 +29,8 @@ dependencies = [ # Pinned exactly: the CLI derives its flags and help text from these # clients, so one that moves changes the CLI's surface. Each release re-pins # deliberately, against the specs vendored in `src/unstract_cli/specs`. - "unstract-client==1.7.0", - "llmwhisperer-client==2.9.0", + "unstract-client==1.7.1", + "llmwhisperer-client==2.9.1", # Both clients pull this in, but the CLI imports its exception classes # directly to classify a failure, so it names the dependency itself. "requests>=2.32.3", diff --git a/src/unstract_cli/specs/docstudio.json b/src/unstract_cli/specs/docstudio.json index 7bd536c..ea8039b 100644 --- a/src/unstract_cli/specs/docstudio.json +++ b/src/unstract_cli/specs/docstudio.json @@ -65,6 +65,14 @@ "readOnly": true, "type": "string" }, + "owner_emails": { + "description": "Email of each owner, earliest first. Empty if none is a person.", + "items": { + "type": "string" + }, + "readOnly": true, + "type": "array" + }, "run_count": { "readOnly": true, "type": "integer" @@ -91,6 +99,7 @@ "is_owner", "last_5_run_statuses", "last_run_time", + "owner_emails", "run_count", "workflow", "workflow_name" @@ -230,10 +239,6 @@ "maximum": 300, "minimum": -1, "type": "integer" - }, - "use_file_history": { - "default": false, - "type": "boolean" } }, "type": "object" diff --git a/src/unstract_cli/specs/llmwhisperer.json b/src/unstract_cli/specs/llmwhisperer.json index bef1fdd..1cd64d2 100644 --- a/src/unstract_cli/specs/llmwhisperer.json +++ b/src/unstract_cli/specs/llmwhisperer.json @@ -1549,7 +1549,8 @@ "schema": { "default": 10.0, "type": "number" - } + }, + "x-internal": true }, { "in": "query", @@ -1585,7 +1586,8 @@ "schema": { "default": false, "type": "boolean" - } + }, + "x-internal": true }, { "in": "query", @@ -1662,7 +1664,8 @@ "schema": { "default": 0.0, "type": "number" - } + }, + "x-internal": true }, { "in": "query", @@ -1671,8 +1674,6 @@ "schema": { "default": "form", "enum": [ - "document_insights", - "excel", "form", "high_quality", "low_cost", @@ -1689,9 +1690,7 @@ "schema": { "default": "layout_preserving", "enum": [ - "dump-text", "layout_preserving", - "line-printer", "text" ], "type": "string" diff --git a/src/unstract_cli/specs/provenance.json b/src/unstract_cli/specs/provenance.json index 3a96802..7304379 100644 --- a/src/unstract_cli/specs/provenance.json +++ b/src/unstract_cli/specs/provenance.json @@ -1,16 +1,16 @@ { "docstudio.json": { - "client": "unstract-client==1.7.0", + "client": "unstract-client==1.7.1", "repo": "https://github.com/Zipstack/unstract", - "commit": "520b98d7acf5a6d138b24d9787bd788b89150b76", + "commit": "5d46c66ccff8ae1ac3c31a3c3d5d64206e329002", "path": "specs/docstudio-oss.json", - "sha256": "68a31eaf72e54daf8173ae3ef42e4174b257c93b1afc608114582a81be135bd1" + "sha256": "c0de3a14254600f436483561ebb737feac4023fdef2fd46d6abf15dcd2182ad9" }, "llmwhisperer.json": { - "client": "llmwhisperer-client==2.9.0", + "client": "llmwhisperer-client==2.9.1", "repo": "https://github.com/Zipstack/unstract-llm-whisperer", - "commit": "750f941ee229e12cc05d8bd85edaab6a337a8758", + "commit": "fa6cf651a594227f2249dfe0990f80f1ff780398", "path": "specs/llmwhisperer.json", - "sha256": "88ecc01e92443ba5ba6079db7f57cf3038f97670cb796f13c326268a3d79f366" + "sha256": "c015d035b9e06f58e80aabdb5190fbb582dc1b4b8ee48c3a6924fe4392edc650" } } diff --git a/tests/derived_flags.json b/tests/derived_flags.json index 1daff8b..0ce4b59 100644 --- a/tests/derived_flags.json +++ b/tests/derived_flags.json @@ -33,17 +33,6 @@ "choices": [], "short": null }, - "--derotate-threshold": { - "name": "derotate_threshold", - "type": "number", - "default": null, - "description": "The page rotation in degrees beyond which the page is straightened and re-read.", - "array": false, - "nullable": false, - "required": false, - "choices": [], - "short": null - }, "--file-name": { "name": "file_name", "type": "string", @@ -77,17 +66,6 @@ "choices": [], "short": null }, - "--ignore-vertical-text": { - "name": "ignore_vertical_text", - "type": "boolean", - "default": null, - "description": "Whether to drop vertically oriented text instead of extracting it.", - "array": false, - "nullable": false, - "required": false, - "choices": [], - "short": null - }, "--include-line-confidence": { "name": "include_line_confidence", "type": "boolean", @@ -169,17 +147,6 @@ "choices": [], "short": null }, - "--min-table-width": { - "name": "min_table_width", - "type": "number", - "default": null, - "description": "The minimum width a table must span, as a fraction of the page width, to be extracted as a table.", - "array": false, - "nullable": false, - "required": false, - "choices": [], - "short": null - }, "--mode": { "name": "mode", "type": "string", @@ -189,8 +156,6 @@ "nullable": false, "required": false, "choices": [ - "document_insights", - "excel", "form", "high_quality", "low_cost", @@ -208,9 +173,7 @@ "nullable": false, "required": false, "choices": [ - "dump-text", "layout_preserving", - "line-printer", "text" ], "short": null @@ -449,17 +412,6 @@ "required": false, "choices": [], "short": null - }, - "--use-file-history": { - "name": "use_file_history", - "type": "boolean", - "default": false, - "description": "Reuse a previous result for the same file.", - "array": false, - "nullable": false, - "required": false, - "choices": [], - "short": null } }, "docstudio:status": { diff --git a/tests/test_discover.py b/tests/test_discover.py index 2d4b804..63b83e8 100644 --- a/tests/test_discover.py +++ b/tests/test_discover.py @@ -58,8 +58,6 @@ def test_full_carries_enough_to_build_a_call(capsys): assert params["source"]["kind"] == "argument" and params["source"]["required"] assert params["mode"]["choices"] == [ - "document_insights", - "excel", "form", "high_quality", "low_cost", diff --git a/tests/test_params.py b/tests/test_params.py index cc0c787..2bf44ad 100644 --- a/tests/test_params.py +++ b/tests/test_params.py @@ -273,7 +273,7 @@ def test_choices_come_from_the_spec_unless_the_overlay_narrows_them(): """A wrong value must fail before the request, not after -- and the list it is checked against is the service's own, not a copy that can fall behind.""" spec_declared = _by_name(operation_params("llmwhisperer", "extract"))["mode"] - assert "excel" in spec_declared.choices + assert "form" in spec_declared.choices assert click_option(spec_declared, {}).type.choices == spec_declared.choices option = click_option(spec_declared, {"mode": {"choices": ["form", "table"]}}) diff --git a/uv.lock b/uv.lock index 518002d..0057c00 100644 --- a/uv.lock +++ b/uv.lock @@ -172,7 +172,7 @@ wheels = [ [[package]] name = "llmwhisperer-client" -version = "2.9.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -180,9 +180,9 @@ dependencies = [ { name = "requests" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/c7/d8c822ac12837073d07b9c4c3b4e3967ac6b6f8c91737899ca192e1f24fa/llmwhisperer_client-2.9.0.tar.gz", hash = "sha256:8db10ba2c3a9a8351f22bce809535489154bdc7531e7a54f7c04c7601b0cd784", size = 3317195, upload-time = "2026-09-01T10:15:39.042Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/8e/87d568d5ddf2c6494c11bedc7844e9ed150e90afe228f77867705bfd3d38/llmwhisperer_client-2.9.1.tar.gz", hash = "sha256:74dbced5a1a481ca5192c3d817a9f9e12bad4d9c3aaac64751ffb0e9b6182f35", size = 3319616, upload-time = "2026-09-18T10:56:06.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/7b/b192239d0b31e6979ba4d47336585de0fe437847d6839ff936bb9cb0310d/llmwhisperer_client-2.9.0-py3-none-any.whl", hash = "sha256:a2895053b21819fed2a6c85bc15ba05c74b883071b980360e32be537a285836a", size = 69257, upload-time = "2026-09-01T10:15:37.751Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/2f221e4ac210dd8a9307849cf0ac35ff87cdd8b344594524d15a52c8f115/llmwhisperer_client-2.9.1-py3-none-any.whl", hash = "sha256:9e9ad402f16562da4dad9d406e06e6c42c077bf8846670040f40e8605c5c82e4", size = 69186, upload-time = "2026-09-18T10:56:05.114Z" }, ] [[package]] @@ -396,19 +396,19 @@ dev = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1,<9" }, - { name = "llmwhisperer-client", specifier = "==2.9.0" }, + { name = "llmwhisperer-client", specifier = "==2.9.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "requests", specifier = ">=2.32.3" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "tomli-w", specifier = ">=1.0" }, - { name = "unstract-client", specifier = "==1.7.0" }, + { name = "unstract-client", specifier = "==1.7.1" }, ] provides-extras = ["dev"] [[package]] name = "unstract-client" -version = "1.7.0" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -418,9 +418,9 @@ dependencies = [ { name = "rich" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/10/ef55a061b55b9ce5fc036a33d7f13ac8abf32d96c8a5d2f29db03248802e/unstract_client-1.7.0.tar.gz", hash = "sha256:39e989e2f9c999f9fe355cb91549cd228b3cc9c48ab4dceb65cd19dfdceb6c86", size = 222590, upload-time = "2026-09-10T12:59:42.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/d0/d0a6704436baca70e5b261058c0cd9a3a4853556bf6ade9fe201eb3f4b88/unstract_client-1.7.1.tar.gz", hash = "sha256:d192295a8eba9f25268915e60cb0435683a1eb000eaf51bc4f87db0e6e9f1288", size = 222809, upload-time = "2026-09-18T10:56:05.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/92/c1d2f64061f7cb09a53f4a8336b34a9b45e7d5bd8f0af7388453fbc3e6e9/unstract_client-1.7.0-py3-none-any.whl", hash = "sha256:d56c5908b03156ac775e4cd9117d78aa39455a32805c49f4fc3ea77d39b6f542", size = 127958, upload-time = "2026-09-10T12:59:40.847Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/10584cb0c1cefd075990c2dac0f5e1cdbe05aebc9de41c4ef55c6f73f936/unstract_client-1.7.1-py3-none-any.whl", hash = "sha256:82125d4fb5876fcd872595205d30ad6a1ba544a2d49ea1d3177910bc34877274", size = 127922, upload-time = "2026-09-18T10:56:03.928Z" }, ] [[package]] From d22ceaed02a4c785b91ad454cd85a49875b456cf Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Fri, 18 Sep 2026 16:32:16 +0530 Subject: [PATCH 2/3] test: the withdrawn options are refused by the shipped commands Driven through the console entry point against the vendored specs rather than a fixture, so a future spec sync that brings one of them back is noticed here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- tests/test_commands.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index f29d432..223c21c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -191,6 +191,33 @@ def test_the_v1_commands_are_registered(): assert set(tree["auth"]["commands"]) == {"login", "whoami"} +@pytest.mark.parametrize( + ("command", "flag"), + [ + (("whisper", "extract"), "--derotate-threshold"), + (("whisper", "extract"), "--min-table-width"), + (("whisper", "extract"), "--ignore-vertical-text"), + (("docstudio", "deployment", "run"), "--use-file-history"), + ], +) +def test_a_withdrawn_option_is_unknown_to_the_shipped_command(capsys, command, flag): + """The services stopped publishing these; a caller still passing one must + learn that before the request, not from a silently ignored setting.""" + code, out, _ = run(capsys, *command, "x.pdf", flag) + assert code == int(ExitCode.USAGE) + assert f"No such option '{flag}'" in envelope(out)["error"]["message"] + + +@pytest.mark.parametrize( + ("flag", "value"), + [("--output-mode", "line-printer"), ("--mode", "excel")], +) +def test_a_withdrawn_choice_is_refused_by_the_shipped_command(capsys, flag, value): + code, out, _ = run(capsys, "whisper", "extract", "x.pdf", flag, value) + assert code == int(ExitCode.USAGE) + assert f"'{value}' is not one of" in envelope(out)["error"]["message"] + + # --------------------------------------------------------------------------- # # whisper extract # --------------------------------------------------------------------------- # From fddb04ab9214b3dbc4eb1262c823e6206d511b6a Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Fri, 18 Sep 2026 16:39:03 +0530 Subject: [PATCH 3/3] test: assert the withdrawn-option refusal in words both supported click versions use Click 8.1 and 8.2 word the unknown-option error differently; the exit code and the flag being named are what the test is about. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ --- tests/test_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 223c21c..4221e19 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -205,7 +205,8 @@ def test_a_withdrawn_option_is_unknown_to_the_shipped_command(capsys, command, f learn that before the request, not from a silently ignored setting.""" code, out, _ = run(capsys, *command, "x.pdf", flag) assert code == int(ExitCode.USAGE) - assert f"No such option '{flag}'" in envelope(out)["error"]["message"] + message = envelope(out)["error"]["message"] + assert "No such option" in message and flag in message @pytest.mark.parametrize(