Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.46.2

### Fixes
* 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.

## 0.46.1

### Fixes
Expand Down
10 changes: 10 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1261,3 +1261,13 @@ Based on:
- [python v0.46.1] .
### Releases
- [PyPI v0.46.1] https://pypi.org/project/unstructured-client/0.46.1 - .

## 2026-08-22 00:00:00
### Changes
Based on:
- OpenAPI Doc
- Speakeasy CLI 1.601.0 (2.680.0) https://github.com/speakeasy-api/speakeasy
### Generated
- [python v0.46.2] .
### Releases
- [PyPI v0.46.2] https://pypi.org/project/unstructured-client/0.46.2 - .
120 changes: 95 additions & 25 deletions _test_unstructured_client/unit/test_custom_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import logging
import re

import httpx
import pytest
import requests
import httpx
from httpx import Response, ConnectError
from httpx import ConnectError, Response

from _test_unstructured_client.unit_utils import FixtureRequest, Mock, method_mock
from unstructured_client import UnstructuredClient
from unstructured_client.models import shared, operations
from unstructured_client.models import operations, shared
from unstructured_client.models.errors import SDKError
from unstructured_client.utils.retries import BackoffStrategy, RetryConfig

Expand All @@ -34,7 +34,10 @@ def test_unit_retry_with_backoff_does_retry(caplog):

def mock_post(request):
request_count[0] += 1
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
if (
request.url == "https://api.unstructuredapp.io/general/v0/general"
and request.method == "POST"
):
return Response(502, request=request)

transport = httpx.MockTransport(mock_post)
Expand Down Expand Up @@ -69,7 +72,10 @@ def test_unit_backoff_strategy_logs_retries_5XX(status_code: int, caplog):
)

def mock_post(request):
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
if (
request.url == "https://api.unstructuredapp.io/general/v0/general"
and request.method == "POST"
):
return Response(status_code, request=request)

transport = httpx.MockTransport(mock_post)
Expand All @@ -83,11 +89,13 @@ def mock_post(request):
partition_parameters=shared.PartitionParameters(files=files)
)

with pytest.raises(Exception):
with pytest.raises(Exception): # noqa: B017
session.general.partition(request=req, retries=retries)

pattern = re.compile(f"Failed to process a request due to API server error with status code {status_code}. "
"Attempting retry number 1 after sleep.")
pattern = re.compile(
f"Failed to process a request due to API server error with status code {status_code}. "
"Attempting retry number 1 after sleep."
)
assert bool(pattern.search(caplog.text))


Expand All @@ -103,9 +111,11 @@ def mock_post(request):
[502, True],
[503, True],
[504, True],
]
],
)
def test_unit_number_of_retries_in_failed_requests(status_code: int, expect_retry: bool):
def test_unit_number_of_retries_in_failed_requests(
status_code: int, expect_retry: bool
):
filename = "README.md"
backoff_strategy = BackoffStrategy(
initial_interval=1, max_interval=10, exponent=1.5, max_elapsed_time=300
Expand All @@ -115,17 +125,19 @@ def test_unit_number_of_retries_in_failed_requests(status_code: int, expect_retr
)

number_of_requests = [0]

def mock_post(request):
if request.url == "https://api.unstructuredapp.io/general/v0/general" and request.method == "POST":
if (
request.url == "https://api.unstructuredapp.io/general/v0/general"
and request.method == "POST"
):
number_of_requests[0] += 1
return Response(status_code, request=request)


transport = httpx.MockTransport(mock_post)
client = httpx.Client(transport=transport)
session = UnstructuredClient(api_key_auth=FAKE_KEY, client=client)


with open(filename, "rb") as f:
files = shared.Files(content=f.read(), file_name=filename)

Expand Down Expand Up @@ -166,11 +178,13 @@ def mock_post(request):
partition_parameters=shared.PartitionParameters(files=files)
)

