omicslab 2.5.1__tar.gz → 2.6.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.
- {omicslab-2.5.1 → omicslab-2.6.0}/PKG-INFO +55 -8
- {omicslab-2.5.1 → omicslab-2.6.0}/README.developer.md +11 -7
- {omicslab-2.5.1 → omicslab-2.6.0}/README.md +54 -7
- {omicslab-2.5.1 → omicslab-2.6.0}/scripts/bootstrap-env.sh +32 -4
- omicslab-2.6.0/src/omicslab/_version.py +1 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/main.py +3 -3
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/runner_commands.py +17 -17
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/utils.py +29 -22
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/ws_commands.py +548 -39
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/studio.py +5 -18
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/jobs.py +234 -14
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/orgs.py +22 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/e2e/conftest.py +6 -7
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_cli.py +37 -2
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_cli_utils.py +42 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_resources.py +232 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_ws_commands.py +314 -1
- omicslab-2.5.1/src/omicslab/_version.py +0 -1
- {omicslab-2.5.1 → omicslab-2.6.0}/.gitignore +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/Makefile +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/.gitignore +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/hcm.pixi.lock +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/hcm.pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/pixi.lock +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/us-east-1.pixi.lock +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/dev/us-east-1.pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/prod/hcm.pixi.lock +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/prod/hcm.pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/prod/pixi.lock +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/prod/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/envs/regions.json +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/pixi.config.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/pyproject.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/scripts/check_publish.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/scripts/write_version_manifest.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/analysis_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/auth_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/context_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/credential_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/fuse_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/job_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/org_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/setup_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/studio_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/token_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/cli/workspace_commands.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/client.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/config.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/exceptions.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_auth.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_broker.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_config_download.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_consts.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_data_prep.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_env.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_exec.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_git.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_git_proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_images.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_log.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_monitor.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_nextflow.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_params.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_proc.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_s3_proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_spec.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_storage.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/_studio_prep.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/_run/analysis.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/client.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/console.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/executor.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/paths.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/job/system.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/analysis.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/base.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/credentials.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/runners.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/storage.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/studio.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/tokens.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/resources/workspaces.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/src/omicslab/selfupdate.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/conftest.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/e2e/__init__.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/e2e/test_cli_e2e.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/e2e/test_tokens.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/fixtures/nfcore-demo-custom.config +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/fixtures/studio_specs/jupyter.json +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/fixtures/studio_specs/rstudio.json +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__auth.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__broker.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__data_prep.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__engine_helpers.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__env.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__get_images_cli.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__git.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__git_proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__images.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__log.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__monitor.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__nextflow.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__params.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__proxy_extra.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__s3_proxy.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__spec.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__storage.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test__studio_docker.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/_run/test_studio_proxy_start.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_cli.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_client.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_client_extra.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_executor.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_executor_extra.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_fuse.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_resource.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_job_system.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_snapshot_request.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_studio_image_download.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_studio_validate_cli.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/job/test_ws_helpers.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_cli_catalog.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_cli_context.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_cli_storage.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_client.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_client_broker.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_config.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_fuse.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_job_exec.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_job_heartbeat.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_misc.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_runner_helpers.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_runner_self_update.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_sandbox_report.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_selfupdate.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_studio_snapshot.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/test_system_monitor_interval.py +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-nextflow/input.txt +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-nextflow/main.nf +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-nextflow/omicslab/main.sh +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-nextflow/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-nextflow/schema.json +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-simple/omicslab/main.sh +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-simple/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-simple/schema.json +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-snakemake/Snakefile +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-snakemake/input.txt +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-snakemake/omicslab/main.sh +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-snakemake/pixi.toml +0 -0
- {omicslab-2.5.1 → omicslab-2.6.0}/tests/tools/hello-snakemake/schema.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: omicslab
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Python SDK and CLI for the OmicsLab Platform
|
|
5
5
|
Project-URL: Homepage, https://platform.omicslab.io
|
|
6
6
|
Project-URL: Repository, https://github.com/vieomics/omicslab-platform
|
|
@@ -52,7 +52,7 @@ omicslab use
|
|
|
52
52
|
omicslab ls && omicslab upload ./data.csv
|
|
53
53
|
|
|
54
54
|
# 4. Launch a job
|
|
55
|
-
omicslab
|
|
55
|
+
omicslab ws analysis launch --workspace <ws_id> --analysis <id> --compute <id>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Storage Commands
|
|
@@ -148,12 +148,13 @@ Analysis and studio jobs share one backend namespace:
|
|
|
148
148
|
`/workspaces/{workspace_id}/jobs/...` — `job_type` ("analysis" | "studio")
|
|
149
149
|
is part of the launch payload / list filter, not a separate URL space.
|
|
150
150
|
`client.jobs` exposes generic methods (`launch`, `list_jobs`, `get_job`,
|
|
151
|
-
`get_job_manifest`, `
|
|
152
|
-
`disconnect_job`, `check_job_ready`, ...) plus the domain
|
|
153
|
-
(`launch_analysis`, `launch_studio`, `
|
|
154
|
-
for convenience. Job
|
|
155
|
-
launch config live in the
|
|
156
|
-
|
|
151
|
+
`get_job_manifest`, `rerun_job`, `terminate_job`, `delete_job`,
|
|
152
|
+
`connect_job`, `disconnect_job`, `check_job_ready`, ...) plus the domain
|
|
153
|
+
aliases (`launch_analysis`, `launch_studio`, `rerun_analysis`,
|
|
154
|
+
`rerun_studio`, `list_analysis`, `list_studio`, ...) for convenience. Job
|
|
155
|
+
resources (`time`/`cpu`/`memory`/`disk`) and the launch config live in the
|
|
156
|
+
per-job manifest on S3, retrievable via `get_job_manifest` /
|
|
157
|
+
`get_job_audit_params` / `get_job_audit_config`.
|
|
157
158
|
|
|
158
159
|
```python
|
|
159
160
|
from omicslab import Client
|
|
@@ -180,6 +181,52 @@ studio = client.jobs.launch_studio(
|
|
|
180
181
|
compute_id="cloud-comp-...",
|
|
181
182
|
)
|
|
182
183
|
access = client.jobs.connect_job("ws-...", studio["id"]) # {"access_url": ...}
|
|
184
|
+
|
|
185
|
+
# Rerun a job from its resolved launch manifest. Only the overrides below
|
|
186
|
+
# change; tool/tag/pinned commit/versioning/config stay fixed to the source.
|
|
187
|
+
rerun = client.jobs.rerun_analysis(
|
|
188
|
+
"ws-...",
|
|
189
|
+
job_id,
|
|
190
|
+
cpu=8,
|
|
191
|
+
time=7200,
|
|
192
|
+
params={"max_cpus": 8}, # merged per top-level key
|
|
193
|
+
)
|
|
194
|
+
# Masked secret params must be supplied again, otherwise the API returns 400.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The same rerun is available for studio sessions via
|
|
198
|
+
`client.jobs.rerun_studio(...)`; the generic `client.jobs.rerun_job(...)`
|
|
199
|
+
works for both. The response carries `old_job_id` and
|
|
200
|
+
`changed_parameters` so callers can show what changed.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# Estimate the max cost before launching
|
|
204
|
+
estimate = client.jobs.estimate_job(
|
|
205
|
+
"ws-...",
|
|
206
|
+
compute_type="cloud",
|
|
207
|
+
compute_id="cloud-comp-...",
|
|
208
|
+
time=7200,
|
|
209
|
+
cpu=8,
|
|
210
|
+
memory=32,
|
|
211
|
+
disk=100,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Status counts + per-period launch trend (workspace or organization scope)
|
|
215
|
+
counts = client.jobs.get_job_counts("ws-...", job_type="analysis", granularity="daily")
|
|
216
|
+
org_counts = client.orgs.get_job_counts("org-...", job_type="analysis")
|
|
217
|
+
|
|
218
|
+
# Snapshot a RUNNING studio session into workspace storage
|
|
219
|
+
client.jobs.snapshot_studio("ws-...", studio["id"], outdir="snapshots")
|
|
220
|
+
|
|
221
|
+
# Server-side search/filter/sort (exact statuses need filter_column="status")
|
|
222
|
+
client.jobs.list_analysis(
|
|
223
|
+
"ws-...",
|
|
224
|
+
search="bacass",
|
|
225
|
+
filter_column="status",
|
|
226
|
+
filter_value="running,failed",
|
|
227
|
+
sort_column="created_at",
|
|
228
|
+
sort_desc=True,
|
|
229
|
+
)
|
|
183
230
|
```
|
|
184
231
|
|
|
185
232
|
## Testing
|
|
@@ -92,11 +92,11 @@ and then hand the raw token to `Client`.
|
|
|
92
92
|
|
|
93
93
|
| Property | Class | Purpose |
|
|
94
94
|
|------------|--------------------------|---------|
|
|
95
|
-
| `orgs` | `OrgResource` | Org CRUD + members + roles |
|
|
95
|
+
| `orgs` | `OrgResource` | Org CRUD + members + roles + job counts |
|
|
96
96
|
| `workspaces` | `WorkspaceResource` | Workspace CRUD, members, pin, linked resources, budget/storage-quota (read-only) |
|
|
97
97
|
| `analysis` | `AnalysisResource` | Tool catalog (batch tools) CRUD/tags/profiles/schema |
|
|
98
98
|
| `studio` | `StudioResource` | Tool catalog (interactive tools) CRUD + tags/profiles/schema |
|
|
99
|
-
| `jobs` | `JobResource` | Launch/list/get/terminate analysis + studio jobs |
|
|
99
|
+
| `jobs` | `JobResource` | Launch/list/get/rerun/estimate/counts/snapshot/terminate analysis + studio jobs |
|
|
100
100
|
| `storage` | `StorageResource` | Upload/download list/delete/rename vs workspace S3 |
|
|
101
101
|
| `credentials` | `CredentialResource` | S3/GitHub/Cloud credentials (user + org scopes) |
|
|
102
102
|
| `tokens` | `TokenResource` | Create/list/revoke API tokens |
|
|
@@ -248,15 +248,16 @@ bootstraps one session-scoped user via raw `httpx`:
|
|
|
248
248
|
```
|
|
249
249
|
POST /api/auth/register/ (unique email)
|
|
250
250
|
POST /api/auth/standard/ → access_token cookie
|
|
251
|
-
|
|
251
|
+
POST /api/organizations/ → create the test org
|
|
252
252
|
POST /api/credentials/organization/{org}/storage/base/ + link s3 (storage cred)
|
|
253
253
|
POST /api/organizations/{org}/workspaces/ (workspace)
|
|
254
254
|
POST /api/tokens/ → omx_ token scoped to org (admin)
|
|
255
255
|
Client(token=..., base_url=...)
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
Registration
|
|
259
|
-
|
|
258
|
+
Registration does not create any organization; the conftest creates the org
|
|
259
|
+
explicitly. A subscription is still activated for it through the payment flow
|
|
260
|
+
(see `test_subscription`).
|
|
260
261
|
|
|
261
262
|
### 7.2 Fixtures
|
|
262
263
|
|
|
@@ -475,8 +476,11 @@ Studio session mounts (no token exposure, snapshot-safe):
|
|
|
475
476
|
`history`; the copy lives in the job dir and is never snapshotted.
|
|
476
477
|
- Users can create their own project envs in the writable workspace
|
|
477
478
|
(`pixi init` + `pixi add`); those envs are in the writable layer and **are**
|
|
478
|
-
included in snapshots.
|
|
479
|
-
|
|
479
|
+
included in snapshots. Pixi keeps its default package cache under the
|
|
480
|
+
session user's home, so that cache is part of the writable layer too and is
|
|
481
|
+
included in snapshots. No cache directory is mounted: sessions need no extra
|
|
482
|
+
mount, the cache never runs out of a fixed size cap, and pixi works for any
|
|
483
|
+
image user without permission fixes.
|
|
480
484
|
- The staged CLI config is mounted from the host into
|
|
481
485
|
`OMICSLAB_CONFIG_DIR=/run/omicslab` before the container starts. The token
|
|
482
486
|
is replaced with a placeholder first; the SDK talks to a host-side
|
|
@@ -24,7 +24,7 @@ omicslab use
|
|
|
24
24
|
omicslab ls && omicslab upload ./data.csv
|
|
25
25
|
|
|
26
26
|
# 4. Launch a job
|
|
27
|
-
omicslab
|
|
27
|
+
omicslab ws analysis launch --workspace <ws_id> --analysis <id> --compute <id>
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Storage Commands
|
|
@@ -120,12 +120,13 @@ Analysis and studio jobs share one backend namespace:
|
|
|
120
120
|
`/workspaces/{workspace_id}/jobs/...` — `job_type` ("analysis" | "studio")
|
|
121
121
|
is part of the launch payload / list filter, not a separate URL space.
|
|
122
122
|
`client.jobs` exposes generic methods (`launch`, `list_jobs`, `get_job`,
|
|
123
|
-
`get_job_manifest`, `
|
|
124
|
-
`disconnect_job`, `check_job_ready`, ...) plus the domain
|
|
125
|
-
(`launch_analysis`, `launch_studio`, `
|
|
126
|
-
for convenience. Job
|
|
127
|
-
launch config live in the
|
|
128
|
-
|
|
123
|
+
`get_job_manifest`, `rerun_job`, `terminate_job`, `delete_job`,
|
|
124
|
+
`connect_job`, `disconnect_job`, `check_job_ready`, ...) plus the domain
|
|
125
|
+
aliases (`launch_analysis`, `launch_studio`, `rerun_analysis`,
|
|
126
|
+
`rerun_studio`, `list_analysis`, `list_studio`, ...) for convenience. Job
|
|
127
|
+
resources (`time`/`cpu`/`memory`/`disk`) and the launch config live in the
|
|
128
|
+
per-job manifest on S3, retrievable via `get_job_manifest` /
|
|
129
|
+
`get_job_audit_params` / `get_job_audit_config`.
|
|
129
130
|
|
|
130
131
|
```python
|
|
131
132
|
from omicslab import Client
|
|
@@ -152,6 +153,52 @@ studio = client.jobs.launch_studio(
|
|
|
152
153
|
compute_id="cloud-comp-...",
|
|
153
154
|
)
|
|
154
155
|
access = client.jobs.connect_job("ws-...", studio["id"]) # {"access_url": ...}
|
|
156
|
+
|
|
157
|
+
# Rerun a job from its resolved launch manifest. Only the overrides below
|
|
158
|
+
# change; tool/tag/pinned commit/versioning/config stay fixed to the source.
|
|
159
|
+
rerun = client.jobs.rerun_analysis(
|
|
160
|
+
"ws-...",
|
|
161
|
+
job_id,
|
|
162
|
+
cpu=8,
|
|
163
|
+
time=7200,
|
|
164
|
+
params={"max_cpus": 8}, # merged per top-level key
|
|
165
|
+
)
|
|
166
|
+
# Masked secret params must be supplied again, otherwise the API returns 400.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The same rerun is available for studio sessions via
|
|
170
|
+
`client.jobs.rerun_studio(...)`; the generic `client.jobs.rerun_job(...)`
|
|
171
|
+
works for both. The response carries `old_job_id` and
|
|
172
|
+
`changed_parameters` so callers can show what changed.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# Estimate the max cost before launching
|
|
176
|
+
estimate = client.jobs.estimate_job(
|
|
177
|
+
"ws-...",
|
|
178
|
+
compute_type="cloud",
|
|
179
|
+
compute_id="cloud-comp-...",
|
|
180
|
+
time=7200,
|
|
181
|
+
cpu=8,
|
|
182
|
+
memory=32,
|
|
183
|
+
disk=100,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Status counts + per-period launch trend (workspace or organization scope)
|
|
187
|
+
counts = client.jobs.get_job_counts("ws-...", job_type="analysis", granularity="daily")
|
|
188
|
+
org_counts = client.orgs.get_job_counts("org-...", job_type="analysis")
|
|
189
|
+
|
|
190
|
+
# Snapshot a RUNNING studio session into workspace storage
|
|
191
|
+
client.jobs.snapshot_studio("ws-...", studio["id"], outdir="snapshots")
|
|
192
|
+
|
|
193
|
+
# Server-side search/filter/sort (exact statuses need filter_column="status")
|
|
194
|
+
client.jobs.list_analysis(
|
|
195
|
+
"ws-...",
|
|
196
|
+
search="bacass",
|
|
197
|
+
filter_column="status",
|
|
198
|
+
filter_value="running,failed",
|
|
199
|
+
sort_column="created_at",
|
|
200
|
+
sort_desc=True,
|
|
201
|
+
)
|
|
155
202
|
```
|
|
156
203
|
|
|
157
204
|
## Testing
|
|
@@ -37,13 +37,32 @@ PIXI_LOCK="${PIXI_LOCK:-pixi.lock}"
|
|
|
37
37
|
PIXI_VERSION="${PIXI_VERSION:-v0.76.1}"
|
|
38
38
|
RETRIES=3
|
|
39
39
|
|
|
40
|
+
# Download helper. Every fetch is bounded and retried so a stalled connection
|
|
41
|
+
# fails in minutes with a clear message instead of hanging until the platform's
|
|
42
|
+
# SSH bootstrap deadline (900s) kills the job with no diagnostics. The bucket
|
|
43
|
+
# sits behind a single gateway that also serves the conda/pypi payload, so
|
|
44
|
+
# transient 5xx/timeouts must be retried rather than abort the whole bootstrap.
|
|
45
|
+
FETCH_CONNECT_TIMEOUT="${FETCH_CONNECT_TIMEOUT:-10}"
|
|
46
|
+
FETCH_MAX_TIME="${FETCH_MAX_TIME:-300}"
|
|
47
|
+
FETCH_RETRIES="${FETCH_RETRIES:-5}"
|
|
48
|
+
fetch() {
|
|
49
|
+
local url="$1" out="$2"
|
|
50
|
+
curl -fsSL \
|
|
51
|
+
--connect-timeout "${FETCH_CONNECT_TIMEOUT}" \
|
|
52
|
+
--max-time "${FETCH_MAX_TIME}" \
|
|
53
|
+
--retry "${FETCH_RETRIES}" \
|
|
54
|
+
--retry-delay 5 \
|
|
55
|
+
--retry-connrefused \
|
|
56
|
+
"$url" -o "$out"
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
# ── 1. pixi (from the tools bucket, absolute path under $HOME) ────────────
|
|
41
60
|
PIXI_BIN="${HOME}/.pixi/bin/pixi"
|
|
42
61
|
if [ ! -x "${PIXI_BIN}" ]; then
|
|
43
62
|
PIXI_URL="$TOOLS_BASE/pixi/bin/pixi-${PIXI_VERSION}-linux-amd64.tar.gz"
|
|
44
63
|
echo "Installing pixi ${PIXI_VERSION} from: $PIXI_URL"
|
|
45
64
|
mkdir -p "$(dirname "${PIXI_BIN}")"
|
|
46
|
-
|
|
65
|
+
fetch "$PIXI_URL" /tmp/pixi.tar.gz
|
|
47
66
|
tar -xzf /tmp/pixi.tar.gz -C "$(dirname "${PIXI_BIN}")"
|
|
48
67
|
rm -f /tmp/pixi.tar.gz
|
|
49
68
|
else
|
|
@@ -136,23 +155,32 @@ echo "Fetching manifest:"
|
|
|
136
155
|
echo " TOML: $TOML_URL"
|
|
137
156
|
echo " LOCK: $LOCK_URL"
|
|
138
157
|
mkdir -p "$OMICSLAB_HOME"
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
fetch "$TOML_URL" "$OMICSLAB_HOME/pixi.toml"
|
|
159
|
+
fetch "$LOCK_URL" "$OMICSLAB_HOME/pixi.lock"
|
|
141
160
|
|
|
142
161
|
# ── 3. pixi install + SDK wheel (with retries) ────────────────────────────
|
|
143
162
|
cd "$OMICSLAB_HOME"
|
|
163
|
+
env_ready=0
|
|
144
164
|
for i in $(seq 1 $RETRIES); do
|
|
145
165
|
if "${PIXI_BIN}" install --frozen; then
|
|
166
|
+
env_ready=1
|
|
146
167
|
break
|
|
147
168
|
fi
|
|
148
169
|
echo "bootstrap-retry-$i"
|
|
149
170
|
sleep 5
|
|
150
171
|
done
|
|
172
|
+
# `set -e` does not fire for a command used as an `if` condition, so without an
|
|
173
|
+
# explicit check the script would carry on with a half-installed environment
|
|
174
|
+
# and fail later at the wheel step with a misleading message.
|
|
175
|
+
if [ "$env_ready" -ne 1 ]; then
|
|
176
|
+
echo "ERROR: pixi install --frozen failed after ${RETRIES} attempts" >&2
|
|
177
|
+
exit 1
|
|
178
|
+
fi
|
|
151
179
|
|
|
152
180
|
# Download the stable alias and resolve the real version from wheel
|
|
153
181
|
# metadata so pip gets a valid PEP 427 filename.
|
|
154
182
|
echo "Installing omicslab SDK wheel..."
|
|
155
|
-
|
|
183
|
+
fetch "$TOOLS_BASE/omicslab-latest.whl" /tmp/omicslab.whl
|
|
156
184
|
VERSION=$("${PIXI_BIN}" run python3 -c "
|
|
157
185
|
import zipfile, os
|
|
158
186
|
z = zipfile.ZipFile('/tmp/omicslab.whl')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.6.0"
|
|
@@ -265,13 +265,13 @@ def pwd():
|
|
|
265
265
|
parts = []
|
|
266
266
|
if org:
|
|
267
267
|
org_name = config.get_org_name() or org[:8]
|
|
268
|
-
parts.append(f"[
|
|
268
|
+
parts.append(f"[bold]{org_name}[/bold]")
|
|
269
269
|
if ws:
|
|
270
270
|
ws_name = config.get_workspace_name() or ws[:8]
|
|
271
|
-
parts.append(f"[
|
|
271
|
+
parts.append(f"[bold]{ws_name}[/bold]")
|
|
272
272
|
if stored_prefix is not None:
|
|
273
273
|
p = "/" if not stored_prefix else stored_prefix.rstrip("/")
|
|
274
|
-
parts.append(f"[
|
|
274
|
+
parts.append(f"[bold]{p}[/bold]")
|
|
275
275
|
|
|
276
276
|
if parts:
|
|
277
277
|
console.print(" ".join(parts))
|
|
@@ -121,18 +121,18 @@ def generate_token(
|
|
|
121
121
|
output(result, format="json")
|
|
122
122
|
else:
|
|
123
123
|
success("Registration token created")
|
|
124
|
-
console.print(f"[
|
|
125
|
-
console.print(f"[
|
|
126
|
-
console.print(f"[
|
|
124
|
+
console.print(f"[bold]Token:[/bold] [bold]{result['token']}[/bold]")
|
|
125
|
+
console.print(f"[bold]Expires:[/bold] {result['expires_at']}")
|
|
126
|
+
console.print(f"[bold]Scope:[/bold] {scope}")
|
|
127
127
|
console.print("")
|
|
128
128
|
console.print("Register a runner with:")
|
|
129
129
|
console.print(
|
|
130
|
-
f" [
|
|
130
|
+
f" [bold]omicslab runner register --token '{result['token']}' --server {config.get_base_url()}[/bold]"
|
|
131
131
|
)
|
|
132
132
|
console.print("")
|
|
133
133
|
console.print("Then start the daemon:")
|
|
134
|
-
console.print(" [
|
|
135
|
-
console.print(" [
|
|
134
|
+
console.print(" [bold]omicslab runner start --daemon[/bold]")
|
|
135
|
+
console.print(" [bold]omicslab runner status[/bold]")
|
|
136
136
|
except Exception as e:
|
|
137
137
|
error(f"Failed to create registration token: {e}")
|
|
138
138
|
raise typer.Exit(1)
|
|
@@ -199,11 +199,11 @@ def register(
|
|
|
199
199
|
output(result, format="json")
|
|
200
200
|
else:
|
|
201
201
|
success(f"Registered as runner: {runner_name}")
|
|
202
|
-
console.print(f"[
|
|
203
|
-
console.print(f"[
|
|
202
|
+
console.print(f"[bold]Runner ID:[/bold] {runner_id}")
|
|
203
|
+
console.print(f"[bold]Server:[/bold] {server}")
|
|
204
204
|
console.print("")
|
|
205
205
|
console.print("Next, start the runner daemon:")
|
|
206
|
-
console.print(" [
|
|
206
|
+
console.print(" [bold]omicslab runner start[/bold]")
|
|
207
207
|
|
|
208
208
|
except typer.Exit:
|
|
209
209
|
raise
|
|
@@ -249,17 +249,17 @@ def start(
|
|
|
249
249
|
if not tool_info.get(t, {}).get("available")
|
|
250
250
|
]
|
|
251
251
|
if missing_critical and has_slurm:
|
|
252
|
-
console.print("[
|
|
252
|
+
console.print("[bold]═══ Pre-flight Tool Check ═══[/bold]")
|
|
253
253
|
for t in missing_critical:
|
|
254
254
|
console.print(f" [red]✗ {t}[/red] — not found in PATH")
|
|
255
|
-
console.print(" [
|
|
255
|
+
console.print(" [bold]Install on HPC:[/bold]")
|
|
256
256
|
console.print(
|
|
257
|
-
" [
|
|
257
|
+
" [bold]pixi global install nextflow[/bold]"
|
|
258
258
|
if "nextflow" in missing_critical
|
|
259
259
|
else ""
|
|
260
260
|
)
|
|
261
261
|
console.print(
|
|
262
|
-
" [
|
|
262
|
+
" [bold]pixi global install apptainer[/bold]"
|
|
263
263
|
if "apptainer" in missing_critical
|
|
264
264
|
else ""
|
|
265
265
|
)
|
|
@@ -377,14 +377,14 @@ def status_command():
|
|
|
377
377
|
server_url = config.get_base_url()
|
|
378
378
|
|
|
379
379
|
if not runner_id:
|
|
380
|
-
console.print("[
|
|
380
|
+
console.print("[bold]Runner is not registered.[/bold]")
|
|
381
381
|
console.print(" Run `omicslab runner register` first.")
|
|
382
382
|
return
|
|
383
383
|
|
|
384
384
|
console.print("[bold]OmicsLab Runner Status[/bold]")
|
|
385
|
-
console.print(f" Runner ID: [
|
|
386
|
-
console.print(f" Name: [
|
|
387
|
-
console.print(f" Server URL: [
|
|
385
|
+
console.print(f" Runner ID: [bold]{runner_id}[/bold]")
|
|
386
|
+
console.print(f" Name: [bold]{runner_name}[/bold]")
|
|
387
|
+
console.print(f" Server URL: [bold]{server_url}[/bold]")
|
|
388
388
|
|
|
389
389
|
running, pid = _runner_is_running()
|
|
390
390
|
if running:
|
|
@@ -134,7 +134,7 @@ _COST_KEYS = frozenset(
|
|
|
134
134
|
def _format_value(key: str, val):
|
|
135
135
|
"""Convert a raw value to a human-readable string."""
|
|
136
136
|
if val is None:
|
|
137
|
-
return "
|
|
137
|
+
return "—"
|
|
138
138
|
if isinstance(val, (dict, list)):
|
|
139
139
|
return json.dumps(val)[:60]
|
|
140
140
|
if isinstance(val, str) and key in _DATETIME_KEYS:
|
|
@@ -158,8 +158,12 @@ def get_client() -> Client:
|
|
|
158
158
|
raise typer.Exit(1)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def output(data, format: str = "table"):
|
|
162
|
-
"""Output data in the specified format.
|
|
161
|
+
def output(data, format: str = "table", columns: list[str] | None = None):
|
|
162
|
+
"""Output data in the specified format.
|
|
163
|
+
|
|
164
|
+
``columns`` selects and orders table columns; when omitted the generic
|
|
165
|
+
display-key heuristic is used.
|
|
166
|
+
"""
|
|
163
167
|
if format == "json":
|
|
164
168
|
console.print(json.dumps(data, indent=2, default=str))
|
|
165
169
|
return
|
|
@@ -171,14 +175,14 @@ def output(data, format: str = "table"):
|
|
|
171
175
|
if not data:
|
|
172
176
|
console.print("[dim]No items found.[/dim]")
|
|
173
177
|
return
|
|
174
|
-
_print_table(data)
|
|
178
|
+
_print_table(data, columns=columns)
|
|
175
179
|
elif isinstance(data, dict):
|
|
176
180
|
if "items" in data and isinstance(data["items"], list):
|
|
177
181
|
items = data["items"]
|
|
178
182
|
if not items:
|
|
179
183
|
console.print("[dim]No items found.[/dim]")
|
|
180
184
|
return
|
|
181
|
-
_print_table(items)
|
|
185
|
+
_print_table(items, columns=columns)
|
|
182
186
|
if "total" in data:
|
|
183
187
|
console.print(
|
|
184
188
|
f"[dim]Page {data.get('page', '?')} of {data.get('total_pages', '?')} — {data['total']} total items[/dim]"
|
|
@@ -187,18 +191,21 @@ def output(data, format: str = "table"):
|
|
|
187
191
|
_print_dict(data)
|
|
188
192
|
|
|
189
193
|
|
|
190
|
-
def _print_table(items: list[dict]):
|
|
191
|
-
"""Print a list of dicts as a rich table.
|
|
194
|
+
def _print_table(items: list[dict], columns: list[str] | None = None):
|
|
195
|
+
"""Print a list of dicts as a rich table.
|
|
196
|
+
|
|
197
|
+
Data cells deliberately keep the terminal's default foreground so the
|
|
198
|
+
table stays readable on both light and dark backgrounds. Only the header
|
|
199
|
+
is emphasised (bold).
|
|
200
|
+
"""
|
|
192
201
|
if not items:
|
|
193
202
|
return
|
|
194
203
|
|
|
195
|
-
keys = _get_display_keys(items)
|
|
196
|
-
table = Table(box=box.SIMPLE_HEAVY)
|
|
204
|
+
keys = columns or _get_display_keys(items)
|
|
205
|
+
table = Table(box=box.SIMPLE_HEAVY, header_style="bold")
|
|
197
206
|
|
|
198
207
|
for key in keys:
|
|
199
|
-
table.add_column(
|
|
200
|
-
key, style="cyan", no_wrap=key in ("id", "role", "state", "type")
|
|
201
|
-
)
|
|
208
|
+
table.add_column(key, no_wrap=key in ("id", "role", "state", "type"))
|
|
202
209
|
|
|
203
210
|
for item in items:
|
|
204
211
|
row = []
|
|
@@ -215,12 +222,12 @@ def _print_dict(data: dict, indent: int = 0):
|
|
|
215
222
|
prefix = " " * indent
|
|
216
223
|
for key, val in data.items():
|
|
217
224
|
if isinstance(val, dict):
|
|
218
|
-
console.print(f"{prefix}[
|
|
225
|
+
console.print(f"{prefix}[bold]{key}:[/bold]")
|
|
219
226
|
_print_dict(val, indent + 1)
|
|
220
227
|
elif isinstance(val, list):
|
|
221
|
-
console.print(f"{prefix}[
|
|
228
|
+
console.print(f"{prefix}[bold]{key}:[/bold] {json.dumps(val)[:120]}")
|
|
222
229
|
else:
|
|
223
|
-
console.print(f"{prefix}[
|
|
230
|
+
console.print(f"{prefix}[bold]{key}:[/bold] {_format_value(key, val)}")
|
|
224
231
|
|
|
225
232
|
|
|
226
233
|
def _get_display_keys(items: list[dict]) -> list[str]:
|
|
@@ -293,8 +300,8 @@ def info(msg: str):
|
|
|
293
300
|
|
|
294
301
|
|
|
295
302
|
def print_panel(title: str, data: dict):
|
|
296
|
-
table = Table(box=box.SIMPLE)
|
|
297
|
-
table.add_column("Key"
|
|
303
|
+
table = Table(box=box.SIMPLE, header_style="bold")
|
|
304
|
+
table.add_column("Key")
|
|
298
305
|
table.add_column("Value")
|
|
299
306
|
for k, v in data.items():
|
|
300
307
|
if isinstance(v, (dict, list)):
|
|
@@ -454,11 +461,11 @@ def print_storage_listing(objects: list[dict]):
|
|
|
454
461
|
sorted_prefixes = sorted(prefixes)
|
|
455
462
|
sorted_files = sorted(files, key=lambda f: f.get("name", ""))
|
|
456
463
|
|
|
457
|
-
table = Table(box=box.SIMPLE)
|
|
458
|
-
table.add_column("Type",
|
|
459
|
-
table.add_column("Name"
|
|
460
|
-
table.add_column("Size",
|
|
461
|
-
table.add_column("Modified"
|
|
464
|
+
table = Table(box=box.SIMPLE, header_style="bold")
|
|
465
|
+
table.add_column("Type", width=4)
|
|
466
|
+
table.add_column("Name")
|
|
467
|
+
table.add_column("Size", justify="right")
|
|
468
|
+
table.add_column("Modified")
|
|
462
469
|
|
|
463
470
|
for p in sorted_prefixes:
|
|
464
471
|
table.add_row("DIR", p.rstrip("/"), "", "")
|