merge-cli 1.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.
- merge_cli-1.0.0/PKG-INFO +126 -0
- merge_cli-1.0.0/README.md +114 -0
- merge_cli-1.0.0/merge_cli/__init__.py +1 -0
- merge_cli-1.0.0/merge_cli/api.py +190 -0
- merge_cli-1.0.0/merge_cli/cli.py +318 -0
- merge_cli-1.0.0/merge_cli/config.py +71 -0
- merge_cli-1.0.0/merge_cli/output.py +202 -0
- merge_cli-1.0.0/merge_cli.egg-info/PKG-INFO +126 -0
- merge_cli-1.0.0/merge_cli.egg-info/SOURCES.txt +13 -0
- merge_cli-1.0.0/merge_cli.egg-info/dependency_links.txt +1 -0
- merge_cli-1.0.0/merge_cli.egg-info/entry_points.txt +2 -0
- merge_cli-1.0.0/merge_cli.egg-info/requires.txt +4 -0
- merge_cli-1.0.0/merge_cli.egg-info/top_level.txt +1 -0
- merge_cli-1.0.0/pyproject.toml +25 -0
- merge_cli-1.0.0/setup.cfg +4 -0
merge_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: merge-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI for MERGE variant pathogenicity prediction
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: click>=8.1
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: keyring>=24.0
|
|
12
|
+
|
|
13
|
+
# MERGE CLI
|
|
14
|
+
|
|
15
|
+
MERGE 变异致病性预测命令行工具。所有计算在服务器端完成,本地只需 Python 3.9+。
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install merge-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. 配置服务器地址(只需配置一次)
|
|
27
|
+
merge config set-url https://your-server.com
|
|
28
|
+
merge config set-token YOUR_API_TOKEN
|
|
29
|
+
|
|
30
|
+
# 2. 单变异预测
|
|
31
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G
|
|
32
|
+
|
|
33
|
+
# 3. 批量预测(上传 VCF,结果发邮件)
|
|
34
|
+
merge batch my_variants.vcf --email you@example.com
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 命令详解
|
|
38
|
+
|
|
39
|
+
### `merge predict` — 单变异预测
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
选项:
|
|
43
|
+
--chrom 染色体(chr17 或 17 均可) [必填]
|
|
44
|
+
--pos 变异位置(1-based) [必填]
|
|
45
|
+
--ref 参考碱基 [必填]
|
|
46
|
+
--alt 突变碱基 [必填]
|
|
47
|
+
--genome hg38 / hg19 [默认: hg38]
|
|
48
|
+
--format table / json / tsv [默认: table]
|
|
49
|
+
--no-ensemble 跳过 MERGE 集成打分
|
|
50
|
+
--no-alphagenome / --no-hyenadna / --no-nt
|
|
51
|
+
--no-alphamissense / --no-esm1b / --no-gpn-msa
|
|
52
|
+
--no-popeve / --no-evo2 / --no-evo1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**输出示例(table 模式):**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
╭──────────────────────────────────────────╮
|
|
59
|
+
│ MERGE 预测结果 │
|
|
60
|
+
├──────────────────┬───────────────────────┤
|
|
61
|
+
│ 变异位点 │ chr17:43092919 A → G │
|
|
62
|
+
│ 基因 / 转录本 │ BRCA1 / ENST00000357654│
|
|
63
|
+
├──────────────────┼───────────────────────┤
|
|
64
|
+
│ MERGE 致病性 │ 87.3% Likely Pathogenic│
|
|
65
|
+
│ 使用模型 │ ClinVar │
|
|
66
|
+
├──────────────────┼───────────────────────┤
|
|
67
|
+
│ AlphaMissense │ 0.9341 │
|
|
68
|
+
│ ESM1b │ -3.2180 │
|
|
69
|
+
│ GPN-MSA │ -1.4420 │
|
|
70
|
+
│ popEVE │ 0.8812 │
|
|
71
|
+
│ AlphaGenome │ 0.0234 │
|
|
72
|
+
│ HyenaDNA │ -2.1100 │
|
|
73
|
+
│ NT │ -1.8830 │
|
|
74
|
+
│ Evo2 LLR │ -4.2210 │
|
|
75
|
+
│ Evo1 Delta │ -3.9910 │
|
|
76
|
+
╰──────────────────┴───────────────────────╯
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**管道输出(TSV 模式,方便写脚本):**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G --format tsv >> results.tsv
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**脚本批量调用单变异(小于 20 个时比上传 VCF 更快):**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
while IFS=$'\t' read -r chrom pos ref alt; do
|
|
89
|
+
merge predict --chrom "$chrom" --pos "$pos" --ref "$ref" --alt "$alt" --format tsv
|
|
90
|
+
done < variants.tsv >> results.tsv
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `merge batch` — 批量 VCF 预测
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
merge batch variants.vcf --email you@example.com
|
|
99
|
+
merge batch variants.vcf.gz --email you@example.com --genome hg19 --no-evo2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
任务提交成功后返回 Job ID,结果通过邮件发送,附件包含:
|
|
103
|
+
- `batch_predictions.csv` — 所有模型分数汇总(可直接用 Excel 打开)
|
|
104
|
+
- `batch_predictions.vcf` — 标准 VCF 格式,含 MERGE 及各模型分数
|
|
105
|
+
- `imputation_details.csv` — MERGE 各特征缺失填补情况
|
|
106
|
+
|
|
107
|
+
服务端限制:每 IP 每日 5 次,同一邮箱间隔 1 小时,单次最多 2000 个变异。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `merge status` — 查询批量任务
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
merge status A3F2C1B0 # 查看一次
|
|
115
|
+
merge status A3F2C1B0 --watch # 每 30 秒轮询
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 环境变量(适合 CI / 服务器环境)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export MERGE_API_URL=https://your-server.com
|
|
124
|
+
export MERGE_API_TOKEN=your-token
|
|
125
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T
|
|
126
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# MERGE CLI
|
|
2
|
+
|
|
3
|
+
MERGE 变异致病性预测命令行工具。所有计算在服务器端完成,本地只需 Python 3.9+。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install merge-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. 配置服务器地址(只需配置一次)
|
|
15
|
+
merge config set-url https://your-server.com
|
|
16
|
+
merge config set-token YOUR_API_TOKEN
|
|
17
|
+
|
|
18
|
+
# 2. 单变异预测
|
|
19
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G
|
|
20
|
+
|
|
21
|
+
# 3. 批量预测(上传 VCF,结果发邮件)
|
|
22
|
+
merge batch my_variants.vcf --email you@example.com
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 命令详解
|
|
26
|
+
|
|
27
|
+
### `merge predict` — 单变异预测
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
选项:
|
|
31
|
+
--chrom 染色体(chr17 或 17 均可) [必填]
|
|
32
|
+
--pos 变异位置(1-based) [必填]
|
|
33
|
+
--ref 参考碱基 [必填]
|
|
34
|
+
--alt 突变碱基 [必填]
|
|
35
|
+
--genome hg38 / hg19 [默认: hg38]
|
|
36
|
+
--format table / json / tsv [默认: table]
|
|
37
|
+
--no-ensemble 跳过 MERGE 集成打分
|
|
38
|
+
--no-alphagenome / --no-hyenadna / --no-nt
|
|
39
|
+
--no-alphamissense / --no-esm1b / --no-gpn-msa
|
|
40
|
+
--no-popeve / --no-evo2 / --no-evo1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**输出示例(table 模式):**
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
╭──────────────────────────────────────────╮
|
|
47
|
+
│ MERGE 预测结果 │
|
|
48
|
+
├──────────────────┬───────────────────────┤
|
|
49
|
+
│ 变异位点 │ chr17:43092919 A → G │
|
|
50
|
+
│ 基因 / 转录本 │ BRCA1 / ENST00000357654│
|
|
51
|
+
├──────────────────┼───────────────────────┤
|
|
52
|
+
│ MERGE 致病性 │ 87.3% Likely Pathogenic│
|
|
53
|
+
│ 使用模型 │ ClinVar │
|
|
54
|
+
├──────────────────┼───────────────────────┤
|
|
55
|
+
│ AlphaMissense │ 0.9341 │
|
|
56
|
+
│ ESM1b │ -3.2180 │
|
|
57
|
+
│ GPN-MSA │ -1.4420 │
|
|
58
|
+
│ popEVE │ 0.8812 │
|
|
59
|
+
│ AlphaGenome │ 0.0234 │
|
|
60
|
+
│ HyenaDNA │ -2.1100 │
|
|
61
|
+
│ NT │ -1.8830 │
|
|
62
|
+
│ Evo2 LLR │ -4.2210 │
|
|
63
|
+
│ Evo1 Delta │ -3.9910 │
|
|
64
|
+
╰──────────────────┴───────────────────────╯
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**管道输出(TSV 模式,方便写脚本):**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G --format tsv >> results.tsv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**脚本批量调用单变异(小于 20 个时比上传 VCF 更快):**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
while IFS=$'\t' read -r chrom pos ref alt; do
|
|
77
|
+
merge predict --chrom "$chrom" --pos "$pos" --ref "$ref" --alt "$alt" --format tsv
|
|
78
|
+
done < variants.tsv >> results.tsv
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### `merge batch` — 批量 VCF 预测
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
merge batch variants.vcf --email you@example.com
|
|
87
|
+
merge batch variants.vcf.gz --email you@example.com --genome hg19 --no-evo2
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
任务提交成功后返回 Job ID,结果通过邮件发送,附件包含:
|
|
91
|
+
- `batch_predictions.csv` — 所有模型分数汇总(可直接用 Excel 打开)
|
|
92
|
+
- `batch_predictions.vcf` — 标准 VCF 格式,含 MERGE 及各模型分数
|
|
93
|
+
- `imputation_details.csv` — MERGE 各特征缺失填补情况
|
|
94
|
+
|
|
95
|
+
服务端限制:每 IP 每日 5 次,同一邮箱间隔 1 小时,单次最多 2000 个变异。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### `merge status` — 查询批量任务
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
merge status A3F2C1B0 # 查看一次
|
|
103
|
+
merge status A3F2C1B0 --watch # 每 30 秒轮询
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 环境变量(适合 CI / 服务器环境)
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export MERGE_API_URL=https://your-server.com
|
|
112
|
+
export MERGE_API_TOKEN=your-token
|
|
113
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T
|
|
114
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
merge_cli/api.py
|
|
3
|
+
封装所有对 Django 后端的 HTTP 请求。
|
|
4
|
+
每个函数对应 views.py 里的一个端点,参数名与 views.py 完全一致。
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .config import get_api_url, get_token
|
|
12
|
+
|
|
13
|
+
# 所有请求的默认超时(秒)
|
|
14
|
+
_TIMEOUT_SINGLE = 180 # 单变异:Evo2 最慢,约 2 分钟
|
|
15
|
+
_TIMEOUT_BATCH = 60 # 批量提交:只是上传文件,后台异步处理
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _headers() -> dict:
|
|
19
|
+
token = get_token()
|
|
20
|
+
h = {"Accept": "application/json"}
|
|
21
|
+
if token:
|
|
22
|
+
h["Authorization"] = f"Token {token}"
|
|
23
|
+
return h
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _base() -> str:
|
|
27
|
+
url = get_api_url()
|
|
28
|
+
if not url:
|
|
29
|
+
print("错误:未配置 API 地址。请先运行:merge config set-url https://your-server.com")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
return url
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────────────────────
|
|
35
|
+
# 1. 单变异预测 POST /predict/
|
|
36
|
+
# 对应 views.py: combined_prediction_view
|
|
37
|
+
# ─────────────────────────────────────────────────────────────
|
|
38
|
+
def predict_single(
|
|
39
|
+
chrom: str, pos: int, ref: str, alt: str,
|
|
40
|
+
genome_version: str = "hg38",
|
|
41
|
+
# 模型开关,与 views.py POST 参数名一一对应
|
|
42
|
+
use_alphagenome: bool = True,
|
|
43
|
+
use_hyenadna: bool = True,
|
|
44
|
+
use_nt: bool = True,
|
|
45
|
+
use_alphamissense: bool = True,
|
|
46
|
+
use_esm1b: bool = True,
|
|
47
|
+
use_gpn_msa: bool = True,
|
|
48
|
+
use_popeve: bool = True,
|
|
49
|
+
use_evo2: bool = True,
|
|
50
|
+
use_evo1: bool = True,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
调用 /predict/ 端点,返回完整 prediction 字典。
|
|
54
|
+
包含 dbnsfp / alphagenome / hyenadna / nt / gpn_msa / popeve / evo2 / evo1 / errors。
|
|
55
|
+
"""
|
|
56
|
+
payload = {
|
|
57
|
+
"chrom": chrom, "pos": pos, "ref": ref, "alt": alt,
|
|
58
|
+
"genome_version": genome_version,
|
|
59
|
+
"use_alphagenome": str(use_alphagenome).lower(),
|
|
60
|
+
"use_hyenadna": str(use_hyenadna).lower(),
|
|
61
|
+
"use_nt": str(use_nt).lower(),
|
|
62
|
+
"use_alphamissense": str(use_alphamissense).lower(),
|
|
63
|
+
"use_esm1b": str(use_esm1b).lower(),
|
|
64
|
+
"use_gpn_msa": str(use_gpn_msa).lower(),
|
|
65
|
+
"use_popeve": str(use_popeve).lower(),
|
|
66
|
+
"use_evo2": str(use_evo2).lower(),
|
|
67
|
+
"use_evo1": str(use_evo1).lower(),
|
|
68
|
+
}
|
|
69
|
+
resp = requests.post(
|
|
70
|
+
f"{_base()}/predict/",
|
|
71
|
+
json=payload,
|
|
72
|
+
headers=_headers(),
|
|
73
|
+
timeout=_TIMEOUT_SINGLE,
|
|
74
|
+
)
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
return resp.json()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ─────────────────────────────────────────────────────────────
|
|
80
|
+
# 2. 集成模型评分 POST /ensemble/
|
|
81
|
+
# 对应 views.py: ensemble_predict_view
|
|
82
|
+
# 通常在 predict_single 成功后自动调用
|
|
83
|
+
# ─────────────────────────────────────────────────────────────
|
|
84
|
+
def predict_ensemble(prediction: dict, all_transcripts: list) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
用 /predict/ 返回的 prediction 字典再调用 /ensemble/,
|
|
87
|
+
获取 MERGE 集成得分和 SHAP 图(base64 PNG)。
|
|
88
|
+
"""
|
|
89
|
+
payload = {"prediction": prediction, "all_transcripts": all_transcripts}
|
|
90
|
+
resp = requests.post(
|
|
91
|
+
f"{_base()}/ensemble/",
|
|
92
|
+
json=payload,
|
|
93
|
+
headers=_headers(),
|
|
94
|
+
timeout=_TIMEOUT_SINGLE,
|
|
95
|
+
)
|
|
96
|
+
resp.raise_for_status()
|
|
97
|
+
return resp.json()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─────────────────────────────────────────────────────────────
|
|
101
|
+
# 3. 批量提交 POST /api/submit_batch_job/
|
|
102
|
+
# 对应 views.py: submit_batch_job
|
|
103
|
+
# ─────────────────────────────────────────────────────────────
|
|
104
|
+
def submit_batch(
|
|
105
|
+
vcf_path: str,
|
|
106
|
+
email: str,
|
|
107
|
+
genome_version: str = "hg38",
|
|
108
|
+
use_alphagenome: bool = True,
|
|
109
|
+
use_hyenadna: bool = True,
|
|
110
|
+
use_nt: bool = True,
|
|
111
|
+
use_alphamissense: bool = True,
|
|
112
|
+
use_esm1b: bool = True,
|
|
113
|
+
use_gpn_msa: bool = True,
|
|
114
|
+
use_popeve: bool = True,
|
|
115
|
+
use_evo2: bool = True,
|
|
116
|
+
use_evo1: bool = True,
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
上传 VCF 文件,异步批量预测,结果发送至 email。
|
|
120
|
+
返回 job_id 供后续查询状态。
|
|
121
|
+
"""
|
|
122
|
+
data = {
|
|
123
|
+
"notify_email": email,
|
|
124
|
+
"genome_version": genome_version,
|
|
125
|
+
"use_alphagenome": str(use_alphagenome).lower(),
|
|
126
|
+
"use_hyenadna": str(use_hyenadna).lower(),
|
|
127
|
+
"use_nt": str(use_nt).lower(),
|
|
128
|
+
"use_alphamissense": str(use_alphamissense).lower(),
|
|
129
|
+
"use_esm1b": str(use_esm1b).lower(),
|
|
130
|
+
"use_gpn_msa": str(use_gpn_msa).lower(),
|
|
131
|
+
"use_popeve": str(use_popeve).lower(),
|
|
132
|
+
"use_evo2": str(use_evo2).lower(),
|
|
133
|
+
"use_evo1": str(use_evo1).lower(),
|
|
134
|
+
}
|
|
135
|
+
with open(vcf_path, "rb") as f:
|
|
136
|
+
resp = requests.post(
|
|
137
|
+
f"{_base()}/api/submit_batch_job/",
|
|
138
|
+
data=data,
|
|
139
|
+
files={"vcf_file": (vcf_path.split("/")[-1], f)},
|
|
140
|
+
headers=_headers(),
|
|
141
|
+
timeout=_TIMEOUT_BATCH,
|
|
142
|
+
)
|
|
143
|
+
resp.raise_for_status()
|
|
144
|
+
return resp.json()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ─────────────────────────────────────────────────────────────
|
|
148
|
+
# 4. 查询批量任务状态 GET /api/batch_job/<job_id>/
|
|
149
|
+
# 对应 views.py: batch_job_status
|
|
150
|
+
# ─────────────────────────────────────────────────────────────
|
|
151
|
+
def get_batch_status(job_id: str) -> dict:
|
|
152
|
+
resp = requests.get(
|
|
153
|
+
f"{_base()}/api/batch_job/{job_id}/",
|
|
154
|
+
headers=_headers(),
|
|
155
|
+
timeout=30,
|
|
156
|
+
)
|
|
157
|
+
resp.raise_for_status()
|
|
158
|
+
return resp.json()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ─────────────────────────────────────────────────────────────
|
|
162
|
+
# 5. 开放注册 POST /api/register/
|
|
163
|
+
# 对应 views.py: register_and_get_token
|
|
164
|
+
# 公开接口,不需要携带 Token
|
|
165
|
+
# ─────────────────────────────────────────────────────────────
|
|
166
|
+
def register(username: str, email: str, password: str) -> dict:
|
|
167
|
+
resp = requests.post(
|
|
168
|
+
f"{_base()}/api/register/",
|
|
169
|
+
json={"username": username, "email": email, "password": password},
|
|
170
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
171
|
+
timeout=30,
|
|
172
|
+
)
|
|
173
|
+
resp.raise_for_status()
|
|
174
|
+
return resp.json()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ─────────────────────────────────────────────────────────────
|
|
178
|
+
# 6. 已有账号重新登录取 Token POST /api/login/
|
|
179
|
+
# 对应 views.py: login_and_get_token
|
|
180
|
+
# 公开接口,不需要携带 Token
|
|
181
|
+
# ─────────────────────────────────────────────────────────────
|
|
182
|
+
def login(username: str, password: str) -> dict:
|
|
183
|
+
resp = requests.post(
|
|
184
|
+
f"{_base()}/api/login/",
|
|
185
|
+
json={"username": username, "password": password},
|
|
186
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
187
|
+
timeout=30,
|
|
188
|
+
)
|
|
189
|
+
resp.raise_for_status()
|
|
190
|
+
return resp.json()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
merge_cli/cli.py
|
|
3
|
+
CLI 入口,四条主命令:
|
|
4
|
+
merge config — 配置 API 地址和 Token
|
|
5
|
+
merge predict — 单变异预测
|
|
6
|
+
merge batch — 批量 VCF 提交
|
|
7
|
+
merge status — 查询批量任务进度
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from . import api, output
|
|
14
|
+
from .config import get_api_url, get_token, set_api_url, set_token, show_config
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# ── 全局模型开关选项(两条命令共用)────────────────────────────
|
|
19
|
+
_MODEL_OPTIONS = [
|
|
20
|
+
click.option("--no-alphagenome", is_flag=True, default=False, help="跳过 AlphaGenome"),
|
|
21
|
+
click.option("--no-hyenadna", is_flag=True, default=False, help="跳过 HyenaDNA"),
|
|
22
|
+
click.option("--no-nt", is_flag=True, default=False, help="跳过 Nucleotide Transformer"),
|
|
23
|
+
click.option("--no-alphamissense", is_flag=True, default=False, help="跳过 AlphaMissense"),
|
|
24
|
+
click.option("--no-esm1b", is_flag=True, default=False, help="跳过 ESM1b"),
|
|
25
|
+
click.option("--no-gpn-msa", is_flag=True, default=False, help="跳过 GPN-MSA"),
|
|
26
|
+
click.option("--no-popeve", is_flag=True, default=False, help="跳过 popEVE"),
|
|
27
|
+
click.option("--no-evo2", is_flag=True, default=False, help="跳过 Evo2"),
|
|
28
|
+
click.option("--no-evo1", is_flag=True, default=False, help="跳过 Evo1"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def _add_model_options(f):
|
|
32
|
+
for opt in reversed(_MODEL_OPTIONS):
|
|
33
|
+
f = opt(f)
|
|
34
|
+
return f
|
|
35
|
+
|
|
36
|
+
def _model_flags(**kwargs) -> dict:
|
|
37
|
+
"""把 --no-xxx 转成 use_xxx=True/False 传给 api 层。"""
|
|
38
|
+
return {
|
|
39
|
+
"use_alphagenome": not kwargs.get("no_alphagenome", False),
|
|
40
|
+
"use_hyenadna": not kwargs.get("no_hyenadna", False),
|
|
41
|
+
"use_nt": not kwargs.get("no_nt", False),
|
|
42
|
+
"use_alphamissense": not kwargs.get("no_alphamissense", False),
|
|
43
|
+
"use_esm1b": not kwargs.get("no_esm1b", False),
|
|
44
|
+
"use_gpn_msa": not kwargs.get("no_gpn_msa", False),
|
|
45
|
+
"use_popeve": not kwargs.get("no_popeve", False),
|
|
46
|
+
"use_evo2": not kwargs.get("no_evo2", False),
|
|
47
|
+
"use_evo1": not kwargs.get("no_evo1", False),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ─────────────────────────────────────────────────────────────
|
|
52
|
+
# 根命令
|
|
53
|
+
# ─────────────────────────────────────────────────────────────
|
|
54
|
+
@click.group()
|
|
55
|
+
@click.version_option("1.0.0", prog_name="merge")
|
|
56
|
+
def cli():
|
|
57
|
+
"""MERGE 变异致病性预测 CLI
|
|
58
|
+
|
|
59
|
+
快速开始:
|
|
60
|
+
|
|
61
|
+
\b
|
|
62
|
+
merge config set-url https://your-server.com
|
|
63
|
+
merge config set-token YOUR_API_TOKEN
|
|
64
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ─────────────────────────────────────────────────────────────
|
|
69
|
+
# merge config
|
|
70
|
+
# ─────────────────────────────────────────────────────────────
|
|
71
|
+
@cli.group()
|
|
72
|
+
def config():
|
|
73
|
+
"""管理 API 地址和认证 Token。"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@config.command("set-url")
|
|
77
|
+
@click.argument("url")
|
|
78
|
+
def config_set_url(url: str):
|
|
79
|
+
"""设置 API 服务器地址,例如 https://your-server.com"""
|
|
80
|
+
set_api_url(url)
|
|
81
|
+
console.print(f"[green]✓[/green] API 地址已保存:{url}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@config.command("set-token")
|
|
85
|
+
@click.argument("token")
|
|
86
|
+
def config_set_token(token: str):
|
|
87
|
+
"""设置 API 认证 Token(保存在系统 keyring,不写入配置文件)"""
|
|
88
|
+
set_token(token)
|
|
89
|
+
console.print("[green]✓[/green] Token 已保存。")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@config.command("show")
|
|
93
|
+
def config_show():
|
|
94
|
+
"""查看当前配置。"""
|
|
95
|
+
info = show_config()
|
|
96
|
+
console.print(f" API 地址 : [bold]{info['api_url']}[/bold]")
|
|
97
|
+
console.print(f" Token : [bold]{info['token']}[/bold]")
|
|
98
|
+
console.print(f" 配置文件 : [dim]{info['config_file']}[/dim]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ─────────────────────────────────────────────────────────────
|
|
102
|
+
# merge predict
|
|
103
|
+
# ─────────────────────────────────────────────────────────────
|
|
104
|
+
@cli.command()
|
|
105
|
+
@click.option("--chrom", required=True, help="染色体,如 chr17 或 17")
|
|
106
|
+
@click.option("--pos", required=True, type=int, help="变异位置(1-based)")
|
|
107
|
+
@click.option("--ref", required=True, help="参考碱基,如 A")
|
|
108
|
+
@click.option("--alt", required=True, help="突变碱基,如 G")
|
|
109
|
+
@click.option("--genome", default="hg38", show_default=True,
|
|
110
|
+
type=click.Choice(["hg38", "hg19"]), help="参考基因组版本")
|
|
111
|
+
@click.option("--format", "fmt",
|
|
112
|
+
default="table", show_default=True,
|
|
113
|
+
type=click.Choice(["table", "json", "tsv"]),
|
|
114
|
+
help="输出格式:table(彩色表格)/ json / tsv")
|
|
115
|
+
@click.option("--no-ensemble", is_flag=True, default=False,
|
|
116
|
+
help="跳过 MERGE 集成打分(只返回各模型原始分数)")
|
|
117
|
+
@_add_model_options
|
|
118
|
+
def predict(chrom, pos, ref, alt, genome, fmt, no_ensemble, **kwargs):
|
|
119
|
+
"""单变异致病性预测。
|
|
120
|
+
|
|
121
|
+
\b
|
|
122
|
+
示例:
|
|
123
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G
|
|
124
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G --genome hg19
|
|
125
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T --no-evo2 --no-evo1
|
|
126
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T --format json > result.json
|
|
127
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T --format tsv >> results.tsv
|
|
128
|
+
"""
|
|
129
|
+
flags = _model_flags(**kwargs)
|
|
130
|
+
|
|
131
|
+
# 规范化 chrom(兼容用户输入 17 或 chr17)
|
|
132
|
+
if not chrom.startswith("chr"):
|
|
133
|
+
chrom = "chr" + chrom
|
|
134
|
+
|
|
135
|
+
with console.status("[bold cyan]正在预测…[/bold cyan]"):
|
|
136
|
+
try:
|
|
137
|
+
resp = api.predict_single(chrom, pos, ref, alt, genome, **flags)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
console.print(f"[red]预测失败:{e}[/red]")
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
if not resp.get("success"):
|
|
143
|
+
console.print(f"[red]服务端错误:{resp.get('error')}[/red]")
|
|
144
|
+
details = resp.get("details", {})
|
|
145
|
+
for src, msg in details.items():
|
|
146
|
+
console.print(f" [dim]{src}: {msg}[/dim]")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
prediction = resp["prediction"]
|
|
150
|
+
ensemble = None
|
|
151
|
+
|
|
152
|
+
if not no_ensemble:
|
|
153
|
+
with console.status("[bold cyan]正在计算 MERGE 集成分数…[/bold cyan]"):
|
|
154
|
+
try:
|
|
155
|
+
# 用 dbnsfp 构造最简单的 all_transcripts
|
|
156
|
+
dbnsfp = prediction.get("dbnsfp") or {}
|
|
157
|
+
transcripts = [dbnsfp] if dbnsfp else []
|
|
158
|
+
ens_resp = api.predict_ensemble(prediction, transcripts)
|
|
159
|
+
if ens_resp.get("success"):
|
|
160
|
+
ensemble = ens_resp
|
|
161
|
+
except Exception as e:
|
|
162
|
+
console.print(f"[yellow]MERGE 集成分数获取失败(不影响原始分数):{e}[/yellow]")
|
|
163
|
+
|
|
164
|
+
output.render_single(prediction, ensemble, fmt=fmt, errors=prediction.get("errors"))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ─────────────────────────────────────────────────────────────
|
|
168
|
+
# merge batch
|
|
169
|
+
# ─────────────────────────────────────────────────────────────
|
|
170
|
+
@cli.command()
|
|
171
|
+
@click.argument("vcf_file", type=click.Path(exists=True))
|
|
172
|
+
@click.option("--email", required=True, help="结果发送邮箱(必填)")
|
|
173
|
+
@click.option("--genome", default="hg38", show_default=True,
|
|
174
|
+
type=click.Choice(["hg38", "hg19"]))
|
|
175
|
+
@_add_model_options
|
|
176
|
+
def batch(vcf_file, email, genome, **kwargs):
|
|
177
|
+
"""批量 VCF 文件预测(异步,结果通过邮件发送)。
|
|
178
|
+
|
|
179
|
+
\b
|
|
180
|
+
服务器限制(由 views.py 中 RATE_LIMIT_CONFIG 控制):
|
|
181
|
+
- 每个 IP 每日最多 5 次异步任务
|
|
182
|
+
- 同一邮箱两次提交间隔 ≥ 1 小时
|
|
183
|
+
- 单次最多 2000 个变异
|
|
184
|
+
|
|
185
|
+
\b
|
|
186
|
+
示例:
|
|
187
|
+
merge batch variants.vcf --email you@example.com
|
|
188
|
+
merge batch variants.vcf.gz --email you@example.com --genome hg19 --no-evo2
|
|
189
|
+
"""
|
|
190
|
+
flags = _model_flags(**kwargs)
|
|
191
|
+
|
|
192
|
+
with console.status("[bold cyan]上传 VCF 文件…[/bold cyan]"):
|
|
193
|
+
try:
|
|
194
|
+
resp = api.submit_batch(vcf_file, email, genome, **flags)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
console.print(f"[red]提交失败:{e}[/red]")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
if not resp.get("success"):
|
|
200
|
+
console.print(f"[red]提交失败:{resp.get('error')}[/red]")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
output.render_batch_submitted(resp["job_id"], email)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ─────────────────────────────────────────────────────────────
|
|
207
|
+
# merge register
|
|
208
|
+
# ─────────────────────────────────────────────────────────────
|
|
209
|
+
@cli.command()
|
|
210
|
+
@click.option("--username", prompt="用户名", help="3-30 位,字母/数字/_/-/.")
|
|
211
|
+
@click.option("--email", prompt="邮箱")
|
|
212
|
+
@click.option("--password", prompt="密码", hide_input=True, confirmation_prompt="再次输入密码确认")
|
|
213
|
+
def register(username, email, password):
|
|
214
|
+
"""注册账号并自动保存 Token,注册后即可直接使用。
|
|
215
|
+
|
|
216
|
+
\b
|
|
217
|
+
示例:
|
|
218
|
+
merge register
|
|
219
|
+
merge register --username alice --email alice@lab.com --password MyPass123!
|
|
220
|
+
"""
|
|
221
|
+
if not get_api_url():
|
|
222
|
+
console.print("[red]错误:请先配置服务器地址:[/red]")
|
|
223
|
+
console.print(" merge config set-url https://your-server.com")
|
|
224
|
+
raise SystemExit(1)
|
|
225
|
+
|
|
226
|
+
with console.status("[bold cyan]注册中…[/bold cyan]"):
|
|
227
|
+
try:
|
|
228
|
+
resp = api.register(username, email, password)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
console.print(f"[red]注册失败:{e}[/red]")
|
|
231
|
+
raise SystemExit(1)
|
|
232
|
+
|
|
233
|
+
if not resp.get("success"):
|
|
234
|
+
console.print(f"[red]注册失败:{resp.get('error')}[/red]")
|
|
235
|
+
raise SystemExit(1)
|
|
236
|
+
|
|
237
|
+
# 自动保存 Token,用户无需手动 set-token
|
|
238
|
+
token = resp["token"]
|
|
239
|
+
set_token(token)
|
|
240
|
+
|
|
241
|
+
console.print(f"\n[bold green]✓ 注册成功![/bold green]")
|
|
242
|
+
console.print(f" 用户名 : [bold]{username}[/bold]")
|
|
243
|
+
console.print(f" Token : [dim]{token[:8]}…[/dim](已自动保存)")
|
|
244
|
+
console.print(f"\n现在可以直接开始使用:")
|
|
245
|
+
console.print(f" [bold]merge predict --chrom chr17 --pos 43092919 --ref A --alt G[/bold]")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ─────────────────────────────────────────────────────────────
|
|
249
|
+
# merge login (已有账号,重新获取 Token)
|
|
250
|
+
# ─────────────────────────────────────────────────────────────
|
|
251
|
+
@cli.command()
|
|
252
|
+
@click.option("--username", prompt="用户名")
|
|
253
|
+
@click.option("--password", prompt="密码", hide_input=True)
|
|
254
|
+
def login(username, password):
|
|
255
|
+
"""已有账号时重新获取并保存 Token。
|
|
256
|
+
|
|
257
|
+
\b
|
|
258
|
+
示例:
|
|
259
|
+
merge login
|
|
260
|
+
merge login --username alice --password MyPass123!
|
|
261
|
+
"""
|
|
262
|
+
if not get_api_url():
|
|
263
|
+
console.print("[red]错误:请先配置服务器地址:[/red]")
|
|
264
|
+
console.print(" merge config set-url https://your-server.com")
|
|
265
|
+
raise SystemExit(1)
|
|
266
|
+
|
|
267
|
+
with console.status("[bold cyan]登录中…[/bold cyan]"):
|
|
268
|
+
try:
|
|
269
|
+
resp = api.login(username, password)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
console.print(f"[red]登录失败:{e}[/red]")
|
|
272
|
+
raise SystemExit(1)
|
|
273
|
+
|
|
274
|
+
if not resp.get("success"):
|
|
275
|
+
console.print(f"[red]登录失败:{resp.get('error')}[/red]")
|
|
276
|
+
raise SystemExit(1)
|
|
277
|
+
|
|
278
|
+
set_token(resp["token"])
|
|
279
|
+
console.print(f"[bold green]✓ 登录成功,Token 已更新。[/bold green]")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ─────────────────────────────────────────────────────────────
|
|
283
|
+
# merge status
|
|
284
|
+
# ─────────────────────────────────────────────────────────────
|
|
285
|
+
@cli.command()
|
|
286
|
+
@click.argument("job_id")
|
|
287
|
+
@click.option("--watch", is_flag=True, default=False,
|
|
288
|
+
help="每 30 秒轮询一次,直到任务完成")
|
|
289
|
+
def status(job_id, watch):
|
|
290
|
+
"""查询批量任务状态。
|
|
291
|
+
|
|
292
|
+
\b
|
|
293
|
+
示例:
|
|
294
|
+
merge status A3F2C1B0
|
|
295
|
+
merge status A3F2C1B0 --watch
|
|
296
|
+
"""
|
|
297
|
+
import time
|
|
298
|
+
|
|
299
|
+
def _poll():
|
|
300
|
+
try:
|
|
301
|
+
data = api.get_batch_status(job_id)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
console.print(f"[red]查询失败:{e}[/red]")
|
|
304
|
+
return None
|
|
305
|
+
output.render_status(data)
|
|
306
|
+
return data
|
|
307
|
+
|
|
308
|
+
data = _poll()
|
|
309
|
+
if not watch or not data:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
while data and data.get("status") == "processing":
|
|
313
|
+
console.print("\n[dim]30 秒后再次查询… Ctrl+C 退出[/dim]")
|
|
314
|
+
try:
|
|
315
|
+
time.sleep(30)
|
|
316
|
+
except KeyboardInterrupt:
|
|
317
|
+
break
|
|
318
|
+
data = _poll()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
merge_cli/config.py
|
|
3
|
+
管理 API 地址和 Token 的持久化存储。
|
|
4
|
+
Token 优先级:命令行 --token > 环境变量 MERGE_API_TOKEN > keyring 存储
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import keyring
|
|
11
|
+
|
|
12
|
+
_SERVICE = "merge-cli"
|
|
13
|
+
_CONFIG_FILE = Path.home() / ".config" / "merge-cli" / "config.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_file() -> dict:
|
|
17
|
+
if _CONFIG_FILE.exists():
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(_CONFIG_FILE.read_text())
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _save_file(data: dict) -> None:
|
|
26
|
+
_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
_CONFIG_FILE.write_text(json.dumps(data, indent=2))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_api_url() -> str:
|
|
31
|
+
env = os.environ.get("MERGE_API_URL")
|
|
32
|
+
if env:
|
|
33
|
+
return env.rstrip("/")
|
|
34
|
+
cfg = _load_file()
|
|
35
|
+
return cfg.get("api_url", "").rstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_api_url(url: str) -> None:
|
|
39
|
+
cfg = _load_file()
|
|
40
|
+
cfg["api_url"] = url.rstrip("/")
|
|
41
|
+
_save_file(cfg)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_token() -> str:
|
|
45
|
+
env = os.environ.get("MERGE_API_TOKEN")
|
|
46
|
+
if env:
|
|
47
|
+
return env
|
|
48
|
+
try:
|
|
49
|
+
tok = keyring.get_password(_SERVICE, "token")
|
|
50
|
+
return tok or ""
|
|
51
|
+
except Exception:
|
|
52
|
+
cfg = _load_file()
|
|
53
|
+
return cfg.get("token", "")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_token(token: str) -> None:
|
|
57
|
+
try:
|
|
58
|
+
keyring.set_password(_SERVICE, "token", token)
|
|
59
|
+
except Exception:
|
|
60
|
+
# keyring 不可用时回退到配置文件
|
|
61
|
+
cfg = _load_file()
|
|
62
|
+
cfg["token"] = token
|
|
63
|
+
_save_file(cfg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def show_config() -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"api_url": get_api_url() or "(未设置)",
|
|
69
|
+
"token": ("***" + get_token()[-4:]) if get_token() else "(未设置)",
|
|
70
|
+
"config_file": str(_CONFIG_FILE),
|
|
71
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
merge_cli/output.py
|
|
3
|
+
把 API 返回的原始 JSON 渲染成三种格式:
|
|
4
|
+
- table : Rich 彩色表格(终端默认)
|
|
5
|
+
- json : 紧凑 JSON(管道/脚本用)
|
|
6
|
+
- tsv : 制表符分隔(直接导入 Excel / R / Python)
|
|
7
|
+
字段来源完全对应 views.py 里 batch_predictions.csv 的列定义。
|
|
8
|
+
"""
|
|
9
|
+
import json as _json
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich import box
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ─── 颜色映射(对应 ensemble_predict.py: interpret_score 返回的 label)───
|
|
22
|
+
_LABEL_STYLE = {
|
|
23
|
+
"Pathogenic": "bold red",
|
|
24
|
+
"Likely Pathogenic": "red",
|
|
25
|
+
"Uncertain": "yellow",
|
|
26
|
+
"Likely Benign": "green",
|
|
27
|
+
"Benign": "bold green",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fmt(val, decimals: int = 4) -> str:
|
|
32
|
+
"""把 None / 'N/A' / float 统一格式化为字符串。"""
|
|
33
|
+
if val is None or val in ("N/A", ".", ""):
|
|
34
|
+
return "-"
|
|
35
|
+
try:
|
|
36
|
+
return f"{float(val):.{decimals}f}"
|
|
37
|
+
except (ValueError, TypeError):
|
|
38
|
+
return str(val)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _score_text(score: Optional[float], label: Optional[str]) -> Text:
|
|
42
|
+
"""把 MERGE 分数 + 解读标签合并成带颜色的 Rich Text。"""
|
|
43
|
+
if score is None:
|
|
44
|
+
return Text("-", style="dim")
|
|
45
|
+
pct = f"{score * 100:.1f}%"
|
|
46
|
+
style = _LABEL_STYLE.get(label or "", "white")
|
|
47
|
+
t = Text(pct, style=style)
|
|
48
|
+
if label:
|
|
49
|
+
t.append(f" {label}", style=style)
|
|
50
|
+
return t
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _extract_flat(prediction: dict, ensemble: Optional[dict] = None) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
把 /predict/ + /ensemble/ 的嵌套 JSON 压平成一行字典,
|
|
56
|
+
字段顺序与 batch_predictions.csv 的列头一致。
|
|
57
|
+
"""
|
|
58
|
+
inp = prediction.get("input", {})
|
|
59
|
+
db = prediction.get("dbnsfp") or {}
|
|
60
|
+
ann = db.get("annotations") or {}
|
|
61
|
+
dlm = db.get("dl_models") or {}
|
|
62
|
+
ag = prediction.get("alphagenome") or {}
|
|
63
|
+
hy = prediction.get("hyenadna") or {}
|
|
64
|
+
nt = prediction.get("nt") or {}
|
|
65
|
+
gpn = prediction.get("gpn_msa") or {}
|
|
66
|
+
pop = prediction.get("popeve") or {}
|
|
67
|
+
evo2 = prediction.get("evo2") or {}
|
|
68
|
+
evo1 = prediction.get("evo1") or {}
|
|
69
|
+
|
|
70
|
+
# ensemble 分数
|
|
71
|
+
ens_score = ens_label = ens_model = None
|
|
72
|
+
if ensemble:
|
|
73
|
+
for _key, _val in ensemble.get("ensemble_results", {}).items():
|
|
74
|
+
ens_score = _val.get("score")
|
|
75
|
+
ens_label = (_val.get("interpretation") or {}).get("label")
|
|
76
|
+
ens_model = _key
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"Chr": inp.get("chrom", ann.get("chr", "-")),
|
|
81
|
+
"Pos": inp.get("pos", ann.get("pos", "-")),
|
|
82
|
+
"Ref": inp.get("ref", ann.get("ref", "-")),
|
|
83
|
+
"Alt": inp.get("alt", ann.get("alt", "-")),
|
|
84
|
+
"Gene": ann.get("genename", "-"),
|
|
85
|
+
"Transcript": ann.get("Ensembl_transcriptid", "-"),
|
|
86
|
+
"MERGE_Score": ens_score,
|
|
87
|
+
"MERGE_Label": ens_label,
|
|
88
|
+
"MERGE_Model": ens_model,
|
|
89
|
+
# dbNSFP 里的 DL 模型(来自 DL_MODELS 字典,col 132/129)
|
|
90
|
+
"AlphaMissense": (dlm.get("AlphaMissense") or {}).get("score"),
|
|
91
|
+
"ESM1b": (dlm.get("ESM1b") or {}).get("score"),
|
|
92
|
+
# 独立模型
|
|
93
|
+
"GPN_MSA": gpn.get("score"),
|
|
94
|
+
"popEVE": pop.get("score"),
|
|
95
|
+
"AlphaGenome_Mean": (ag.get("statistics") or {}).get("raw_score_mean"),
|
|
96
|
+
"HyenaDNA": hy.get("score"),
|
|
97
|
+
"NT": nt.get("score"),
|
|
98
|
+
"Evo2_LLR": evo2.get("llr_score"),
|
|
99
|
+
"Evo2_Ref": evo2.get("ref_score"),
|
|
100
|
+
"Evo2_Var": evo2.get("var_score"),
|
|
101
|
+
"Evo2_Interp": evo2.get("interpretation"),
|
|
102
|
+
"Evo1_Delta": evo1.get("evo1_delta_score"),
|
|
103
|
+
"Evo1_Ref": evo1.get("ref_score"),
|
|
104
|
+
"Evo1_Var": evo1.get("var_score"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ─────────────────────────────────────────────────────────────
|
|
109
|
+
# 公开接口
|
|
110
|
+
# ─────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
def render_single(prediction: dict, ensemble: Optional[dict],
|
|
113
|
+
fmt: str = "table", errors: dict = None) -> None:
|
|
114
|
+
"""渲染单变异预测结果。"""
|
|
115
|
+
flat = _extract_flat(prediction, ensemble)
|
|
116
|
+
|
|
117
|
+
if fmt == "json":
|
|
118
|
+
_json.dump({"prediction": prediction, "ensemble": ensemble}, sys.stdout, indent=2, ensure_ascii=False)
|
|
119
|
+
print()
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if fmt == "tsv":
|
|
123
|
+
print("\t".join(flat.keys()))
|
|
124
|
+
print("\t".join(_fmt(v) for v in flat.values()))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# ── Rich table ──────────────────────────────────────────
|
|
128
|
+
t = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan",
|
|
129
|
+
title="MERGE 预测结果", title_style="bold")
|
|
130
|
+
t.add_column("项目", style="dim", no_wrap=True, min_width=18)
|
|
131
|
+
t.add_column("结果", min_width=30)
|
|
132
|
+
|
|
133
|
+
# 变异基本信息
|
|
134
|
+
t.add_section()
|
|
135
|
+
t.add_row("变异位点",
|
|
136
|
+
f"[bold]{flat['Chr']}:{flat['Pos']} {flat['Ref']} → {flat['Alt']}[/bold]")
|
|
137
|
+
t.add_row("基因 / 转录本",
|
|
138
|
+
f"{_fmt(flat['Gene'])} / {_fmt(flat['Transcript'])}")
|
|
139
|
+
|
|
140
|
+
# MERGE 集成分数
|
|
141
|
+
t.add_section()
|
|
142
|
+
t.add_row("MERGE 致病性",
|
|
143
|
+
_score_text(flat["MERGE_Score"], flat["MERGE_Label"]))
|
|
144
|
+
t.add_row("使用模型", _fmt(flat["MERGE_Model"]))
|
|
145
|
+
|
|
146
|
+
# 各模型分数
|
|
147
|
+
t.add_section()
|
|
148
|
+
for label, key in [
|
|
149
|
+
("AlphaMissense", "AlphaMissense"),
|
|
150
|
+
("ESM1b", "ESM1b"),
|
|
151
|
+
("GPN-MSA", "GPN_MSA"),
|
|
152
|
+
("popEVE", "popEVE"),
|
|
153
|
+
("AlphaGenome", "AlphaGenome_Mean"),
|
|
154
|
+
("HyenaDNA", "HyenaDNA"),
|
|
155
|
+
("NT", "NT"),
|
|
156
|
+
("Evo2 LLR", "Evo2_LLR"),
|
|
157
|
+
("Evo1 Delta", "Evo1_Delta"),
|
|
158
|
+
]:
|
|
159
|
+
val = flat[key]
|
|
160
|
+
t.add_row(label, _fmt(val))
|
|
161
|
+
|
|
162
|
+
console.print(t)
|
|
163
|
+
|
|
164
|
+
# 警告/错误
|
|
165
|
+
if errors:
|
|
166
|
+
console.print("\n[yellow]部分模型未返回结果:[/yellow]")
|
|
167
|
+
for src, msg in errors.items():
|
|
168
|
+
console.print(f" [dim]{src}:[/dim] {msg}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def render_batch_submitted(job_id: str, email: str) -> None:
|
|
172
|
+
console.print(f"\n[bold green]✓ 批量任务已提交[/bold green]")
|
|
173
|
+
console.print(f" Job ID : [bold]{job_id}[/bold]")
|
|
174
|
+
console.print(f" 邮箱 : {email}")
|
|
175
|
+
console.print(f" 完成后结果将发送至邮箱(附件含 3 个文件)")
|
|
176
|
+
console.print(f"\n查询进度:[bold]merge status {job_id}[/bold]")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def render_status(status_data: dict) -> None:
|
|
180
|
+
"""渲染批量任务状态。"""
|
|
181
|
+
if not status_data.get("found"):
|
|
182
|
+
console.print(f"[red]任务不存在或服务已重启。[/red]")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
state = status_data.get("status", "unknown")
|
|
186
|
+
color = {"processing": "yellow", "complete": "green", "error": "red"}.get(state, "white")
|
|
187
|
+
|
|
188
|
+
t = Table(box=box.SIMPLE, show_header=False)
|
|
189
|
+
t.add_column("key", style="dim")
|
|
190
|
+
t.add_column("value")
|
|
191
|
+
t.add_row("状态", f"[{color}]{state}[/{color}]")
|
|
192
|
+
t.add_row("提交时间", status_data.get("submitted_at", "-"))
|
|
193
|
+
t.add_row("成功变异数", str(status_data.get("n_ok", "-")))
|
|
194
|
+
t.add_row("警告/错误", str(status_data.get("n_err", "-")))
|
|
195
|
+
t.add_row("邮件已发送", "是" if status_data.get("mail_sent") else "否")
|
|
196
|
+
console.print(t)
|
|
197
|
+
|
|
198
|
+
logs = status_data.get("log", [])
|
|
199
|
+
if logs:
|
|
200
|
+
console.print("\n[dim]── 任务日志 ──────────────────────────[/dim]")
|
|
201
|
+
for line in logs[-20:]: # 最多显示最后 20 行
|
|
202
|
+
console.print(f" [dim]{line}[/dim]")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: merge-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI for MERGE variant pathogenicity prediction
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: click>=8.1
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: keyring>=24.0
|
|
12
|
+
|
|
13
|
+
# MERGE CLI
|
|
14
|
+
|
|
15
|
+
MERGE 变异致病性预测命令行工具。所有计算在服务器端完成,本地只需 Python 3.9+。
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install merge-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. 配置服务器地址(只需配置一次)
|
|
27
|
+
merge config set-url https://your-server.com
|
|
28
|
+
merge config set-token YOUR_API_TOKEN
|
|
29
|
+
|
|
30
|
+
# 2. 单变异预测
|
|
31
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G
|
|
32
|
+
|
|
33
|
+
# 3. 批量预测(上传 VCF,结果发邮件)
|
|
34
|
+
merge batch my_variants.vcf --email you@example.com
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 命令详解
|
|
38
|
+
|
|
39
|
+
### `merge predict` — 单变异预测
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
选项:
|
|
43
|
+
--chrom 染色体(chr17 或 17 均可) [必填]
|
|
44
|
+
--pos 变异位置(1-based) [必填]
|
|
45
|
+
--ref 参考碱基 [必填]
|
|
46
|
+
--alt 突变碱基 [必填]
|
|
47
|
+
--genome hg38 / hg19 [默认: hg38]
|
|
48
|
+
--format table / json / tsv [默认: table]
|
|
49
|
+
--no-ensemble 跳过 MERGE 集成打分
|
|
50
|
+
--no-alphagenome / --no-hyenadna / --no-nt
|
|
51
|
+
--no-alphamissense / --no-esm1b / --no-gpn-msa
|
|
52
|
+
--no-popeve / --no-evo2 / --no-evo1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**输出示例(table 模式):**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
╭──────────────────────────────────────────╮
|
|
59
|
+
│ MERGE 预测结果 │
|
|
60
|
+
├──────────────────┬───────────────────────┤
|
|
61
|
+
│ 变异位点 │ chr17:43092919 A → G │
|
|
62
|
+
│ 基因 / 转录本 │ BRCA1 / ENST00000357654│
|
|
63
|
+
├──────────────────┼───────────────────────┤
|
|
64
|
+
│ MERGE 致病性 │ 87.3% Likely Pathogenic│
|
|
65
|
+
│ 使用模型 │ ClinVar │
|
|
66
|
+
├──────────────────┼───────────────────────┤
|
|
67
|
+
│ AlphaMissense │ 0.9341 │
|
|
68
|
+
│ ESM1b │ -3.2180 │
|
|
69
|
+
│ GPN-MSA │ -1.4420 │
|
|
70
|
+
│ popEVE │ 0.8812 │
|
|
71
|
+
│ AlphaGenome │ 0.0234 │
|
|
72
|
+
│ HyenaDNA │ -2.1100 │
|
|
73
|
+
│ NT │ -1.8830 │
|
|
74
|
+
│ Evo2 LLR │ -4.2210 │
|
|
75
|
+
│ Evo1 Delta │ -3.9910 │
|
|
76
|
+
╰──────────────────┴───────────────────────╯
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**管道输出(TSV 模式,方便写脚本):**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
merge predict --chrom chr17 --pos 43092919 --ref A --alt G --format tsv >> results.tsv
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**脚本批量调用单变异(小于 20 个时比上传 VCF 更快):**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
while IFS=$'\t' read -r chrom pos ref alt; do
|
|
89
|
+
merge predict --chrom "$chrom" --pos "$pos" --ref "$ref" --alt "$alt" --format tsv
|
|
90
|
+
done < variants.tsv >> results.tsv
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `merge batch` — 批量 VCF 预测
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
merge batch variants.vcf --email you@example.com
|
|
99
|
+
merge batch variants.vcf.gz --email you@example.com --genome hg19 --no-evo2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
任务提交成功后返回 Job ID,结果通过邮件发送,附件包含:
|
|
103
|
+
- `batch_predictions.csv` — 所有模型分数汇总(可直接用 Excel 打开)
|
|
104
|
+
- `batch_predictions.vcf` — 标准 VCF 格式,含 MERGE 及各模型分数
|
|
105
|
+
- `imputation_details.csv` — MERGE 各特征缺失填补情况
|
|
106
|
+
|
|
107
|
+
服务端限制:每 IP 每日 5 次,同一邮箱间隔 1 小时,单次最多 2000 个变异。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `merge status` — 查询批量任务
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
merge status A3F2C1B0 # 查看一次
|
|
115
|
+
merge status A3F2C1B0 --watch # 每 30 秒轮询
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 环境变量(适合 CI / 服务器环境)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export MERGE_API_URL=https://your-server.com
|
|
124
|
+
export MERGE_API_TOKEN=your-token
|
|
125
|
+
merge predict --chrom chr1 --pos 100000 --ref C --alt T
|
|
126
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
merge_cli/__init__.py
|
|
4
|
+
merge_cli/api.py
|
|
5
|
+
merge_cli/cli.py
|
|
6
|
+
merge_cli/config.py
|
|
7
|
+
merge_cli/output.py
|
|
8
|
+
merge_cli.egg-info/PKG-INFO
|
|
9
|
+
merge_cli.egg-info/SOURCES.txt
|
|
10
|
+
merge_cli.egg-info/dependency_links.txt
|
|
11
|
+
merge_cli.egg-info/entry_points.txt
|
|
12
|
+
merge_cli.egg-info/requires.txt
|
|
13
|
+
merge_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
merge_cli
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "merge-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "CLI for MERGE variant pathogenicity prediction"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"click>=8.1",
|
|
15
|
+
"requests>=2.31",
|
|
16
|
+
"rich>=13.0",
|
|
17
|
+
"keyring>=24.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
merge = "merge_cli.cli:cli"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["."]
|
|
25
|
+
include = ["merge_cli*"]
|