Skip to content

Commit ebe88c5

Browse files
awalker4claude
andcommitted
Merge origin/main into chore/decommission-speakeasy-scaffolding
Resolve RELEASES.md modify/delete conflict by keeping the deletion; main's new entries (0.46.1, 0.46.2) are Speakeasy ledger boilerplate and the human-facing notes already live in CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 4fbfbd3 + 8a077d7 commit ebe88c5

15 files changed

Lines changed: 585 additions & 352 deletions

File tree

‎.genignore‎

Lines changed: 0 additions & 41 deletions
This file was deleted.

‎.github/workflows/claude.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ jobs:
3131

3232
- name: Run Claude Code
3333
id: claude
34-
uses: anthropics/claude-code-action@beta
34+
uses: anthropics/claude-code-action@v1
3535
with:
3636
anthropic_api_key: ${{ secrets.GH_ANTHROPIC_API_KEY }}
37-
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
37+
claude_args: |
38+
--allowedTools "Bash(git:*),Read,Glob,Grep"

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 0.46.2
2+
3+
### Fixes
4+
* Accept the API URL the Transform Platform hands you as `server_url`. The app's API Keys page and the docs give you `https://platform-api.transform.unstructured.io/api/v1`, which works with curl but 404'd every Platform call in the SDK: the URL cleaner only stripped a path for `unstructuredapp.io` hosts, so the `/api/v1` survived and the operation's own `/api/v1/jobs/` was appended on top, producing `/api/v1/api/v1/jobs/`. Hosts under `unstructured.io` are now recognized too, and are matched on domain boundaries so a lookalike host like `unstructuredapp.io.example.com` keeps its path and scheme. A `server_url` passed to an individual operation is cleaned as well — previously only the client-level URL was, so `client.jobs.list_jobs(request={}, server_url=...)` still 404'd.
5+
6+
## 0.46.1
7+
8+
### Fixes
9+
* Fail loudly when a split-PDF chunk returns HTTP 200 with an empty JSON body in NDJSON elements-file mode. The chunk used to be logged and skipped, so the combined `elements_file` was silently short by those pages while the call still returned 200 — and `split_pdf_allow_failed=False` did not catch it, because an empty 200 counts as a successful chunk. Recombination now raises `EmptyChunkResponseError` (a `ValueError`), matching the buffered path, which raises `JSONDecodeError` on the same response. Emptiness is judged against the chunk's own `Content-Type`: JSON has no empty document (a chunk with no elements is `[]`), while `application/x-ndjson` encodes zero records as zero lines, so an empty NDJSON chunk is well formed and still contributes nothing.
10+
111
## 0.46.0
212

313
### Features

‎_test_unstructured_client/unit/test_custom_hooks.py‎

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import logging
44
import re
55

6+
import httpx
67
import pytest
78
import requests
8-
import httpx
9-
from httpx import Response, ConnectError
9+
from httpx import ConnectError, Response
1010

1111
from _test_unstructured_client.unit_utils import FixtureRequest, Mock, method_mock
1212
from unstructured_client import UnstructuredClient
13-
from unstructured_client.models import shared, operations
13+
from unstructured_client._hooks.custom.clean_server_url_hook import clean_server_url
14+
from unstructured_client.models import operations, shared
1415
from unstructured_client.models.errors import SDKError
1516
from unstructured_client.utils.retries import BackoffStrategy, RetryConfig
1617

@@ -34,7 +35,10 @@ def test_unit_retry_with_backoff_does_retry(caplog):
3435

3536
def mock_post(request):
3637
request_count[0] += 1
37-
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
38+
if (
39+
request.url == "https://api.unstructuredapp.io/general/v0/general"
40+
and request.method == "POST"
41+
):
3842
return Response(502, request=request)
3943

4044
transport = httpx.MockTransport(mock_post)
@@ -69,7 +73,10 @@ def test_unit_backoff_strategy_logs_retries_5XX(status_code: int, caplog):
6973
)
7074

7175
def mock_post(request):
72-
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
76+
if (
77+
request.url == "https://api.unstructuredapp.io/general/v0/general"
78+
and request.method == "POST"
79+
):
7380
return Response(status_code, request=request)
7481

