Skip to main content

strand-sdk

Python client for the Strand Platform — H&E → multiplex protein inference.

Agent-friendly docs: The full API reference is published as Markdown at https://app.strandai.com/docs/api.md, and the LLM index lives at https://app.strandai.com/llms.txt.

Requires Python 3.12+.

pip install strand-sdk
# or with bioinformatics extras (AnnData / zarr):
pip install "strand-sdk[anndata]"

If your environment can't reach PyPI, you can install directly from the repository as a fallback:

pip install "git+https://github.com/Strand-AI/strand-sdk-python.git"

Quickstart

One blocking call runs the full pipeline — upload, submit, wait, download:

from strand import Client

client = Client(api_key="sk-strand-...")
result = client.predict(
    "biopsy.ome.tiff",
    markers=["HER2", "CD8", "PD1"],
    output_dir="./outputs/",
)
print(f"Used {result.credits_used} credits; wrote {len(result.marker_outputs)} markers")

client.predict(...) returns a PredictResult with job_id, status, credits_used, marker_outputs (paths under output_dir), and results (a JobResults handle for selective reads). It raises JobFailedError if the job fails, JobTimeoutError if the deadline elapses, and surfaces InsufficientCreditsError / RateLimitError on submit issues.

Pass on_progress=lambda stage, frac: ... to follow the four stages ("upload", "submit", "wait", "download"). frac is always a float in [0.0, 1.0]0.0 at stage start, 1.0 at stage end, with intermediate values where available (e.g. upload byte progress).

Fire-and-forget: wait=False

A full pipeline run blocks for 15+ minutes. To kick a job off and do other work in the meantime, pass wait=Falsepredict(...) returns a Job handle as soon as the upload + submit complete:

job = client.predict("slide.svs", markers=["CD3", "CD8"], wait=False)
print(f"submitted {job.id}")

# ...do other work, or shut down the process — the job runs server-side.

# Later (same process or a fresh one via `client.jobs.get(job_id)`):
job.wait()
result = job.download_results()      # AnnData
# or stream events:
for event in job.stream_events():
    print(event.status, event.progress)

Static typing follows the wait flag — wait=True returns PredictResult, wait=False returns Job, so IDE completions stay correct without a runtime check.

Choosing a model

predict.submit and predict(...) accept an optional model=. Live Lattice versions:

  • "v0.7" — current dispatchable version and default.

Omit model to let the platform pick the current default ("v0.7"). The older "v0.4", "v0.5", and "v0.6" labels can appear on historical jobs but are sunset for new submissions.

result = client.predict(
    "slide.svs",
    markers=["CD8", "Ki67", "PanCK"],
    model="v0.7",
)
print(result.model)  # → "v0.7" (the v0.X label the platform actually ran)

PredictResult.model and JobStatus.model always carry the canonical v0.X label. Historical jobs from before the versioning rollout may surface as "v0.1" (the renumbered legacy 35-marker base — sunset; readable but not dispatchable).

Migration from v10-* names

The earlier "v10", "v10-fullpanel", and "v10-fullpanel-v2" names were dropped on 2026-06-03 — the server returns 400 unknown_model for all of them. Pass the canonical v0.X id instead:

Legacy id Replacement
"v10-fullpanel-v2" "v0.5"
"v10-fullpanel" "v0.4"
"v10" (sunset — no replacement; the underlying 35-marker base was retired)

Recovering from a failed pipeline without re-uploading

If client.predict(...) raises after the upload step succeeded, the resulting upload_id is attached to the error so you can resume the job without paying to re-upload the WSI:

from strand import Client, JobFailedError, StrandError

client = Client()
try:
    result = client.predict("slide.svs", markers=["CD3", "CD8"])
except JobFailedError as e:
    if e.upload_id:
        # Re-submit against the same upload — no re-upload needed.
        job = client.predict.submit(e.upload_id, markers=["CD3", "CD8"])
        job.wait()
except StrandError as e:
    # Same idea for `JobTimeoutError`, `InsufficientCreditsError`, etc.
    if e.upload_id:
        ...

Skipping re-uploads with content-hash dedup

Agentic / batch workflows often re-run on the same WSI. Set if_not_exists=True on upload_file to skip the actual byte upload when the platform already has the file:

upload = client.uploads.upload_file("slide.svs", if_not_exists=True)

The SDK streams a sha256 of the local file and posts it on the upload-init request. If a non-archived sample in your org already has that hash, the existing Upload is returned and no bytes leave your machine. On a miss the upload proceeds normally and the hash is stored for next time.

Tradeoff: sha256 of a 600 MB WSI takes ~1-2s on modern hardware — worth it to skip a multi-minute upload. Leave the default (False) when you want the upload to run unconditionally.

Catching unknown markers

predict.submit and predict.estimate validate marker names against the platform's panel before reserving credits or queueing a job. Unknown names surface as UnknownMarkerError (a BadRequestError subclass):

from strand import UnknownMarkerError

