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#
pip install printy3dConfigure#
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.
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#
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#
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#
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#
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#
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 memoryEvery 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#
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_idA format the job did not produce starts a charged conversion job (test keys: 501); a failed conversion raises ExportFailedError. Exports
Errors#
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:
| Class | Raised when |
|---|---|
APIConnectionError | No response (DNS, refused, reset) after retries. |
APITimeoutError | A request timed out; a write may have reached the API. |
JobFailedError | A wait saw failed; .job is the job, code its error_code. |
JobCancelledError | A wait saw cancelled. |
WaitTimeoutError | A wait gave up; .last is the last state seen. |
FileRejectedError | Upload verification rejected the file. |
ExportFailedError | An export's conversion failed. |
IntegrityError | Downloaded bytes did not match size or SHA-256. |
UploadIncompleteError | Upload 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.
# 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#
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#
| Resource | Methods |
|---|---|
client.org | get() |
client.formats | list() |
client.profiles | list() |
client.api_keys | list(), iter(), get(), create(), rotate(), revoke() |
client.files | upload(), create(), create_resumable(), complete(), get(), list(), iter(), wait() |
client.jobs | create(), get(), list(), iter(), cancel(), events(), iter_events(), wait() |
client.artifacts | list(), download_url(), read(), download() |
client.exports | create(), get(), wait() |
client.balance | get(), transactions(), iter_transactions() |
client | upload(), 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.