tamarind-cli 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.
- tamarind_cli-0.1.0/.github/workflows/publish.yml +41 -0
- tamarind_cli-0.1.0/.gitignore +11 -0
- tamarind_cli-0.1.0/PKG-INFO +131 -0
- tamarind_cli-0.1.0/README.md +111 -0
- tamarind_cli-0.1.0/docs/architecture.md +49 -0
- tamarind_cli-0.1.0/install.sh +38 -0
- tamarind_cli-0.1.0/pyproject.toml +44 -0
- tamarind_cli-0.1.0/src/tamarind/__init__.py +16 -0
- tamarind_cli-0.1.0/src/tamarind/catalog.py +70 -0
- tamarind_cli-0.1.0/src/tamarind/cli/__init__.py +1 -0
- tamarind_cli-0.1.0/src/tamarind/cli/commands/__init__.py +1 -0
- tamarind_cli-0.1.0/src/tamarind/cli/commands/auth.py +90 -0
- tamarind_cli-0.1.0/src/tamarind/cli/commands/catalog.py +114 -0
- tamarind_cli-0.1.0/src/tamarind/cli/commands/files.py +113 -0
- tamarind_cli-0.1.0/src/tamarind/cli/commands/jobs.py +311 -0
- tamarind_cli-0.1.0/src/tamarind/cli/inputs.py +115 -0
- tamarind_cli-0.1.0/src/tamarind/cli/main.py +122 -0
- tamarind_cli-0.1.0/src/tamarind/cli/output.py +68 -0
- tamarind_cli-0.1.0/src/tamarind/config.py +152 -0
- tamarind_cli-0.1.0/src/tamarind/errors.py +59 -0
- tamarind_cli-0.1.0/src/tamarind/http.py +160 -0
- tamarind_cli-0.1.0/src/tamarind/jobs.py +106 -0
- tamarind_cli-0.1.0/src/tamarind/rest.py +192 -0
- tamarind_cli-0.1.0/tests/test_cli_commands.py +92 -0
- tamarind_cli-0.1.0/tests/test_config.py +32 -0
- tamarind_cli-0.1.0/tests/test_inputs.py +78 -0
- tamarind_cli-0.1.0/tests/test_jobs.py +78 -0
- tamarind_cli-0.1.0/tests/test_rest.py +148 -0
- tamarind_cli-0.1.0/uv.lock +406 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes tamarind-cli to PyPI on a GitHub Release (or manual dispatch).
|
|
4
|
+
#
|
|
5
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token in the repo. One-time setup
|
|
6
|
+
# on PyPI: add a trusted publisher for project `tamarind-cli` pointing at this
|
|
7
|
+
# repo + this workflow (`publish.yml`). If you prefer a token instead, delete the
|
|
8
|
+
# `permissions:`/OIDC bits and set a `PYPI_API_TOKEN` secret, then pass
|
|
9
|
+
# `password: ${{ secrets.PYPI_API_TOKEN }}` to the publish step.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v5
|
|
22
|
+
- name: Build sdist + wheel
|
|
23
|
+
run: uv build
|
|
24
|
+
- name: Smoke-check the build
|
|
25
|
+
run: uv run --with dist/*.whl tamarind --version
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tamarind-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for the Tamarind Bio platform — submit, monitor, and download protein/molecule jobs from your terminal or an AI agent.
|
|
5
|
+
Project-URL: Homepage, https://tamarind.bio
|
|
6
|
+
Project-URL: Documentation, https://app.tamarind.bio/api-docs
|
|
7
|
+
Project-URL: Source, https://github.com/Tamarind-Bio/tamarind-cli
|
|
8
|
+
Author: Tamarind Bio
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,alphafold,bioinformatics,boltz,cli,protein
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Requires-Dist: typer>=0.12
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Tamarind CLI
|
|
22
|
+
|
|
23
|
+
Command-line interface for the [Tamarind Bio](https://tamarind.bio) platform.
|
|
24
|
+
Discover tools, submit and monitor protein / nucleic-acid / small-molecule jobs,
|
|
25
|
+
and download results — from your terminal, a script, CI, or an AI coding agent
|
|
26
|
+
(Claude Code, Codex, …).
|
|
27
|
+
|
|
28
|
+
The CLI is a thin client over the same API the [Tamarind MCP
|
|
29
|
+
server](https://mcp.tamarind.bio) uses, so the two stay in lockstep. See
|
|
30
|
+
[`docs/architecture.md`](docs/architecture.md) for how drift is prevented.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Once published to PyPI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
|
|
38
|
+
# or:
|
|
39
|
+
uv tool install tamarind-cli # or: pipx install tamarind-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Before then (or to track the repo directly), install from git:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install "git+https://github.com/Tamarind-Bio/tamarind-cli"
|
|
46
|
+
# or: pipx install "git+https://github.com/Tamarind-Bio/tamarind-cli"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> Releasing: tag a GitHub Release and the [`publish.yml`](.github/workflows/publish.yml)
|
|
50
|
+
> workflow builds and uploads to PyPI via Trusted Publishing (configure the
|
|
51
|
+
> trusted publisher for `tamarind-cli` on PyPI first).
|
|
52
|
+
|
|
53
|
+
## Authenticate
|
|
54
|
+
|
|
55
|
+
Get an API key from the Tamarind web app (Settings → API), then either:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export TAMARIND_API_KEY="sk_..." # best for agents / CI
|
|
59
|
+
# or
|
|
60
|
+
tamarind auth login # stores it in ~/.tamarind/config.json
|
|
61
|
+
tamarind auth status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Find a tool
|
|
68
|
+
tamarind tools --function structure-prediction --modality protein
|
|
69
|
+
tamarind tools --search boltz
|
|
70
|
+
|
|
71
|
+
# 2. Inspect its parameters and grab a runnable example
|
|
72
|
+
tamarind schema boltz
|
|
73
|
+
tamarind schema boltz --example > job.yaml
|
|
74
|
+
|
|
75
|
+
# 3. Validate, then submit
|
|
76
|
+
tamarind validate boltz --input job.yaml
|
|
77
|
+
tamarind submit boltz --input job.yaml --name my-run --wait --download ./out
|
|
78
|
+
|
|
79
|
+
# 4. Monitor / fetch
|
|
80
|
+
tamarind jobs
|
|
81
|
+
tamarind status my-run
|
|
82
|
+
tamarind results my-run --download ./out
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Set individual fields inline instead of a file:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
tamarind submit boltz \
|
|
89
|
+
--set inputFormat=sequence \
|
|
90
|
+
--set sequence=MKTVRQERLKSIVRIL... \
|
|
91
|
+
--name quick-fold
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Output for agents
|
|
95
|
+
|
|
96
|
+
Every command emits JSON when stdout is not a TTY, or with `--json`. Exit codes
|
|
97
|
+
are stable: `0` ok, `3` auth, `4` not-found, `5` validation, `6` rate-limit.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
tamarind jobs --json | jq '.jobs[] | select(.JobStatus=="Running")'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| Group | Commands |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Discover | `tools`, `modalities`, `functions`, `schema` |
|
|
108
|
+
| Submit | `validate`, `submit`, `batch` |
|
|
109
|
+
| Monitor | `jobs`, `status`, `wait`, `results`, `logs` |
|
|
110
|
+
| Files | `files list`, `files upload`, `files delete`, `files folders` |
|
|
111
|
+
| Lifecycle | `cancel`, `delete` |
|
|
112
|
+
| Auth | `auth login`, `auth status`, `auth logout` |
|
|
113
|
+
|
|
114
|
+
Run `tamarind <command> --help` for full options.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Setting | Flag | Env var | Default |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| API key | `--api-key` | `TAMARIND_API_KEY` | — |
|
|
121
|
+
| Job API base | `--api-base` | `TAMARIND_API_BASE` | `https://app.tamarind.bio/api/` |
|
|
122
|
+
| Catalog base | `--catalog-base` | `TAMARIND_CATALOG_BASE` | `https://mcp.tamarind.bio` |
|
|
123
|
+
| Profile | `--profile` | `TAMARIND_PROFILE` | `default` |
|
|
124
|
+
|
|
125
|
+
Profiles (key + endpoints) are stored in `~/.tamarind/config.json`. Use a
|
|
126
|
+
profile to point at staging:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
tamarind --profile staging --api-base https://staging.tamarind.bio/api/ auth login
|
|
130
|
+
TAMARIND_PROFILE=staging tamarind tools
|
|
131
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Tamarind CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [Tamarind Bio](https://tamarind.bio) platform.
|
|
4
|
+
Discover tools, submit and monitor protein / nucleic-acid / small-molecule jobs,
|
|
5
|
+
and download results — from your terminal, a script, CI, or an AI coding agent
|
|
6
|
+
(Claude Code, Codex, …).
|
|
7
|
+
|
|
8
|
+
The CLI is a thin client over the same API the [Tamarind MCP
|
|
9
|
+
server](https://mcp.tamarind.bio) uses, so the two stay in lockstep. See
|
|
10
|
+
[`docs/architecture.md`](docs/architecture.md) for how drift is prevented.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Once published to PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
|
|
18
|
+
# or:
|
|
19
|
+
uv tool install tamarind-cli # or: pipx install tamarind-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Before then (or to track the repo directly), install from git:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv tool install "git+https://github.com/Tamarind-Bio/tamarind-cli"
|
|
26
|
+
# or: pipx install "git+https://github.com/Tamarind-Bio/tamarind-cli"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Releasing: tag a GitHub Release and the [`publish.yml`](.github/workflows/publish.yml)
|
|
30
|
+
> workflow builds and uploads to PyPI via Trusted Publishing (configure the
|
|
31
|
+
> trusted publisher for `tamarind-cli` on PyPI first).
|
|
32
|
+
|
|
33
|
+
## Authenticate
|
|
34
|
+
|
|
35
|
+
Get an API key from the Tamarind web app (Settings → API), then either:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export TAMARIND_API_KEY="sk_..." # best for agents / CI
|
|
39
|
+
# or
|
|
40
|
+
tamarind auth login # stores it in ~/.tamarind/config.json
|
|
41
|
+
tamarind auth status
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Find a tool
|
|
48
|
+
tamarind tools --function structure-prediction --modality protein
|
|
49
|
+
tamarind tools --search boltz
|
|
50
|
+
|
|
51
|
+
# 2. Inspect its parameters and grab a runnable example
|
|
52
|
+
tamarind schema boltz
|
|
53
|
+
tamarind schema boltz --example > job.yaml
|
|
54
|
+
|
|
55
|
+
# 3. Validate, then submit
|
|
56
|
+
tamarind validate boltz --input job.yaml
|
|
57
|
+
tamarind submit boltz --input job.yaml --name my-run --wait --download ./out
|
|
58
|
+
|
|
59
|
+
# 4. Monitor / fetch
|
|
60
|
+
tamarind jobs
|
|
61
|
+
tamarind status my-run
|
|
62
|
+
tamarind results my-run --download ./out
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set individual fields inline instead of a file:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
tamarind submit boltz \
|
|
69
|
+
--set inputFormat=sequence \
|
|
70
|
+
--set sequence=MKTVRQERLKSIVRIL... \
|
|
71
|
+
--name quick-fold
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Output for agents
|
|
75
|
+
|
|
76
|
+
Every command emits JSON when stdout is not a TTY, or with `--json`. Exit codes
|
|
77
|
+
are stable: `0` ok, `3` auth, `4` not-found, `5` validation, `6` rate-limit.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tamarind jobs --json | jq '.jobs[] | select(.JobStatus=="Running")'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
| Group | Commands |
|
|
86
|
+
|---|---|
|
|
87
|
+
| Discover | `tools`, `modalities`, `functions`, `schema` |
|
|
88
|
+
| Submit | `validate`, `submit`, `batch` |
|
|
89
|
+
| Monitor | `jobs`, `status`, `wait`, `results`, `logs` |
|
|
90
|
+
| Files | `files list`, `files upload`, `files delete`, `files folders` |
|
|
91
|
+
| Lifecycle | `cancel`, `delete` |
|
|
92
|
+
| Auth | `auth login`, `auth status`, `auth logout` |
|
|
93
|
+
|
|
94
|
+
Run `tamarind <command> --help` for full options.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
| Setting | Flag | Env var | Default |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| API key | `--api-key` | `TAMARIND_API_KEY` | — |
|
|
101
|
+
| Job API base | `--api-base` | `TAMARIND_API_BASE` | `https://app.tamarind.bio/api/` |
|
|
102
|
+
| Catalog base | `--catalog-base` | `TAMARIND_CATALOG_BASE` | `https://mcp.tamarind.bio` |
|
|
103
|
+
| Profile | `--profile` | `TAMARIND_PROFILE` | `default` |
|
|
104
|
+
|
|
105
|
+
Profiles (key + endpoints) are stored in `~/.tamarind/config.json`. Use a
|
|
106
|
+
profile to point at staging:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tamarind --profile staging --api-base https://staging.tamarind.bio/api/ auth login
|
|
110
|
+
TAMARIND_PROFILE=staging tamarind tools
|
|
111
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Architecture & no-drift design
|
|
2
|
+
|
|
3
|
+
The CLI, the [MCP server](https://mcp.tamarind.bio), and the web app are all
|
|
4
|
+
**thin clients** over the same platform. The CLI never re-implements business
|
|
5
|
+
logic; it only knows how to call two well-defined surfaces. This is what keeps
|
|
6
|
+
the CLI and the MCP from drifting as the platform evolves.
|
|
7
|
+
|
|
8
|
+
## Two surfaces, two single sources of truth
|
|
9
|
+
|
|
10
|
+
### 1. Job/file REST surface — source of truth: the OpenAPI spec
|
|
11
|
+
|
|
12
|
+
`submit`, `validate`, `batch`, `jobs`, `status`/`wait`, `results`, `files`,
|
|
13
|
+
`cancel`, `delete` map 1:1 onto operations in `openapi-mcp.yaml` — the same
|
|
14
|
+
spec the MCP server is generated from (`FastMCP.from_openapi`). The CLI's
|
|
15
|
+
[`rest.py`](../src/tamarind/rest.py) is a literal, logic-free mapping of that
|
|
16
|
+
spec. Because both clients derive from one spec, a contract test can fail CI if
|
|
17
|
+
they diverge.
|
|
18
|
+
|
|
19
|
+
These calls go directly to the job API (`https://app.tamarind.bio/api/`) with
|
|
20
|
+
the `x-api-key` header.
|
|
21
|
+
|
|
22
|
+
### 2. Discovery surface — source of truth: a shared catalog module
|
|
23
|
+
|
|
24
|
+
`tools`, `modalities`, `functions`, and `schema` need per-org visibility logic
|
|
25
|
+
(which tools an account may see, per-parameter gating, example generation) that
|
|
26
|
+
must run server-side. So the CLI does **not** read the catalog database; it
|
|
27
|
+
calls the `/catalog/*` HTTP routes ([`catalog.py`](../src/tamarind/catalog.py)),
|
|
28
|
+
which return exactly the JSON the MCP's discovery tools return.
|
|
29
|
+
|
|
30
|
+
The MCP tools and the `/catalog/*` routes are served by the **same shared
|
|
31
|
+
implementation**, so a tool looks identical no matter which client you use.
|
|
32
|
+
Because the logic lives in one module, *where* discovery is hosted (the MCP host
|
|
33
|
+
today; potentially the main API or a dedicated service later) is a deployment
|
|
34
|
+
detail that can change without any client change and without drift.
|
|
35
|
+
|
|
36
|
+
## Why not a single binary that re-encodes the API?
|
|
37
|
+
|
|
38
|
+
A from-scratch client in another language would re-encode the request shapes and
|
|
39
|
+
the catalog logic — two copies that drift the moment the platform changes.
|
|
40
|
+
Keeping the CLI a thin, spec-derived Python client that shares the OpenAPI
|
|
41
|
+
contract (and, server-side, the catalog module) with the MCP makes drift a
|
|
42
|
+
structural impossibility plus a CI-enforced check, rather than something to
|
|
43
|
+
remember.
|
|
44
|
+
|
|
45
|
+
## Configuration indirection
|
|
46
|
+
|
|
47
|
+
Endpoints are configurable (`--api-base`, `--catalog-base`, profiles), so the
|
|
48
|
+
same binary points at prod or staging, and the discovery host can move later
|
|
49
|
+
without a new release.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Tamarind CLI installer.
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
|
|
5
|
+
#
|
|
6
|
+
# Installs the `tamarind` command into an isolated environment using whatever
|
|
7
|
+
# Python tooling is available (uv > pipx > pip --user). Override the package
|
|
8
|
+
# source with TAMARIND_CLI_SPEC (e.g. a local path or a git URL).
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
SPEC="${TAMARIND_CLI_SPEC:-tamarind-cli}"
|
|
12
|
+
|
|
13
|
+
say() { printf '\033[0;36m%s\033[0m\n' "$*"; }
|
|
14
|
+
warn() { printf '\033[0;33m%s\033[0m\n' "$*" >&2; }
|
|
15
|
+
die() { printf '\033[0;31merror: %s\033[0m\n' "$*" >&2; exit 1; }
|
|
16
|
+
|
|
17
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
18
|
+
|
|
19
|
+
if have uv; then
|
|
20
|
+
say "Installing $SPEC with uv…"
|
|
21
|
+
uv tool install --force "$SPEC"
|
|
22
|
+
elif have pipx; then
|
|
23
|
+
say "Installing $SPEC with pipx…"
|
|
24
|
+
pipx install --force "$SPEC"
|
|
25
|
+
elif have python3; then
|
|
26
|
+
say "Installing $SPEC with pip (--user)…"
|
|
27
|
+
python3 -m pip install --user --upgrade "$SPEC"
|
|
28
|
+
else
|
|
29
|
+
die "Need uv, pipx, or python3 on PATH. Install uv: https://docs.astral.sh/uv/"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if have tamarind; then
|
|
33
|
+
say "Installed: $(tamarind --version)"
|
|
34
|
+
say "Next: export TAMARIND_API_KEY=... (or run 'tamarind auth login'), then 'tamarind tools'."
|
|
35
|
+
else
|
|
36
|
+
warn "Installed, but 'tamarind' is not on PATH yet."
|
|
37
|
+
warn "Add your tool bin dir to PATH (uv: 'uv tool update-shell'; pipx: 'pipx ensurepath')."
|
|
38
|
+
fi
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tamarind-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line interface for the Tamarind Bio platform — submit, monitor, and download protein/molecule jobs from your terminal or an AI agent."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Tamarind Bio" }]
|
|
9
|
+
keywords = ["bioinformatics", "protein", "alphafold", "boltz", "cli", "agents"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
"typer>=0.12",
|
|
13
|
+
"PyYAML>=6.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://tamarind.bio"
|
|
18
|
+
Documentation = "https://app.tamarind.bio/api-docs"
|
|
19
|
+
Source = "https://github.com/Tamarind-Bio/tamarind-cli"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
tamarind = "tamarind.cli.main:run"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"respx>=0.21",
|
|
28
|
+
"ruff>=0.5",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/tamarind"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
addopts = "-q"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py310"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Tamarind Bio CLI and Python client.
|
|
2
|
+
|
|
3
|
+
This package is a thin client over the Tamarind platform. Two surfaces:
|
|
4
|
+
|
|
5
|
+
- REST passthrough (``tamarind.rest``): submit/validate/batch, jobs, result,
|
|
6
|
+
files, cancel, delete — these hit the Tamarind API directly with an API key.
|
|
7
|
+
The request/response contract is the same OpenAPI spec the Tamarind MCP server
|
|
8
|
+
is built from, so the CLI and the MCP cannot drift on this surface.
|
|
9
|
+
|
|
10
|
+
- Discovery (``tamarind.catalog``): tools, schema, modalities, functions. The
|
|
11
|
+
catalog lives behind per-org visibility logic that runs server-side, so the
|
|
12
|
+
CLI consumes it over HTTP (the ``/catalog/*`` routes) rather than reading the
|
|
13
|
+
database directly.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Discovery / catalog client.
|
|
2
|
+
|
|
3
|
+
The tool catalog and per-tool schemas are gated by per-org visibility logic
|
|
4
|
+
that runs server-side, so the CLI consumes them over HTTP from the catalog
|
|
5
|
+
service (the ``/catalog/*`` routes) rather than reading the database directly.
|
|
6
|
+
|
|
7
|
+
These routes return exactly the JSON the MCP's discovery tools return
|
|
8
|
+
(``getAvailableTools``, ``listModalities``, ``listTags``, ``getJobSchema``),
|
|
9
|
+
because both are served by the same shared implementation. So whatever a tool
|
|
10
|
+
looks like in the MCP, it looks identical here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .http import HTTPClient
|
|
18
|
+
|
|
19
|
+
CATALOG_PREFIX = "catalog"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_tools(
|
|
23
|
+
client: HTTPClient,
|
|
24
|
+
*,
|
|
25
|
+
modality: str | None = None,
|
|
26
|
+
function: str | None = None,
|
|
27
|
+
search: str | None = None,
|
|
28
|
+
custom: bool | None = None,
|
|
29
|
+
) -> dict:
|
|
30
|
+
"""GET /catalog/tools — the filtered tool catalog (mirrors getAvailableTools)."""
|
|
31
|
+
params = {
|
|
32
|
+
"modality": modality,
|
|
33
|
+
"function": function,
|
|
34
|
+
"search": search,
|
|
35
|
+
"custom": "true" if custom else None,
|
|
36
|
+
}
|
|
37
|
+
return client.get_json(f"{CATALOG_PREFIX}/tools", params=params)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_modalities(client: HTTPClient) -> dict:
|
|
41
|
+
"""GET /catalog/modalities — molecule types you can filter by."""
|
|
42
|
+
return client.get_json(f"{CATALOG_PREFIX}/modalities")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def list_functions(client: HTTPClient) -> dict:
|
|
46
|
+
"""GET /catalog/functions — functions (tags) you can filter by."""
|
|
47
|
+
return client.get_json(f"{CATALOG_PREFIX}/functions")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_schema(client: HTTPClient, job_type: str) -> dict:
|
|
51
|
+
"""GET /catalog/tools/{jobType}/schema — full parameter schema + example job."""
|
|
52
|
+
return client.get_json(f"{CATALOG_PREFIX}/tools/{job_type}/schema")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# -- helpers for rendering / example extraction ---------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def example_settings(schema: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
"""Pull a runnable ``settings`` dict out of a schema's exampleJob, if present."""
|
|
60
|
+
example = schema.get("exampleJob") or {}
|
|
61
|
+
return dict(example.get("settings") or {})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def required_param_names(schema: dict[str, Any]) -> list[str]:
|
|
65
|
+
"""Names of parameters marked required (top-level; ignores task-gated ones)."""
|
|
66
|
+
out = []
|
|
67
|
+
for p in schema.get("parameters", []):
|
|
68
|
+
if p.get("required") and p.get("name"):
|
|
69
|
+
out.append(p["name"])
|
|
70
|
+
return out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Typer command-line interface for Tamarind."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command groups."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""`tamarind auth` — credential management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ... import rest
|
|
10
|
+
from ...config import mask_key, save_profile
|
|
11
|
+
from ...errors import AuthError
|
|
12
|
+
from ...http import HTTPClient
|
|
13
|
+
from .. import output
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _check_key(api_base: str, api_key: str) -> bool:
|
|
19
|
+
"""Return True if the key authenticates against the job API."""
|
|
20
|
+
with HTTPClient(api_base, api_key) as client:
|
|
21
|
+
try:
|
|
22
|
+
rest.get_jobs(client, limit=1)
|
|
23
|
+
return True
|
|
24
|
+
except AuthError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def login(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
api_key: Optional[str] = typer.Option(
|
|
32
|
+
None, "--api-key", help="API key. If omitted, you'll be prompted.", show_default=False
|
|
33
|
+
),
|
|
34
|
+
no_verify: bool = typer.Option(False, "--no-verify", help="Skip verifying the key."),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Store an API key in the current profile (~/.tamarind/config.json).
|
|
37
|
+
|
|
38
|
+
Get a key from https://app.tamarind.bio (Settings → API), or set
|
|
39
|
+
TAMARIND_API_KEY in the environment to skip storing one.
|
|
40
|
+
"""
|
|
41
|
+
state = ctx.obj
|
|
42
|
+
cfg = state.config()
|
|
43
|
+
key = api_key or typer.prompt("Tamarind API key", hide_input=True)
|
|
44
|
+
key = key.strip()
|
|
45
|
+
|
|
46
|
+
if not no_verify and not _check_key(cfg.api_base, key):
|
|
47
|
+
raise AuthError("That API key was rejected by the API. Not saved.")
|
|
48
|
+
|
|
49
|
+
save_profile(cfg.profile, api_key=key)
|
|
50
|
+
output.emit(
|
|
51
|
+
{"ok": True, "profile": cfg.profile, "verified": not no_verify},
|
|
52
|
+
state.output,
|
|
53
|
+
human=f"Saved API key to profile '{cfg.profile}'.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def status(ctx: typer.Context) -> None:
|
|
59
|
+
"""Show the active profile, endpoints, and whether the key works."""
|
|
60
|
+
state = ctx.obj
|
|
61
|
+
cfg = state.config()
|
|
62
|
+
verified = cfg.has_key and _check_key(cfg.api_base, cfg.api_key)
|
|
63
|
+
result = {
|
|
64
|
+
"profile": cfg.profile,
|
|
65
|
+
"apiKey": mask_key(cfg.api_key),
|
|
66
|
+
"hasKey": cfg.has_key,
|
|
67
|
+
"verified": verified,
|
|
68
|
+
"apiBase": cfg.api_base,
|
|
69
|
+
"catalogBase": cfg.catalog_base,
|
|
70
|
+
}
|
|
71
|
+
human = (
|
|
72
|
+
f"profile: {cfg.profile}\n"
|
|
73
|
+
f"api key: {mask_key(cfg.api_key)} ({'verified' if verified else 'not verified'})\n"
|
|
74
|
+
f"job api: {cfg.api_base}\n"
|
|
75
|
+
f"catalog api: {cfg.catalog_base}"
|
|
76
|
+
)
|
|
77
|
+
output.emit(result, state.output, human=human)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command()
|
|
81
|
+
def logout(ctx: typer.Context) -> None:
|
|
82
|
+
"""Remove the stored API key from the current profile."""
|
|
83
|
+
state = ctx.obj
|
|
84
|
+
cfg = state.config()
|
|
85
|
+
save_profile(cfg.profile, api_key="", make_current=False)
|
|
86
|
+
output.emit(
|
|
87
|
+
{"ok": True, "profile": cfg.profile},
|
|
88
|
+
state.output,
|
|
89
|
+
human=f"Cleared API key for profile '{cfg.profile}'.",
|
|
90
|
+
)
|