7582
transport = httpx.MockTransport(mock_post)
@@ -83,11 +90,13 @@ def mock_post(request):
8390
partition_parameters=shared.PartitionParameters(files=files)
8491
)
8592

86-
with pytest.raises(Exception):
93+
with pytest.raises(Exception): # noqa: B017
8794
session.general.partition(request=req, retries=retries)
8895

89-
pattern = re.compile(f"Failed to process a request due to API server error with status code {status_code}. "
90-
"Attempting retry number 1 after sleep.")
96+
pattern = re.compile(
97+
f"Failed to process a request due to API server error with status code {status_code}. "
98+
"Attempting retry number 1 after sleep."
99+
)
91100
assert bool(pattern.search(caplog.text))
92101

93102

@@ -103,9 +112,11 @@ def mock_post(request):
103112
[502, True],
104113
[503, True],
105114
[504, True],
106-
]
115+
],
107116
)
108-
def test_unit_number_of_retries_in_failed_requests(status_code: int, expect_retry: bool):
117+
def test_unit_number_of_retries_in_failed_requests(
118+
status_code: int, expect_retry: bool
119+
):
109120
filename = "README.md"
110121
backoff_strategy = BackoffStrategy(
111122
initial_interval=1, max_interval=10, exponent=1.5, max_elapsed_time=300
@@ -115,17 +126,19 @@ def test_unit_number_of_retries_in_failed_requests(status_code: int, expect_retr
115126
)
116127

117128
number_of_requests = [0]
129+
118130
def mock_post(request):
119-
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
131+
if (
132+
request.url == "https://api.unstructuredapp.io/general/v0/general"
133+
and request.method == "POST"
134+
):
120135
number_of_requests[0] += 1
121136
return Response(status_code, request=request)
122137

123-
124138
transport = httpx.MockTransport(mock_post)
125139
client = httpx.Client(transport=transport)
126140
session = UnstructuredClient(api_key_auth=FAKE_KEY, client=client)
127141

128-
129142
with open(filename, "rb") as f:
130143
files = shared.Files(content=f.read(), file_name=filename)
131144

@@ -166,11 +179,13 @@ def mock_post(request):
166179
partition_parameters=shared.PartitionParameters(files=files)
167180
)
168181

169-
with pytest.raises(Exception):
182+
with pytest.raises(Exception): # noqa: B017
170183
session.general.partition(request=req, retries=retries)
171184

172-
pattern = re.compile("Failed to process a request due to transport error .*? "
173-
"Attempting retry number 1 after sleep.")
185+
pattern = re.compile(
186+
"Failed to process a request due to transport error .*? "
187+
"Attempting retry number 1 after sleep."
188+
)
174189
assert bool(pattern.search(caplog.text))
175190

176191

@@ -187,31 +202,93 @@ def mock_post(request):
187202
],
188203
)
189204
def test_unit_clean_server_url_fixes_malformed_paid_api_url(server_url: str):
190-
client = UnstructuredClient(
191-
server_url=server_url,
192-
api_key_auth=FAKE_KEY,
193-
)
194205
assert (
195-
client.general.sdk_configuration.server_url
206+
clean_server_url(server_url)
196207
== "https://unstructured-000mock.api.unstructuredapp.io"
197208
)
198209

199210

