Perception
Sources
Every tool takes a source: the document to work on.
| Source | Fields | What it is |
|---|---|---|
UrlSource | url: str, file_name: str | None = None | A public https:// link the document service downloads. |
WorkspaceFileSource | workspace_id: str, file_id: str | A file you uploaded. UploadedFile.as_source() builds one. |
ParseResultSource | job_id: str | One of your finished parse jobs, so Parse, Split, Extract or Ground reuse it instead of parsing and paying again. Classify doesn't take it. |
Each also has a type ("url", "workspace_file", "parse_result") that is set for you, and an unknown field raises pydantic.ValidationError. Source is the union of the three. You can also pass:
- A string, which must start with
https://and becomes aUrlSource. Anything else raisesNaceError. - A dictionary in the wire form, such as
{"type": "workspace_file", "workspace_id": "...", "file_id": "..."}.
The document service picks the parser from a URL source's file_name and refuses an extension the tool doesn't read with unsupported_file_type (see Format support). When you leave file_name out, the SDK takes it from the URL: the last path segment, without its query or fragment, percent-decoded. That segment must have an extension, a . that is neither its first nor its last character; otherwise the SDK raises NaceError before sending anything. A file_name you pass is sent as it is.
client.documents.parse("https://example.com/files/report%20v2.pdf") # file_name="report v2.pdf"
client.documents.parse(UrlSource(url="https://example.com/download?id=42", file_name="report.pdf"))
client.documents.parse(UrlSource(url="https://arxiv.org/pdf/1706.03762", file_name="attention.pdf"))Name the file whenever the last segment isn't the file's real name: https://arxiv.org/pdf/1706.03762 would otherwise be sent as 1706.03762, which the document service refuses.
Create a job
The five tools create a job and return it as a DocumentJob: status="queued", or the finished job when wait_seconds was long enough. Besides source and the tool's own options, each takes:
| Parameter | Type | Required | Description |
|---|---|---|---|
wait_seconds | int | No | 0 to 60. Holds the request open until the job finishes, up to this long; the SDK adds the same time to that request's timeout. Default: None, which returns at once. |
idempotency_key | str | No | Up to 200 characters. Default: one minted per call. |
retry | RetryPolicy | No | Replaces the client's retry policy for this call. |
timeout | float | httpx.Timeout | No | Replaces the client's timeout for this call. |
extra_headers | Mapping[str, str] | No | Extra headers for this call. |
extra_body | Mapping[str, Any] | No | Body fields merged last, so they win over every other field. |
name | str | No | A display name for the job, up to 200 characters. |
project_id | str | No | A UUID you mint to group jobs you submit together. |
Every other keyword argument, name and project_id included, becomes a body field of that tool as you give it, except that None values are left out. The document service refuses a field it doesn't know, a misspelled one included, with 422. A wait_seconds outside 0 to 60, or an empty or too-long idempotency_key, raises NaceError before anything is sent.
Parse, Split, Classify and Extract take page_ranges as a list of {"start": int, "end": int}: one-based and inclusive, at most 200 ranges and 500 pages in all.
wait_seconds is best effort: a job that takes longer comes back queued or running, with result still None. Check status before you read result:
job = client.documents.parse("https://example.com/report.pdf", wait_seconds=60)
if job.status != "succeeded":
job = client.jobs.wait(job.job_id) # raises NaceJobFailedError if it failed
print(job.result["document"]["markdown"])A create that holds the request open is retried like any other call, but the default retry budget is 30 seconds in all, so an attempt that fails after waiting longer than that isn't retried. Raise RetryPolicy.timeout to retry those; see Retries.
Idempotency. Every create carries an Idempotency-Key, so the SDK's own retries return the same job and never bill twice. The minted key is new on each call, though. To retry a create yourself, after a 429, a 503 job_pending or a crash, pass your own idempotency_key and reuse it. When a create fails after the SDK's retries, the error's idempotency_key holds the key it sent, so after a 503 job_pending you can resume the same job with client.documents.parse(..., idempotency_key=error.idempotency_key) and the same body. The same key with a different body raises NaceConflictError (idempotency_conflict). See Retry safely.
Charge id. A create's response carries Drex's id for the charge in the x-drex-document-job header. Read it from the job's HTTP response. A create that reached the document service and failed there, such as a 429 or a 503 job_pending, carries it in the error's headers too; one Drex refused before that, such as a 402 or a 409 idempotency_conflict, has none:
job = client.documents.parse("https://example.com/report.pdf")
print(job.request_id, job.raw_http_response.headers.get("x-drex-document-job"))Quote both when you contact support about a charge.