strand-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ .mypy_cache/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ __pycache__/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ uv.lock
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ Copyright 2026 Strand AI, Inc.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: strand-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Strand Platform API — H&E → multiplex protein inference.
5
+ Project-URL: Homepage, https://strandai.com
6
+ Project-URL: Documentation, https://docs.strandai.com
7
+ Project-URL: Repository, https://github.com/Strand-AI/strand-sdk-python
8
+ Project-URL: Source, https://github.com/Strand-AI/strand-sdk-python
9
+ Project-URL: Issues, https://github.com/Strand-AI/strand-sdk-python/issues
10
+ Project-URL: Changelog, https://github.com/Strand-AI/strand-sdk-python/blob/main/CHANGELOG.md
11
+ Author-email: Strand AI <support@strandai.com>
12
+ License: Apache-2.0
13
+ License-File: LICENSE
14
+ Keywords: anndata,bioinformatics,h&e,imputation,pathology,spatial-omics,strand
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: httpx-sse>=0.4
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: typing-extensions>=4.10
29
+ Provides-Extra: anndata
30
+ Requires-Dist: anndata>=0.10; extra == 'anndata'
31
+ Requires-Dist: numpy>=1.24; extra == 'anndata'
32
+ Provides-Extra: dev
33
+ Requires-Dist: anndata>=0.10; extra == 'dev'
34
+ Requires-Dist: mypy>=1.11; extra == 'dev'
35
+ Requires-Dist: numpy>=1.24; extra == 'dev'
36
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
37
+ Requires-Dist: pytest>=8; extra == 'dev'
38
+ Requires-Dist: respx>=0.21; extra == 'dev'
39
+ Requires-Dist: ruff>=0.6; extra == 'dev'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # strand-sdk
43
+
44
+ Python client for the [Strand Platform](https://strandai.com) — H&E → multiplex protein inference.
45
+
46
+ **Agent-friendly docs:** The full API reference is published as Markdown at [https://app.strandai.com/docs/api.md](https://app.strandai.com/docs/api.md), and the LLM index lives at [https://app.strandai.com/llms.txt](https://app.strandai.com/llms.txt).
47
+
48
+ ```bash
49
+ pip install strand-sdk
50
+ # or with bioinformatics extras (AnnData / zarr):
51
+ pip install "strand-sdk[anndata]"
52
+ ```
53
+
54
+ If your environment can't reach PyPI, you can install directly from the
55
+ repository as a fallback:
56
+
57
+ ```bash
58
+ pip install "git+https://github.com/Strand-AI/strand-sdk-python.git"
59
+ ```
60
+
61
+ ## Quickstart
62
+
63
+ One blocking call runs the full pipeline — upload, submit, wait, download:
64
+
65
+ ```python
66
+ from strand import Client
67
+
68
+ client = Client(api_key="sk-strand-...")
69
+ result = client.predict(
70
+ "biopsy.ome.tiff",
71
+ markers=["HER2", "CD8", "PD1"],
72
+ output_dir="./outputs/",
73
+ )
74
+ print(f"Used {result.credits_used} credits; wrote {len(result.marker_outputs)} markers")
75
+ ```
76
+
77
+ `client.predict(...)` returns a `PredictResult` with `job_id`, `status`,
78
+ `credits_used`, `marker_outputs` (paths under `output_dir`), and `results`
79
+ (a `JobResults` handle for selective reads). It raises `JobFailedError` if the
80
+ job fails, `JobTimeoutError` if the deadline elapses, and surfaces
81
+ `InsufficientCreditsError` / `RateLimitError` on submit issues.
82
+
83
+ Pass `on_progress=lambda stage, frac: ...` to follow the four stages
84
+ (`"upload"`, `"submit"`, `"wait"`, `"download"`).
85
+
86
+ ### Lower-level primitives
87
+
88
+ `client.predict` is also a namespace, so the underlying steps stay available
89
+ for fine-grained control:
90
+
91
+ ```python
92
+ upload = client.uploads.upload_file("slide.svs")
93
+ estimate = client.predict.estimate(upload.id, markers=["CD3", "CD8", "Ki67"])
94
+ print(f"Will cost ≈ {estimate.estimated_credits} credits")
95
+
96
+ job = client.predict.submit(upload.id, markers=["CD3", "CD8", "Ki67"])
97
+ job.wait() # blocks until terminal status
98
+ adata = job.download_results() # AnnData
99
+ ```
100
+
101
+ ## Configuration
102
+
103
+ | Source | Variable / argument | Default |
104
+ |---|---|---|
105
+ | Env | `STRAND_API_KEY` | required |
106
+ | Env | `STRAND_BASE_URL` | `https://app.strandai.com` |
107
+ | Arg | `Client(api_key=..., base_url=..., timeout=..., max_retries=...)` | — |
108
+
109
+ ## Layout
110
+
111
+ ```
112
+ src/strand/
113
+ __init__.py public surface re-exports
114
+ _client.py Client (top-level)
115
+ _uploads.py uploads namespace (incl. resumable chunked upload helper)
116
+ _predict.py predict namespace — `client.predict(...)` (full pipeline) + `.estimate` / `.submit`
117
+ _jobs.py Job (wait / stream_events / download_results)
118
+ _results.py OME-Zarr v3 download + AnnData conversion
119
+ _models.py user-facing snake_case dataclasses
120
+ _http.py internal httpx wrapper with typed error mapping
121
+ _errors.py typed exceptions
122
+ openapi.json pinned snapshot of the platform spec (drift-check)
123
+ ```
124
+
125
+ ## Verifying against the platform OpenAPI spec
126
+
127
+ Transport is hand-written for ergonomic snake_case fields and AnnData
128
+ integration. To check the SDK against an updated spec:
129
+
130
+ ```bash
131
+ # regenerate a reference client and diff the request/response surface
132
+ uv tool run --from "openapi-python-client>=0.21" --with "click<8.2" \
133
+ openapi-python-client generate \
134
+ --path openapi.json \
135
+ --output-path /tmp/strand-sdk-ref \
136
+ --meta none --overwrite
137
+ ```
138
+
139
+ To refresh `openapi.json` itself against a live server:
140
+
141
+ ```bash
142
+ curl https://app.strandai.com/api/v1/openapi.json -o openapi.json
143
+ # or against local dev:
144
+ # curl http://localhost:3000/api/v1/openapi.json -o openapi.json
145
+ ```
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ uv sync --all-extras
151
+ uv run pytest
152
+ uv run ruff check src tests
153
+ uv run mypy src
154
+ ```
155
+
156
+ ## Issues & contributing
157
+
158
+ File bug reports and feature requests at
159
+ [Strand-AI/strand-sdk-python/issues](https://github.com/Strand-AI/strand-sdk-python/issues).
160
+
161
+ We don't accept external pull requests on the SDK at this time. If you'd like
162
+ to contribute or have ideas you'd like to discuss, email
163
+ [support@strandai.com](mailto:support@strandai.com).
164
+
165
+ ## License
166
+
167
+ Apache 2.0
@@ -0,0 +1,126 @@
1
+ # strand-sdk
2
+
3
+ Python client for the [Strand Platform](https://strandai.com) — H&E → multiplex protein inference.
4
+
5
+ **Agent-friendly docs:** The full API reference is published as Markdown at [https://app.strandai.com/docs/api.md](https://app.strandai.com/docs/api.md), and the LLM index lives at [https://app.strandai.com/llms.txt](https://app.strandai.com/llms.txt).
6
+
7
+ ```bash
8
+ pip install strand-sdk
9
+ # or with bioinformatics extras (AnnData / zarr):
10
+ pip install "strand-sdk[anndata]"
11
+ ```
12
+
13
+ If your environment can't reach PyPI, you can install directly from the
14
+ repository as a fallback:
15
+
16
+ ```bash
17
+ pip install "git+https://github.com/Strand-AI/strand-sdk-python.git"
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ One blocking call runs the full pipeline — upload, submit, wait, download:
23
+
24
+ ```python
25
+ from strand import Client
26
+
27
+ client = Client(api_key="sk-strand-...")
28
+ result = client.predict(
29
+ "biopsy.ome.tiff",
30
+ markers=["HER2", "CD8", "PD1"],
31
+ output_dir="./outputs/",
32
+ )
33
+ print(f"Used {result.credits_used} credits; wrote {len(result.marker_outputs)} markers")
34
+ ```
35
+
36
+ `client.predict(...)` returns a `PredictResult` with `job_id`, `status`,
37
+ `credits_used`, `marker_outputs` (paths under `output_dir`), and `results`
38
+ (a `JobResults` handle for selective reads). It raises `JobFailedError` if the
39
+ job fails, `JobTimeoutError` if the deadline elapses, and surfaces
40
+ `InsufficientCreditsError` / `RateLimitError` on submit issues.
41
+
42
+ Pass `on_progress=lambda stage, frac: ...` to follow the four stages
43
+ (`"upload"`, `"submit"`, `"wait"`, `"download"`).
44
+
45
+ ### Lower-level primitives
46
+
47
+ `client.predict` is also a namespace, so the underlying steps stay available
48
+ for fine-grained control:
49
+
50
+ ```python
51
+ upload = client.uploads.upload_file("slide.svs")
52
+ estimate = client.predict.estimate(upload.id, markers=["CD3", "CD8", "Ki67"])
53
+ print(f"Will cost ≈ {estimate.estimated_credits} credits")
54
+
55
+ job = client.predict.submit(upload.id, markers=["CD3", "CD8", "Ki67"])
56
+ job.wait() # blocks until terminal status
57
+ adata = job.download_results() # AnnData
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ | Source | Variable / argument | Default |
63
+ |---|---|---|
64
+ | Env | `STRAND_API_KEY` | required |
65
+ | Env | `STRAND_BASE_URL` | `https://app.strandai.com` |
66
+ | Arg | `Client(api_key=..., base_url=..., timeout=..., max_retries=...)` | — |
67
+
68
+ ## Layout
69
+
70
+ ```
71
+ src/strand/
72
+ __init__.py public surface re-exports
73
+ _client.py Client (top-level)
74
+ _uploads.py uploads namespace (incl. resumable chunked upload helper)
75
+ _predict.py predict namespace — `client.predict(...)` (full pipeline) + `.estimate` / `.submit`
76
+ _jobs.py Job (wait / stream_events / download_results)
77
+ _results.py OME-Zarr v3 download + AnnData conversion
78
+ _models.py user-facing snake_case dataclasses
79
+ _http.py internal httpx wrapper with typed error mapping
80
+ _errors.py typed exceptions
81
+ openapi.json pinned snapshot of the platform spec (drift-check)
82
+ ```
83
+
84
+ ## Verifying against the platform OpenAPI spec
85
+
86
+ Transport is hand-written for ergonomic snake_case fields and AnnData
87
+ integration. To check the SDK against an updated spec:
88
+
89
+ ```bash
90
+ # regenerate a reference client and diff the request/response surface
91
+ uv tool run --from "openapi-python-client>=0.21" --with "click<8.2" \
92
+ openapi-python-client generate \
93
+ --path openapi.json \
94
+ --output-path /tmp/strand-sdk-ref \
95
+ --meta none --overwrite
96
+ ```
97
+
98
+ To refresh `openapi.json` itself against a live server:
99
+
100
+ ```bash
101
+ curl https://app.strandai.com/api/v1/openapi.json -o openapi.json
102
+ # or against local dev:
103
+ # curl http://localhost:3000/api/v1/openapi.json -o openapi.json
104
+ ```
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ uv sync --all-extras
110
+ uv run pytest
111
+ uv run ruff check src tests
112
+ uv run mypy src
113
+ ```
114
+
115
+ ## Issues & contributing
116
+
117
+ File bug reports and feature requests at
118
+ [Strand-AI/strand-sdk-python/issues](https://github.com/Strand-AI/strand-sdk-python/issues).
119
+
120
+ We don't accept external pull requests on the SDK at this time. If you'd like
121
+ to contribute or have ideas you'd like to discuss, email
122
+ [support@strandai.com](mailto:support@strandai.com).
123
+
124
+ ## License
125
+
126
+ Apache 2.0
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "strand-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python client for the Strand Platform API — H&E → multiplex protein inference."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [
13
+ { name = "Strand AI", email = "support@strandai.com" },
14
+ ]
15
+ keywords = ["bioinformatics", "spatial-omics", "pathology", "h&e", "imputation", "anndata", "strand"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "httpx>=0.27",
30
+ "httpx-sse>=0.4",
31
+ "typing-extensions>=4.10",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ anndata = ["anndata>=0.10", "numpy>=1.24"]
36
+ dev = [
37
+ "pytest>=8",
38
+ "pytest-asyncio>=0.23",
39
+ "respx>=0.21",
40
+ "ruff>=0.6",
41
+ "mypy>=1.11",
42
+ "anndata>=0.10",
43
+ "numpy>=1.24",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://strandai.com"
48
+ Documentation = "https://docs.strandai.com"
49
+ Repository = "https://github.com/Strand-AI/strand-sdk-python"
50
+ Source = "https://github.com/Strand-AI/strand-sdk-python"
51
+ Issues = "https://github.com/Strand-AI/strand-sdk-python/issues"
52
+ Changelog = "https://github.com/Strand-AI/strand-sdk-python/blob/main/CHANGELOG.md"
53
+
54
+ [tool.hatch.build.targets.wheel]
55
+ packages = ["src/strand"]
56
+
57
+ [tool.hatch.build.targets.sdist]
58
+ include = ["src/strand", "README.md", "LICENSE"]
59
+
60
+ [tool.ruff]
61
+ line-length = 100
62
+ target-version = "py310"
63
+ src = ["src", "tests"]
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "I", "B", "UP", "SIM", "RUF"]
67
+ ignore = ["E501"]
68
+
69
+ [tool.mypy]
70
+ python_version = "3.10"
71
+ strict = true
72
+ files = ["src/strand"]
73
+
74
+ [[tool.mypy.overrides]]
75
+ module = ["anndata", "httpx_sse"]
76
+ ignore_missing_imports = true
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ asyncio_mode = "auto"
81
+ filterwarnings = ["error"]
@@ -0,0 +1,62 @@
1
+ """Strand Platform Python SDK.
2
+
3
+ Quickstart — one-call pipeline:
4
+
5
+ >>> from strand import Client
6
+ >>> client = Client() # reads STRAND_API_KEY
7
+ >>> result = client.predict(
8
+ ... "slide.svs",
9
+ ... markers=["CD3", "CD8"],
10
+ ... output_dir="./outputs/",
11
+ ... )
12
+ >>> print(f"used {result.credits_used} credits")
13
+
14
+ Lower-level primitives stay available for fine-grained control:
15
+
16
+ >>> upload = client.uploads.upload_file("slide.svs")
17
+ >>> job = client.predict.submit(upload.id, markers=["CD3", "CD8"])
18
+ >>> job.wait()
19
+ >>> adata = job.download_results()
20
+
21
+ See `https://app.strandai.com/docs/api` for the underlying REST API reference.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from ._client import Client
27
+ from ._errors import (
28
+ AuthError,
29
+ BadRequestError,
30
+ InsufficientCreditsError,
31
+ JobFailedError,
32
+ JobTimeoutError,
33
+ NotFoundError,
34
+ RateLimitError,
35
+ StrandError,
36
+ UploadError,
37
+ )
38
+ from ._jobs import Job, JobEvent
39
+ from ._models import Estimate, JobStatus, PredictResult, Upload
40
+ from ._results import JobResults
41
+
42
+ __all__ = [
43
+ "AuthError",
44
+ "BadRequestError",
45
+ "Client",
46
+ "Estimate",
47
+ "InsufficientCreditsError",
48
+ "Job",
49
+ "JobEvent",
50
+ "JobFailedError",
51
+ "JobResults",
52
+ "JobStatus",
53
+ "JobTimeoutError",
54
+ "NotFoundError",
55
+ "PredictResult",
56
+ "RateLimitError",
57
+ "StrandError",
58
+ "Upload",
59
+ "UploadError",
60
+ ]
61
+
62
+ __version__ = "0.1.0"
@@ -0,0 +1,95 @@
1
+ """Top-level `Client` — entry point for the SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import httpx
8
+
9
+ from ._http import DEFAULT_TIMEOUT, HttpSession
10
+ from ._predict import Predict
11
+ from ._uploads import Uploads
12
+
13
+ if TYPE_CHECKING:
14
+ from ._jobs import Job
15
+
16
+
17
+ class _JobsNamespace:
18
+ """`client.jobs` namespace — fetch / look up jobs by id."""
19
+
20
+ def __init__(self, client: Client) -> None:
21
+ self._client = client
22
+
23
+ def get(self, job_id: str) -> Job:
24
+ """Return a `Job` handle and pre-populate its cached status."""
25
+ from ._jobs import Job
26
+
27
+ job = Job(id=job_id, reserved_credits=None, client=self._client)
28
+ job.refresh()
29
+ return job
30
+
31
+
32
+ class Client:
33
+ """Strand Platform API client.
34
+
35
+ Args:
36
+ api_key: API key (`sk-strand-...`). Falls back to `STRAND_API_KEY` env var.
37
+ base_url: API base URL. Defaults to `STRAND_BASE_URL` env var, else
38
+ `https://app.strandai.com`. Should not include the `/api/v1` suffix.
39
+ timeout: Per-request timeout in seconds (or an `httpx.Timeout`).
40
+ http_client: Pre-built `httpx.Client` for advanced use (e.g., custom
41
+ transport, retries, ASGI mounting in tests). The SDK will NOT
42
+ override the client's `Authorization` header — if you pass one,
43
+ wire auth headers yourself.
44
+
45
+ Example — one-call pipeline:
46
+
47
+ >>> client = Client(api_key="sk-strand-...")
48
+ >>> result = client.predict(
49
+ ... "slide.svs",
50
+ ... markers=["CD3", "CD8"],
51
+ ... output_dir="./outputs/",
52
+ ... )
53
+ >>> print(f"used {result.credits_used} credits")
54
+
55
+ Lower-level primitives (`client.predict` is also a namespace):
56
+
57
+ >>> upload = client.uploads.upload_file("slide.svs")
58
+ >>> estimate = client.predict.estimate(upload.id, markers=["CD3"])
59
+ >>> job = client.predict.submit(upload.id, markers=["CD3"])
60
+ >>> job.wait()
61
+ >>> adata = job.download_results()
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ *,
67
+ api_key: str | None = None,
68
+ base_url: str | None = None,
69
+ timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
70
+ http_client: httpx.Client | None = None,
71
+ ) -> None:
72
+ self._http = HttpSession(
73
+ api_key=api_key, base_url=base_url, timeout=timeout, client=http_client
74
+ )
75
+ self.uploads = Uploads(self._http)
76
+ self.predict = Predict(self._http, self)
77
+ self.jobs = _JobsNamespace(self)
78
+
79
+ @property
80
+ def base_url(self) -> str:
81
+ return self._http.base_url
82
+
83
+ @property
84
+ def api_root(self) -> str:
85
+ return self._http.api_root
86
+
87
+ def close(self) -> None:
88
+ """Close the underlying httpx client."""
89
+ self._http.close()
90
+
91
+ def __enter__(self) -> Client:
92
+ return self
93
+
94
+ def __exit__(self, *_: object) -> None:
95
+ self.close()
@@ -0,0 +1,92 @@
1
+ """Typed exceptions for the Strand SDK.
2
+
3
+ All HTTP-level failures raised by the public surface inherit from `StrandError`.
4
+ Network-level failures (`httpx.HTTPError` and friends) pass through unchanged so
5
+ callers can apply their own retry logic — we only wrap responses that the
6
+ platform itself returned with a documented error shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+
14
+ class StrandError(Exception):
15
+ """Base class for SDK errors raised against documented API responses."""
16
+
17
+ def __init__(
18
+ self,
19
+ message: str,
20
+ *,
21
+ status_code: int | None = None,
22
+ error_code: str | None = None,
23
+ body: dict[str, Any] | None = None,
24
+ ) -> None:
25
+ super().__init__(message)
26
+ self.message = message
27
+ self.status_code = status_code
28
+ self.error_code = error_code
29
+ self.body = body or {}
30
+
31
+
32
+ class AuthError(StrandError):
33
+ """401 — missing / invalid / expired API key."""
34
+
35
+
36
+ class BadRequestError(StrandError):
37
+ """400 — request body or arguments rejected by the server."""
38
+
39
+
40
+ class NotFoundError(StrandError):
41
+ """404 — referenced resource (upload, job, file) does not exist or isn't accessible."""
42
+
43
+
44
+ class InsufficientCreditsError(StrandError):
45
+ """402 — org has insufficient credits to reserve for this job.
46
+
47
+ Attributes:
48
+ required: Credits required to run the job, as returned by the server.
49
+ balance: Best-effort cached org balance from the most recent estimate, if available.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ message: str,
55
+ *,
56
+ required: int | None = None,
57
+ balance: int | None = None,
58
+ body: dict[str, Any] | None = None,
59
+ ) -> None:
60
+ super().__init__(message, status_code=402, error_code="insufficient_credits", body=body)
61
+ self.required = required
62
+ self.balance = balance
63
+
64
+
65
+ class RateLimitError(StrandError):
66
+ """429 — per-org concurrent job cap exceeded. `retry_after` is in seconds."""
67
+
68
+ def __init__(
69
+ self,
70
+ message: str,
71
+ *,
72
+ retry_after: int | None = None,
73
+ body: dict[str, Any] | None = None,
74
+ ) -> None:
75
+ super().__init__(message, status_code=429, error_code="rate_limited", body=body)
76
+ self.retry_after = retry_after
77
+
78
+
79
+ class JobFailedError(StrandError):
80
+ """Raised by `Job.wait()` when the job terminates with `status == "failed"`."""
81
+
82
+ def __init__(self, message: str, *, job_id: str) -> None:
83
+ super().__init__(message, error_code="job_failed")
84
+ self.job_id = job_id
85
+
86
+
87
+ class JobTimeoutError(StrandError):
88
+ """Raised by `Job.wait(timeout=...)` when the wait deadline elapses before terminal status."""
89
+
90
+
91
+ class UploadError(StrandError):
92
+ """Raised when the resumable upload session aborts or returns an unexpected response."""