proteworks 0.1.10__tar.gz → 0.1.11__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.
- {proteworks-0.1.10 → proteworks-0.1.11}/PKG-INFO +29 -8
- {proteworks-0.1.10 → proteworks-0.1.11}/README.md +28 -7
- {proteworks-0.1.10 → proteworks-0.1.11}/pyproject.toml +4 -1
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/__init__.py +2 -2
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/api.py +85 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/auth.py +53 -17
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/cli.py +154 -3
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/config.py +45 -8
- proteworks-0.1.11/src/proteworks/executions.py +105 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/profile.py +1 -1
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/server.py +16 -14
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/tools_files.py +44 -0
- proteworks-0.1.11/src/proteworks/mcp/tools_skills.py +60 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/tools_workflows.py +101 -4
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp_host.py +12 -1
- proteworks-0.1.11/src/proteworks/recycle.py +270 -0
- proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/SKILL.md +155 -0
- proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/kernel.py +163 -0
- proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/scripts/validate_molx.js +42 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-files/SKILL.md +52 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-studio/SKILL.md +35 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-msa-generation/SKILL.md +28 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-proteinmpnn/SKILL.md +28 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-refolding/SKILL.md +58 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-rf-diffusion3/SKILL.md +31 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-rfd4-proteina/SKILL.md +86 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-structure-prediction/SKILL.md +51 -0
- proteworks-0.1.11/src/proteworks/skills/proteworks-workflows/SKILL.md +64 -0
- proteworks-0.1.11/src/proteworks/skills_catalog.py +201 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/workflows.py +273 -13
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/PKG-INFO +29 -8
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/SOURCES.txt +19 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_cli.py +300 -23
- proteworks-0.1.11/tests/test_executions.py +176 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_files.py +45 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_workflows.py +155 -3
- proteworks-0.1.11/tests/test_recycle.py +123 -0
- proteworks-0.1.11/tests/test_rfd4_proteina_workflow.py +231 -0
- proteworks-0.1.11/tests/test_skills_catalog.py +120 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_token_store.py +30 -3
- proteworks-0.1.10/tests/test_rfd4_proteina_workflow.py +0 -78
- {proteworks-0.1.10 → proteworks-0.1.11}/setup.cfg +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/browse.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/folders.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/index.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/__init__.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/errors.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/paths.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/runtime.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/tools_orgs.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/mcp/tools_studio.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/orgs.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/pull.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/push.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/redact.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/results.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/session.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/__init__.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/common.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/__init__.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/base.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/proteinmpnn.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/refolding.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/rfd3.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/rfd4_proteina.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/kinds/structure_prediction.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio/registry.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks/studio_proteinmpnn.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/dependency_links.txt +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/entry_points.txt +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/requires.txt +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/src/proteworks.egg-info/top_level.txt +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_browse.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_config_dir.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_folders.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_adversarial.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_errors.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_host.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_paths.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_mcp_studio.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_redact.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_session.py +0 -0
- {proteworks-0.1.10 → proteworks-0.1.11}/tests/test_transfer_result.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proteworks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Sync files with Proteworks using file-browser paths.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -52,6 +52,8 @@ proteworks mcp setup-claude-science
|
|
|
52
52
|
proteworks mcp doctor
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
To switch accounts: `proteworks login --force` (clears the local token and the browser Auth0 session, then opens the sign-in UI). `proteworks logout` only forgets the local token.
|
|
56
|
+
|
|
55
57
|
After any upgrade: run step 2 again (same `install-system` command), then `doctor`. Do not `uv tool install` again unless `/usr/local/bin/proteworks` is missing.
|
|
56
58
|
|
|
57
59
|
### Claude Science connector
|
|
@@ -99,20 +101,37 @@ proteworks push ./1UBQ.pdb '~/My Files/test/'
|
|
|
99
101
|
proteworks push '*.pdb' '~/My Files/pdbs/'
|
|
100
102
|
proteworks push designs.zip '~/My Files/dest/' # auto-extracts
|
|
101
103
|
proteworks mkdir '~/My Files/campaigns/round6/'
|
|
104
|
+
proteworks recycle '~/My Files/campaigns/old-run/' # soft delete → recycle bin
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push).
|
|
107
|
+
MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push), and `move_to_recycle_bin` for soft-deleting a file, folder/, or wildcard (dry_run by default; set `confirm=true` to execute).
|
|
105
108
|
|
|
106
109
|
Login returns to `http://127.0.0.1:8765/callback`. Add that exact URL to the Auth0 application's Allowed Callback URLs.
|
|
107
110
|
|
|
108
111
|
Example prompts: list `~/My Files/`, dry-run pull a folder, pull it, push results back.
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
### Agent skills (MCP)
|
|
114
|
+
|
|
115
|
+
Playbooks ship with the server. Agents should:
|
|
116
|
+
|
|
117
|
+
1. `list_skills` — catalog (name, description, files)
|
|
118
|
+
2. `get_skill(name)` — `SKILL.md` instructions (default)
|
|
119
|
+
3. `get_skill(name, path="…")` — helpers such as `kernel.py`
|
|
120
|
+
|
|
121
|
+
Bundled today (canonical under `src/proteworks/skills/`; `skills/` holds symlinks for host import):
|
|
111
122
|
|
|
112
|
-
- [`
|
|
113
|
-
- [`
|
|
123
|
+
- [`protein-studio-molx`](src/proteworks/skills/protein-studio-molx/) — build Protein Studio `.molx` sessions (`SKILL.md` + `kernel.py`)
|
|
124
|
+
- [`proteworks-workflows`](src/proteworks/skills/proteworks-workflows/) — shared prepare/submit/monitor hub
|
|
125
|
+
- [`proteworks-workflow-rfd4-proteina`](src/proteworks/skills/proteworks-workflow-rfd4-proteina/) — RFD4 modes including binder rescue
|
|
126
|
+
- [`proteworks-workflow-rf-diffusion3`](src/proteworks/skills/proteworks-workflow-rf-diffusion3/)
|
|
127
|
+
- [`proteworks-workflow-proteinmpnn`](src/proteworks/skills/proteworks-workflow-proteinmpnn/)
|
|
128
|
+
- [`proteworks-workflow-refolding`](src/proteworks/skills/proteworks-workflow-refolding/)
|
|
129
|
+
- [`proteworks-workflow-structure-prediction`](src/proteworks/skills/proteworks-workflow-structure-prediction/)
|
|
130
|
+
- [`proteworks-workflow-msa-generation`](src/proteworks/skills/proteworks-workflow-msa-generation/)
|
|
131
|
+
- [`proteworks-studio`](src/proteworks/skills/proteworks-studio/) — `.studio` inspect / save-selected
|
|
132
|
+
- [`proteworks-files`](src/proteworks/skills/proteworks-files/) — list/pull/push/recycle
|
|
114
133
|
|
|
115
|
-
|
|
134
|
+
Add another skill by creating `src/proteworks/skills/<name>/SKILL.md` (plus optional helpers), then symlink `skills/<name>/SKILL.md` → the packaged file for GitHub Skills import. No new MCP tool names are required for playbook-only adds.
|
|
116
135
|
|
|
117
136
|
### Tools
|
|
118
137
|
|
|
@@ -120,16 +139,18 @@ Import via Settings → Skills → Import from GitHub (or upload the folder). Ho
|
|
|
120
139
|
| --- | --- |
|
|
121
140
|
| `auth_status` | Login state, API URL, active org (never the token) |
|
|
122
141
|
| `organization_list` / `organization_set` | Choose organization (`organization_set` needs `confirm=true`) |
|
|
142
|
+
| `list_skills` / `get_skill` | Bundled playbooks (e.g. `.molx`); no login required |
|
|
123
143
|
| `list_files` | Browse a remote path; returns `file_id` + `relative_path` |
|
|
124
144
|
| `pull` / `push` | Transfer with `dry_run` (default true); real transfers need `confirm_transfer=true` |
|
|
125
|
-
| `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
|
|
145
|
+
| `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `rfd4-proteina`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
|
|
126
146
|
| `get_workflow_schema` | Full schema with UI-matching defaults |
|
|
127
147
|
| `prepare_workflow_submission` | Merge defaults + overrides; **no submit** (draft body) |
|
|
128
148
|
| `submit_workflow` | Same merge, then submit |
|
|
129
|
-
| `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / structure_prediction); normalized rows + scores |
|
|
149
|
+
| `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / rfd4-proteina / structure_prediction); normalized rows + scores |
|
|
130
150
|
| `save_selected_studio` | Same as Studio Save selected → subset `.studio` uploaded + retention finalized (not yet for structure_prediction) |
|
|
131
151
|
| `inspect_proteinmpnn_studio` | Legacy ProteinMPNN flat-row inspect (prefer `inspect_studio`) |
|
|
132
152
|
| `prepare_refolding_from_proteinmpnn_studio` | Build refolding payload + SA handoff from MPNN studio selection (**no submit**) |
|
|
153
|
+
| `list_executions` | List/filter past runs (`workflow_name`, `status`, `output_folder`); includes `design_count` totals |
|
|
133
154
|
| `get_execution` / `list_execution_results` / `cancel_execution` | Status / results / cancel (`cancel_execution` needs `confirm=true`) |
|
|
134
155
|
| `estimate_workflow_cost` | Cost estimate from merged params (same validation as prepare) |
|
|
135
156
|
|
|
@@ -38,6 +38,8 @@ proteworks mcp setup-claude-science
|
|
|
38
38
|
proteworks mcp doctor
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
To switch accounts: `proteworks login --force` (clears the local token and the browser Auth0 session, then opens the sign-in UI). `proteworks logout` only forgets the local token.
|
|
42
|
+
|
|
41
43
|
After any upgrade: run step 2 again (same `install-system` command), then `doctor`. Do not `uv tool install` again unless `/usr/local/bin/proteworks` is missing.
|
|
42
44
|
|
|
43
45
|
### Claude Science connector
|
|
@@ -85,20 +87,37 @@ proteworks push ./1UBQ.pdb '~/My Files/test/'
|
|
|
85
87
|
proteworks push '*.pdb' '~/My Files/pdbs/'
|
|
86
88
|
proteworks push designs.zip '~/My Files/dest/' # auto-extracts
|
|
87
89
|
proteworks mkdir '~/My Files/campaigns/round6/'
|
|
90
|
+
proteworks recycle '~/My Files/campaigns/old-run/' # soft delete → recycle bin
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push).
|
|
93
|
+
MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push), and `move_to_recycle_bin` for soft-deleting a file, folder/, or wildcard (dry_run by default; set `confirm=true` to execute).
|
|
91
94
|
|
|
92
95
|
Login returns to `http://127.0.0.1:8765/callback`. Add that exact URL to the Auth0 application's Allowed Callback URLs.
|
|
93
96
|
|
|
94
97
|
Example prompts: list `~/My Files/`, dry-run pull a folder, pull it, push results back.
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
### Agent skills (MCP)
|
|
100
|
+
|
|
101
|
+
Playbooks ship with the server. Agents should:
|
|
102
|
+
|
|
103
|
+
1. `list_skills` — catalog (name, description, files)
|
|
104
|
+
2. `get_skill(name)` — `SKILL.md` instructions (default)
|
|
105
|
+
3. `get_skill(name, path="…")` — helpers such as `kernel.py`
|
|
106
|
+
|
|
107
|
+
Bundled today (canonical under `src/proteworks/skills/`; `skills/` holds symlinks for host import):
|
|
97
108
|
|
|
98
|
-
- [`
|
|
99
|
-
- [`
|
|
109
|
+
- [`protein-studio-molx`](src/proteworks/skills/protein-studio-molx/) — build Protein Studio `.molx` sessions (`SKILL.md` + `kernel.py`)
|
|
110
|
+
- [`proteworks-workflows`](src/proteworks/skills/proteworks-workflows/) — shared prepare/submit/monitor hub
|
|
111
|
+
- [`proteworks-workflow-rfd4-proteina`](src/proteworks/skills/proteworks-workflow-rfd4-proteina/) — RFD4 modes including binder rescue
|
|
112
|
+
- [`proteworks-workflow-rf-diffusion3`](src/proteworks/skills/proteworks-workflow-rf-diffusion3/)
|
|
113
|
+
- [`proteworks-workflow-proteinmpnn`](src/proteworks/skills/proteworks-workflow-proteinmpnn/)
|
|
114
|
+
- [`proteworks-workflow-refolding`](src/proteworks/skills/proteworks-workflow-refolding/)
|
|
115
|
+
- [`proteworks-workflow-structure-prediction`](src/proteworks/skills/proteworks-workflow-structure-prediction/)
|
|
116
|
+
- [`proteworks-workflow-msa-generation`](src/proteworks/skills/proteworks-workflow-msa-generation/)
|
|
117
|
+
- [`proteworks-studio`](src/proteworks/skills/proteworks-studio/) — `.studio` inspect / save-selected
|
|
118
|
+
- [`proteworks-files`](src/proteworks/skills/proteworks-files/) — list/pull/push/recycle
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
Add another skill by creating `src/proteworks/skills/<name>/SKILL.md` (plus optional helpers), then symlink `skills/<name>/SKILL.md` → the packaged file for GitHub Skills import. No new MCP tool names are required for playbook-only adds.
|
|
102
121
|
|
|
103
122
|
### Tools
|
|
104
123
|
|
|
@@ -106,16 +125,18 @@ Import via Settings → Skills → Import from GitHub (or upload the folder). Ho
|
|
|
106
125
|
| --- | --- |
|
|
107
126
|
| `auth_status` | Login state, API URL, active org (never the token) |
|
|
108
127
|
| `organization_list` / `organization_set` | Choose organization (`organization_set` needs `confirm=true`) |
|
|
128
|
+
| `list_skills` / `get_skill` | Bundled playbooks (e.g. `.molx`); no login required |
|
|
109
129
|
| `list_files` | Browse a remote path; returns `file_id` + `relative_path` |
|
|
110
130
|
| `pull` / `push` | Transfer with `dry_run` (default true); real transfers need `confirm_transfer=true` |
|
|
111
|
-
| `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
|
|
131
|
+
| `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `rfd4-proteina`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
|
|
112
132
|
| `get_workflow_schema` | Full schema with UI-matching defaults |
|
|
113
133
|
| `prepare_workflow_submission` | Merge defaults + overrides; **no submit** (draft body) |
|
|
114
134
|
| `submit_workflow` | Same merge, then submit |
|
|
115
|
-
| `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / structure_prediction); normalized rows + scores |
|
|
135
|
+
| `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / rfd4-proteina / structure_prediction); normalized rows + scores |
|
|
116
136
|
| `save_selected_studio` | Same as Studio Save selected → subset `.studio` uploaded + retention finalized (not yet for structure_prediction) |
|
|
117
137
|
| `inspect_proteinmpnn_studio` | Legacy ProteinMPNN flat-row inspect (prefer `inspect_studio`) |
|
|
118
138
|
| `prepare_refolding_from_proteinmpnn_studio` | Build refolding payload + SA handoff from MPNN studio selection (**no submit**) |
|
|
139
|
+
| `list_executions` | List/filter past runs (`workflow_name`, `status`, `output_folder`); includes `design_count` totals |
|
|
119
140
|
| `get_execution` / `list_execution_results` / `cancel_execution` | Status / results / cancel (`cancel_execution` needs `confirm=true`) |
|
|
120
141
|
| `estimate_workflow_cost` | Cost estimate from merged params (same validation as prepare) |
|
|
121
142
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "proteworks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.11"
|
|
4
4
|
description = "Sync files with Proteworks using file-browser paths."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -25,5 +25,8 @@ build-backend = "setuptools.build_meta"
|
|
|
25
25
|
[tool.setuptools.packages.find]
|
|
26
26
|
where = ["src"]
|
|
27
27
|
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
proteworks = ["skills/**/*"]
|
|
30
|
+
|
|
28
31
|
[tool.pytest.ini_options]
|
|
29
32
|
testpaths = ["tests"]
|
|
@@ -209,6 +209,47 @@ class ApiClient:
|
|
|
209
209
|
data={"name": name, "parent_id": parent_id},
|
|
210
210
|
)
|
|
211
211
|
|
|
212
|
+
def get_recycle_bin_folder(self) -> dict:
|
|
213
|
+
"""GET /api/v1/folders/recycle-bin (current user's recycle bin)."""
|
|
214
|
+
return self.request("GET", "/api/v1/folders/recycle-bin")
|
|
215
|
+
|
|
216
|
+
def get_directory(
|
|
217
|
+
self,
|
|
218
|
+
folder_id: str,
|
|
219
|
+
*,
|
|
220
|
+
files_limit: int = 1,
|
|
221
|
+
folders_limit: int = 1,
|
|
222
|
+
) -> dict:
|
|
223
|
+
"""GET /api/v1/directory/{folder_id} (includes view_in_recycle_bin)."""
|
|
224
|
+
return self.request(
|
|
225
|
+
"GET",
|
|
226
|
+
f"/api/v1/directory/{folder_id}",
|
|
227
|
+
params={
|
|
228
|
+
"files_limit": files_limit,
|
|
229
|
+
"folders_limit": folders_limit,
|
|
230
|
+
"fields": "summary",
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def move_files_to_recycle_bin(self, file_ids: list[str]) -> dict:
|
|
235
|
+
"""POST /api/v1/files/move-to-recycle-bin (soft-delete; not permanent)."""
|
|
236
|
+
return self.request(
|
|
237
|
+
"POST",
|
|
238
|
+
"/api/v1/files/move-to-recycle-bin",
|
|
239
|
+
json_body={"file_ids": list(file_ids)},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def move_folder_to_recycle_bin(self, folder_id: str) -> dict:
|
|
243
|
+
"""POST /api/v1/folders/{id}/move-to-recycle-bin (async job for trees)."""
|
|
244
|
+
return self.request(
|
|
245
|
+
"POST",
|
|
246
|
+
f"/api/v1/folders/{folder_id}/move-to-recycle-bin",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def get_folder_recycle_move_job(self, job_id: str) -> dict:
|
|
250
|
+
"""GET /api/v1/folders/recycle-move-jobs/{job_id}."""
|
|
251
|
+
return self.request("GET", f"/api/v1/folders/recycle-move-jobs/{job_id}")
|
|
252
|
+
|
|
212
253
|
def upload_init(
|
|
213
254
|
self,
|
|
214
255
|
filename: str,
|
|
@@ -414,9 +455,53 @@ class ApiClient:
|
|
|
414
455
|
def get_agent_schema(self, workflow_name: str) -> dict:
|
|
415
456
|
return self.request("GET", f"/api/v1/workflows/agent-schemas/{workflow_name}")
|
|
416
457
|
|
|
458
|
+
def resolve_structure_prediction_methods(
|
|
459
|
+
self,
|
|
460
|
+
*,
|
|
461
|
+
parameters: Optional[dict] = None,
|
|
462
|
+
complex_ir: Optional[dict] = None,
|
|
463
|
+
enable_all_capable: bool = False,
|
|
464
|
+
) -> dict:
|
|
465
|
+
"""POST capable-predictor resolution for structure-prediction prepare."""
|
|
466
|
+
body: dict[str, Any] = {"enable_all_capable": bool(enable_all_capable)}
|
|
467
|
+
if parameters is not None:
|
|
468
|
+
body["parameters"] = parameters
|
|
469
|
+
if complex_ir is not None:
|
|
470
|
+
body["complex_ir"] = complex_ir
|
|
471
|
+
return self.request(
|
|
472
|
+
"POST",
|
|
473
|
+
"/api/v1/workflows/agent-schemas/structure-prediction/resolve-methods",
|
|
474
|
+
json_body=body,
|
|
475
|
+
)
|
|
476
|
+
|
|
417
477
|
def submit_workflow(self, body: dict) -> dict:
|
|
418
478
|
return self.request("POST", "/api/v1/workflows/submit", json_body=body)
|
|
419
479
|
|
|
480
|
+
def list_executions(
|
|
481
|
+
self,
|
|
482
|
+
*,
|
|
483
|
+
limit: int = 50,
|
|
484
|
+
since: Optional[str] = None,
|
|
485
|
+
workflow_name: Optional[str] = None,
|
|
486
|
+
status: Optional[str] = None,
|
|
487
|
+
output_folder: Optional[str] = None,
|
|
488
|
+
user_id: Optional[str] = None,
|
|
489
|
+
) -> list:
|
|
490
|
+
"""GET /api/v1/workflows/executions with optional filters."""
|
|
491
|
+
params: dict[str, Any] = {"limit": int(limit)}
|
|
492
|
+
if since:
|
|
493
|
+
params["since"] = since
|
|
494
|
+
if workflow_name:
|
|
495
|
+
params["workflow_name"] = workflow_name
|
|
496
|
+
if status:
|
|
497
|
+
params["status"] = status
|
|
498
|
+
if output_folder:
|
|
499
|
+
params["output_folder"] = output_folder
|
|
500
|
+
if user_id:
|
|
501
|
+
params["user_id"] = user_id
|
|
502
|
+
payload = self.request("GET", "/api/v1/workflows/executions", params=params)
|
|
503
|
+
return list(payload or [])
|
|
504
|
+
|
|
420
505
|
def get_execution(self, execution_id: str) -> dict:
|
|
421
506
|
return self.request("GET", f"/api/v1/workflows/executions/{execution_id}")
|
|
422
507
|
|
|
@@ -28,20 +28,33 @@ def pkce_pair() -> tuple[str, str]:
|
|
|
28
28
|
return verifier, challenge
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def authorize_url(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
def authorize_url(
|
|
32
|
+
settings: Settings,
|
|
33
|
+
redirect_uri: str,
|
|
34
|
+
state: str,
|
|
35
|
+
challenge: str,
|
|
36
|
+
*,
|
|
37
|
+
prompt: str | None = "login",
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Build the Auth0 authorize URL.
|
|
40
|
+
|
|
41
|
+
``prompt=login`` (the default) forces the credentials UI even when the browser
|
|
42
|
+
already has an Auth0 SSO cookie, so ``proteworks login`` can switch accounts.
|
|
43
|
+
Pass ``prompt=None`` only in tests that need the silent SSO path.
|
|
44
|
+
"""
|
|
45
|
+
params: dict[str, str] = {
|
|
46
|
+
"response_type": "code",
|
|
47
|
+
"client_id": settings.auth0_client_id,
|
|
48
|
+
"redirect_uri": redirect_uri,
|
|
49
|
+
"scope": "openid profile email",
|
|
50
|
+
"audience": settings.auth0_audience,
|
|
51
|
+
"state": state,
|
|
52
|
+
"code_challenge": challenge,
|
|
53
|
+
"code_challenge_method": "S256",
|
|
54
|
+
}
|
|
55
|
+
if prompt:
|
|
56
|
+
params["prompt"] = prompt
|
|
57
|
+
return f"https://{settings.auth0_domain}/authorize?{urlencode(params)}"
|
|
45
58
|
|
|
46
59
|
|
|
47
60
|
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
@@ -84,8 +97,29 @@ def open_browser(url: str) -> bool:
|
|
|
84
97
|
os.close(saved_stderr)
|
|
85
98
|
|
|
86
99
|
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
100
|
+
def logout(token_store: TokenStore, stderr) -> None:
|
|
101
|
+
"""Forget the local access token (keychain + credentials file)."""
|
|
102
|
+
token_store.clear()
|
|
103
|
+
print("Signed out.", file=stderr)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def login(settings: Settings, token_store: TokenStore, stderr, *, force: bool = False) -> None:
|
|
107
|
+
"""Open a browser, receive the loopback redirect, and save the access token.
|
|
108
|
+
|
|
109
|
+
Always requests ``prompt=login`` so Auth0 does not silently reuse a browser SSO
|
|
110
|
+
session. With ``force=True``, also clear any local token first and open Auth0's
|
|
111
|
+
logout endpoint so federated IdP cookies (e.g. Google) do not auto-pick the
|
|
112
|
+
previous account.
|
|
113
|
+
"""
|
|
114
|
+
if force:
|
|
115
|
+
token_store.clear()
|
|
116
|
+
logout_url = (
|
|
117
|
+
f"https://{settings.auth0_domain}/v2/logout?"
|
|
118
|
+
+ urlencode({"client_id": settings.auth0_client_id})
|
|
119
|
+
)
|
|
120
|
+
print("Clearing the browser Auth0 session…", file=stderr)
|
|
121
|
+
open_browser(logout_url)
|
|
122
|
+
|
|
89
123
|
verifier, challenge = pkce_pair()
|
|
90
124
|
state = secrets.token_urlsafe(24)
|
|
91
125
|
try:
|
|
@@ -95,8 +129,10 @@ def login(settings: Settings, token_store: TokenStore, stderr) -> None:
|
|
|
95
129
|
f"Port {LOOPBACK_PORT} is in use, so login cannot listen on {loopback_redirect_uri()}. ({exc})"
|
|
96
130
|
) from exc
|
|
97
131
|
redirect_uri = loopback_redirect_uri()
|
|
98
|
-
url = authorize_url(settings, redirect_uri, state, challenge)
|
|
132
|
+
url = authorize_url(settings, redirect_uri, state, challenge, prompt="login")
|
|
99
133
|
print("Opening a browser to sign in.", file=stderr)
|
|
134
|
+
if force:
|
|
135
|
+
print("Choose the account you want, then continue.", file=stderr)
|
|
100
136
|
opened = open_browser(url)
|
|
101
137
|
if not opened:
|
|
102
138
|
print("Open this URL to sign in:", file=stderr)
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from proteworks import __version__
|
|
10
11
|
from proteworks.api import ApiClient, ApiError
|
|
11
|
-
from proteworks.auth import login
|
|
12
|
+
from proteworks.auth import login, logout
|
|
12
13
|
from proteworks.config import ConfigStore, Settings, TokenStore, load_settings
|
|
13
14
|
from proteworks.mcp_host import (
|
|
14
15
|
SYSTEM_BIN_DIR_DEFAULT,
|
|
@@ -24,9 +25,11 @@ from proteworks.orgs import (
|
|
|
24
25
|
regular_organizations,
|
|
25
26
|
select_organization,
|
|
26
27
|
)
|
|
28
|
+
from proteworks.executions import summarize_execution_list
|
|
27
29
|
from proteworks.folders import mkdir_remote
|
|
28
30
|
from proteworks.pull import export_organization, pull_path
|
|
29
31
|
from proteworks.push import push_paths
|
|
32
|
+
from proteworks.recycle import RecycleError, recycle_remote
|
|
30
33
|
from proteworks.redact import install_redacting_logs
|
|
31
34
|
from proteworks.session import SessionError, authenticated_client, open_session
|
|
32
35
|
|
|
@@ -50,6 +53,13 @@ examples:
|
|
|
50
53
|
proteworks mkdir '~/My Files/campaigns/round6/'
|
|
51
54
|
"""
|
|
52
55
|
|
|
56
|
+
_RECYCLE_EXAMPLES = """\
|
|
57
|
+
examples:
|
|
58
|
+
proteworks recycle '~/My Files/campaigns/run/design.cif'
|
|
59
|
+
proteworks recycle '~/My Files/campaigns/old-run/'
|
|
60
|
+
proteworks recycle '~/My Files/msa/*.cif' --dry-run
|
|
61
|
+
"""
|
|
62
|
+
|
|
53
63
|
_EXPORT_EXAMPLES = """\
|
|
54
64
|
examples:
|
|
55
65
|
proteworks export ./backup
|
|
@@ -61,6 +71,13 @@ examples:
|
|
|
61
71
|
proteworks mcp doctor
|
|
62
72
|
"""
|
|
63
73
|
|
|
74
|
+
_EXECUTIONS_EXAMPLES = """\
|
|
75
|
+
examples:
|
|
76
|
+
proteworks executions list --workflow-name rf-diffusion3 --status Succeeded
|
|
77
|
+
proteworks executions list --workflow-name rfd4-proteina,rf-diffusion3 --output-folder 'TFR/'
|
|
78
|
+
proteworks executions list --output-folder '~/My Files/TFR/' --status Succeeded --limit 200
|
|
79
|
+
"""
|
|
80
|
+
|
|
64
81
|
|
|
65
82
|
def build_parser() -> argparse.ArgumentParser:
|
|
66
83
|
"""Build the parser, then describe commands by walking what was registered."""
|
|
@@ -76,7 +93,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
76
93
|
)
|
|
77
94
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
78
95
|
|
|
79
|
-
sub.add_parser(
|
|
96
|
+
login_parser = sub.add_parser(
|
|
97
|
+
"login",
|
|
98
|
+
help="Sign in and save an access token in the OS keychain",
|
|
99
|
+
)
|
|
100
|
+
login_parser.add_argument(
|
|
101
|
+
"--force",
|
|
102
|
+
action="store_true",
|
|
103
|
+
help="Clear local credentials and the browser Auth0 session, then sign in (use to switch accounts)",
|
|
104
|
+
)
|
|
105
|
+
sub.add_parser("logout", help="Forget the local access token")
|
|
80
106
|
|
|
81
107
|
org = sub.add_parser("organization", help="List organizations or choose the active one")
|
|
82
108
|
org_sub = org.add_subparsers(dest="org_command", required=True)
|
|
@@ -129,6 +155,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
129
155
|
)
|
|
130
156
|
mkdir.add_argument("remote", help="Remote folder path ending with /")
|
|
131
157
|
|
|
158
|
+
recycle = sub.add_parser(
|
|
159
|
+
"recycle",
|
|
160
|
+
help="Move a remote file, folder/, or wildcard into the recycle bin (soft delete)",
|
|
161
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
162
|
+
epilog=_RECYCLE_EXAMPLES,
|
|
163
|
+
)
|
|
164
|
+
recycle.add_argument(
|
|
165
|
+
"remote",
|
|
166
|
+
help="File-browser path such as '~/My Files/run/design.cif' or '~/My Files/run/'. Quote wildcards.",
|
|
167
|
+
)
|
|
168
|
+
recycle.add_argument(
|
|
169
|
+
"--dry-run",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Print what would be moved without calling the recycle-bin API",
|
|
172
|
+
)
|
|
173
|
+
|
|
132
174
|
export = sub.add_parser(
|
|
133
175
|
"export",
|
|
134
176
|
help="Download the active organization's files (organization admins)",
|
|
@@ -138,6 +180,50 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
138
180
|
export.add_argument("dest", help="Local directory")
|
|
139
181
|
export.add_argument("--dry-run", action="store_true", help="Print the files that would be downloaded")
|
|
140
182
|
|
|
183
|
+
executions = sub.add_parser(
|
|
184
|
+
"executions",
|
|
185
|
+
help="List past workflow executions",
|
|
186
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
187
|
+
epilog=_EXECUTIONS_EXAMPLES,
|
|
188
|
+
)
|
|
189
|
+
executions_sub = executions.add_subparsers(dest="executions_command", required=True)
|
|
190
|
+
executions_list = executions_sub.add_parser(
|
|
191
|
+
"list",
|
|
192
|
+
help="List executions (filter by workflow name, status, output folder)",
|
|
193
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
194
|
+
epilog=_EXECUTIONS_EXAMPLES,
|
|
195
|
+
)
|
|
196
|
+
executions_list.add_argument(
|
|
197
|
+
"--workflow-name",
|
|
198
|
+
help=(
|
|
199
|
+
"Workflow name prefix (comma-separated). "
|
|
200
|
+
"rf-diffusion3 matches rf-diffusion3 and rf-diffusion3-modal."
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
executions_list.add_argument(
|
|
204
|
+
"--status",
|
|
205
|
+
help="Status filter (comma-separated), e.g. Succeeded",
|
|
206
|
+
)
|
|
207
|
+
executions_list.add_argument(
|
|
208
|
+
"--output-folder",
|
|
209
|
+
help="Output folder path prefix, e.g. TFR/ or '~/My Files/TFR/'",
|
|
210
|
+
)
|
|
211
|
+
executions_list.add_argument(
|
|
212
|
+
"--since",
|
|
213
|
+
help="Only executions updated since this ISO 8601 timestamp",
|
|
214
|
+
)
|
|
215
|
+
executions_list.add_argument(
|
|
216
|
+
"--limit",
|
|
217
|
+
type=int,
|
|
218
|
+
default=50,
|
|
219
|
+
help="Max executions to return (default 50, max 200)",
|
|
220
|
+
)
|
|
221
|
+
executions_list.add_argument(
|
|
222
|
+
"--json",
|
|
223
|
+
action="store_true",
|
|
224
|
+
help="Print full JSON summary instead of a table",
|
|
225
|
+
)
|
|
226
|
+
|
|
141
227
|
mcp = sub.add_parser(
|
|
142
228
|
"mcp",
|
|
143
229
|
help="System install and local MCP helpers (doctor, optional Science setup)",
|
|
@@ -200,7 +286,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
200
286
|
" proteworks push '*.pdb' '~/My Files/pdbs/'\n"
|
|
201
287
|
" proteworks push designs.zip '~/My Files/dest/'\n"
|
|
202
288
|
" proteworks mkdir '~/My Files/campaigns/round6/'\n"
|
|
289
|
+
" proteworks recycle '~/My Files/campaigns/old-run/'\n"
|
|
203
290
|
" proteworks export ./backup\n"
|
|
291
|
+
" proteworks executions list --workflow-name rf-diffusion3 --status Succeeded\n"
|
|
204
292
|
" sudo proteworks mcp install-system\n"
|
|
205
293
|
" proteworks mcp doctor\n"
|
|
206
294
|
)
|
|
@@ -230,12 +318,15 @@ def main(argv: list[str] | None = None, *, settings: Settings | None = None, tok
|
|
|
230
318
|
if settings.api_url_overridden:
|
|
231
319
|
print(f"using {settings.api_url}", file=sys.stderr)
|
|
232
320
|
if args.command == "login":
|
|
233
|
-
login(settings, token_store, sys.stderr)
|
|
321
|
+
login(settings, token_store, sys.stderr, force=bool(args.force))
|
|
234
322
|
config = ConfigStore(settings.config_dir / "config.json")
|
|
235
323
|
data = config.read()
|
|
236
324
|
data["api_url"] = settings.api_url
|
|
237
325
|
config.write(data)
|
|
238
326
|
return 0
|
|
327
|
+
if args.command == "logout":
|
|
328
|
+
logout(token_store, sys.stderr)
|
|
329
|
+
return 0
|
|
239
330
|
if args.command == "organization" and args.org_command == "list":
|
|
240
331
|
auth = authenticated_client(settings=settings, token_store=token_store)
|
|
241
332
|
if isinstance(auth, SessionError):
|
|
@@ -305,6 +396,20 @@ def main(argv: list[str] | None = None, *, settings: Settings | None = None, tok
|
|
|
305
396
|
action = "created" if result.get("created") else "exists"
|
|
306
397
|
print(f"{action} {result.get('path')} folder_id={result.get('folder_id')}", file=sys.stderr)
|
|
307
398
|
return 0
|
|
399
|
+
if args.command == "recycle":
|
|
400
|
+
try:
|
|
401
|
+
recycle_remote(
|
|
402
|
+
session.api,
|
|
403
|
+
args.remote,
|
|
404
|
+
dry_run=bool(args.dry_run),
|
|
405
|
+
stderr=sys.stderr,
|
|
406
|
+
)
|
|
407
|
+
except (ApiError, RecycleError, ValueError) as exc:
|
|
408
|
+
print(str(exc), file=sys.stderr)
|
|
409
|
+
return 1
|
|
410
|
+
return 0
|
|
411
|
+
if args.command == "executions" and args.executions_command == "list":
|
|
412
|
+
return _executions_list(session.api, args, sys.stdout, sys.stderr)
|
|
308
413
|
parser.print_help(sys.stderr)
|
|
309
414
|
return 2
|
|
310
415
|
finally:
|
|
@@ -359,5 +464,51 @@ def _organization_set(api: ApiClient, config: ConfigStore, settings: Settings, n
|
|
|
359
464
|
return 0
|
|
360
465
|
|
|
361
466
|
|
|
467
|
+
def _executions_list(api: ApiClient, args: argparse.Namespace, stdout, stderr) -> int:
|
|
468
|
+
limit = max(1, min(int(args.limit), 200))
|
|
469
|
+
try:
|
|
470
|
+
executions = api.list_executions(
|
|
471
|
+
limit=limit,
|
|
472
|
+
since=args.since,
|
|
473
|
+
workflow_name=args.workflow_name,
|
|
474
|
+
status=args.status,
|
|
475
|
+
output_folder=args.output_folder,
|
|
476
|
+
)
|
|
477
|
+
except ApiError as exc:
|
|
478
|
+
print(str(exc), file=stderr)
|
|
479
|
+
return 1
|
|
480
|
+
summary = summarize_execution_list(
|
|
481
|
+
[row for row in executions if isinstance(row, dict)]
|
|
482
|
+
)
|
|
483
|
+
if args.json:
|
|
484
|
+
print(json.dumps(summary, indent=2, default=str), file=stdout)
|
|
485
|
+
return 0
|
|
486
|
+
rows = summary.get("executions") or []
|
|
487
|
+
if not rows:
|
|
488
|
+
print("No executions matched.", file=stderr)
|
|
489
|
+
return 0
|
|
490
|
+
for row in rows:
|
|
491
|
+
designs = row.get("design_count")
|
|
492
|
+
designs_s = str(designs) if designs is not None else "?"
|
|
493
|
+
folder = row.get("output_folder") or "-"
|
|
494
|
+
print(
|
|
495
|
+
f"{row.get('created_at') or '-'} {row.get('status') or '-':12} "
|
|
496
|
+
f"{row.get('workflow_name') or '-':22} designs={designs_s:<6} "
|
|
497
|
+
f"id={row.get('id')} out={folder}",
|
|
498
|
+
file=stdout,
|
|
499
|
+
)
|
|
500
|
+
by_wf = summary.get("by_workflow") or {}
|
|
501
|
+
total = summary.get("total_designs")
|
|
502
|
+
print("", file=stderr)
|
|
503
|
+
for name, bucket in sorted(by_wf.items()):
|
|
504
|
+
print(
|
|
505
|
+
f"{name}: {bucket.get('runs', 0)} runs, {bucket.get('designs', 0)} designs",
|
|
506
|
+
file=stderr,
|
|
507
|
+
)
|
|
508
|
+
if total is not None:
|
|
509
|
+
print(f"total designs: {total}", file=stderr)
|
|
510
|
+
return 0
|
|
511
|
+
|
|
512
|
+
|
|
362
513
|
if __name__ == "__main__":
|
|
363
514
|
raise SystemExit(main())
|