Quickstart¶
Install with pip install superb-ai, then authenticate and import a dataset end-to-end.
Auth (3 lines)¶
from superb_ai import Client
client = Client(tenant="acme", api_key="sbd_pk_...") # or set SUPERB_AI_TENANT + SUPERB_AI_API_KEY
# with both env vars set, `Client()` alone works — nothing to hardcode
assert client.projects.list().items is not None # smoke: you're in
Machine keys (sbd_pk_…) are the primary credential. The client.auth.* surface is
JWT-bootstrap-only (browser login dance) — a key holder never calls it. Each key
acts as ONE user; multi-actor workflows need one client per user key.
Client map (namespaces)¶
client. → tenants users api_keys auth account invitations datasets assets projects
classes members guidelines labeling_time tags workflow workflow_batch project_assets
annotations revisions comments versions exports jobs training models deployments
auto_label foundation segment activity visualization
account (my workspaces / accept invitations) is JWT-session-only — an
sbd_pk_… key gets 401 there; machine keys already know their workspace.
invitations (admin: invite email + role, list, revoke) works with keys.
Pagination: list methods return Page[T] — iterate the page object to auto-fetch all
pages (for x in client.annotations.list(pid): ...). include_total=True adds
.total. Non-list collection reads (classes, tags, progress, queue stats) return
complete models, not pages.
Async jobs: kickoff methods return a JobHandle — call .wait(timeout=...); it
raises JobFailedError on failure and JobTimeoutError (job keeps running) on
timeout. handle.result_summary carries per-kind outcome keys.
Batch first — the anti-flood rule¶
Never loop a single-item call when N > ~5. Every hot path has a batch twin (sync, explicit ids, ≤1000) and most have a bulk twin (async job, id-list or filter-scoped). Pick by N:
| doing this per-item? | ≤1000 → sync batch | any size → async bulk (JobHandle) |
|---|---|---|
| create annotations | annotations.asset_batch_create (1 asset) / annotations.project_batch_create |
annotations.import_ndjson |
| edit/delete annotations | annotations.batch_edit (atomic mixed) / annotations.batch_delete (≤500 ids) |
project_assets.bulk_delete_annotations (+class/source predicates) |
| assign/approve/reject/unassign | workflow_batch.batch_* |
workflow_batch.bulk_* (filter-scoped) |
| add/remove project assets | project_assets.batch_add / batch_remove |
project_assets.bulk_add / bulk_remove |
| tag assets | tags.batch_update |
tags.bulk_update |
| delete assets | assets.batch_delete |
assets.bulk_delete |
| upload files | assets.upload_paths (does presign+PUT batching for you) |
— |
| predict on many assets | — | auto_label.create_run (never a predict loop) |
Flood etiquette (the rate buckets in the Limits section are the budget):
- Polling: use the built-in waiters (JobHandle.wait, versions.wait_ready,
training.wait_run — they poll at ~3s). Don't hand-roll tighter loops; for
long jobs (training, big imports) pass a larger poll_interval.
- Counting: include_total=True on one list call — never "iterate and count",
and never per-item GETs to check existence (list with a filter instead).
- Reading many items: iterate the Page (sequential, one request per page at
the max limit) — don't fan out parallel GET-by-id.
- Upload concurrency ≤16 on shared/dev tenants; the write bucket is shared
by everyone on the tenant.
Worked example: import a COCO dataset end-to-end¶
The full path: upload images → create project with class vocabulary → convert COCO GT → bulk-import → verify. Every step below is load-bearing.
1a. Upload images (never hand-roll the presign dance)¶
from pathlib import Path
ds = client.datasets.create(name="COCO val2017")
paths = sorted(Path("/data/val2017").glob("*.jpg"))
client.assets.upload_paths(ds.id, paths, concurrency=8) # presign→PUT, batched
# Asset rows are created ASYNCHRONOUSLY by the platform's storage events.
# Poll until they're all ready:
import time
while True:
ready = client.datasets.stats(ds.id).by_status.get("ready", 0)
if ready >= len(paths): break
time.sleep(10)
asset_by_name = {a.filename: a.id for page in [client.assets.list(ds.id, limit=100)]
for a in page} # iterate the Page for ALL assets
Keep concurrency<=16 on shared/dev environments (rate limits are per-tenant).
1b. Create the project + class vocabulary¶
One geometry type per class (allowed_types). 80 COCO classes = 80 class specs:
classes = [{"name": c["name"], "color": f"#{hash(c['name']) & 0xffffff:06x}",
"allowed_types": ["bbox"]} for c in coco["categories"]]
project = client.projects.create(name="Detection", dataset_id=ds.id,
scope={"all": True}, classes=classes)
class_id = {c.name: c.id for c in client.classes.list(project.id).classes}
Keypoint classes additionally need a topology (COCO person = 17 nodes; edges are COCO's 1-indexed skeleton converted to 0-indexed pairs):
KP_NAMES = ["nose","left_eye","right_eye","left_ear","right_ear","left_shoulder",
"right_shoulder","left_elbow","right_elbow","left_wrist","right_wrist",
"left_hip","right_hip","left_knee","right_knee","left_ankle","right_ankle"]
topology = {"version": 1, "nodes": [{"id": i, "name": n} for i, n in enumerate(KP_NAMES)],
"edges": [[a-1, b-1] for a, b in coco_skeleton_1_indexed]}
classes = [{"name": "person", "color": "#ff0000", "allowed_types": ["keypoint"],
"keypoint_topology": topology}]
1c. Convert COCO annotations → platform geometry (ALL THREE KINDS)¶
def bbox_geometry(coco_ann): # COCO bbox = [x, y, w, h] pixels
x, y, w, h = coco_ann["bbox"]
if w <= 0 or h <= 0: return None # server rejects degenerate boxes
return {"type": "bbox", "x": x, "y": y, "w": w, "h": h}
def polygon_geometry(coco_ann):
if coco_ann.get("iscrowd"): return None # RLE crowds are NOT convertible — SKIP
parts = []
for flat in coco_ann["segmentation"]: # list of flat [x1,y1,x2,y2,...] rings
pts = list(zip(flat[0::2], flat[1::2]))
if len(pts) >= 3: # rings need >= 3 vertices
parts.append({"exterior": pts, "holes": []})
return {"type": "polygon", "polygons": parts} if parts else None
def keypoint_geometry(coco_ann): # [x,y,v] * 17; v in {0,1,2} maps 1:1
if coco_ann.get("num_keypoints", 0) == 0: return None
k = coco_ann["keypoints"]
return {"type": "keypoint",
"points": [{"x": k[i], "y": k[i+1], "visibility": k[i+2]}
for i in range(0, len(k), 3)]}
1d. Bulk import (>10k rows → the ASYNC staged path, one call does it all)¶
rows = [{"id": str(uuid.uuid5(NS, str(ann["id"]))), # deterministic ids => re-import
"asset_id": asset_by_name[img_name[ann["image_id"]]], # is an idempotent upsert
"class_id": class_id[cat_name[ann["category_id"]]],
"type": "bbox", "geometry": bbox_geometry(ann)}
for ann in coco["annotations"] if bbox_geometry(ann)]
job = client.annotations.import_ndjson(project.id, rows, source="imported")
job.wait(timeout=900)
total = client.annotations.list(project.id, limit=1, include_total=True).total
assert total == len(rows)
(≤1000 rows? client.annotations.project_batch_create(project.id, annotations=rows,
source="manual") is the sync path — instant, all-or-nothing.)