Printy3D

SDKs

Python SDK

printy3d wraps every /v1 endpoint with typed models, one-call uploads, waiting, verified downloads, automatic idempotency keys and safe retries. Python 3.9 or newer, no dependencies beyond the standard library.

Install#

shell
pip install printy3d

Configure#

The client reads PRINTY3D_API_KEY and, optionally, PRINTY3D_API_URL (default https://printy3d-api-staging.onrender.com). Create a key in the console: a p3d_test_ key runs free, simulated jobs for building your integration; a p3d_live_ key, available once your account is approved, runs the real engine.

python
from printy3d import Printy3D

client = Printy3D()                      # PRINTY3D_API_KEY, PRINTY3D_API_URL
client = Printy3D(
    api_key="p3d_test_...",              # or pass it explicitly
    timeout=150,                         # seconds per API request
    transfer_timeout=120,                # seconds for storage uploads and downloads
    max_retries=4,                       # retries of an idempotent request
    max_retry_delay=60,                  # longest Retry-After slept through
)
print(client.org.get().name)

A first job#

python
job = client.inspect("model.stl")         # upload + inspect + wait
print(job.analysis.dimensions_mm, job.analysis.is_watertight)

job = client.repair("model.zip", profile="character", params={"printer": "mimaki"})
saved = client.download_artifacts(job.id, "out/")   # every artifact, verified
for item in saved:
    print(item.artifact.label, item.path)

inspect, repair, tool and run take a local path (uploaded first), an uploaded File or a file_… id, create the job with a fresh Idempotency-Key and wait for it. Pass wait=False to get the queued job back at once.

Upload#

python
file = client.upload("scan.zip")              # returns the File once it is "ready"
file = client.upload(
    "big.obj",
    resumable=True,                                # force the multipart upload
    on_progress=lambda sent, total: print(f"{sent}/{total}"),
    timeout=900,                                   # seconds to wait for verification
)

Files up to 10 MiB go up in one presigned PUT; larger files use the resumable multipart upload, which needs an approved account. The SDK computes the size and SHA-256, resumes missing parts for up to three rounds, completes the file and waits for verification. A rejected file raises FileRejectedError (its code is the rejected_reason). Details: Uploads.

Inspect, repair and tools#

python
job = client.inspect(file)
recommended = job.analysis.recommended             # RepairParams for this file

job = client.repair(file, params={"scale": {"mode": "target", "value": 6, "unit": "in"}})
job = client.repair(file, profile="mechanical", params={"preserve_shells": True})

job = client.tool(file, "cut", {"planes": [{"origin": [0, 40, 0], "normal": [0, 1, 0]}],
                                "keep_both_sides": True})
job = client.tool(file, "measure", {"point_a": [0, 0, 0], "point_b": [10, 0, 0]})
print(job.result.result["euclidean_distance"])

job = client.run(file, recipe="tool", tool="convert", params={"target_format": "3mf"})

Parameters are plain dicts, typed as TypedDicts (RepairParams, CutParams, LatticeParams, …) for your editor. Every parameter is described on Repair and the Tools pages.

Waiting and progress#

python
from printy3d import JobFailedError, WaitTimeoutError

job = client.repair(file, wait=False)               # queued; returns at once

def show(event):                                    # each new event, in order
    print(event.sequence, event.status, event.stage, event.progress)

try:
    job = client.jobs.wait(job.id, timeout=2 * 3600, poll_interval=2, on_event=show)
except JobFailedError as e:
    print("failed:", e.code, e.job.error_detail)
except WaitTimeoutError as e:
    print("still running server-side:", e.last.status)

jobs.wait polls through transient failures (no response, 429, 502–504), returns on succeeded, raises JobFailedError or JobCancelledError (or returns the job with raise_on_failure=False) and gives up with WaitTimeoutError after timeout seconds (default 4 hours; None waits forever).

Download#

python
for artifact in client.artifacts.list(job.id):
    print(artifact.label, artifact.media_type, artifact.size_bytes)

client.download_artifacts(job.id, "out/", labels=["result_glb", "result_obj_zip"])
path = client.download(artifact, "out/")            # one artifact; dest is a file or a folder
data = client.artifacts.read(artifact)              # bytes in memory

Every download fetches a fresh presigned URL, checks the size and SHA-256 (raising IntegrityError on a mismatch) and writes through a .partial file, so a half-written file never has the final name.

Exports#

python
export = client.export(job.id, "stl")               # waits until "ready"
client.download(export.artifact, "out/")

export = client.export(job.id, "3mf", wait=False)  # "ready", or "converting" with conversion_job_id

A format the job did not produce starts a charged conversion job (test keys: 501); a failed conversion raises ExportFailedError. Exports

Errors#

python
from printy3d import Printy3DError

try:
    job = client.repair("model.stl")
except Printy3DError as e:
    if e.code == "insufficient_credits":
        print(f"{e.required} credits needed, {e.available} available")
    elif e.code == "capacity_exhausted":
        print(f"no capacity ({e.reason}); retry in {e.retry_after} s")
    elif e.code == "validation_failed":
        for field in e.errors:
            print(field["field"], field["message"])
    else:
        print(e.status, e.code, e.detail, "request", e.request_id)

Every API error is a Printy3DError with the problem's status, code, title, detail, errors, request_id, retry_after and the code-specific extras (required, available, required_scope, reason). Other outcomes are subclasses:

ClassRaised when
APIConnectionErrorNo response (DNS, refused, reset) after retries.
APITimeoutErrorA request timed out; a write may have reached the API.
JobFailedErrorA wait saw failed; .job is the job, code its error_code.
JobCancelledErrorA wait saw cancelled.
WaitTimeoutErrorA wait gave up; .last is the last state seen.
FileRejectedErrorUpload verification rejected the file.
ExportFailedErrorAn export's conversion failed.
IntegrityErrorDownloaded bytes did not match size or SHA-256.
UploadIncompleteErrorUpload parts were still missing after every resume round.

Retries and idempotency#

The client retries a request only when repeating it is safe: GET, DELETE and the POSTs that carry an Idempotency-Key (file, job and key creation, rotation). It retries on no response, 429, 502–504 and 409 idempotency_in_progress, with exponential backoff and jitter (0.5 s doubling to 20 s), or the server's Retry-After. A Retry-After above max_retry_delay is raised at once with retry_after set. max_retries=0 turns retries off.

python
# One key per logical operation: safe across process restarts.
job = client.repair(file, idempotency_key=f"order-{order_id}")
job = client.jobs.create(input_file_id=file.id, recipe="repair", idempotency_key=f"order-{order_id}")

API keys and credits#

python
issued = client.api_keys.create(name="ci", scopes=["files:write", "jobs:write", "jobs:read",
                                                "artifacts:read"])
secret = issued.secret                               # shown once; None on an idempotent replay
rotated = client.api_keys.rotate(issued.api_key.id, grace_seconds=3600)
client.api_keys.revoke(rotated.api_key.id)
for key in client.api_keys.iter():
    print(key.prefix, key.kind, key.revoked_at)

balance = client.balance.get()
print(balance.available_credits, balance.low)
for tx in client.balance.iter_transactions():
    print(tx.sequence, tx.kind, tx.available_after)

Managing keys needs a key with keys:manage; the console does the same without one.

All methods#

ResourceMethods
client.orgget()
client.formatslist()
client.profileslist()
client.api_keyslist(), iter(), get(), create(), rotate(), revoke()
client.filesupload(), create(), create_resumable(), complete(), get(), list(), iter(), wait()
client.jobscreate(), get(), list(), iter(), cancel(), events(), iter_events(), wait()
client.artifactslist(), download_url(), read(), download()
client.exportscreate(), get(), wait()
client.balanceget(), transactions(), iter_transactions()
clientupload(), run(), inspect(), repair(), tool(), download(), download_artifacts(), export()

Models#

Responses are dataclasses (Job, File, Artifact, Analysis, RepairResult, Export, Balance, …) with the API's field names. Fields the SDK does not know yet are kept in .extra, so a newer API never breaks an older client.