with pytest.raises(Exception):
with pytest.raises(Exception): # noqa: B017
session.general.partition(request=req, retries=retries)

pattern = re.compile("Failed to process a request due to transport error .*? "
"Attempting retry number 1 after sleep.")
pattern = re.compile(
"Failed to process a request due to transport error .*? "
"Attempting retry number 1 after sleep."
)
assert bool(pattern.search(caplog.text))


Expand All @@ -197,22 +211,79 @@ def test_unit_clean_server_url_fixes_malformed_paid_api_url(server_url: str):
)


@pytest.mark.parametrize(
"server_url",
[
# -- the value the Transform Platform's API Keys page hands you --
"https://platform-api.transform.unstructured.io/api/v1",
"http://platform-api.transform.unstructured.io/api/v1",
"platform-api.transform.unstructured.io/api/v1",
# -- well-formed url --
"https://platform-api.transform.unstructured.io",
"platform-api.transform.unstructured.io",
],
)
def test_unit_clean_server_url_fixes_malformed_transform_platform_url(server_url: str):
client = UnstructuredClient(
server_url=server_url,
api_key_auth=FAKE_KEY,
)
assert (
client.general.sdk_configuration.server_url
== "https://platform-api.transform.unstructured.io"
)


@pytest.mark.parametrize(
"server_url,expected_url",
[
# -- a host that merely CONTAINS an Unstructured domain is not ours, so its
# -- path and scheme are left alone --
(
"http://unstructuredapp.io.example.com/api/v1",
"http://unstructuredapp.io.example.com/api/v1",
),
(
"http://not-unstructured.io/api/v1",
"http://not-unstructured.io/api/v1",
),
],
)
def test_unit_clean_server_url_leaves_lookalike_domains_alone(
server_url: str, expected_url: str
):
client = UnstructuredClient(
server_url=server_url,
api_key_auth=FAKE_KEY,
)
assert client.general.sdk_configuration.server_url == expected_url


@pytest.mark.parametrize(
"server_url,expected_url",
[
("http://localhost:8000", "http://localhost:8000"),
("localhost:8000", "http://localhost:8000"),
("localhost:8000/general/v0/general", "http://localhost:8000/general/v0/general"),
("http://localhost:8000/general/v0/general", "http://localhost:8000/general/v0/general"),
(
"localhost:8000/general/v0/general",
"http://localhost:8000/general/v0/general",
),
(
"http://localhost:8000/general/v0/general",
"http://localhost:8000/general/v0/general",
),
],
)
def test_unit_clean_server_url_fixes_non_unst_domain_url(server_url: str, expected_url: str):
def test_unit_clean_server_url_fixes_non_unst_domain_url(
server_url: str, expected_url: str
):
client = UnstructuredClient(
server_url=server_url,
api_key_auth=FAKE_KEY,
)
assert client.general.sdk_configuration.server_url == expected_url


@pytest.mark.parametrize(
"server_url",
[
Expand All @@ -222,7 +293,9 @@ def test_unit_clean_server_url_fixes_non_unst_domain_url(server_url: str, expect
"unstructured-000mock.api.unstructuredapp.io/general/v0/general",
],
)
def test_unit_clean_server_url_fixes_malformed_urls_with_positional_arguments(server_url: str):
def test_unit_clean_server_url_fixes_malformed_urls_with_positional_arguments(
server_url: str,
):
client = UnstructuredClient(FAKE_KEY, server_url=server_url)
assert (
client.general.sdk_configuration.server_url
Expand All @@ -247,11 +320,8 @@ def mock_post(request):
)
with pytest.raises(SDKError, match="API error occurred: Status 401"):
session.general.partition(request=req)

assert any(
"Server responded with 401"
in message for message in caplog.messages
)

assert any("Server responded with 401" in message for message in caplog.messages)


# -- fixtures --------------------------------------------------------------------------------
Expand Down
Loading
Loading