Perception

Uploads

Uploads are free and work at any balance. The bytes go from your machine straight to the document service with a one-use token, never through Drex and never with your API key. See Uploads.

Upload a file

client.documents.upload(content, *, path=None, file_name=None, on_conflict=None)

ParameterTypeRequiredDescription
contentstr | PathLike | bytes | BinaryIOYesA path to the file, its bytes, or an open binary file.
pathstrNoPath in the workspace, relative (no leading /). Keep the file's extension. Default: file_name.
file_namestrFor bytes and nameless streamsThe name sent with the file, and the default path. Default: the name of the path or stream. Bytes without it raise ValueError.
on_conflict"reject" / "new_version"NoWhat happens when a different file already has path. reject (the document service's default) fails with path_conflict; new_version stores the upload as that file's next version.

It returns an UploadedFile. Pass file.as_source() to any tool.

upload.py
from nace_sdk import NaceClient

with NaceClient() as client:
    file = client.documents.upload("invoice.pdf", path="invoices/invoice.pdf", on_conflict="new_version")
    job = client.documents.extract(file.as_source(), schema={"type": "object", "properties": {"total": {"type": "number"}}}, wait_seconds=60)
  • Keep the extension on path. The document service types an upload from its first bytes, and from the extension when those don't settle it; it refuses a file it can't type with unsupported_file_type. See Format support.
  • Files of 32 MiB or more (UPLOAD_SESSION_THRESHOLD_BYTES) go up in parts through an upload session, automatically.
  • A path conflict raises NaceConflictError with code path_conflict for a file sent whole. For a file sent in parts it is found when the parts are assembled, and raises NaceError naming the failed upload job and path_conflict.
  • Uploading the same bytes to the same path again returns the file already there.
  • A file still assembling after about ten minutes raises NaceError. The document service keeps assembling it; upload the same file to the same path again later to get it.

Upload grants

client.documents.create_upload_grant(**options) mints a one-use token for one upload, so another client can upload without your API key. It returns an UploadGrant.

ParameterTypeRequiredDescription
pathstrNoPin the upload to exactly this path.
max_bytesintNoRefuse a larger file. Default: the document service's limit for the file type.
total_size_bytesintNoThe exact size the file must have.
ttl_secondsintNoHow long the token works, 1 to 3600. Default: 3600.

Mint the grant on your server and hand upload_url and token to the browser. The browser POSTs the file there as multipart form data, with a metadata part such as {"path": "...", "on_conflict": "new_version"}, only the X-Upload-Token header, and credentials: "omit". The upload's answer carries the file under result.file; send its workspace_id and file_id back to your server and use them as a WorkspaceFileSource. /v1 routes send no Access-Control-Allow-Origin header, so browser code can't call Drex itself; keep every Drex call on your server.

grant = client.documents.create_upload_grant(path="invoices/invoice.pdf", total_size_bytes=48213, ttl_seconds=600)
print(grant.upload_url, grant.expires_at)  # hand grant.upload_url and grant.token to the uploader

Upload sessions

A session uploads a large file in parts, and can resume after a dropped connection. upload runs one for you; drive one yourself to upload parts in parallel or across process restarts.

MethodParametersReturnsDescription
create_upload_session(**options)path: str, total_size_bytes: int (required); ttl_seconds: int (1 to 5400, default 5400), on_conflict, idempotency_keyUploadSessionOpens the session. Carries an Idempotency-Key, yours or one minted per call, so a retry never opens a second one.
upload_part(session, number, data)session: UploadSession, number: int (from 0), data: bytesNoneSends one part: session.chunk_size bytes, the last one shorter. Sending a part again replaces it.
complete_upload_session(session, *, idempotency_key=None)session, idempotency_key: strUploadedFileAssembles the parts into one file and waits for it, for about ten minutes. Calling again with the same key resumes the same completion.
get_upload_session(session)sessionUploadSessionStatusThe session's state and the parts that landed.
abort_upload_session(session)sessionNoneAbandons the session and discards its parts. Aborting a finished or aborted session does nothing.

The part, status, complete and abort calls go straight to the session's upload_session_url with its session_token. complete_upload_session raises NaceError when the upload fails, for example with path_conflict, or when the file is still assembling after its wait; that message names the key to call it again with.

A session refuses calls that don't fit its state. None of these is retried:

  • session_busy (NaceConflictError): sending a part or aborting while the session is completing, or completing it with a different key while a completion runs. Call complete_upload_session again with the same key to resume that completion.
  • upload_expired (NaceConflictError): sending a part to a session that completed or was aborted, or completing an aborted one. Completing a completed session again returns its file.
  • After expires_at the session token stops working, and every call on the session raises NaceAuthenticationError (401). Open a new session.
upload_session.py
from pathlib import Path

from nace_sdk import NaceClient

source = Path("scan.tiff")
with NaceClient() as client:
    session = client.documents.create_upload_session(path="scans/scan.tiff", total_size_bytes=source.stat().st_size)
    with source.open("rb") as handle:
        # After a drop, send only the parts that haven't landed.
        landed = {part["part_number"] for part in client.documents.get_upload_session(session).parts}
        for number in range(session.total_parts):
            if number not in landed:
                handle.seek(number * session.chunk_size)
                client.documents.upload_part(session, number, handle.read(session.chunk_size))
    file = client.documents.complete_upload_session(session)

To resume from another process, save session.model_dump() and rebuild it with UploadSession.model_validate(saved).

Delete a file

client.documents.delete_file(workspace_id, file_id) deletes a file you uploaded: every version of it and everything derived from it. It can't be undone. It's free and works at any balance.

ParameterTypeRequiredDescription
workspace_idstrYesThe file's workspace_id, from UploadedFile. Only your account's workspace.
file_idstrYesThe file's file_id, from UploadedFile.

It returns a FileDeletion. The call waits while the deletion runs: status is deleted once every trace is confirmed gone, or deleting if it is still running, and calling again then reports on the same deletion. Another account's workspace raises NaceNotFoundError with code workspace_not_found, and a file that isn't there file_not_found.

delete_file.py
from nace_sdk import NaceClient

with NaceClient() as client:
    file = client.documents.upload("invoice.pdf")
    deleted = client.documents.delete_file(file.workspace_id, file.file_id)
    print(deleted.status)  # deleted

On this page