Skip to content

Commit fbb5ef0

Browse files
authored
Merge pull request #33 from theproteinbot/codex/e2e-fixes
Harden inference API and add secure backend/deserialization architecture
2 parents 9f7a99a + 84f3e42 commit fbb5ef0

30 files changed

Lines changed: 1775 additions & 435 deletions

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@ catpred/data/__pycache__/esm_utils.cpython-312.pyc
3232
catpred/data/__pycache__/data.cpython-312.pyc
3333
catpred/data/__pycache__/cache_utils.cpython-312.pyc
3434
catpred/data/__pycache__/__init__.cpython-312.pyc
35+
36+
# Generic Python/OS artifacts
37+
__pycache__/
38+
*.pyo
39+
*.pyd
40+
.DS_Store
41+
*.egg-info/
42+
.venv/
43+
.ipynb_checkpoints/
44+
45+
# Local validation artifacts
46+
.e2e-assets/
47+
.e2e-tests/

README.md

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- [System Requirements](#requirements)
2424
- [Installation](#installing)
2525
- [Prediction](#predict)
26+
- [Web API (Optional)](#web-api-optional)
2627
- [Reproducibility](#reproduce)
2728
- [Acknowledgements](#acknw)
2829
- [License](#license)
@@ -65,7 +66,8 @@ Then proceed to either option below to complete the installation. If installing
6566
```bash
6667
mkdir catpred_pipeline catpred_pipeline/results
6768
cd catpred_pipeline
68-
wget https://catpred.s3.us-east-1.amazonaws.com/capsule_data_update.tar.gz
69+
wget -c --tries=5 --timeout=30 https://catpred.s3.us-east-1.amazonaws.com/capsule_data_update.tar.gz || \
70+
wget -c --tries=5 --timeout=30 https://catpred.s3.amazonaws.com/capsule_data_update.tar.gz
6971
tar -xzf capsule_data_update.tar.gz
7072
git clone https://github.com/maranasgroup/catpred.git
7173
cd catpred
@@ -74,10 +76,147 @@ conda activate catpred
7476
pip install -e .
7577
````
7678

79+
`stride` is Linux-only and optional for the default demos. If needed for your workflow, install it separately on Linux:
80+
81+
```bash
82+
conda install -c kimlab stride
83+
```
84+
7785
### 🔮 Prediction <a name="predict"></a>
7886

7987
The Jupyter Notebook `batch_demo.ipynb` and the Python script `demo_run.py` show the usage of pre-trained models for prediction.
8088

89+
Input CSV requirements for `demo_run.py` and batch prediction:
90+
- Required columns: `SMILES`, `sequence`, `pdbpath`.
91+
- `pdbpath` must be unique per unique sequence. Reusing the same `pdbpath` for different sequences can produce incorrect cached embeddings.
92+
- Reusing the same `pdbpath` for repeated measurements of the same sequence is supported.
93+
94+
The helper script used to build protein records is:
95+
96+
```bash
97+
python ./scripts/create_pdbrecords.py --data_file <input.csv> --out_file <input.json.gz>
98+
```
99+
100+
CatPred currently expects one sequence per row. Multi-protein complexes (e.g., heteromers/homodimers) are not explicitly modeled as separate chains in the default prediction workflow.
101+
102+
For released benchmark datasets, the number of entries with 3D structure can be smaller than the total sequence/substrate pairs; 3D-derived artifacts are available only for the subset with valid structure mapping.
103+
104+
### 🌍 Web API (Optional)
105+
106+
CatPred also provides an optional FastAPI service for prediction workflows.
107+
108+
Install web dependencies:
109+
110+
```bash
111+
pip install -e ".[web]"
112+
```
113+
114+
Run the API:
115+
116+
```bash
117+
catpred_web --host 0.0.0.0 --port 8000
118+
```
119+
120+
Endpoints:
121+
- `GET /health` — liveness check.
122+
- `GET /ready` — backend configuration/readiness.
123+
- `POST /predict` — run inference.
124+
125+
By default, the API is hardened for service use:
126+
- `input_file` requests are disabled (use `input_rows` instead).
127+
- request-time overrides of `repo_root` / `python_executable` are disabled.
128+
- `results_dir` is constrained under `CATPRED_API_RESULTS_ROOT`.
129+
130+
Minimal `POST /predict` example for local inference using `input_rows`:
131+
132+
```bash
133+
curl -X POST http://127.0.0.1:8000/predict \
134+
-H "Content-Type: application/json" \
135+
-d '{
136+
"parameter": "kcat",
137+
"checkpoint_dir": "../data/pretrained/reproduce_checkpoints/kcat",
138+
"input_rows": [
139+
{"SMILES": "CCO", "sequence": "ACDEFGHIK", "pdbpath": "seq_a"},
140+
{"SMILES": "CCN", "sequence": "LMNPQRSTV", "pdbpath": "seq_b"}
141+
],
142+
"results_dir": "batch1",
143+
"backend": "local"
144+
}'
145+
```
146+
147+
You can keep local inference as default and optionally enable Modal as another backend:
148+
149+
```bash
150+
export CATPRED_DEFAULT_BACKEND=local
151+
export CATPRED_MODAL_ENDPOINT="https://<your-modal-endpoint>"
152+
export CATPRED_MODAL_TOKEN="<optional-token>"
153+
export CATPRED_MODAL_FALLBACK_TO_LOCAL=1
154+
```
155+
156+
Use `"backend": "modal"` in `/predict` requests to route through Modal. If fallback is enabled (env var above or request field `fallback_to_local`), failed modal requests can automatically reroute to local inference.
157+
158+
Optional API environment variables:
159+
160+
```bash
161+
# Root directories used by API path constraints
162+
export CATPRED_API_INPUT_ROOT="/absolute/path/for/input-csvs"
163+
export CATPRED_API_RESULTS_ROOT="/absolute/path/for/results"
164+
export CATPRED_API_CHECKPOINT_ROOT="/absolute/path/for/checkpoints"
165+
166+
# Enable only for trusted local workflows (not recommended for public deployments)
167+
export CATPRED_API_ALLOW_INPUT_FILE=1
168+
export CATPRED_API_ALLOW_UNSAFE_OVERRIDES=1
169+
170+
# Request limits
171+
export CATPRED_API_MAX_INPUT_ROWS=1000
172+
export CATPRED_API_MAX_INPUT_FILE_BYTES=5000000
173+
```
174+
175+
Deserialization hardening controls:
176+
177+
```bash
178+
# Trusted roots used by secure loaders (colon-separated list on Unix)
179+
export CATPRED_TRUSTED_DESERIALIZATION_ROOTS="/srv/catpred:/srv/catpred-data"
180+
181+
# Backward-compatible default is enabled (1). Set to 0 to block unsafe pickle-based loading.
182+
# Use 0 only after validating your artifacts are safe-load compatible.
183+
export CATPRED_ALLOW_UNSAFE_DESERIALIZATION=1
184+
```
185+
186+
### 🧪 Fine-Tuning On Custom Data
187+
188+
You can fine-tune CatPred on your own regression targets using `train.py`.
189+
190+
1. Prepare train/val/test CSVs with at least:
191+
- `SMILES`
192+
- `sequence`
193+
- `pdbpath` (unique per unique sequence)
194+
- one numeric target column (for example: `log10kcat_max`)
195+
196+
2. Build a protein-records file that covers all `pdbpath` values in your splits:
197+
198+
```bash
199+
python ./scripts/create_pdbrecords.py --data_file <combined_or_train_csv> --out_file <protein_records.json.gz>
200+
```
201+
202+
3. Train:
203+
204+
```bash
205+
python train.py \
206+
--protein_records_path <protein_records.json.gz> \
207+
--data_path <train.csv> \
208+
--separate_val_path <val.csv> \
209+
--separate_test_path <test.csv> \
210+
--dataset_type regression \
211+
--smiles_columns SMILES \
212+
--target_columns <target_column_name> \
213+
--add_esm_feats \
214+
--loss_function mve \
215+
--save_dir <output_checkpoint_dir>
216+
```
217+
218+
For working end-to-end examples, see the training commands in scripts such as `scripts/reproduce_figS10_catpred.sh`.
219+
81220
### 🔄 Reproducing Publication Results <a name="reproduce"></a>
82221

83222
We provide three separate ways for reproducing the results of the publication.

catpred/__init__.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1-
import catpred.data
2-
import catpred.features
3-
import catpred.models
4-
import catpred.train
5-
import catpred.uncertainty
6-
7-
import catpred.args
8-
import catpred.constants
9-
import catpred.nn_utils
10-
import catpred.utils
11-
import catpred.rdkit
1+
from __future__ import annotations
2+
3+
import importlib
124

135
__version__ = "0.0.1"
6+
7+
_LAZY_SUBMODULES = {
8+
"args",
9+
"constants",
10+
"data",
11+
"features",
12+
"inference",
13+
"models",
14+
"nn_utils",
15+
"rdkit",
16+
"security",
17+
"train",
18+
"uncertainty",
19+
"utils",
20+
}
21+
22+
__all__ = sorted(_LAZY_SUBMODULES) + ["__version__"]
23+
24+
25+
def __getattr__(name: str):
26+
if name in _LAZY_SUBMODULES:
27+
module = importlib.import_module(f"catpred.{name}")
28+
globals()[name] = module
29+
return module
30+
raise AttributeError(f"module 'catpred' has no attribute '{name}'")

catpred/args.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import os
33
from tempfile import TemporaryDirectory
4-
import pickle
54
from typing import List, Optional
65
from typing_extensions import Literal
76
from packaging import version
@@ -15,6 +14,7 @@
1514
import catpred.data.utils
1615
from catpred.data import set_cache_mol, empty_cache
1716
from catpred.features import get_available_features_generators
17+
from catpred.security import load_index_artifact
1818

1919

2020
Metric = Literal['auc', 'prc-auc', 'rmse', 'mae', 'mse', 'r2', 'accuracy', 'cross_entropy', 'binary_cross_entropy', 'sid', 'wasserstein', 'f1', 'mcc', 'bounded_rmse', 'bounded_mae', 'bounded_mse']
@@ -815,8 +815,10 @@ def process_args(self) -> None:
815815
raise ValueError('When using crossval or index_predetermined split type, must provide crossval_index_file.')
816816

817817
if self.split_type in ['crossval', 'index_predetermined']:
818-
with open(self.crossval_index_file, 'rb') as rf:
819-
self._crossval_index_sets = pickle.load(rf)
818+
self._crossval_index_sets = load_index_artifact(
819+
self.crossval_index_file,
820+
purpose="cross-validation index file",
821+
)
820822
self.num_folds = len(self.crossval_index_sets)
821823
self.seed = 0
822824

catpred/data/cache_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import hashlib
55
from functools import wraps
66
from pathlib import Path
7+
from catpred.security import load_torch_artifact
78

89
def exists(val):
910
return val is not None
@@ -92,7 +93,11 @@ def inner(t, *args, __cache_key = None, **kwargs):
9293

9394
if entry_path.exists():
9495
log(f'cache hit: fetching {t} from {str(entry_path)}')
95-
return torch.load(str(entry_path))
96+
return load_torch_artifact(
97+
str(entry_path),
98+
purpose="esm cache entry",
99+
roots=[CACHE_PATH],
100+
)
96101

97102
out = fn(t, *args, **kwargs)
98103

0 commit comments

Comments
 (0)