Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 0 additions & 41 deletions .genignore

This file was deleted.

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 - .
149 changes: 112 additions & 37 deletions _test_unstructured_client/unit/test_custom_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
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._hooks.custom.clean_server_url_hook import clean_server_url
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 +35,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 +73,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 +90,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 +112,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 +126,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 +179,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 @@ -187,31 +202,93 @@ def mock_post(request):
],
)
def test_unit_clean_server_url_fixes_malformed_paid_api_url(server_url: str):
client = UnstructuredClient(
server_url=server_url,
api_key_auth=FAKE_KEY,
)
assert (
client.general.sdk_configuration.server_url
clean_server_url(server_url)
== "https://unstructured-000mock.api.unstructuredapp.io"
)


@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):
assert (
clean_server_url(server_url) == "https://platform-api.transform.unstructured.io"
)


@pytest.mark.parametrize(
"server_url,expected_url",
[
# -- the terminal root dot is a valid fully qualified name and still ours, so
# -- the path goes; the host is left exactly as the caller wrote it, because the
# -- dot is a deliberate DNS choice that changes the Host header and SNI --
(
"https://platform-api.transform.unstructured.io./api/v1",
"https://platform-api.transform.unstructured.io.",
),
(
"https://unstructured-000mock.api.unstructuredapp.io./general/v0/general",
"https://unstructured-000mock.api.unstructuredapp.io.",
),
],
)
def test_unit_clean_server_url_handles_a_fully_qualified_host(
server_url: str, expected_url: str
):
assert clean_server_url(server_url) == expected_url


@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
):
assert clean_server_url(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):
client = UnstructuredClient(
server_url=server_url,
api_key_auth=FAKE_KEY,
)
assert client.general.sdk_configuration.server_url == expected_url
def test_unit_clean_server_url_fixes_non_unst_domain_url(
server_url: str, expected_url: str
):
assert clean_server_url(server_url) == expected_url


@pytest.mark.parametrize(
"server_url",
Expand All @@ -222,10 +299,11 @@ 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):
client = UnstructuredClient(FAKE_KEY, server_url=server_url)
def test_unit_clean_server_url_fixes_malformed_urls_with_positional_arguments(
server_url: str,
):
assert (
client.general.sdk_configuration.server_url
clean_server_url(server_url)
== "https://unstructured-000mock.api.unstructuredapp.io"
)

Expand All @@ -247,11 +325,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