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.
- strand_sdk-0.1.0/.gitignore +9 -0
- strand_sdk-0.1.0/LICENSE +17 -0
- strand_sdk-0.1.0/PKG-INFO +167 -0
- strand_sdk-0.1.0/README.md +126 -0
- strand_sdk-0.1.0/pyproject.toml +81 -0
- strand_sdk-0.1.0/src/strand/__init__.py +62 -0
- strand_sdk-0.1.0/src/strand/_client.py +95 -0
- strand_sdk-0.1.0/src/strand/_errors.py +92 -0
- strand_sdk-0.1.0/src/strand/_http.py +213 -0
- strand_sdk-0.1.0/src/strand/_jobs.py +205 -0
- strand_sdk-0.1.0/src/strand/_models.py +147 -0
- strand_sdk-0.1.0/src/strand/_predict.py +144 -0
- strand_sdk-0.1.0/src/strand/_results.py +348 -0
- strand_sdk-0.1.0/src/strand/_uploads.py +156 -0
- strand_sdk-0.1.0/src/strand/py.typed +0 -0
strand_sdk-0.1.0/LICENSE
ADDED
|
@@ -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."""
|