try:
    client.predict.submit(upload.id, markers=["CD3", "MysteryMarker"])
except UnknownMarkerError as e:
    print("Unknown:", e.unknown)              # ["MysteryMarker"]
    print("Try one of:", e.known_subset[:5])  # sample of valid names

Lower-level primitives

client.predict is also a namespace, so the underlying steps stay available for fine-grained control:

upload = client.uploads.upload_file("slide.svs")
estimate = client.predict.estimate(upload.id, markers=["CD3", "CD8", "Ki67"])
print(f"Will cost ≈ {estimate.estimated_credits} credits")

job = client.predict.submit(upload.id, markers=["CD3", "CD8", "Ki67"])
job.wait()                                    # blocks until terminal status
adata = job.download_results()                # AnnData

Cancelling an in-flight job

Call Job.cancel() (or the top-level client.jobs.cancel(job_id) shortcut) to request termination of a running job. Cancel is atomic: the job's status flips to cancelled, the credit reservation is refunded, and any markers that have already been written stay on the sample. The GPU worker is not interrupted, but its remaining outputs are ignored.

job = client.predict.submit(upload.id, markers=["CD3", "CD8"])
# ...later, from another thread or process:
client.jobs.cancel(job.id)

Calling cancel on a job that is already completed, failed, or cancelled raises BadRequestError.

Reusing prior uploads

client.uploads exposes list + get so you can re-submit against an existing WSI without re-uploading:

page = client.uploads.list(limit=20)
for u in page.uploads:
    print(u.id, u.filename, u.status, u.created_at)

upload = client.uploads.get("upload_abc123")
job = client.predict.submit(upload.id, markers=["CD3", "CD8"])

Pages are newest-first and stable under inserts. Pass the response's next_cursor back as cursor= to get the next page.

Configuration

Source Variable / argument Default
Env STRAND_API_KEY required
Env STRAND_BASE_URL https://app.strandai.com
Arg Client(api_key=..., base_url=..., timeout=..., max_retries=...)

Layout

src/strand/
  __init__.py        public surface re-exports
  _client.py         Client (top-level)
  _uploads.py        uploads namespace (incl. resumable chunked upload helper)
  _predict.py        predict namespace — `client.predict(...)` (full pipeline) + `.estimate` / `.submit`
  _jobs.py           Job (wait / stream_events / download_results)
  _results.py        OME-Zarr v3 download + AnnData conversion
  _models.py         user-facing snake_case dataclasses
  _http.py           internal httpx wrapper with typed error mapping
  _errors.py         typed exceptions
openapi.json         pinned snapshot of the platform spec (drift-check)

Verifying against the platform OpenAPI spec

Transport is hand-written for ergonomic snake_case fields and AnnData integration. To check the SDK against an updated spec:

# regenerate a reference client and diff the request/response surface
uv tool run --from "openapi-python-client>=0.21" --with "click<8.2" \
    openapi-python-client generate \
    --path openapi.json \
    --output-path /tmp/strand-sdk-ref \
    --meta none --overwrite

To refresh openapi.json itself against a live server:

curl https://app.strandai.com/api/v1/openapi.json -o openapi.json
# or against local dev:
# curl http://localhost:3000/api/v1/openapi.json -o openapi.json

Development

uv sync --all-extras
uv run pytest
uv run ruff check src tests
uv run mypy src

Issues & contributing

File bug reports and feature requests at Strand-AI/strand-sdk-python/issues.

We don't accept external pull requests on the SDK at this time. If you'd like to contribute or have ideas you'd like to discuss, email support@strandai.com.

License

Apache 2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

strand_sdk-0.6.0.tar.gz (25.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

strand_sdk-0.6.0-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

Details for the file strand_sdk-0.6.0.tar.gz.

File metadata

  • Download URL: strand_sdk-0.6.0.tar.gz
  • Upload date:
  • Size: 25.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for strand_sdk-0.6.0.tar.gz
Algorithm Hash digest
SHA256 e796558f4e1200c2112a27920305fadd10de349e504d0e85b3faabfb86be042a
MD5 541f608d0612d6a3e4e6c3f612f036d5
BLAKE2b-256 2c4f67c2423ead387728c4185b7fedc65689ee4eb8637b41575fcfba079ff06c

See more details on using hashes here.

Provenance

The following attestation bundles were made for strand_sdk-0.6.0.tar.gz:

Publisher: sdks-publish.yml on Strand-AI/strand-official

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file strand_sdk-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: strand_sdk-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for strand_sdk-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 93150b18f2886843103e34f885576e39738a0fe8b2b00973c7bcabae9d4c5b3e
MD5 7c9cd4522226abfe71207adf7c4b9c0a
BLAKE2b-256 90ea3086f58d690ddee24bc22d0b09c0f00962b940763ffc9d07609a5eb0b3dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for strand_sdk-0.6.0-py3-none-any.whl:

Publisher: sdks-publish.yml on Strand-AI/strand-official

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

This release

0.6.0 This release

2 files

0.5.1

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page