| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
fix: accept the Transform Platform API URL as server_url (0.46.2) (#352) ## What & why **Problem:** The Transform Platform's API Keys page hands you `https://platform-api.transform.unstructured.io/api/v1`, and the docs tell you to pass that value as `server_url`. Do it and every Platform call in this SDK fails with a 404: listing jobs, creating a workflow, checking a connector. The same URL works with curl, so the URL looks right and the SDK looks broken, and there is nothing in the error to point at the real cause. Anyone starting from the app's own copy button hits this on their first call. **Change:** Treat hosts under `unstructured.io` as Unstructured API hosts, so a copied `/api/v1` suffix is stripped from `server_url` the way it already was for `unstructuredapp.io`. Also clean the base URL for an operation-level `server_url=` override, which bypassed the cleaning hook entirely. ## Linked ticket none Client-facing follow-up: reported while writing the Transform Python quickstart, where every SDK sample had to be written against a URL different from the one the app displays. ## The bug Every Platform operation in this SDK already carries its own path prefix. `jobs.list_jobs` requests `/api/v1/jobs/`, `workflows.create_workflow` requests `/api/v1/workflows/`, and so on. So the base URL must not carry `/api/v1` of its own. `clean_server_url` exists to strip exactly that kind of pasted-in path, but it only did so when the host contained `unstructuredapp.io`: ```python if "unstructuredapp.io" in parsed_url.netloc: ... clean_url = urlunparse(parsed_url._replace(path="", ...)) else: # For other domains, we want to keep the path clean_url = urlunparse(parsed_url._replace(params="", query="", fragment="")) ``` `platform-api.transform.unstructured.io` does not match, so the path was kept and the operation path was appended on top, giving `/api/v1/api/v1/jobs/`, which matches no route. `basesdk.py` is generated, so the customization needs protecting: it is now in `.genignore`, the mechanism this repo already uses for `general.py`, `users.py`, `retries.py` and `partition.py`, with a guard test alongside the existing ones asserting that both the `_get_url` call and the `.genignore` entry survive. Without it a regeneration silently drops the fix and the doubled prefix returns. Freezing the file freezes the generated request, retry and hook plumbing too, so the entry carries the same un-freeze procedure `general.py` documents. Three smaller problems came out of the same code while fixing it. The host test was a substring match, so `unstructuredapp.io.example.com` was treated as one of ours and had its path stripped and its scheme forced to HTTPS; it is now matched on domain boundaries and left alone. A `server_url=` passed to a single operation never reached the cleaning hook at all, because the hook runs at SDK init; that override is now cleaned in `BaseSDK._get_url`, the one point every operation's base URL passes through. And a fully qualified host carrying the terminal root dot (`api.unstructuredapp.io.`) has to be recognized explicitly, since the old substring test matched it by accident and the domain-boundary test does not; the path is stripped as before and the host keeps its dot, which changes the Host header and SNI and is the caller's choice to make. ## What the patch changes, and what it does not Every `server_url` shape the existing tests, the docs and the app use, run through `clean_server_url` on `main` and on this branch. Seven results change; sixteen are byte-identical. | `server_url` | `main` | this branch | | | --- | --- | --- | --- | | `https://platform-api.transform.unstructured.io/api/v1` | `https://platform-api.transform.unstructured.io/api/v1` | `https://platform-api.transform.unstructured.io` | changed | | `http://platform-api.transform.unstructured.io/api/v1` | `http://platform-api.transform.unstructured.io/api/v1` | `https://platform-api.transform.unstructured.io` | changed | | `platform-api.transform.unstructured.io/api/v1` | `http://platform-api.transform.unstructured.io/api/v1` | `https://platform-api.transform.unstructured.io` | changed | | `platform-api.transform.unstructured.io` | `http://platform-api.transform.unstructured.io` | `https://platform-api.transform.unstructured.io` | changed | | `https://platform-api.unstructured.io/api/v1` | `https://platform-api.unstructured.io/api/v1` | `https://platform-api.unstructured.io` | changed | | `http://unstructuredapp.io.example.com/api/v1` | `https://unstructuredapp.io.example.com` | `http://unstructuredapp.io.example.com/api/v1` | changed | | `http://myunstructuredapp.io/api/v1` | `https://myunstructuredapp.io` | `http://myunstructuredapp.io/api/v1` | changed | | `https://platform-api.transform.unstructured.io` | `https://platform-api.transform.unstructured.io` | same | | | `https://platform.unstructuredapp.io/api/v1` | `https://platform.unstructuredapp.io` | same | | | `https://api.unstructuredapp.io/general/v0/general` | `https://api.unstructuredapp.io` | same | | | `unstructured-000mock.api.unstructuredapp.io/general/v0/general` | `https://unstructured-000mock.api.unstructuredapp.io` | same | | | `http://localhost:8000` | `http://localhost:8000` | same | | | `localhost:8000` | `http://localhost:8000` | same | | | `http://localhost:8000/my/endpoint/` | `http://localhost:8000/my/endpoint` | same | | | `localhost:8000/general/v0/general` | `http://localhost:8000/general/v0/general` | same | | | `https://unstructured.example.com/api/v1` | `https://unstructured.example.com/api/v1` | same | | | `http://not-unstructured.io/api/v1` | `http://not-unstructured.io/api/v1` | same | | The first five changed rows are the reported bug. The last two are the substring-match fix: those hosts are not ours, so they keep their path and their scheme. ## Impact **Customers:** Anyone using the Python SDK against the Transform Platform can now paste the API URL shown in the app, or set it from the documented `UNSTRUCTURED_API_URL`, and have jobs, workflows, sources, destinations and templates calls work. Today that exact value 404s on every call. Users who already worked around it by passing the bare host are unaffected; that keeps working. Users on `unstructuredapp.io` are unaffected; their URLs were already cleaned. **Internal (devs / ops / other teams):** The docs can stop steering readers away from the URL the product displays. No service imports this code; it is a client library published to PyPI. **Wire contract / clients:** No request or response shape changes. The only behavior change is which URL a request is sent to, and only for base URLs that were previously producing a doubled path. The one case where a user could notice a difference is a self-hosted deployment on a host under `unstructuredapp.io` or `unstructured.io` that genuinely serves the API beneath a subpath; that path is now stripped. Hosts outside those domains keep their path exactly as before, which the existing localhost subpath tests cover. **Deployment target considerations:** This is a PyPI client library, not a deployed service, so SaaS / DI / in-VPC / on-prem / SND deploys are unaffected. Air-gapped users pointing the SDK at their own hostname keep the existing keep-the-path behavior, since their host is not under an Unstructured domain. ## A note on the diff size The last commit is `ruff format` over the files this change touches, plus seven `noqa` directives for pre-existing lint that cannot be auto-fixed without changing behaviour. It is formatting only and carries no behaviour change, so reading the first two commits on their own gives you the whole fix. Two of the `noqa`s are worth knowing about: `raise err` in `basesdk.py` re-raises whatever an after-error hook returned, which is not always the active exception, so ruff's suggested bare `raise` would be a real bug. ## Risk / rollback Low. Small changes to URL normalization plus a `.genignore` entry, revert-safe, no migration and no flag. ## How it was verified Ran the unit suite on Python 3.11, 3.12 and 3.13 and the contract suite, plus `pylint` (10.00/10) and `mypy`, all green, matching what CI runs. `uv.lock` is unchanged, so the `UV_LOCKED=1` install holds. Reproduced the bug and then the fix against the live Transform Platform API without an API key, which is enough to tell the two apart: a route that exists answers 401, a route that does not answers 404. Not exercised with a real API key end to end, and not exercised against a self-hosted deployment. ## Proof Repro, against the live API, before the fix: ``` $ curl -s -o /dev/null -w '%{http_code}\n' https://platform-api.transform.unstructured.io/api/v1/jobs/ 401 $ curl -s -o /dev/null -w '%{http_code}\n' https://platform-api.transform.unstructured.io/api/v1/api/v1/jobs/ 404 ``` Through the SDK, before the fix: ``` server_url='https://platform-api.transform.unstructured.io' request sent: https://platform-api.transform.unstructured.io/api/v1/jobs/ status: 401 server_url='https://platform-api.transform.unstructured.io/api/v1' request sent: https://platform-api.transform.unstructured.io/api/v1/api/v1/jobs/ status: 404 ``` Failing tests at `HEAD` before the fix, `_test_unstructured_client/unit/test_server_urls.py::test_platform_request_url_has_a_single_api_prefix` plus the hook tests: ``` FAILED test_custom_hooks.py::test_unit_clean_server_url_fixes_malformed_transform_platform_url[https://platform-api.transform.unstructured.io/api/v1] FAILED test_custom_hooks.py::test_unit_clean_server_url_fixes_malformed_transform_platform_url[http://platform-api.transform.unstructured.io/api/v1] FAILED test_custom_hooks.py::test_unit_clean_server_url_fixes_malformed_transform_platform_url[platform-api.transform.unstructured.io/api/v1] FAILED test_custom_hooks.py::test_unit_clean_server_url_fixes_malformed_transform_platform_url[platform-api.transform.unstructured.io] FAILED test_custom_hooks.py::test_unit_clean_server_url_leaves_lookalike_domains_alone[http://unstructuredapp.io.example.com/api/v1] E Failed: transform platform ... Expected https://platform-api.transform.unstructured.io, got https://platform-api.transform.unstructured.io/api/v1 ``` After the fix, the same live check across both ways of passing the URL: ``` client-level, bare host sent: https://platform-api.transform.unstructured.io/api/v1/jobs/ status: 401 client-level, URL from the app (/api/v1) sent: https://platform-api.transform.unstructured.io/api/v1/jobs/ status: 401 operation-level, bare host sent: https://platform-api.transform.unstructured.io/api/v1/jobs/ status: 401 operation-level, URL from the app (/api/v1) sent: https://platform-api.transform.unstructured.io/api/v1/jobs/ status: 401 ``` Every case now reaches the real route. Suites after the fix: unit and contract both pass, `pylint` 10.00/10, `mypy` clean. ## Dependencies / merge order none ## Worked Example ```python from unstructured_client import UnstructuredClient # The value the app's API Keys page gives you, pasted as-is. client = UnstructuredClient( api_key_auth="YOUR_KEY", server_url="https://platform-api.transform.unstructured.io/api/v1", ) client.jobs.list_jobs(request={}) # before: GET https://platform-api.transform.unstructured.io/api/v1/api/v1/jobs/ -> 404 {"detail":"Not Found"} # after: GET https://platform-api.transform.unstructured.io/api/v1/jobs/, the real route ``` ## Release Bumped to 0.46.2 with CHANGELOG and RELEASES entries. --------- Co-authored-by: paulkarayan <pk@unstructured.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
fix: raise on an empty HTTP-200 split chunk in NDJSON mode (0.46.1) (#… …348) ## The bug In NDJSON elements-file mode, a split-PDF chunk that returned HTTP 200 with an empty body was logged and skipped, so the combined `elements_file` was silently short by that chunk's pages while the call still returned 200. Nothing downstream could detect it: `combine_chunk_files_to_ndjson` hands back only the combined path, and `split_pdf_allow_failed=False` does not cover the case because an empty 200 counts as a *successful* chunk. The buffered path fails outright on the same response (`res.json()` raises `JSONDecodeError`), so enabling NDJSON mode converted a hard failure into silent truncation. ## The fix Recombination now raises `EmptyChunkResponseError` (a `ValueError`, matching where the buffered path's `JSONDecodeError` lands) instead of skipping. Emptiness is judged against the chunk's own `Content-Type`, because the two formats disagree about what an empty body means: - **JSON** has no empty document — a chunk with no elements is `[]` — so an empty body is malformed and raises. - **`application/x-ndjson`** encodes zero records as zero lines, so an empty body is well formed. It contributes nothing and does not fail the partition; otherwise a split whose pages are blank would break once a server honors the accept header. - An **unknown or missing** media type is read as JSON. That is what the deployed API returns, and guessing NDJSON would reinstate the silent truncation. `combine_chunk_files_to_ndjson` takes an optional `media_types` list, positionally matched to `chunk_paths`; omitting it keeps the strict reading. `_elements_from_task_responses` collects each chunk's `Content-Type` before the cached branch overwrites the body with a temp-file path — the header survives both cache branches, so the cached case carries a real media type too. ## Tests `_test_unstructured_client/unit/test_ndjson_elements_file.py`: - empty JSON chunk raises, over 3 empty-ish bodies × `application/json` / unset / with-charset - empty NDJSON chunk is zero records, over 3 bodies × 3 media-type spellings (parameters, casing) - every chunk empty yields an empty output rather than an error - `ValueError` parity with the buffered path, and the strict default when `media_types` is omitted - hook level, both cache modes: an empty JSON chunk fails the operation and leaves no partial, spilled, or combined file behind; an empty NDJSON chunk is accepted with the surrounding chunks' elements intact Mutating `_is_ndjson_media_type` to `return False` fails 12 of these, including both hook-level cache-mode tests, so the coverage is load-bearing. 271 unit + 64 contract tests pass; pylint 10/10 on both changed modules. ## Release Bumped to 0.46.1 with CHANGELOG and RELEASES entries. Consumers that pin `unstructured-client >=0.46.0` for NDJSON elements-file mode should raise the floor to `>=0.46.1`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: NDJSON elements-file mode for partition (0.46.0) (#347) ## What Adds an opt-in NDJSON response mode to `partition()` that returns elements as a **path to a file on disk** instead of a parsed list, and ships it as **0.46.0**. ```python from unstructured_client.general import PartitionAcceptEnum res = client.general.partition( request=req, accept_header_override=PartitionAcceptEnum.APPLICATION_X_NDJSON, ) try: with open(res.elements_file, encoding="utf-8") as f: for line in f: element = json.loads(line) ... finally: os.unlink(res.elements_file) ``` `PartitionResponse.elements_file` is set instead of `PartitionResponse.elements`. **The caller owns the file and must delete it.** Requesting `application/json` remains the default and is entirely unchanged. ## Why On the split-PDF path the SDK rebuilt the whole document in memory in order to return it: a list per chunk, a flattened list, a `json.dumps` blob in `create_response`, and then the SDK's re-parse of that blob — four copies live at once, with the serialization step dominating peak usage. For documents with large `metadata.image_base64` payloads this is the difference between a job completing and being OOM-killed. In the new mode the per-chunk temp files are concatenated on disk and never parsed, so peak memory is roughly one chunk rather than the whole document. ## How - `combine_chunk_files_to_ndjson` concatenates chunk files on disk. Each chunk is sniffed for its first non-whitespace character, so a server returning `application/json` still works; chunks that are already NDJSON are copied through without parsing. - `ndjson_mode` depends **only** on the `Accept` header, never on `split_pdf_cache_tmp_data`. Those are set by different parties, so gating on both let them disagree — the server would return NDJSON while the hook took the JSON path and `res.json()` raised on a body this client had itself requested. - Both caching modes are handled. A cached chunk contributes its existing temp-file path; an uncached one spills its body verbatim and then **releases** it, since every response is retained in `api_successful_responses` and leaving `_content` set would keep the document resident regardless. - The combined file is deliberately written outside the operation's `TemporaryDirectory`, which `_clear_operation` removes as soon as `after_success` returns. ## Temp-file ownership Everything this path creates is accounted for: - Spilled chunk bodies are written inside the operation's temp directory and unlinked once combined. - The combined file is deleted when a chunk failure means it is never handed back to the caller. - Recombination writes to a staging file that is atomically renamed into place only on success, so a malformed chunk cannot orphan a partial file. - No combined file is created at all when every chunk failed. ## Security The elements-file marker is an **httpx response extension**, not a response header. Extensions are populated by the transport, so a remote server cannot set the key. A header would be wire-controlled, and since callers are documented to open `elements_file` and then delete it, that would hand a hostile server an arbitrary local file to destroy. A real server body is always copied to a file this client creates. ## Regeneration `elements_file` is client-side only and can never come from the OpenAPI spec, so a regeneration would silently drop it. Both `general.py` and `models/operations/partition.py` are now in `.genignore`, and `test_regeneration_guards.py` fails if either entry is lost. ## Known limitation `elements_file` is set for every input, so callers need one code path. The **memory saving**, however, applies only to split PDFs. An input is sent whole when it is not a PDF, when `split_pdf_page=False`, or when it has two pages or fewer — `_before_request_unlocked` short-circuits on `split_size >= page_count` and `get_optimal_split_size` floors at `MIN_PAGES_PER_SPLIT = 2`. For those, the body is read fully into memory before being written to disk, so peak is roughly 2x the body rather than bounded. Bounding it means `stream=True` for NDJSON requests, which makes `raw_response.content` raise on the returned closed response — a user-visible change worth its own review. Tracked separately. Note also that the deployed API does not currently emit `application/x-ndjson`, so the unsplit path reaches the JSON-to-NDJSON conversion rather than the streamed-body branch. That is not merely a spec omission: the service does not negotiate the response format on `Accept` at all. It selects the format from the `output_format` form field, and consults `Accept` only to choose `multipart/mixed` and to reject conflicting media types on multi-file uploads. NDJSON was therefore never going to arrive via `Accept`. The service's `406 NOT_ACCEPTABLE` on an unrecognized `Accept` is gated on multi-file uploads. This SDK sends a single file per request — `PartitionParameters.files` is one `Files`, and the split-PDF hook sends one chunk per request — so that branch is unreachable from here and the unsplit path cannot raise `SDKError` because of it. Server-side NDJSON support is tracked separately. ## Testing - New `_test_unstructured_client/unit/test_ndjson_elements_file.py` — recombination across JSON-array / NDJSON / mixed chunk formats, order preservation, byte-exact payload round-trip, non-ASCII, temp-file lifecycle on success and failure, and regression guards for the header-spoofing and partial-output defects. - 235 unit tests and 64 contract tests pass; `pylint` 10.00/10; `mypy` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/Unstructured-IO/unstructured-python-client/pull/347?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: env-var configurable httpx pool + TLS in split_pdf_hook (0.45.0) ( #344) ## What Adds env-var knobs for the `httpx.AsyncClient` used by `split_pdf_hook.run_tasks`, and ships them as **0.45.0**. Defaults match httpx — fully backward compatible. ### Connection-pool limits - `UNSTRUCTURED_CLIENT_MAX_CONNECTIONS` (default `100`) - `UNSTRUCTURED_CLIENT_MAX_KEEPALIVE_CONNECTIONS` (default `20`) - `UNSTRUCTURED_CLIENT_KEEPALIVE_EXPIRY` (default `5.0` seconds) ### TLS trust store (server verification) Honors the standard env vars other Python tooling already respects, so a single setting applies uniformly: - `SSL_CERT_FILE` (stdlib `ssl` convention) - `REQUESTS_CA_BUNDLE` (requests / httpx-ecosystem convention; used if `SSL_CERT_FILE` is unset) ### mTLS client certificate - `UNSTRUCTURED_CLIENT_TLS_CLIENT_CERT` — PEM file (httpx reads key from the same file by default) - `UNSTRUCTURED_CLIENT_TLS_CLIENT_KEY` — optional, when cert and key live in separate files ### Observability - Extends the existing `split_pdf event=plan_created` INFO log to include the resolved pool values and trust-store / mTLS mode, so the active config is visible in production logs without leaking filesystem paths. ### Release - Bumps `_version.py` to `0.45.0`, adds a `0.45.0` `CHANGELOG.md` section, and appends a matching `RELEASES.md` entry. ## Why When the SDK runs in an environment where load balancing happens at TCP-connect time rather than per-request (a common Kubernetes setup with a plain ClusterIP and no service mesh), httpx's default keepalive pooling can lock onto a subset of backends. Newly added backends never receive traffic because existing connections stay glued to the originally-resolved set. Letting operators force shorter keepalive (e.g. `MAX_KEEPALIVE_CONNECTIONS=1` + a low `KEEPALIVE_EXPIRY`) makes the client re-establish connections more frequently, redistributing across the available backends. The TLS additions are for SDK consumers running behind corporate proxies with custom CAs, or against backends that require mTLS — previously they had to subclass / monkey-patch to get a custom `verify` or `cert` into the split-PDF client. ## How to use ```yaml env: # Pool reshuffling for connect-time-only LBs - name: UNSTRUCTURED_CLIENT_MAX_KEEPALIVE_CONNECTIONS value: "1" - name: UNSTRUCTURED_CLIENT_KEEPALIVE_EXPIRY value: "30.0" # Custom trust store (standard env var, picked up by httpx, requests, ssl) - name: SSL_CERT_FILE value: /etc/ssl/internal-ca-bundle.pem # mTLS - name: UNSTRUCTURED_CLIENT_TLS_CLIENT_CERT value: /etc/ssl/client.crt - name: UNSTRUCTURED_CLIENT_TLS_CLIENT_KEY value: /etc/ssl/client.key ``` --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
chore: bump to 0.44.1 to release retry budget fields (#343) ## Summary Cuts a 0.44.1 release so the `BackoffStrategy` retry-budget fields that landed on main in #342 actually ship to PyPI. v0.44.0 was tagged before #342 merged, so PyPI v0.44.0 does not include those fields. ## Changes - `src/unstructured_client/_version.py`: bump `__version__` and `__user_agent__` to `0.44.1`. - `CHANGELOG.md`: split the combined 0.44.0 entry — `min_attempts` / `absolute_max_elapsed_time_ms` move under a new `## 0.44.1` section so the changelog matches what's actually in each PyPI artifact. - `RELEASES.md`: append a 0.44.1 entry following the existing Speakeasy-publish format. ## Test plan - [x] No code changes; only metadata files - [ ] CI green - [ ] PyPI publish workflow picks up the bump on merge
feat(PLU-354): remove connector config models from the SDK (#341) ## Summary - Replace the typed unions in `CreateSourceConnectorConfig`, `CreateDestinationConnectorConfig`, `UpdateSourceConnectorConfig`, `UpdateDestinationConnectorConfig`, and the `*ConnectorInformation` configs with `Dict[str, Any]`. - Delete the per-connector config models and their docs (e.g. `S3SourceConnectorConfig`, `AzureDestinationConnectorConfig`, `OpenSearchConnectorConfig`, …). - Bump version to 0.44.0 and add a breaking-change changelog entry. This decouples the SDK from backend connector schemas — adding/removing fields on a connector no longer requires an SDK release. These were deprecated in our docs last year. None of our SDK snippets use them. ## Test plan - [ ] CI green - [ ] Smoke: create a source connector with a plain dict config against SND and confirm the request goes through <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > This is a breaking change to the SDK surface area: connector `config` fields switch from typed models/unions to `Dict[str, Any]`, and many generated connector config classes/docs are removed, which may break downstream type checks and runtime imports. > > **Overview** > **Decouples connector configuration from generated SDK models.** Connector `config` fields for create/update and `*ConnectorInformation` responses now use `Dict[str, Any]` instead of per-connector typed unions/models. > > Removes the generated connector config model classes and their documentation, updates contract tests to assert `config` is a `dict`, and bumps the SDK version to `0.44.0` with a breaking-change entry in `CHANGELOG.md` (plus release metadata in `RELEASES.md`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit eb0ae92. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: retry on all TransportError subclasses (ReadError, WriteError, e… …tc.) (#334) ## Summary - Replaces individual `except` blocks for `ConnectError`, `RemoteProtocolError`, and `TimeoutException` with a single catch for their parent class `httpx.TransportError` - This covers `ReadError` (TCP connection reset mid-response with empty message), `WriteError`, and all other transport-level failures - Previously, `ReadError` fell through to the catch-all `Exception` handler and was wrapped as `PermanentError`, failing immediately without retry ## Context Follow-up to #332. After deploying the `RemoteProtocolError` fix, we observed `httpx.ReadError` (empty message) failures when api pods crashed mid-response. The TCP connection was reset during the response read phase, which httpx classifies as `ReadError` rather than `RemoteProtocolError`. The httpx exception hierarchy: ``` TransportError ├── ConnectError (was retried) ├── RemoteProtocolError (was retried since #332) ├── ReadError (was NOT retried — now fixed) ├── WriteError (was NOT retried — now fixed) ├── PoolTimeout (was NOT retried — now fixed) └── ... TimeoutException (was retried, subclass of TransportError) ├── ConnectTimeout ├── ReadTimeout ├── WriteTimeout └── PoolTimeout ``` Catching `TransportError` is the correct level — all transport errors are transient and should be retried when `retry_connection_errors=True`. ## Test plan - [x] Parametrized tests for all TransportError subclasses (sync + async) - [ ] Each subclass retried when `retry_connection_errors=True` - [ ] Each subclass raises immediately when `retry_connection_errors=False` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands which network failures are treated as retryable, which can change error/latency behavior for callers and potentially mask persistent transport issues until backoff is exhausted. > > **Overview** > **Broadened retry handling for transport failures.** The retry wrapper now catches `httpx.TransportError` in both sync and async paths, so additional transport-level errors (e.g. `ReadError`, `WriteError`, and timeout subclasses) are retried when `retry_connection_errors=True` instead of being treated as permanent. > > Tests were updated to parameterize across multiple `TransportError` subclasses for both sync and async retry behavior, and the package version/release notes were bumped to `0.42.12`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bdd403c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct RELEASES.md format for Speakeasy publish (#333) ## Summary - Adds missing trailing spaces to `- OpenAPI Doc` line in the 0.42.11 RELEASES.md entry - Adds missing trailing newline at end of file - The Speakeasy publish action failed with `error parsing last release info` because the format didn't match the expected pattern This unblocks the 0.42.11 PyPI publish. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: documentation-only formatting tweaks (trailing spaces/newline) to satisfy Speakeasy release parsing; no runtime code changes. > > **Overview** > Fixes the `2026-03-25` (`v0.42.11`) entry in `RELEASES.md` to match Speakeasy’s expected release format by restoring the trailing spaces on `- OpenAPI Doc` and ensuring the file ends with a proper final newline (so the last `PyPI v0.42.11` line is parsed correctly). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 39fd2ae. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| Back | FazBrowse Home | New Git URL |