deepmirror 0.0.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.
- deepmirror-0.0.0/LICENSE +21 -0
- deepmirror-0.0.0/PKG-INFO +20 -0
- deepmirror-0.0.0/README.md +120 -0
- deepmirror-0.0.0/deepmirror/__init__.py +31 -0
- deepmirror-0.0.0/deepmirror/api.py +264 -0
- deepmirror-0.0.0/deepmirror/cli.py +193 -0
- deepmirror-0.0.0/deepmirror/config.py +31 -0
- deepmirror-0.0.0/deepmirror.egg-info/PKG-INFO +20 -0
- deepmirror-0.0.0/deepmirror.egg-info/SOURCES.txt +14 -0
- deepmirror-0.0.0/deepmirror.egg-info/dependency_links.txt +1 -0
- deepmirror-0.0.0/deepmirror.egg-info/entry_points.txt +2 -0
- deepmirror-0.0.0/deepmirror.egg-info/requires.txt +14 -0
- deepmirror-0.0.0/deepmirror.egg-info/top_level.txt +1 -0
- deepmirror-0.0.0/pyproject.toml +40 -0
- deepmirror-0.0.0/setup.cfg +4 -0
- deepmirror-0.0.0/tests/test_train.py +47 -0
deepmirror-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 deepmirror
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepmirror
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Command line interface for the deepmirror public API
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: click>=8.1.8
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Requires-Dist: pandas>=2.2.2
|
|
10
|
+
Requires-Dist: pydantic>=2.6.3
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.9.1
|
|
12
|
+
Requires-Dist: scikit-learn>=1.6.1
|
|
13
|
+
Requires-Dist: matplotlib>=3.10.3
|
|
14
|
+
Requires-Dist: tqdm>=4.67.1
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit>=3.6.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pylint>=3.3.6; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.11.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# deepmirror-client
|
|
2
|
+
|
|
3
|
+
`deepmirror-cli` is a command-line interface for interacting with the [deepmirror API](https://api.app.deepmirror.ai/public/docs). It allows you to train models, run predictions, and submit structure prediction jobs directly from your terminal.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install deepmirror
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🔐 Authentication
|
|
16
|
+
|
|
17
|
+
Before using most commands, you need to log in to get your API token:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dm login EMAIL
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This saves your token and host in `~/.config/deepmirror/` for reuse.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🧠 Model Commands
|
|
28
|
+
|
|
29
|
+
### 📋 List Available Models
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dm model list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 📄 View Model Metadata
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dm model metadata MODEL_ID
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 🔎 Get Full Model Info
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dm model info MODEL_ID
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🏋️ Train a Custom Model
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dm train --model-name mymodel \
|
|
53
|
+
--csv-file path/to/data.csv \
|
|
54
|
+
--smiles-column smiles \
|
|
55
|
+
--value-column target \
|
|
56
|
+
[--classification]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `--classification` enables classification mode.
|
|
60
|
+
- Default SMILES column is `smiles`, target column is `target`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 🔮 Run Inference
|
|
65
|
+
|
|
66
|
+
You can run inference using either a CSV file or direct SMILES input:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# From a CSV or TXT file
|
|
70
|
+
dm predict --model-name mymodel --csv-file inputs.csv
|
|
71
|
+
|
|
72
|
+
# Direct SMILES
|
|
73
|
+
dm predict --model-name mymodel --smiles "CCO"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🧬 Structure Prediction
|
|
79
|
+
|
|
80
|
+
### 🧠 Predict Protein-Ligand Structure
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dm structure predict protein.pdb ligand.sdf --model chai
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- Default model is `chai`.
|
|
87
|
+
- Protein and ligand must be valid file paths.
|
|
88
|
+
|
|
89
|
+
### 📦 Download Prediction Result
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
dm structure download TASK_ID result.zip
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 📃 List Submitted Jobs
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
dm structure list
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ⚙️ Configuration
|
|
104
|
+
|
|
105
|
+
- API host and token are saved under `~/.config/deepmirror/`.
|
|
106
|
+
- You can override the API host:
|
|
107
|
+
|
|
108
|
+
- via `--host` option on any command
|
|
109
|
+
- or by setting `DEEPMIRROR_API_ENV=local`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 💡 Tips
|
|
114
|
+
|
|
115
|
+
- If a token is missing or expired, commands will prompt you to log in again.
|
|
116
|
+
- Use `--help` on any command for more details, e.g.:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
dm train --help
|
|
120
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""deepmirror CLI package."""
|
|
2
|
+
|
|
3
|
+
from .api import (
|
|
4
|
+
authenticate,
|
|
5
|
+
download_structure_prediction,
|
|
6
|
+
get_predict_hlm,
|
|
7
|
+
list_models,
|
|
8
|
+
list_structure_tasks,
|
|
9
|
+
model_info,
|
|
10
|
+
model_metadata,
|
|
11
|
+
predict,
|
|
12
|
+
predict_hlm,
|
|
13
|
+
save_token,
|
|
14
|
+
structure_predict,
|
|
15
|
+
train,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"authenticate",
|
|
20
|
+
"list_models",
|
|
21
|
+
"predict",
|
|
22
|
+
"save_token",
|
|
23
|
+
"list_structure_tasks",
|
|
24
|
+
"download_structure_prediction",
|
|
25
|
+
"structure_predict",
|
|
26
|
+
"train",
|
|
27
|
+
"model_metadata",
|
|
28
|
+
"predict_hlm",
|
|
29
|
+
"get_predict_hlm",
|
|
30
|
+
"model_info",
|
|
31
|
+
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""deepmirror API client for interacting with the deepmirror platform.
|
|
2
|
+
|
|
3
|
+
This module provides functions for authentication, model management, and inference
|
|
4
|
+
using the deepmirror API. It handles API token management and provides a clean
|
|
5
|
+
interface for making API requests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from .config import settings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def save_token(token: str) -> None:
|
|
17
|
+
"""Save the API token to the config directory."""
|
|
18
|
+
settings.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
settings.TOKEN_FILE.write_text(token)
|
|
20
|
+
settings.TOKEN_FILE.chmod(0o600)
|
|
21
|
+
print(f"API token saved to {settings.TOKEN_FILE}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_token() -> str:
|
|
25
|
+
"""Load the API token from the config directory."""
|
|
26
|
+
if not settings.TOKEN_FILE.exists():
|
|
27
|
+
raise RuntimeError("API token not found; please login or provide one")
|
|
28
|
+
return settings.TOKEN_FILE.read_text().strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def authenticate(username: str, password: str) -> str:
|
|
32
|
+
"""Authenticate with the deepmirror API."""
|
|
33
|
+
host = settings.HOST
|
|
34
|
+
url = f"{host}/api/v3/public/authenticate?token_lifetime_minutes=720"
|
|
35
|
+
data = {
|
|
36
|
+
"grant_type": "password",
|
|
37
|
+
"username": username,
|
|
38
|
+
"password": password,
|
|
39
|
+
"scope": "",
|
|
40
|
+
"client_id": "string",
|
|
41
|
+
"client_secret": "string",
|
|
42
|
+
}
|
|
43
|
+
headers = {
|
|
44
|
+
"accept": "application/json",
|
|
45
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
46
|
+
}
|
|
47
|
+
response = requests.post(url, data=data, headers=headers, timeout=29)
|
|
48
|
+
if response.status_code != 200:
|
|
49
|
+
raise RuntimeError(f"Login failed: {response.text}")
|
|
50
|
+
return response.json().get("api_token")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_models() -> Any:
|
|
54
|
+
"""List all models."""
|
|
55
|
+
token = load_token()
|
|
56
|
+
url = f"{settings.HOST}/api/v3/public/models/"
|
|
57
|
+
response = requests.post(url, json={"api_token": token}, timeout=29)
|
|
58
|
+
if response.status_code != 200:
|
|
59
|
+
raise RuntimeError(response.text)
|
|
60
|
+
return response.json()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def train(
|
|
64
|
+
model_name: str,
|
|
65
|
+
csv_file: str,
|
|
66
|
+
smiles_column: str,
|
|
67
|
+
value_column: str,
|
|
68
|
+
classification: bool = False,
|
|
69
|
+
) -> Any:
|
|
70
|
+
"""Train a model."""
|
|
71
|
+
token = load_token()
|
|
72
|
+
df = pd.read_csv(csv_file)
|
|
73
|
+
if smiles_column not in df.columns or value_column not in df.columns:
|
|
74
|
+
raise ValueError("CSV missing required columns")
|
|
75
|
+
x = df[smiles_column].astype(str).tolist()
|
|
76
|
+
y = df[value_column].astype(float).tolist()
|
|
77
|
+
payload = {
|
|
78
|
+
"model_name": model_name,
|
|
79
|
+
"api_token": token,
|
|
80
|
+
"x": x,
|
|
81
|
+
"y": y,
|
|
82
|
+
"is_classification": classification,
|
|
83
|
+
}
|
|
84
|
+
response = requests.post(
|
|
85
|
+
f"{settings.HOST}/api/v3/public/train/", json=payload, timeout=29
|
|
86
|
+
)
|
|
87
|
+
if response.status_code != 200:
|
|
88
|
+
raise RuntimeError(response.text)
|
|
89
|
+
return response.json()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def predict_hlm(
|
|
93
|
+
smiles: list[str],
|
|
94
|
+
) -> Any:
|
|
95
|
+
"""Predict using HLM."""
|
|
96
|
+
token = load_token()
|
|
97
|
+
payload = {
|
|
98
|
+
"api_token": token,
|
|
99
|
+
"x": smiles,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
response = requests.post(
|
|
103
|
+
f"{settings.HOST}/api/v3/public/predict-hlm/", json=payload, timeout=29
|
|
104
|
+
)
|
|
105
|
+
if response.status_code != 200:
|
|
106
|
+
raise RuntimeError(response.text)
|
|
107
|
+
return response.json()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_predict_hlm(
|
|
111
|
+
task_id: str,
|
|
112
|
+
) -> Any:
|
|
113
|
+
"""Get predictions from HLM."""
|
|
114
|
+
token = load_token()
|
|
115
|
+
response = requests.post(
|
|
116
|
+
f"{settings.HOST}/api/v3/public/predict-hlm/{task_id}",
|
|
117
|
+
json={"api_token": token},
|
|
118
|
+
timeout=29,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if response.status_code not in [200, 202]:
|
|
122
|
+
raise RuntimeError(response.text)
|
|
123
|
+
return response.json()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def predict(
|
|
127
|
+
model_name: str,
|
|
128
|
+
csv_file: str | None = None,
|
|
129
|
+
smiles_column: str | None = None,
|
|
130
|
+
smiles: list[str] | None = None,
|
|
131
|
+
) -> Any:
|
|
132
|
+
"""Predict using a model."""
|
|
133
|
+
token = load_token()
|
|
134
|
+
# Handle direct SMILES input
|
|
135
|
+
if smiles is not None:
|
|
136
|
+
inputs = smiles
|
|
137
|
+
# Handle CSV input with pandas
|
|
138
|
+
elif csv_file and csv_file.endswith(".csv") and smiles_column:
|
|
139
|
+
df = pd.read_csv(csv_file)
|
|
140
|
+
if smiles_column not in df.columns:
|
|
141
|
+
raise ValueError(f"CSV missing required column: {smiles_column}")
|
|
142
|
+
inputs = df[smiles_column].astype(str).tolist()
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"Either csv_file & smiles_column or smiles must be provided"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
payload = {"model_name": model_name, "input": inputs, "api_token": token}
|
|
149
|
+
response = requests.post(
|
|
150
|
+
f"{settings.HOST}/api/v3/public/inference/", json=payload, timeout=29
|
|
151
|
+
)
|
|
152
|
+
if response.status_code != 200:
|
|
153
|
+
raise RuntimeError(response.text)
|
|
154
|
+
|
|
155
|
+
predictions = response.json()
|
|
156
|
+
|
|
157
|
+
# Add predictions to CSV if using CSV input
|
|
158
|
+
if csv_file and csv_file.endswith(".csv") and smiles_column:
|
|
159
|
+
df["prediction"] = predictions["prediction"]
|
|
160
|
+
df["confidence"] = predictions["confidence"]
|
|
161
|
+
output_file = csv_file.replace(".csv", "_predictions.csv")
|
|
162
|
+
df.to_csv(output_file, index=False)
|
|
163
|
+
print(f"Predictions saved to {output_file}")
|
|
164
|
+
return {"output_file": output_file}
|
|
165
|
+
|
|
166
|
+
# Create predictions DataFrame for text file or direct SMILES input
|
|
167
|
+
results_df = pd.DataFrame(
|
|
168
|
+
{
|
|
169
|
+
"smiles": inputs,
|
|
170
|
+
"prediction": predictions["prediction"],
|
|
171
|
+
"confidence": predictions["confidence"],
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if csv_file:
|
|
176
|
+
output_file = csv_file.rsplit(".", 1)[0] + "_predictions.csv"
|
|
177
|
+
results_df.to_csv(output_file, index=False)
|
|
178
|
+
return {"output_file": output_file}
|
|
179
|
+
|
|
180
|
+
return predictions
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def structure_predict(
|
|
184
|
+
protein: str,
|
|
185
|
+
ligand: str,
|
|
186
|
+
model: str,
|
|
187
|
+
) -> Any:
|
|
188
|
+
"""Predict a structure."""
|
|
189
|
+
token = load_token()
|
|
190
|
+
payload = {
|
|
191
|
+
"protein": protein,
|
|
192
|
+
"ligand": ligand,
|
|
193
|
+
"model": model,
|
|
194
|
+
"api_token": token,
|
|
195
|
+
}
|
|
196
|
+
response = requests.post(
|
|
197
|
+
f"{settings.HOST}/api/v3/public/structure_prediction/",
|
|
198
|
+
json=payload,
|
|
199
|
+
timeout=29,
|
|
200
|
+
)
|
|
201
|
+
if response.status_code != 200:
|
|
202
|
+
raise RuntimeError(response.text)
|
|
203
|
+
return response.json()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def list_structure_tasks() -> Any:
|
|
207
|
+
"""List all structure prediction tasks."""
|
|
208
|
+
token = load_token()
|
|
209
|
+
payload = {"api_token": token}
|
|
210
|
+
response = requests.post(
|
|
211
|
+
f"{settings.HOST}/api/v3/public/structure_prediction/get_all_tasks/",
|
|
212
|
+
json=payload,
|
|
213
|
+
timeout=29,
|
|
214
|
+
)
|
|
215
|
+
if response.status_code != 200:
|
|
216
|
+
raise RuntimeError(response.text)
|
|
217
|
+
return response.json()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def download_structure_prediction(
|
|
221
|
+
task_id: str,
|
|
222
|
+
) -> bytes:
|
|
223
|
+
"""Download a structure prediction task."""
|
|
224
|
+
token = load_token()
|
|
225
|
+
response = requests.post(
|
|
226
|
+
f"{settings.HOST}/api/v3/public/structure_prediction/download/{task_id}",
|
|
227
|
+
headers={
|
|
228
|
+
"accept": "application/json",
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
},
|
|
231
|
+
json={
|
|
232
|
+
"api_token": token,
|
|
233
|
+
},
|
|
234
|
+
timeout=29,
|
|
235
|
+
)
|
|
236
|
+
if response.status_code != 200:
|
|
237
|
+
raise RuntimeError(response.text)
|
|
238
|
+
return response.content
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def model_metadata(model_id: str) -> Any:
|
|
242
|
+
"""Get metadata for a specific model."""
|
|
243
|
+
token = load_token()
|
|
244
|
+
response = requests.post(
|
|
245
|
+
f"{settings.HOST}/api/v3/public/models/metadata/{model_id}",
|
|
246
|
+
json={"api_token": token},
|
|
247
|
+
timeout=29,
|
|
248
|
+
)
|
|
249
|
+
if response.status_code != 200:
|
|
250
|
+
raise RuntimeError(response.text)
|
|
251
|
+
return response.json()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def model_info(model_id: str) -> Any:
|
|
255
|
+
"""Get detailed information for a specific model."""
|
|
256
|
+
token = load_token()
|
|
257
|
+
response = requests.post(
|
|
258
|
+
f"{settings.HOST}/api/v3/public/models/{model_id}",
|
|
259
|
+
json={"api_token": token},
|
|
260
|
+
timeout=29,
|
|
261
|
+
)
|
|
262
|
+
if response.status_code != 200:
|
|
263
|
+
raise RuntimeError(response.text)
|
|
264
|
+
return response.json()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Command line interface for interacting with the deepmirror API.
|
|
2
|
+
|
|
3
|
+
This module provides a CLI for authenticating, managing models, and making
|
|
4
|
+
predictions using the deepmirror platform. It wraps the API client functionality
|
|
5
|
+
in an easy-to-use command line tool.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import getpass
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from . import api
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _save_token(token: str) -> None:
|
|
17
|
+
api.save_token(token)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_token(token: str | None) -> str:
|
|
21
|
+
if token:
|
|
22
|
+
return token
|
|
23
|
+
try:
|
|
24
|
+
return api.load_token()
|
|
25
|
+
except RuntimeError as exc:
|
|
26
|
+
raise click.UsageError(
|
|
27
|
+
"API token required. Please login or pass --token"
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def cli() -> None:
|
|
33
|
+
"""Interact with the deepmirror public API."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
@click.argument("username", required=True)
|
|
38
|
+
def login(username: str) -> None:
|
|
39
|
+
"""Authenticate and obtain an API token."""
|
|
40
|
+
if not username:
|
|
41
|
+
username = input("Email: ")
|
|
42
|
+
password = getpass.getpass("Password: ")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
token = api.authenticate(username, password)
|
|
46
|
+
except RuntimeError as exc:
|
|
47
|
+
raise click.ClickException(str(exc)) from exc
|
|
48
|
+
_save_token(token)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.group()
|
|
52
|
+
def model() -> None:
|
|
53
|
+
"""Model operations."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@model.command("list")
|
|
57
|
+
def model_list() -> None:
|
|
58
|
+
"""List available models for inference."""
|
|
59
|
+
try:
|
|
60
|
+
data = api.list_models()
|
|
61
|
+
except RuntimeError as exc:
|
|
62
|
+
raise click.ClickException(str(exc)) from exc
|
|
63
|
+
click.echo(json.dumps(data, indent=2))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@model.command()
|
|
67
|
+
@click.argument("model_id")
|
|
68
|
+
def metadata(model_id: str) -> None:
|
|
69
|
+
"""Get metadata for a specific model."""
|
|
70
|
+
try:
|
|
71
|
+
data = api.model_metadata(model_id)
|
|
72
|
+
except RuntimeError as exc:
|
|
73
|
+
raise click.ClickException(str(exc)) from exc
|
|
74
|
+
click.echo(json.dumps(data, indent=2))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@model.command()
|
|
78
|
+
@click.argument("model_id")
|
|
79
|
+
def info(model_id: str) -> None:
|
|
80
|
+
"""Get detailed information for a specific model."""
|
|
81
|
+
try:
|
|
82
|
+
data = api.model_info(model_id)
|
|
83
|
+
except RuntimeError as exc:
|
|
84
|
+
raise click.ClickException(str(exc)) from exc
|
|
85
|
+
click.echo(json.dumps(data, indent=2))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@cli.command()
|
|
89
|
+
@click.option("--model-name", required=True)
|
|
90
|
+
@click.option("--csv-file", type=click.Path(exists=True), required=True)
|
|
91
|
+
@click.option("--smiles-column", default="smiles", show_default=True)
|
|
92
|
+
@click.option("--value-column", default="target", show_default=True)
|
|
93
|
+
@click.option("--classification", is_flag=True, default=False)
|
|
94
|
+
def train(
|
|
95
|
+
model_name: str,
|
|
96
|
+
csv_file: str,
|
|
97
|
+
smiles_column: str,
|
|
98
|
+
value_column: str,
|
|
99
|
+
classification: bool,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Train a custom model from a CSV file."""
|
|
102
|
+
try:
|
|
103
|
+
data = api.train(
|
|
104
|
+
model_name,
|
|
105
|
+
csv_file,
|
|
106
|
+
smiles_column,
|
|
107
|
+
value_column,
|
|
108
|
+
classification,
|
|
109
|
+
)
|
|
110
|
+
except (ValueError, RuntimeError) as exc:
|
|
111
|
+
raise click.ClickException(str(exc)) from exc
|
|
112
|
+
click.echo(json.dumps(data, indent=2))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@cli.command()
|
|
116
|
+
@click.option("--model-name", required=True)
|
|
117
|
+
@click.option("--input-file", type=click.Path(exists=True))
|
|
118
|
+
@click.option(
|
|
119
|
+
"--input-header",
|
|
120
|
+
default="smiles",
|
|
121
|
+
help="Column name for SMILES in CSV input",
|
|
122
|
+
)
|
|
123
|
+
@click.option("--input-smiles", multiple=True, help="Direct SMILES input")
|
|
124
|
+
def predict(
|
|
125
|
+
model_name: str,
|
|
126
|
+
csv_file: str | None,
|
|
127
|
+
smiles_column: str | None,
|
|
128
|
+
smiles: tuple[str, ...] | None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Run prediction using a trained model.
|
|
131
|
+
|
|
132
|
+
Takes input as either a CSV file with SMILES column, a text file with SMILES per line,
|
|
133
|
+
or direct SMILES strings via --input-smiles.
|
|
134
|
+
"""
|
|
135
|
+
if not csv_file and not smiles:
|
|
136
|
+
raise click.UsageError("Either --csv-file or --smiles must be provided")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
data = api.predict(
|
|
140
|
+
model_name,
|
|
141
|
+
csv_file=csv_file,
|
|
142
|
+
smiles_column=smiles_column,
|
|
143
|
+
smiles=list(smiles) if smiles else None,
|
|
144
|
+
)
|
|
145
|
+
except (ValueError, RuntimeError) as exc:
|
|
146
|
+
raise click.ClickException(str(exc)) from exc
|
|
147
|
+
click.echo(json.dumps(data, indent=2))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@cli.group()
|
|
151
|
+
def structure() -> None:
|
|
152
|
+
"""Structure prediction operations."""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@structure.command("predict")
|
|
156
|
+
@click.argument("protein")
|
|
157
|
+
@click.argument("ligand")
|
|
158
|
+
@click.option("--model-name", default="chai", show_default=True)
|
|
159
|
+
def structure_predict(protein: str, ligand: str, model_name: str) -> None:
|
|
160
|
+
"""Submit a structure prediction job."""
|
|
161
|
+
try:
|
|
162
|
+
data = api.structure_predict(protein, ligand, model_name)
|
|
163
|
+
except RuntimeError as exc:
|
|
164
|
+
raise click.ClickException(str(exc)) from exc
|
|
165
|
+
click.echo(json.dumps(data, indent=2))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@structure.command("list")
|
|
169
|
+
def structure_list() -> None:
|
|
170
|
+
"""List submitted structure prediction tasks."""
|
|
171
|
+
try:
|
|
172
|
+
data = api.list_structure_tasks()
|
|
173
|
+
except RuntimeError as exc:
|
|
174
|
+
raise click.ClickException(str(exc)) from exc
|
|
175
|
+
click.echo(json.dumps(data, indent=2))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@structure.command("download")
|
|
179
|
+
@click.argument("task_id")
|
|
180
|
+
@click.argument("output_file", type=click.Path())
|
|
181
|
+
def structure_download(task_id: str, output_file: str) -> None:
|
|
182
|
+
"""Download structure prediction results."""
|
|
183
|
+
try:
|
|
184
|
+
data = api.download_structure_prediction(task_id)
|
|
185
|
+
with open(output_file, "wb") as f:
|
|
186
|
+
f.write(data)
|
|
187
|
+
except RuntimeError as exc:
|
|
188
|
+
raise click.ClickException(str(exc)) from exc
|
|
189
|
+
click.echo(f"Downloaded structure prediction to {output_file}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
cli()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Configuration for the deepmirror CLI.
|
|
2
|
+
|
|
3
|
+
This module defines the settings for the deepmirror CLI, including the API host,
|
|
4
|
+
and the directory where the API token is stored.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic_settings import BaseSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# pylint: disable=too-few-public-methods
|
|
14
|
+
class Settings(BaseSettings):
|
|
15
|
+
"""Settings for the deepmirror CLI."""
|
|
16
|
+
|
|
17
|
+
HOST: str = "https://api.app.deepmirror.ai"
|
|
18
|
+
|
|
19
|
+
if os.name == "nt":
|
|
20
|
+
BASE_DIR: Path = Path(os.getenv("APPDATA", str(Path.home()))) / "Local"
|
|
21
|
+
else:
|
|
22
|
+
BASE_DIR: Path = Path(
|
|
23
|
+
os.getenv("XDG_CONFIG_HOME", str(Path.home() / ".config"))
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
CONFIG_DIR: Path = BASE_DIR / "deepmirror"
|
|
27
|
+
TOKEN_FILE: Path = CONFIG_DIR / "token"
|
|
28
|
+
HOST_FILE: Path = CONFIG_DIR / "host"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
settings = Settings()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepmirror
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Command line interface for the deepmirror public API
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: click>=8.1.8
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Requires-Dist: pandas>=2.2.2
|
|
10
|
+
Requires-Dist: pydantic>=2.6.3
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.9.1
|
|
12
|
+
Requires-Dist: scikit-learn>=1.6.1
|
|
13
|
+
Requires-Dist: matplotlib>=3.10.3
|
|
14
|
+
Requires-Dist: tqdm>=4.67.1
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit>=3.6.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pylint>=3.3.6; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.11.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
deepmirror/__init__.py
|
|
5
|
+
deepmirror/api.py
|
|
6
|
+
deepmirror/cli.py
|
|
7
|
+
deepmirror/config.py
|
|
8
|
+
deepmirror.egg-info/PKG-INFO
|
|
9
|
+
deepmirror.egg-info/SOURCES.txt
|
|
10
|
+
deepmirror.egg-info/dependency_links.txt
|
|
11
|
+
deepmirror.egg-info/entry_points.txt
|
|
12
|
+
deepmirror.egg-info/requires.txt
|
|
13
|
+
deepmirror.egg-info/top_level.txt
|
|
14
|
+
tests/test_train.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepmirror
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "deepmirror"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Command line interface for the deepmirror public API"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
"click>=8.1.8", # BSD-3
|
|
9
|
+
"requests>=2.31.0", # Apache-2.0
|
|
10
|
+
"pandas>=2.2.2", # BSD-3
|
|
11
|
+
"pydantic>=2.6.3", # MIT
|
|
12
|
+
"pydantic-settings>=2.9.1", # MIT
|
|
13
|
+
"scikit-learn>=1.6.1", # BSD-3
|
|
14
|
+
"matplotlib>=3.10.3", # PSF
|
|
15
|
+
"tqdm>=4.67.1", # MPL-2.0
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0.0",
|
|
21
|
+
"pre-commit>=3.6.0",
|
|
22
|
+
"pylint>=3.3.6",
|
|
23
|
+
"ruff>=0.11.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
dm = "deepmirror.cli:cli"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools>=61.2", "wheel"]
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["deepmirror"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 80
|
|
40
|
+
target-version = 'py313'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Test that the train functionality can correcelty load data from a CSV file."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from deepmirror import api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def csv_path(tmp_path):
|
|
13
|
+
"""Create a temporary CSV file for testing."""
|
|
14
|
+
path = tmp_path / "data.csv"
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_train_valid_columns(test_csv_path: Path) -> None:
|
|
19
|
+
"""Test training with valid columns."""
|
|
20
|
+
test_csv_path.write_text("smiles,value\nCCO,1\n")
|
|
21
|
+
with mock.patch("deepmirror.api.requests.post") as mock_post:
|
|
22
|
+
mock_post.return_value.status_code = 200
|
|
23
|
+
mock_post.return_value.json.return_value = {"ok": True}
|
|
24
|
+
result = api.train(
|
|
25
|
+
"mymodel",
|
|
26
|
+
str(test_csv_path),
|
|
27
|
+
"smiles",
|
|
28
|
+
"value",
|
|
29
|
+
False,
|
|
30
|
+
)
|
|
31
|
+
assert result == {"ok": True}
|
|
32
|
+
payload = mock_post.call_args.kwargs["json"]
|
|
33
|
+
assert payload["x"] == ["CCO"]
|
|
34
|
+
assert payload["y"] == [1.0]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_train_missing_columns(test_csv_path: Path) -> None:
|
|
38
|
+
"""Test training with missing columns."""
|
|
39
|
+
test_csv_path.write_text("a,b\n1,2\n")
|
|
40
|
+
with pytest.raises(ValueError):
|
|
41
|
+
api.train(
|
|
42
|
+
"mymodel",
|
|
43
|
+
str(test_csv_path),
|
|
44
|
+
"smiles",
|
|
45
|
+
"value",
|
|
46
|
+
False,
|
|
47
|
+
)
|