211+
@pytest.mark.parametrize(
212+
"server_url",
213+
[
214+
# -- the value the Transform Platform's API Keys page hands you --
215+
"https://platform-api.transform.unstructured.io/api/v1",
216+
"http://platform-api.transform.unstructured.io/api/v1",
217+
"platform-api.transform.unstructured.io/api/v1",
218+
# -- well-formed url --
219+
"https://platform-api.transform.unstructured.io",
220+
"platform-api.transform.unstructured.io",
221+
],
222+
)
223+
def test_unit_clean_server_url_fixes_malformed_transform_platform_url(server_url: str):
224+
assert (
225+
clean_server_url(server_url) == "https://platform-api.transform.unstructured.io"
226+
)
227+
228+
229+
@pytest.mark.parametrize(
230+
"server_url,expected_url",
231+
[
232+
# -- the terminal root dot is a valid fully qualified name and still ours, so
233+
# -- the path goes; the host is left exactly as the caller wrote it, because the
234+
# -- dot is a deliberate DNS choice that changes the Host header and SNI --
235+
(
236+
"https://platform-api.transform.unstructured.io./api/v1",
237+
"https://platform-api.transform.unstructured.io.",
238+
),
239+
(
240+
"https://unstructured-000mock.api.unstructuredapp.io./general/v0/general",
241+
"https://unstructured-000mock.api.unstructuredapp.io.",
242+
),
243+
],
244+
)
245+
def test_unit_clean_server_url_handles_a_fully_qualified_host(
246+
server_url: str, expected_url: str
247+
):
248+
assert clean_server_url(server_url) == expected_url
249+
250+
251+
@pytest.mark.parametrize(
252+
"server_url,expected_url",
253+
[
254+
# -- a host that merely CONTAINS an Unstructured domain is not ours, so its
255+
# -- path and scheme are left alone --
256+
(
257+
"http://unstructuredapp.io.example.com/api/v1",
258+
"http://unstructuredapp.io.example.com/api/v1",
259+
),
260+
(
261+
"http://not-unstructured.io/api/v1",
262+
"http://not-unstructured.io/api/v1",
263+
),
264+
],
265+
)
266+
def test_unit_clean_server_url_leaves_lookalike_domains_alone(
267+
server_url: str, expected_url: str
268+
):
269+
assert clean_server_url(server_url) == expected_url
270+
271+
200272
@pytest.mark.parametrize(
201273
"server_url,expected_url",
202274
[
203275
("http://localhost:8000", "http://localhost:8000"),
204276
("localhost:8000", "http://localhost:8000"),
205-
("localhost:8000/general/v0/general", "http://localhost:8000/general/v0/general"),
206-
("http://localhost:8000/general/v0/general", "http://localhost:8000/general/v0/general"),
277+
(
278+
"localhost:8000/general/v0/general",
279+
"http://localhost:8000/general/v0/general",
280+
),
281+
(
282+
"http://localhost:8000/general/v0/general",
283+
"http://localhost:8000/general/v0/general",
284+
),
207285
],
208286
)
209-
def test_unit_clean_server_url_fixes_non_unst_domain_url(server_url: str, expected_url: str):
210-
client = UnstructuredClient(
211-
server_url=server_url,
212-
api_key_auth=FAKE_KEY,
213-
)
214-
assert client.general.sdk_configuration.server_url == expected_url
287+
def test_unit_clean_server_url_fixes_non_unst_domain_url(
288+
server_url: str, expected_url: str
289+
):
290+
assert clean_server_url(server_url) == expected_url
291+
215292

216293
@pytest.mark.parametrize(
217294
"server_url",
@@ -222,10 +299,11 @@ def test_unit_clean_server_url_fixes_non_unst_domain_url(server_url: str, expect
222299
"unstructured-000mock.api.unstructuredapp.io/general/v0/general",
223300
],
224301
)
225-
def test_unit_clean_server_url_fixes_malformed_urls_with_positional_arguments(server_url: str):
226-
client = UnstructuredClient(FAKE_KEY, server_url=server_url)
302+
def test_unit_clean_server_url_fixes_malformed_urls_with_positional_arguments(
303+
server_url: str,
304+
):
227305
assert (
228-
client.general.sdk_configuration.server_url
306+
clean_server_url(server_url)
229307
== "https://unstructured-000mock.api.unstructuredapp.io"
230308
)
231309

@@ -247,11 +325,8 @@ def mock_post(request):
247325
)
248326
with pytest.raises(SDKError, match="API error occurred: Status 401"):
249327
session.general.partition(request=req)
250-
251-
assert any(
252-
"Server responded with 401"
253-
in message for message in caplog.messages
254-
)
328+
329+
assert any("Server responded with 401" in message for message in caplog.messages)
255330

256331

257332
# -- fixtures --------------------------------------------------------------------------------

0 commit comments

Comments
 (0)