diff --git a/CHANGELOG.md b/CHANGELOG.md
index a916ba82..767f4366 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.47.0
+
+### Features
+* Add `skip_preflight` to `shared.CreateWorkflow` and `shared.UpdateWorkflow` to opt a workflow out of the job pre-flight check, and to `shared.WorkflowInformation` to read the current setting back. It behaves like `reprocess_all`: omitting it on update leaves the stored value unchanged, while sending `false` opts back in. `shared.BodyCreateJob` accepts it as a plain argument and folds it into the `request_data` JSON where the server reads it, so callers no longer have to hand-edit that string; an explicit argument wins over a value already present in it.
+
+### Fixes
+* `JobStatus` and `JobProcessingStatus` gained `REJECTED`, so `jobs.get_job` and `jobs.list_jobs` no longer raise on a preflight-rejected job. Previously a single rejected job in a page failed the entire `list_jobs` call, since the page is validated as one list. Both enums now also preserve an unrecognised server-side status verbatim instead of raising, so the next status the server adds is not a client break.
+
## 0.46.2
### Fixes
diff --git a/README.md b/README.md
index 39f9dc3a..39c33cb1 100644
--- a/README.md
+++ b/README.md
@@ -533,6 +533,115 @@ s = UnstructuredClient(debug_logger=logging.getLogger("unstructured_client"))
+### Skipping the job preflight check
+
+Jobs run a preflight check by default, which validates connector credentials and configuration
+before any documents are processed. Set `skip_preflight` to opt out.
+
+On a workflow, the setting is stored and applies to every run of that workflow — including
+scheduled runs and `run_workflow`:
+
+```python
+from unstructured_client import UnstructuredClient
+from unstructured_client.models import operations, shared
+
+uc_client = UnstructuredClient(api_key_auth="YOUR_API_KEY")
+
+created = uc_client.workflows.create_workflow(
+ request=operations.CreateWorkflowRequest(
+ create_workflow=shared.CreateWorkflow(
+ name="my-workflow",
+ workflow_type=shared.WorkflowType.ADVANCED,
+ source_id="...",
+ destination_id="...",
+ skip_preflight=True,
+ )
+ )
+)
+print(created.workflow_information.skip_preflight) # True
+```
+
+On update, **omitting** `skip_preflight` leaves the stored value alone; send `False` to opt back in:
+
+```python
+workflow_id = created.workflow_information.id
+
+# Renames the workflow; leaves skip_preflight as it was
+uc_client.workflows.update_workflow(
+ request=operations.UpdateWorkflowRequest(
+ workflow_id=workflow_id,
+ update_workflow=shared.UpdateWorkflow(name="renamed"),
+ )
+)
+
+# Turns preflight back on
+uc_client.workflows.update_workflow(
+ request=operations.UpdateWorkflowRequest(
+ workflow_id=workflow_id,
+ update_workflow=shared.UpdateWorkflow(skip_preflight=False),
+ )
+)
+```
+
+For a one-off job, pass it to `create_job`. The API carries this field inside the `request_data`
+JSON rather than as its own form field, and the SDK writes it there for you — so you do not need to
+edit that string yourself. An explicit `skip_preflight` argument wins over any value already in it:
+
+```python
+import json
+
+uc_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(
+ request_data=json.dumps({"job_nodes": [...]}),
+ skip_preflight=True,
+ )
+ )
+)
+```
+
+One exception, and the reason the example above uses `job_nodes` rather than `template_id`: a job
+created from a `template_id` may not run a preflight check in the first place, because it has no
+customer-supplied connector configuration to validate. Setting `skip_preflight` on such a job is
+accepted but has no effect — it is a no-op, not an error.
+
+`skip_preflight=None` means "no preference". Note that it is *sent* — the workflow models put an
+explicit `null` on the wire rather than omitting the field — but what the server does with that
+`null` differs per call:
+
+| Call | On the wire | Effect |
+| --- | --- | --- |
+| `create_workflow` | `"skip_preflight": null` | treated as not set — preflight runs |
+| `update_workflow` | `"skip_preflight": null` | **leaves the stored value unchanged** — if the workflow already had preflight skipped, it stays skipped |
+| `create_job` | field absent from `request_data` | treated as not set — preflight runs |
+
+So `None` is not a way to turn preflight back on for an existing workflow. Pass
+`skip_preflight=False` explicitly for that.
+
+On `create_job`, passing `None` also *clears* a value an earlier call had folded in, so a body you
+built with `skip_preflight=True` and then reset to `None` sends no flag at all. Leaving the argument
+**unset** is different: `request_data` is then passed through byte for byte, including a
+`skip_preflight` you wrote into the JSON yourself.
+
+Note on job statuses: `JobStatus` and `JobProcessingStatus` preserve a value this client version
+does not know rather than raising, so a status added server-side is not a client break. The
+trade-off is that a polling loop written as `while job.status not in (COMPLETED, FAILED, STOPPED,
+REJECTED)` would not recognise a *new* terminal status and would spin until its own timeout. If you
+poll, give the loop a deadline and treat an unrecognised status as a reason to stop and inspect,
+rather than assuming it is non-terminal.
+
+These enums are also not input validators. Do not use `some_string in shared.JobStatus` to
+decide whether a status is one this client knows: `in` on an enum changed twice in CPython, so
+across the versions this SDK supports the same expression raises `TypeError` on 3.11, returns
+`True` on 3.12 (where `in` consults the same lookup that tolerates unknown values), and returns
+`False` on 3.13. Validate against `shared.JobStatus.__members__` instead, which is a plain
+mapping of the declared members and behaves the same everywhere:
+
+```python
+if status not in shared.JobStatus.__members__:
+ ... # a status this client version does not declare
+```
+
### Maturity
This SDK is in beta, and there may be breaking changes between versions without a major version update. Therefore, we recommend pinning usage
diff --git a/RELEASES.md b/RELEASES.md
index 858f1076..26a83083 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1271,3 +1271,13 @@ Based on:
- [python v0.46.2] .
### Releases
- [PyPI v0.46.2] https://pypi.org/project/unstructured-client/0.46.2 - .
+
+## 2026-08-27 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.47.0] .
+### Releases
+- [PyPI v0.47.0] https://pypi.org/project/unstructured-client/0.47.0 - .
diff --git a/_test_contract/platform_api/test_jobs.py b/_test_contract/platform_api/test_jobs.py
index bfd39508..1ee19566 100644
--- a/_test_contract/platform_api/test_jobs.py
+++ b/_test_contract/platform_api/test_jobs.py
@@ -1,6 +1,10 @@
+import json
+import re
from datetime import datetime
+from urllib.parse import parse_qs
import pytest
+from pydantic import ValidationError
from unstructured_client import UnstructuredClient
from unstructured_client.models import shared, operations
@@ -222,3 +226,389 @@ def test_create_job(httpx_mock, platform_client: UnstructuredClient, platform_ap
assert job.status == "SCHEDULED"
assert job.job_type == "template"
assert job.created_at == datetime.fromisoformat("2025-06-22T11:37:21.648+00:00")
+
+
+def _job(job_id: str, status: str) -> dict:
+ return {
+ "created_at": "2025-06-22T11:37:21.648Z",
+ "id": job_id,
+ "status": status,
+ "runtime": None,
+ "workflow_id": "16b80fee-64dc-472d-8f26-1d7729b6423d",
+ "workflow_name": "test_workflow",
+ }
+
+
+def test_get_job_rejected(httpx_mock, platform_client: UnstructuredClient, platform_api_url: str):
+ """A preflight-rejected job deserializes instead of raising."""
+ job_id = "fcdc4994-eea5-425c-91fa-e03f2bd8030d"
+ url = f"{platform_api_url}/api/v1/jobs/{job_id}"
+
+ httpx_mock.add_response(
+ method="GET",
+ headers={"Content-Type": "application/json"},
+ json=_job(job_id, "REJECTED"),
+ url=url,
+ )
+
+ job_response = platform_client.jobs.get_job(
+ request=operations.GetJobRequest(job_id=job_id)
+ )
+ assert job_response.status_code == 200
+ assert job_response.job_information.status == shared.JobStatus.REJECTED
+
+
+def test_list_jobs_mixed_page_with_rejected(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """One rejected job must not fail the whole page - the page is validated as one list."""
+ url = f"{platform_api_url}/api/v1/jobs/"
+
+ httpx_mock.add_response(
+ method="GET",
+ headers={"Content-Type": "application/json"},
+ json=[
+ _job("fcdc4994-eea5-425c-91fa-e03f2bd8030d", "IN_PROGRESS"),
+ _job("2b1e7f10-0d9a-4f4d-9d1e-6c9f0a1b2c3d", "REJECTED"),
+ _job("3c2f8a21-1e0b-5a5e-8e2f-7d0a1b2c3d4e", "COMPLETED"),
+ ],
+ url=url,
+ )
+
+ jobs_response = platform_client.jobs.list_jobs(request=operations.ListJobsRequest())
+ assert jobs_response.status_code == 200
+
+ statuses = [job.status for job in jobs_response.response_list_jobs]
+ assert statuses == [
+ shared.JobStatus.IN_PROGRESS,
+ shared.JobStatus.REJECTED,
+ shared.JobStatus.COMPLETED,
+ ]
+
+
+def test_get_job_details_rejected(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """processing_status is a second closed enum that REJECTED must not break."""
+ job_id = "fcdc4994-eea5-425c-91fa-e03f2bd8030d"
+ url = f"{platform_api_url}/api/v1/jobs/{job_id}/details"
+
+ httpx_mock.add_response(
+ method="GET",
+ headers={"Content-Type": "application/json"},
+ json={"id": job_id, "processing_status": "REJECTED", "node_stats": []},
+ url=url,
+ )
+
+ details_response = platform_client.jobs.get_job_details(
+ request=operations.GetJobDetailsRequest(job_id=job_id)
+ )
+ assert details_response.status_code == 200
+ assert (
+ details_response.job_details.processing_status
+ == shared.JobProcessingStatus.REJECTED
+ )
+
+
+@pytest.mark.parametrize("unknown_status", ["PAUSED", "SOME_FUTURE_STATUS"])
+def test_list_jobs_tolerates_unknown_status(
+ httpx_mock,
+ platform_client: UnstructuredClient,
+ platform_api_url: str,
+ unknown_status: str,
+):
+ """A status added server-side after this release must not break the client.
+
+ The raw value is preserved, so callers can log exactly what the server sent.
+ """
+ url = f"{platform_api_url}/api/v1/jobs/"
+
+ httpx_mock.add_response(
+ method="GET",
+ headers={"Content-Type": "application/json"},
+ json=[
+ _job("fcdc4994-eea5-425c-91fa-e03f2bd8030d", "IN_PROGRESS"),
+ _job("2b1e7f10-0d9a-4f4d-9d1e-6c9f0a1b2c3d", unknown_status),
+ ],
+ url=url,
+ )
+
+ jobs_response = platform_client.jobs.list_jobs(request=operations.ListJobsRequest())
+ assert jobs_response.status_code == 200
+
+ unknown = jobs_response.response_list_jobs[1].status
+ assert unknown.value == unknown_status
+ assert isinstance(unknown, shared.JobStatus)
+ # The unknown value must not leak into the declared member set.
+ assert unknown_status not in shared.JobStatus.__members__
+
+
+def _sent_field_names(httpx_mock) -> set[str]:
+ """Every form field name the SDK sent, for either encoding."""
+ request = httpx_mock.get_requests()[0]
+ body = request.read().decode()
+ if "multipart/form-data" in request.headers.get("content-type", ""):
+ # (? str:
+ """Pull the `request_data` form field out of the body the SDK actually sent.
+
+ Handles both encodings: httpx sends `multipart/form-data` once there is a file part and
+ falls back to `application/x-www-form-urlencoded` when the body is fields only.
+ """
+ requests = httpx_mock.get_requests()
+ assert len(requests) == 1
+ request = requests[0]
+ body = request.read().decode()
+
+ if "multipart/form-data" in request.headers.get("content-type", ""):
+ match = re.search(r'name="request_data"\r\n\r\n(.*?)\r\n--', body, re.DOTALL)
+ assert match, f"no request_data part in multipart body: {body!r}"
+ return match.group(1)
+
+ fields = parse_qs(body)
+ assert "request_data" in fields, f"no request_data field in body: {body!r}"
+ return fields["request_data"][0]
+
+
+def _mock_create_job(httpx_mock, platform_api_url: str) -> None:
+ httpx_mock.add_response(
+ method="POST",
+ headers={"Content-Type": "application/json"},
+ json=_job("fcdc4994-eea5-425c-91fa-e03f2bd8030d", "IN_PROGRESS"),
+ url=f"{platform_api_url}/api/v1/jobs/",
+ )
+
+
+@pytest.mark.parametrize("skip_preflight", [True, False])
+def test_create_job_folds_skip_preflight_into_request_data(
+ httpx_mock,
+ platform_client: UnstructuredClient,
+ platform_api_url: str,
+ skip_preflight: bool,
+):
+ """`skip_preflight` rides inside the `request_data` JSON, not as its own form field.
+
+ The spec types the multipart field as a plain string and describes the payload only in
+ `contentSchema`, so this is the only place the server reads it.
+ """
+ _mock_create_job(httpx_mock, platform_api_url)
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(
+ request_data=json.dumps({"template_id": "some-template"}),
+ skip_preflight=skip_preflight,
+ )
+ )
+ )
+
+ assert json.loads(_sent_request_data(httpx_mock)) == {
+ "template_id": "some-template",
+ "skip_preflight": skip_preflight,
+ }
+ # It must not also appear as a form field of its own. Assert on the parsed field names
+ # rather than a substring of the encoded body, which would pass by coincidence.
+ assert _sent_field_names(httpx_mock) == {"request_data"}
+
+
+def test_create_job_leaves_request_data_untouched_when_unset(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """Unset means the caller's string is passed through byte for byte."""
+ _mock_create_job(httpx_mock, platform_api_url)
+ request_data = '{"template_id":"some-template"}'
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(request_data=request_data)
+ )
+ )
+
+ assert _sent_request_data(httpx_mock) == request_data
+
+
+def test_create_job_skip_preflight_param_overrides_json(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """The explicit parameter wins over a value already in the JSON string."""
+ _mock_create_job(httpx_mock, platform_api_url)
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(
+ request_data='{"template_id":"some-template","skip_preflight":true}',
+ skip_preflight=False,
+ )
+ )
+ )
+
+ assert json.loads(_sent_request_data(httpx_mock))["skip_preflight"] is False
+
+
+def test_create_job_skip_preflight_requires_json_object_request_data():
+ """A non-JSON `request_data` cannot carry the flag, so say so at construction time."""
+ with pytest.raises(ValidationError, match="must be a JSON object"):
+ shared.BodyCreateJob(request_data="not json at all", skip_preflight=True)
+
+ with pytest.raises(ValidationError, match="must be a JSON object"):
+ shared.BodyCreateJob(request_data="[1, 2, 3]", skip_preflight=True)
+
+
+def test_create_job_typed_dict_path_folds_skip_preflight(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """The TypedDict request shape routes through `utils.unmarshal`, so the validator still runs."""
+ _mock_create_job(httpx_mock, platform_api_url)
+
+ platform_client.jobs.create_job(
+ request={
+ "body_create_job": {
+ "request_data": '{"template_id":"some-template"}',
+ "skip_preflight": True,
+ }
+ }
+ )
+
+ assert json.loads(_sent_request_data(httpx_mock))["skip_preflight"] is True
+
+
+def test_create_job_folds_skip_preflight_with_input_files(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """The real production shape: a file upload, which switches the body to multipart.
+
+ Without a file part httpx sends urlencoded, so this is the only test that exercises the
+ multipart branch - and it is the branch where "not a form field of its own" matters most,
+ since a stray part would be indistinguishable from a real one.
+ """
+ _mock_create_job(httpx_mock, platform_api_url)
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(
+ request_data=json.dumps({"job_nodes": []}),
+ input_files=[
+ shared.InputFiles(
+ content=b"hello",
+ file_name="a.pdf",
+ content_type="application/pdf",
+ )
+ ],
+ skip_preflight=True,
+ )
+ )
+ )
+
+ request = httpx_mock.get_requests()[0]
+ assert "multipart/form-data" in request.headers["content-type"]
+ assert json.loads(_sent_request_data(httpx_mock)) == {
+ "job_nodes": [],
+ "skip_preflight": True,
+ }
+ assert _sent_field_names(httpx_mock) == {"request_data", "input_files[]"}
+
+
+def test_create_job_preserves_high_precision_numbers_on_the_wire(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """Byte preservation has to survive form encoding, not just the model.
+
+ The model-level fidelity tests assert on `body.request_data`. This one reads the value back
+ out of the encoded request body, which is what the server actually parses.
+ """
+ _mock_create_job(httpx_mock, platform_api_url)
+ # More precision than a float64 holds, plus an exponent form and a significant trailing zero.
+ request_data = '{"job_nodes":[{"threshold":0.123456789012345678901,"scale":1e5,"pad":1.50}]}'
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(
+ body_create_job=shared.BodyCreateJob(
+ request_data=request_data, skip_preflight=True
+ )
+ )
+ )
+
+ sent = _sent_request_data(httpx_mock)
+ for literal in ("0.123456789012345678901", "1e5", "1.50"):
+ assert literal in sent, f"{literal} was rewritten on the wire: {sent!r}"
+ assert json.loads(sent)["skip_preflight"] is True
+
+
+def test_create_job_with_files_and_flag_survives_reassignment(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """Multipart + fold + idempotency together.
+
+ Each is covered alone; this is the combination a real caller hits when they build a body,
+ attach files, then adjust the flag before sending.
+ """
+ _mock_create_job(httpx_mock, platform_api_url)
+
+ body = shared.BodyCreateJob(
+ request_data='{"job_nodes":[],"scale":1e5}',
+ input_files=[
+ shared.InputFiles(
+ content=b"hello", file_name="a.pdf", content_type="application/pdf"
+ )
+ ],
+ skip_preflight=True,
+ )
+ # Attaching more files must not disturb the already-folded payload.
+ body.input_files = list(body.input_files) + [
+ shared.InputFiles(
+ content=b"world", file_name="b.pdf", content_type="application/pdf"
+ )
+ ]
+ assert "1e5" in body.request_data, "an unrelated assignment reformatted request_data"
+
+ # Flipping the flag must land the new value.
+ body.skip_preflight = False
+
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(body_create_job=body)
+ )
+
+ request = httpx_mock.get_requests()[0]
+ assert "multipart/form-data" in request.headers["content-type"]
+ assert json.loads(_sent_request_data(httpx_mock))["skip_preflight"] is False
+ assert _sent_field_names(httpx_mock) == {"request_data", "input_files[]"}
+
+
+def test_create_job_reusing_one_body_sends_the_value_set_at_call_time(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """A reused body is mutable, so each call must carry whatever was set before it.
+
+ Deterministic stand-in for the shared-mutable-state hazard: the model folds on assignment,
+ so a body shared across calls is only safe if callers mutate it between them, never during.
+ """
+ url = f"{platform_api_url}/api/v1/jobs/"
+ for _ in range(2):
+ httpx_mock.add_response(
+ method="POST",
+ headers={"Content-Type": "application/json"},
+ json=_job("fcdc4994-eea5-425c-91fa-e03f2bd8030d", "IN_PROGRESS"),
+ url=url,
+ )
+
+ body = shared.BodyCreateJob(request_data='{"job_nodes":[]}', skip_preflight=True)
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(body_create_job=body)
+ )
+ body.skip_preflight = False
+ platform_client.jobs.create_job(
+ request=operations.CreateJobRequest(body_create_job=body)
+ )
+
+ requests = httpx_mock.get_requests()
+ assert len(requests) == 2
+ sent = [
+ json.loads(parse_qs(r.read().decode())["request_data"][0])["skip_preflight"]
+ for r in requests
+ ]
+ assert sent == [True, False]
diff --git a/_test_contract/platform_api/test_workflows.py b/_test_contract/platform_api/test_workflows.py
index f6abe1a9..7c0c2b61 100644
--- a/_test_contract/platform_api/test_workflows.py
+++ b/_test_contract/platform_api/test_workflows.py
@@ -1,3 +1,4 @@
+import json
from datetime import datetime
import pytest
@@ -256,4 +257,193 @@ def test_run_workflow(httpx_mock, platform_client: UnstructuredClient, platform_
new_job = run_workflow_response.job_information
assert new_job.id == "fcdc4994-eea5-425c-91fa-e03f2bd8030d"
assert new_job.workflow_name == "test_workflow"
- assert new_job.status == "IN_PROGRESS"
\ No newline at end of file
+ assert new_job.status == "IN_PROGRESS"
+
+WORKFLOW_ID = "16b80fee-64dc-472d-8f26-1d7729b6423d"
+
+
+def _workflow_json(**overrides) -> dict:
+ payload = {
+ "created_at": "2025-06-22T11:37:21.648Z",
+ "destinations": ["aeebecc7-9d8e-4625-bf1d-815c2f084869"],
+ "id": WORKFLOW_ID,
+ "name": "test_workflow",
+ "sources": ["f1f7b1b2-8e4b-4a2b-8f1d-3e3c7c9e5a3c"],
+ "workflow_nodes": [],
+ "status": "active",
+ "workflow_type": "advanced",
+ }
+ payload.update(overrides)
+ return payload
+
+
+def _sent_body(httpx_mock) -> dict:
+ requests = httpx_mock.get_requests()
+ assert len(requests) == 1
+ return json.loads(requests[0].read())
+
+
+@pytest.mark.parametrize(
+ ("skip_preflight", "expected"),
+ [(True, True), (False, False)],
+)
+def test_create_workflow_sends_skip_preflight(
+ httpx_mock,
+ platform_client: UnstructuredClient,
+ platform_api_url: str,
+ skip_preflight: bool,
+ expected: bool,
+):
+ """An explicit value reaches the wire, `False` included."""
+ httpx_mock.add_response(
+ method="POST",
+ url=f"{platform_api_url}/api/v1/workflows/",
+ status_code=200,
+ json=_workflow_json(skip_preflight=skip_preflight),
+ )
+
+ platform_client.workflows.create_workflow(
+ request=operations.CreateWorkflowRequest(
+ create_workflow=shared.CreateWorkflow(
+ name="test_workflow",
+ workflow_type="advanced",
+ skip_preflight=skip_preflight,
+ )
+ )
+ )
+
+ assert _sent_body(httpx_mock)["skip_preflight"] is expected
+
+
+def test_create_workflow_omits_skip_preflight_when_unset(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """Unset must be *absent*, not `false`.
+
+ Regression guard for the `optional_fields` list in `CreateWorkflow.serialize_model`: leave
+ `skip_preflight` out of it and the serializer emits the field on every create call.
+ """
+ httpx_mock.add_response(
+ method="POST",
+ url=f"{platform_api_url}/api/v1/workflows/",
+ status_code=200,
+ json=_workflow_json(),
+ )
+
+ platform_client.workflows.create_workflow(
+ request=operations.CreateWorkflowRequest(
+ create_workflow=shared.CreateWorkflow(
+ name="test_workflow", workflow_type="advanced"
+ )
+ )
+ )
+
+ assert "skip_preflight" not in _sent_body(httpx_mock)
+
+
+@pytest.mark.parametrize("skip_preflight", [True, False])
+def test_update_workflow_sends_skip_preflight(
+ httpx_mock,
+ platform_client: UnstructuredClient,
+ platform_api_url: str,
+ skip_preflight: bool,
+):
+ """`False` must be sent, not dropped - it is how a caller opts back in to preflight."""
+ httpx_mock.add_response(
+ method="PUT",
+ url=f"{platform_api_url}/api/v1/workflows/{WORKFLOW_ID}",
+ status_code=200,
+ json=_workflow_json(skip_preflight=skip_preflight),
+ )
+
+ platform_client.workflows.update_workflow(
+ request=operations.UpdateWorkflowRequest(
+ workflow_id=WORKFLOW_ID,
+ update_workflow=shared.UpdateWorkflow(skip_preflight=skip_preflight),
+ )
+ )
+
+ assert _sent_body(httpx_mock)["skip_preflight"] is skip_preflight
+
+
+def test_update_workflow_omits_skip_preflight_when_unset(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """Omitted means "leave unchanged" server-side, so an unrelated update must not send it."""
+ httpx_mock.add_response(
+ method="PUT",
+ url=f"{platform_api_url}/api/v1/workflows/{WORKFLOW_ID}",
+ status_code=200,
+ json=_workflow_json(skip_preflight=True),
+ )
+
+ platform_client.workflows.update_workflow(
+ request=operations.UpdateWorkflowRequest(
+ workflow_id=WORKFLOW_ID,
+ update_workflow=shared.UpdateWorkflow(name="renamed"),
+ )
+ )
+
+ body = _sent_body(httpx_mock)
+ assert body == {"name": "renamed"}
+
+
+@pytest.mark.parametrize(
+ ("response_json", "expected"),
+ [
+ (_workflow_json(skip_preflight=True), True),
+ (_workflow_json(skip_preflight=False), False),
+ # Absent in the response - defaults to False rather than None.
+ (_workflow_json(), False),
+ ],
+)
+def test_workflow_information_reads_skip_preflight(
+ httpx_mock,
+ platform_client: UnstructuredClient,
+ platform_api_url: str,
+ response_json: dict,
+ expected: bool,
+):
+ """The response echo must be readable.
+
+ Without the field on the model, `extra="ignore"` silently drops what the server sent and a
+ caller cannot tell whether preflight is skipped.
+ """
+ httpx_mock.add_response(
+ method="GET",
+ url=f"{platform_api_url}/api/v1/workflows/{WORKFLOW_ID}",
+ status_code=200,
+ json=response_json,
+ )
+
+ response = platform_client.workflows.get_workflow(
+ request=operations.GetWorkflowRequest(workflow_id=WORKFLOW_ID)
+ )
+
+ assert response.workflow_information.skip_preflight is expected
+
+
+def test_update_workflow_skip_preflight_only_sends_nothing_else(
+ httpx_mock, platform_client: UnstructuredClient, platform_api_url: str
+):
+ """A partial update must carry *only* what the caller set.
+
+ The other tests assert the key is present. This one asserts nothing else leaked in, which is
+ what makes "omit means unchanged" safe: any extra key here would overwrite server state the
+ caller never mentioned.
+ """
+ httpx_mock.add_response(
+ method="PUT",
+ url=f"{platform_api_url}/api/v1/workflows/{WORKFLOW_ID}",
+ status_code=200,
+ json=_workflow_json(skip_preflight=True),
+ )
+
+ platform_client.workflows.update_workflow(
+ request=operations.UpdateWorkflowRequest(
+ workflow_id=WORKFLOW_ID,
+ update_workflow=shared.UpdateWorkflow(skip_preflight=True),
+ )
+ )
+
+ assert _sent_body(httpx_mock) == {"skip_preflight": True}
diff --git a/_test_unstructured_client/unit/test_skip_preflight_models.py b/_test_unstructured_client/unit/test_skip_preflight_models.py
new file mode 100644
index 00000000..5c84d801
--- /dev/null
+++ b/_test_unstructured_client/unit/test_skip_preflight_models.py
@@ -0,0 +1,340 @@
+"""Behaviour of the hand-maintained `skip_preflight` and job-status model code.
+
+These assert what the models *do*, not how they are protected from regeneration: `.genignore`
+was removed from this repo, so a test asserting entries in it would assert nothing. What is
+left here is the real cover - if a regeneration (or a careless edit) drops `skip_preflight`,
+the fold, or the enums' forward tolerance, these fail.
+"""
+
+import inspect
+import json
+from pathlib import Path
+
+import pytest
+
+from unstructured_client.models import shared
+from unstructured_client.types import Unset
+from unstructured_client.utils.metadata import MultipartFormMetadata, find_field_metadata
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+
+def test_job_status_enums_stay_forward_tolerant():
+ """REJECTED and the `_missing_` hook are hand-written; nothing regenerates them.
+
+ REJECTED is in the live spec, but the `_missing_` hook cannot be: a generated enum is
+ closed. Nothing regenerates this file, so this test is what protects both.
+ """
+ for enum in (shared.JobStatus, shared.JobProcessingStatus):
+ assert enum.REJECTED.value == "REJECTED"
+
+ # An unrecognised value is preserved verbatim rather than raising or
+ # collapsing to a sentinel.
+ unknown = enum("SOME_FUTURE_STATUS")
+ assert isinstance(unknown, enum)
+ assert unknown.value == "SOME_FUTURE_STATUS"
+ # Repeated lookups must return the same object, not grow a new one each time.
+ assert unknown is enum("SOME_FUTURE_STATUS")
+ # ...and must not leak into the declared member set.
+ assert "SOME_FUTURE_STATUS" not in enum.__members__
+
+ for name in ("jobstatus", "jobprocessingstatus"):
+ docs = (REPO_ROOT / f"docs/models/shared/{name}.md").read_text()
+ assert "REJECTED" in docs
+
+
+def test_workflow_models_expose_skip_preflight():
+ """`skip_preflight` is the preflight opt-out; it must stay on all four models.
+
+ It mirrors `reprocess_all` exactly: `OptionalNullable[bool] = UNSET` on the two request
+ models so an unset value is omitted rather than sent, and `Optional[bool] = False` on the
+ response model so the server's echo is readable.
+ """
+ for model in (shared.CreateWorkflow, shared.UpdateWorkflow):
+ field = model.model_fields["skip_preflight"]
+ assert isinstance(field.default, Unset), (
+ f"{model.__name__}.skip_preflight must default to UNSET so an unset value is "
+ "omitted from the request body, not sent as false"
+ )
+ # Both lists in serialize_model matter: missing from optional_fields, the serializer's
+ # `elif` branch emits the field on every call.
+ source = inspect.getsource(model.serialize_model)
+ assert source.count('"skip_preflight"') == 2, (
+ f"{model.__name__}.serialize_model must list skip_preflight in BOTH "
+ "optional_fields and nullable_fields"
+ )
+
+ info_field = shared.WorkflowInformation.model_fields["skip_preflight"]
+ assert info_field.default is False
+
+ job_field = shared.BodyCreateJob.model_fields["skip_preflight"]
+ assert isinstance(job_field.default, Unset)
+ # Deliberately carries no multipart metadata: the validator folds it into request_data, and
+ # serialize_multipart_form skips any field without MultipartFormMetadata.
+ assert find_field_metadata(job_field, MultipartFormMetadata) is None
+
+
+def test_body_create_job_folds_skip_preflight_into_request_data():
+ """Unset must be tested with `isinstance(..., Unset)`, never `is UNSET`.
+
+ Pydantic deep-copies the default per instance, so an identity check against the singleton
+ silently fails open and rewrites `request_data` on every call.
+ """
+ untouched = '{"template_id":"t"}'
+ assert shared.BodyCreateJob(request_data=untouched).request_data == untouched
+
+ for value in (True, False):
+ body = shared.BodyCreateJob(request_data=untouched, skip_preflight=value)
+ assert json.loads(body.request_data) == {"template_id": "t", "skip_preflight": value}
+
+
+def test_unset_is_publicly_importable():
+ """`Unset` must stay exported, or `body_create_job` cannot import it.
+
+ `UNSET` alone is not enough: pydantic deep-copies the default per model instance, so callers
+ need the *type* to test for unset-ness rather than an identity check against the singleton.
+ Without this export the only route is a private import of `utils.values._is_set` from a
+ models module, which crosses a package boundary for an underscore-prefixed name.
+ """
+ from unstructured_client import types
+
+ assert "Unset" in types.__all__
+ assert isinstance(types.UNSET, types.Unset)
+
+ # The property that makes the type necessary in the first place.
+ body = shared.BodyCreateJob(request_data="{}")
+ assert body.skip_preflight is not types.UNSET
+ assert isinstance(body.skip_preflight, types.Unset)
+
+
+def test_body_create_job_folds_on_assignment_not_only_construction():
+ """`skip_preflight` must survive being set after construction.
+
+ The field carries no multipart metadata, so a dropped fold reaches the wire as nothing at
+ all and raises nothing - the job would run *with* preflight while the caller believes it is
+ off. `validate_assignment=True` on the model is what closes that; the fold writes through
+ `__dict__` to avoid re-entering itself.
+ """
+ body = shared.BodyCreateJob(request_data='{"a":1}')
+ assert body.request_data == '{"a":1}', "unset must not rewrite the payload"
+
+ body.skip_preflight = True
+ assert json.loads(body.request_data) == {"a": 1, "skip_preflight": True}
+
+ # Toggling back must write false, not leave the previous true in place.
+ body.skip_preflight = False
+ assert json.loads(body.request_data) == {"a": 1, "skip_preflight": False}
+
+ # Replacing the payload must re-apply the flag rather than lose it.
+ body.request_data = '{"z":9}'
+ assert json.loads(body.request_data) == {"z": 9, "skip_preflight": False}
+
+ # The base model config must be merged, not replaced.
+ assert shared.BodyCreateJob.model_config["populate_by_name"] is True
+ assert shared.BodyCreateJob.model_config["validate_assignment"] is True
+
+
+def test_body_create_job_folds_skip_preflight_through_model_copy():
+ """`model_copy(update=...)` bypasses validation, so the fold has to be re-applied.
+
+ The dangerous direction is the second case: a body that already folded `true`, copied with
+ `skip_preflight=False`, would keep sending `true` and run the job with preflight disabled
+ while the caller believes they just re-enabled it.
+ """
+ lost = shared.BodyCreateJob(request_data='{"a":1}').model_copy(
+ update={"skip_preflight": True}
+ )
+ assert json.loads(lost.request_data)["skip_preflight"] is True
+
+ reenabled = shared.BodyCreateJob(
+ request_data='{"a":1}', skip_preflight=True
+ ).model_copy(update={"skip_preflight": False})
+ assert json.loads(reenabled.request_data)["skip_preflight"] is False
+
+ # A copy with no update must not change the payload.
+ body = shared.BodyCreateJob(request_data='{"a":1}', skip_preflight=True)
+ for copied in (body.model_copy(), body.model_copy(deep=True)):
+ assert copied.request_data == body.request_data
+
+
+@pytest.mark.parametrize(
+ "request_data",
+ [
+ '{"x": 0.123456789012345678901}', # more precision than a float64 can hold
+ '{"x": 1e5}', # exponent form survives
+ '{"x": 1E+2}',
+ '{"x": 1.50}', # trailing zero is significant to some consumers
+ '{"n": "café"}', # non-ASCII stays as written
+ '{"a":1}',
+ ],
+)
+def test_body_create_job_fold_preserves_the_callers_bytes(request_data):
+ """Adding the flag must not re-encode the rest of the payload.
+
+ A json.loads/json.dumps round trip preserves *values* but not their representation, and
+ `request_data` carries caller configuration that may reach precision-sensitive consumers.
+ The fold splices instead, so every other byte survives verbatim.
+ """
+ folded = shared.BodyCreateJob(
+ request_data=request_data, skip_preflight=True
+ ).request_data
+
+ original_body = request_data.strip()[1:-1].strip()
+ assert original_body in folded, f"{original_body!r} was rewritten: {folded!r}"
+ assert json.loads(folded)["skip_preflight"] is True
+
+
+@pytest.mark.parametrize("request_data", ["{}", "{ }", '{"a":1} ', ' {"a":1}'])
+def test_body_create_job_fold_handles_empty_and_padded_objects(request_data):
+ """The splice has to cope with an empty object and with surrounding whitespace."""
+ folded = shared.BodyCreateJob(
+ request_data=request_data, skip_preflight=True
+ ).request_data
+ assert json.loads(folded)["skip_preflight"] is True
+
+
+@pytest.mark.parametrize(
+ "request_data",
+ [
+ '{\n "a": 1\n}\n', # indentation and a trailing newline
+ '{\n "x": 0.123456789012345678901\n}\n', # ...alongside a number that must not move
+ '{"a":1} ', # trailing spaces
+ ' {"a":1}', # leading spaces
+ "{ }", # the gap inside an otherwise empty object
+ ],
+)
+def test_body_create_job_fold_preserves_surrounding_whitespace(request_data):
+ """Whitespace is insignificant to a parser, but the fold claims to preserve every byte.
+
+ Splicing the flag in must put the layout back: the indentation before the closing brace,
+ the newline after it, and anything ahead of the opening brace. Otherwise the payload is
+ quietly reformatted, which is the exact thing the splice exists to avoid.
+ """
+ folded = shared.BodyCreateJob(
+ request_data=request_data, skip_preflight=True
+ ).request_data
+
+ # Removing exactly what was inserted must give back the original, byte for byte.
+ inserted = '"skip_preflight": true'
+ restored = folded.replace(f",{inserted}", "", 1) if f",{inserted}" in folded else folded.replace(inserted, "", 1)
+ assert restored == request_data, (
+ f"fold did not preserve the payload: {request_data!r} -> {folded!r}"
+ )
+ assert json.loads(folded)["skip_preflight"] is True
+
+
+def test_body_create_job_fold_is_a_no_op_once_correct():
+ """Re-running the fold must not touch an already-correct payload.
+
+ `validate_assignment=True` re-runs every model validator on *any* field assignment, so
+ without an early return, touching an unrelated field would drive the payload down the
+ re-encode branch and silently reformat it - undoing the byte-preservation above.
+ """
+ body = shared.BodyCreateJob(request_data='{"x": 1e5}', skip_preflight=True)
+ folded = body.request_data
+ assert "1e5" in folded
+
+ body.input_files = None
+ assert body.request_data == folded, "an unrelated assignment reformatted request_data"
+
+ unchanged = body.model_copy(update={"input_files": None})
+ assert unchanged.request_data == folded
+
+
+def test_body_create_job_none_clears_a_previously_folded_flag():
+ """Clearing the flag must clear it from the payload too.
+
+ `None` is "no preference", which the server reads as not set. Leaving a stale
+ `"skip_preflight": true` behind would run the job with preflight off *after* the caller
+ cleared the flag - silent, and in the permissive direction.
+ """
+ body = shared.BodyCreateJob(request_data='{"a":1}', skip_preflight=True)
+ assert json.loads(body.request_data)["skip_preflight"] is True
+
+ body.skip_preflight = None
+ assert "skip_preflight" not in json.loads(body.request_data)
+
+ copied = shared.BodyCreateJob(
+ request_data='{"a":1}', skip_preflight=True
+ ).model_copy(update={"skip_preflight": None})
+ assert "skip_preflight" not in json.loads(copied.request_data)
+
+
+def test_body_create_job_unset_still_passes_a_caller_written_flag_through():
+ """`Unset` and `None` are not the same, and only `None` may rewrite the payload.
+
+ A caller who put `skip_preflight` in the JSON themselves and never touched the argument
+ has said nothing for the SDK to override, so those bytes must survive verbatim. Clearing
+ them here would be the SDK silently reversing the caller's own instruction.
+ """
+ written = '{"a":1,"skip_preflight":true}'
+ assert shared.BodyCreateJob(request_data=written).request_data == written
+
+
+@pytest.mark.parametrize(
+ "request_data", ["not json at all", '{"x": 1e5}', "{}"]
+)
+def test_body_create_job_none_is_harmless_when_there_is_nothing_to_clear(request_data):
+ """"No preference" has nothing to reject and nothing to rewrite.
+
+ Unlike the True/False paths it must not raise on a non-JSON payload: nothing was folded
+ into it, so there is nothing to undo.
+ """
+ assert (
+ shared.BodyCreateJob(
+ request_data=request_data, skip_preflight=None
+ ).request_data
+ == request_data
+ )
+
+
+@pytest.mark.parametrize(
+ "request_data",
+ [
+ '{"a":1}',
+ "{}",
+ "{ }",
+ '{\n "x": 1e5\n}\n',
+ '{"x": 0.123456789012345678901}',
+ '{"n": "café"}',
+ '{"a": {"b": 1}}',
+ ],
+)
+def test_body_create_job_fold_then_clear_is_a_round_trip(request_data):
+ """Clearing the flag must restore the caller's bytes, not re-encode them.
+
+ The fold writes a fixed fragment, so clearing can be its exact inverse. Re-encoding here
+ would preserve values but not their representation (`1e5` -> `100000.0`), which would be
+ inconsistent: protecting the payload when adding the flag and discarding it when removing
+ the flag.
+ """
+ body = shared.BodyCreateJob(request_data=request_data, skip_preflight=True)
+ assert json.loads(body.request_data)["skip_preflight"] is True
+
+ body.skip_preflight = None
+ assert body.request_data == request_data
+
+
+def test_body_create_job_clear_does_not_touch_a_nested_skip_preflight():
+ """The removal must find *our* fragment, not one nested inside the payload.
+
+ A naive first-match removal would strip the nested key and corrupt the caller's data. The
+ candidate is parsed and compared before being accepted, so a wrong match is rejected.
+ """
+ nested = '{"x":{"skip_preflight": true},"a":1}'
+ body = shared.BodyCreateJob(request_data=nested, skip_preflight=False)
+ body.skip_preflight = None
+
+ assert body.request_data == nested
+ assert json.loads(body.request_data) == {"x": {"skip_preflight": True}, "a": 1}
+
+
+def test_body_create_job_clear_falls_back_when_the_key_was_written_by_hand():
+ """A caller's own spacing will not match the fold's fragment, so this re-encodes.
+
+ Correct but not byte-preserving, and the flag must still be gone.
+ """
+ body = shared.BodyCreateJob(
+ request_data='{"a":1, "skip_preflight" : true}', skip_preflight=None
+ )
+ assert json.loads(body.request_data) == {"a": 1}
diff --git a/docs/models/shared/bodycreatejob.md b/docs/models/shared/bodycreatejob.md
index 1632b184..6600a64b 100644
--- a/docs/models/shared/bodycreatejob.md
+++ b/docs/models/shared/bodycreatejob.md
@@ -6,4 +6,5 @@
| Field | Type | Required | Description |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `input_files` | List[[shared.InputFiles](../../models/shared/inputfiles.md)] | :heavy_minus_sign: | N/A |
-| `request_data` | *str* | :heavy_check_mark: | N/A |
\ No newline at end of file
+| `request_data` | *str* | :heavy_check_mark: | N/A |
+| `skip_preflight` | *OptionalNullable[bool]* | :heavy_minus_sign: | Skip the job preflight check for this job. Preflight runs by default. Folded into the `request_data` JSON on serialization rather than sent as its own form field. |
\ No newline at end of file
diff --git a/docs/models/shared/createworkflow.md b/docs/models/shared/createworkflow.md
index a2f5f669..99aafb19 100644
--- a/docs/models/shared/createworkflow.md
+++ b/docs/models/shared/createworkflow.md
@@ -9,6 +9,7 @@
| `name` | *str* | :heavy_check_mark: | N/A |
| `reprocess_all` | *OptionalNullable[bool]* | :heavy_minus_sign: | N/A |
| `schedule` | [OptionalNullable[shared.Schedule]](../../models/shared/schedule.md) | :heavy_minus_sign: | N/A |
+| `skip_preflight` | *OptionalNullable[bool]* | :heavy_minus_sign: | Skip the job preflight check for this workflow. Preflight runs by default. |
| `source_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
| `template_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
| `workflow_nodes` | List[[shared.WorkflowNode](../../models/shared/workflownode.md)] | :heavy_minus_sign: | N/A |
diff --git a/docs/models/shared/jobprocessingstatus.md b/docs/models/shared/jobprocessingstatus.md
index 0e666dff..17da7ee8 100644
--- a/docs/models/shared/jobprocessingstatus.md
+++ b/docs/models/shared/jobprocessingstatus.md
@@ -10,4 +10,8 @@
| `SUCCESS` | SUCCESS |
| `COMPLETED_WITH_ERRORS` | COMPLETED_WITH_ERRORS |
| `STOPPED` | STOPPED |
-| `FAILED` | FAILED |
\ No newline at end of file
+| `FAILED` | FAILED |
+| `REJECTED` | REJECTED |
+
+An unrecognised value returned by the server is preserved verbatim as a pseudo-member
+rather than raising, so a new server-side status is not a client break.
diff --git a/docs/models/shared/jobstatus.md b/docs/models/shared/jobstatus.md
index 8846b157..9cab30dc 100644
--- a/docs/models/shared/jobstatus.md
+++ b/docs/models/shared/jobstatus.md
@@ -9,4 +9,8 @@
| `IN_PROGRESS` | IN_PROGRESS |
| `COMPLETED` | COMPLETED |
| `STOPPED` | STOPPED |
-| `FAILED` | FAILED |
\ No newline at end of file
+| `FAILED` | FAILED |
+| `REJECTED` | REJECTED |
+
+An unrecognised value returned by the server is preserved verbatim as a pseudo-member
+rather than raising, so a new server-side status is not a client break.
diff --git a/docs/models/shared/updateworkflow.md b/docs/models/shared/updateworkflow.md
index 5f3b2014..3ba9d491 100644
--- a/docs/models/shared/updateworkflow.md
+++ b/docs/models/shared/updateworkflow.md
@@ -9,6 +9,7 @@
| `name` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
| `reprocess_all` | *OptionalNullable[bool]* | :heavy_minus_sign: | N/A |
| `schedule` | [OptionalNullable[shared.UpdateWorkflowSchedule]](../../models/shared/updateworkflowschedule.md) | :heavy_minus_sign: | N/A |
+| `skip_preflight` | *OptionalNullable[bool]* | :heavy_minus_sign: | Skip the job preflight check for this workflow. Omit to leave the current value unchanged; send `false` to opt back in. |
| `source_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
| `template_id` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
| `workflow_nodes` | List[[shared.WorkflowNode](../../models/shared/workflownode.md)] | :heavy_minus_sign: | N/A |
diff --git a/docs/models/shared/workflowinformation.md b/docs/models/shared/workflowinformation.md
index f8c4b867..e121f749 100644
--- a/docs/models/shared/workflowinformation.md
+++ b/docs/models/shared/workflowinformation.md
@@ -11,6 +11,7 @@
| `name` | *str* | :heavy_check_mark: | N/A | |
| `reprocess_all` | *Optional[bool]* | :heavy_minus_sign: | N/A | |
| `schedule` | [OptionalNullable[shared.WorkflowSchedule]](../../models/shared/workflowschedule.md) | :heavy_minus_sign: | N/A | {
"crontab_entries": [
{
"cron_expression": "0 0 * * *"
}
]
} |
+| `skip_preflight` | *Optional[bool]* | :heavy_minus_sign: | Whether the job preflight check is skipped for this workflow. | |
| `sources` | List[*str*] | :heavy_check_mark: | N/A | |
| `status` | [shared.WorkflowState](../../models/shared/workflowstate.md) | :heavy_check_mark: | N/A | |
| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | N/A | |
diff --git a/src/unstructured_client/_version.py b/src/unstructured_client/_version.py
index 85d5419c..51a85b03 100644
--- a/src/unstructured_client/_version.py
+++ b/src/unstructured_client/_version.py
@@ -3,10 +3,10 @@
import importlib.metadata
__title__: str = "unstructured-client"
-__version__: str = "0.46.2"
+__version__: str = "0.47.0"
__openapi_doc_version__: str = "1.2.31"
__gen_version__: str = "2.680.0"
-__user_agent__: str = "speakeasy-sdk/python 0.46.2 2.680.0 1.2.31 unstructured-client"
+__user_agent__: str = "speakeasy-sdk/python 0.47.0 2.680.0 1.2.31 unstructured-client"
try:
if __package__ is not None:
diff --git a/src/unstructured_client/models/shared/body_create_job.py b/src/unstructured_client/models/shared/body_create_job.py
index 129293ff..e3b50450 100644
--- a/src/unstructured_client/models/shared/body_create_job.py
+++ b/src/unstructured_client/models/shared/body_create_job.py
@@ -2,8 +2,9 @@
from __future__ import annotations
import io
+import json
import pydantic
-from pydantic import model_serializer
+from pydantic import ConfigDict, model_serializer, model_validator
from typing import IO, List, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
from unstructured_client.types import (
@@ -12,10 +13,49 @@
OptionalNullable,
UNSET,
UNSET_SENTINEL,
+ Unset,
)
from unstructured_client.utils import FieldMetadata, MultipartFormMetadata
+def _drop_skip_preflight(raw: str, payload: dict) -> str:
+ """Remove `skip_preflight` from `raw`, keeping every other byte where possible.
+
+ The fold writes a known, fixed fragment, so clearing it can be its exact inverse rather
+ than a re-encode. That matters: a re-encode preserves values but not their representation
+ (``1e5`` becomes ``100000.0``), and it would be inconsistent to protect the caller's bytes
+ when adding the flag and discard them when removing it.
+
+ The candidate is parsed and compared before being accepted, so a fragment that happens to
+ match a *nested* `skip_preflight` is rejected rather than silently corrupting the payload.
+ A caller who wrote the key with their own spacing will not match the fragment at all; that
+ falls back to a re-encode, which is correct but not byte-preserving.
+ """
+ expected = {k: v for k, v in payload.items() if k != "skip_preflight"}
+
+ current = payload["skip_preflight"]
+ if current is True or current is False:
+ literal = "true" if current else "false"
+ # Longest first: the comma form is what a non-empty object gets.
+ for fragment in (
+ f',"skip_preflight": {literal}',
+ f'"skip_preflight": {literal}',
+ ):
+ # From the right: the fold always appends after existing content, so when the
+ # payload also carries a nested `skip_preflight`, ours is the later one.
+ index = raw.rfind(fragment)
+ if index == -1:
+ continue
+ candidate = raw[:index] + raw[index + len(fragment) :]
+ try:
+ if json.loads(candidate) == expected:
+ return candidate
+ except json.JSONDecodeError:
+ continue
+
+ return json.dumps(expected, ensure_ascii=False)
+
+
class InputFilesTypedDict(TypedDict):
content: Union[bytes, IO[bytes], io.BufferedReader]
file_name: str
@@ -43,9 +83,16 @@ class InputFiles(BaseModel):
class BodyCreateJobTypedDict(TypedDict):
request_data: str
input_files: NotRequired[Nullable[List[InputFilesTypedDict]]]
+ skip_preflight: NotRequired[Nullable[bool]]
class BodyCreateJob(BaseModel):
+ # The only model here with a derived invariant across two fields: `skip_preflight` has to
+ # end up inside `request_data`. Without this, `body.skip_preflight = True` after
+ # construction would be silently dropped - the field carries no multipart metadata, so
+ # nothing reaches the wire and nothing raises. Merged with the base config, not replacing it.
+ model_config = ConfigDict(validate_assignment=True)
+
request_data: Annotated[str, FieldMetadata(multipart=True)]
input_files: Annotated[
@@ -53,10 +100,128 @@ class BodyCreateJob(BaseModel):
FieldMetadata(multipart=MultipartFormMetadata(file=True)),
] = UNSET
+ skip_preflight: OptionalNullable[bool] = UNSET
+ r"""Skip the job preflight check for this job. Preflight runs by default.
+
+ A convenience parameter, not a wire field: multipart has no ``skip_preflight`` part, so the
+ server reads it out of the ``request_data`` JSON. The validator below writes it there, which
+ is why this field deliberately carries no ``MultipartFormMetadata`` -- ``serialize_multipart_form``
+ skips any field without it, so it never becomes a form field of its own.
+ """
+
+ @model_validator(mode="after")
+ def fold_skip_preflight_into_request_data(self) -> "BodyCreateJob":
+ """Validator entry point. The work is in ``_fold_skip_preflight`` so that
+ ``model_copy`` can reuse it - a decorated validator is a descriptor, not a callable."""
+ return self._fold_skip_preflight()
+
+ def _fold_skip_preflight(self) -> "BodyCreateJob":
+ """Write ``skip_preflight`` into ``request_data``, where the server reads it.
+
+ Set explicitly, it wins over any value already in the JSON string: the parameter is the
+ more specific expression of intent, and that includes ``None`` - "no preference" clears
+ a value an earlier fold left behind. Left *unset*, ``request_data`` is passed through
+ byte for byte, including a ``skip_preflight`` the caller wrote themselves.
+ """
+ # Tested with `isinstance`, not `is UNSET`: pydantic deep-copies the default per
+ # instance, so an identity check against the UNSET singleton always fails and the
+ # fold would run on every call.
+ if isinstance(self.skip_preflight, Unset):
+ # Never mentioned. `request_data` is the caller's bytes, including a
+ # `skip_preflight` they wrote themselves; pass it through untouched.
+ return self
+
+ if self.skip_preflight is None:
+ # Explicitly "no preference", which the server reads as "not set". If an earlier
+ # fold left a value behind, drop it: the explicit argument wins over whatever is
+ # in the string, and a stale `true` here would run the job with preflight off
+ # after the caller cleared the flag.
+ try:
+ payload = json.loads(self.request_data)
+ except json.JSONDecodeError:
+ # Nothing was ever folded into a non-JSON payload, so there is nothing to
+ # undo. Unlike the True/False paths, "no preference" has no reason to reject.
+ return self
+ if isinstance(payload, dict) and "skip_preflight" in payload:
+ self.__dict__["request_data"] = _drop_skip_preflight(
+ self.request_data, payload
+ )
+ return self
+
+ raw = self.request_data
+ try:
+ payload = json.loads(raw)
+ except json.JSONDecodeError as exc:
+ raise ValueError(
+ "request_data must be a JSON object to use skip_preflight; "
+ f"could not parse it: {exc}"
+ ) from exc
+ if not isinstance(payload, dict):
+ raise ValueError(
+ "request_data must be a JSON object to use skip_preflight, "
+ f"got {type(payload).__name__}"
+ )
+
+ literal = "true" if self.skip_preflight else "false"
+ if payload.get("skip_preflight") is bool(self.skip_preflight):
+ # Already folded and already correct. Returning early keeps this a genuine no-op:
+ # `validate_assignment=True` re-runs the whole validator on *any* field assignment,
+ # so without this, touching an unrelated field would push the payload down the
+ # re-encode branch below and silently reformat it (1e5 -> 100000.0).
+ return self
+
+ if "skip_preflight" in payload:
+ # The caller already put the key in the string. Replacing it in place would mean
+ # locating its span in the raw text, so rewrite the document instead - and accept
+ # that a re-encode is not lexically identical (0.1000 -> 0.1, 1e5 -> 100000.0).
+ payload["skip_preflight"] = bool(self.skip_preflight)
+ folded = json.dumps(payload, ensure_ascii=False)
+ else:
+ # The common case, and deliberately a text splice rather than a re-encode: a
+ # json.loads/json.dumps round trip is lossy for number *representation* even
+ # though it preserves value, and request_data carries caller configuration that
+ # may be read by precision-sensitive consumers. Splicing leaves every other byte
+ # exactly as handed to us.
+ #
+ # json.loads succeeded and produced a dict, so the last non-space character is
+ # guaranteed to be the closing brace. Split the text around it and put every piece
+ # back, so indentation and trailing newlines survive too - insignificant to a JSON
+ # parser, but "every other byte" has to mean every other byte.
+ stripped = raw.rstrip()
+ trailing = raw[len(stripped) :] # whitespace after the closing brace
+ head = stripped[:-1] # everything before the closing brace
+ inner = head.rstrip() # ...without the whitespace that preceded it
+ gap = head[len(inner) :] # ...and that whitespace, kept aside
+ separator = "" if inner.endswith("{") else ","
+ folded = (
+ f'{inner}{separator}"skip_preflight": {literal}{gap}}}{trailing}'
+ )
+
+ # Written through __dict__ on purpose: a normal assignment would re-enter this
+ # validator under validate_assignment=True and recurse until the stack blows.
+ self.__dict__["request_data"] = folded
+ return self
+
+ def model_copy(self, *, update=None, deep=False) -> "BodyCreateJob":
+ """Re-apply the fold after a copy.
+
+ `model_copy` bypasses validation by design, so without this an updated
+ `skip_preflight` never reaches `request_data`. The dangerous direction is
+ `update={"skip_preflight": False}` on a body that already had it folded as `true`:
+ the copy would keep sending `true` and run the job with preflight disabled while the
+ caller believes they just re-enabled it.
+ """
+ copied = super().model_copy(update=update, deep=deep)
+ if update and not {"skip_preflight", "request_data"}.isdisjoint(update):
+ return copied._fold_skip_preflight() # pylint: disable=protected-access
+ # A copy that touches neither field is left strictly alone. Re-folding here would push
+ # an already-folded payload down the re-encode branch and reformat it for no reason.
+ return copied
+
@model_serializer(mode="wrap")
def serialize_model(self, handler):
- optional_fields = ["input_files"]
- nullable_fields = ["input_files"]
+ optional_fields = ["input_files", "skip_preflight"]
+ nullable_fields = ["input_files", "skip_preflight"]
null_default_fields = []
serialized = handler(self)
diff --git a/src/unstructured_client/models/shared/createworkflow.py b/src/unstructured_client/models/shared/createworkflow.py
index 741f838d..feecc6e5 100644
--- a/src/unstructured_client/models/shared/createworkflow.py
+++ b/src/unstructured_client/models/shared/createworkflow.py
@@ -36,6 +36,7 @@ class CreateWorkflowTypedDict(TypedDict):
destination_id: NotRequired[Nullable[str]]
reprocess_all: NotRequired[Nullable[bool]]
schedule: NotRequired[Nullable[Schedule]]
+ skip_preflight: NotRequired[Nullable[bool]]
source_id: NotRequired[Nullable[str]]
template_id: NotRequired[Nullable[str]]
workflow_nodes: NotRequired[Nullable[List[WorkflowNodeTypedDict]]]
@@ -52,6 +53,9 @@ class CreateWorkflow(BaseModel):
schedule: OptionalNullable[Schedule] = UNSET
+ skip_preflight: OptionalNullable[bool] = UNSET
+ r"""Skip the job preflight check for this workflow. Preflight runs by default."""
+
source_id: OptionalNullable[str] = UNSET
template_id: OptionalNullable[str] = UNSET
@@ -64,6 +68,7 @@ def serialize_model(self, handler):
"destination_id",
"reprocess_all",
"schedule",
+ "skip_preflight",
"source_id",
"template_id",
"workflow_nodes",
@@ -72,6 +77,7 @@ def serialize_model(self, handler):
"destination_id",
"reprocess_all",
"schedule",
+ "skip_preflight",
"source_id",
"template_id",
"workflow_nodes",
diff --git a/src/unstructured_client/models/shared/jobprocessingstatus.py b/src/unstructured_client/models/shared/jobprocessingstatus.py
index 5e68d14c..393b20b2 100644
--- a/src/unstructured_client/models/shared/jobprocessingstatus.py
+++ b/src/unstructured_client/models/shared/jobprocessingstatus.py
@@ -2,6 +2,7 @@
from __future__ import annotations
from enum import Enum
+from typing import Optional, cast
class JobProcessingStatus(str, Enum):
@@ -11,3 +12,19 @@ class JobProcessingStatus(str, Enum):
COMPLETED_WITH_ERRORS = "COMPLETED_WITH_ERRORS"
STOPPED = "STOPPED"
FAILED = "FAILED"
+ REJECTED = "REJECTED"
+
+ @classmethod
+ def _missing_(cls, value: object) -> Optional["JobProcessingStatus"]:
+ """Preserve a status this client does not know yet, instead of raising.
+
+ The server adds values on its own schedule; a closed enum breaks clients.
+ """
+ if not isinstance(value, str):
+ return None
+ pseudo = str.__new__(cls, value)
+ pseudo._name_ = value
+ pseudo._value_ = value
+ return cast(
+ "JobProcessingStatus", cls._value2member_map_.setdefault(value, pseudo)
+ )
diff --git a/src/unstructured_client/models/shared/jobstatus.py b/src/unstructured_client/models/shared/jobstatus.py
index bf847dbb..b9077dc9 100644
--- a/src/unstructured_client/models/shared/jobstatus.py
+++ b/src/unstructured_client/models/shared/jobstatus.py
@@ -2,6 +2,7 @@
from __future__ import annotations
from enum import Enum
+from typing import Optional, cast
class JobStatus(str, Enum):
@@ -10,3 +11,19 @@ class JobStatus(str, Enum):
COMPLETED = "COMPLETED"
STOPPED = "STOPPED"
FAILED = "FAILED"
+ REJECTED = "REJECTED"
+
+ @classmethod
+ def _missing_(cls, value: object) -> Optional["JobStatus"]:
+ """Preserve a status this client does not know yet, instead of raising.
+
+ The server adds values on its own schedule; a closed enum breaks clients.
+ """
+ if not isinstance(value, str):
+ return None
+ pseudo = str.__new__(cls, value)
+ pseudo._name_ = value
+ pseudo._value_ = value
+ return cast(
+ "JobStatus", cls._value2member_map_.setdefault(value, pseudo)
+ )
diff --git a/src/unstructured_client/models/shared/updateworkflow.py b/src/unstructured_client/models/shared/updateworkflow.py
index 3f474988..e1bebec4 100644
--- a/src/unstructured_client/models/shared/updateworkflow.py
+++ b/src/unstructured_client/models/shared/updateworkflow.py
@@ -35,6 +35,7 @@ class UpdateWorkflowTypedDict(TypedDict):
name: NotRequired[Nullable[str]]
reprocess_all: NotRequired[Nullable[bool]]
schedule: NotRequired[Nullable[UpdateWorkflowSchedule]]
+ skip_preflight: NotRequired[Nullable[bool]]
source_id: NotRequired[Nullable[str]]
template_id: NotRequired[Nullable[str]]
workflow_nodes: NotRequired[Nullable[List[WorkflowNodeTypedDict]]]
@@ -50,6 +51,12 @@ class UpdateWorkflow(BaseModel):
schedule: OptionalNullable[UpdateWorkflowSchedule] = UNSET
+ skip_preflight: OptionalNullable[bool] = UNSET
+ r"""Skip the job preflight check for this workflow.
+
+ Omit to leave the current value unchanged; send ``False`` to opt back in.
+ """
+
source_id: OptionalNullable[str] = UNSET
template_id: OptionalNullable[str] = UNSET
@@ -65,6 +72,7 @@ def serialize_model(self, handler):
"name",
"reprocess_all",
"schedule",
+ "skip_preflight",
"source_id",
"template_id",
"workflow_nodes",
@@ -75,6 +83,7 @@ def serialize_model(self, handler):
"name",
"reprocess_all",
"schedule",
+ "skip_preflight",
"source_id",
"template_id",
"workflow_nodes",
diff --git a/src/unstructured_client/models/shared/workflowinformation.py b/src/unstructured_client/models/shared/workflowinformation.py
index 2025fa84..7b3f1263 100644
--- a/src/unstructured_client/models/shared/workflowinformation.py
+++ b/src/unstructured_client/models/shared/workflowinformation.py
@@ -28,6 +28,7 @@ class WorkflowInformationTypedDict(TypedDict):
workflow_nodes: List[WorkflowNodeTypedDict]
reprocess_all: NotRequired[bool]
schedule: NotRequired[Nullable[WorkflowScheduleTypedDict]]
+ skip_preflight: NotRequired[bool]
updated_at: NotRequired[Nullable[datetime]]
workflow_type: NotRequired[Nullable[WorkflowType]]
@@ -51,13 +52,22 @@ class WorkflowInformation(BaseModel):
schedule: OptionalNullable[WorkflowSchedule] = UNSET
+ skip_preflight: Optional[bool] = False
+ r"""Whether the job preflight check is skipped for this workflow."""
+
updated_at: OptionalNullable[datetime] = UNSET
workflow_type: OptionalNullable[WorkflowType] = UNSET
@model_serializer(mode="wrap")
def serialize_model(self, handler):
- optional_fields = ["reprocess_all", "schedule", "updated_at", "workflow_type"]
+ optional_fields = [
+ "reprocess_all",
+ "schedule",
+ "skip_preflight",
+ "updated_at",
+ "workflow_type",
+ ]
nullable_fields = ["schedule", "updated_at", "workflow_type"]
null_default_fields = []
diff --git a/src/unstructured_client/types/__init__.py b/src/unstructured_client/types/__init__.py
index fc76fe0c..9385267f 100644
--- a/src/unstructured_client/types/__init__.py
+++ b/src/unstructured_client/types/__init__.py
@@ -8,6 +8,7 @@
UnrecognizedStr,
UNSET,
UNSET_SENTINEL,
+ Unset,
)
__all__ = [
@@ -18,4 +19,5 @@
"UnrecognizedStr",
"UNSET",
"UNSET_SENTINEL",
+ "Unset",
]