327 lines
11 KiB
Python
Executable File
327 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Functional Annotation Skill (API Mode) - 功能注释分析技能(API模式)
|
|
使用 eggNOG API 在线获取 GO 注释,无需下载数据库
|
|
|
|
Usage:
|
|
python run_api.py --input protein.fa --output ./annotation
|
|
python run_api.py --demo --output ./annotation
|
|
|
|
特点:
|
|
- 无需下载数据库
|
|
- 适合小数据集(<1000基因)
|
|
- 速度较慢(约1秒/基因)
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set
|
|
|
|
import requests
|
|
import yaml
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# eggNOG API endpoints
|
|
EGGNOG_SCAN_URL = "http://eggnogapi5.embl.de/fast_webscan"
|
|
EGGNOG_GO_URL = "http://eggnogapi5.embl.de/nog_data/json/go_terms"
|
|
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
"Accept": "application/json"
|
|
}
|
|
|
|
|
|
class AnnotationAPISkill:
|
|
"""功能注释API技能主类"""
|
|
|
|
def __init__(self, config_path: Optional[str] = None):
|
|
self.config = self._load_config(config_path)
|
|
self.commands = []
|
|
self.checksums = {}
|
|
self.stats = {}
|
|
|
|
def _load_config(self, config_path: Optional[str]) -> Dict:
|
|
if config_path and os.path.exists(config_path):
|
|
with open(config_path) as f:
|
|
return yaml.safe_load(f)
|
|
return {
|
|
"parameters": {
|
|
"api": {
|
|
"delay": 1.0,
|
|
"timeout": 60,
|
|
"db": "euk"
|
|
}
|
|
}
|
|
}
|
|
|
|
def _compute_checksum(self, filepath: Path) -> str:
|
|
sha256 = hashlib.sha256()
|
|
with open(filepath, "rb") as f:
|
|
for chunk in iter(lambda: f.read(8192), b""):
|
|
sha256.update(chunk)
|
|
return sha256.hexdigest()
|
|
|
|
def _record_file(self, filepath: Path, file_type: str = "output"):
|
|
if filepath.exists():
|
|
self.checksums[str(filepath)] = {
|
|
"sha256": self._compute_checksum(filepath),
|
|
"size": filepath.stat().st_size,
|
|
"type": file_type
|
|
}
|
|
|
|
def read_fasta(self, fasta_file: Path) -> List[tuple]:
|
|
"""读取FASTA文件"""
|
|
sequences = []
|
|
with open(fasta_file) as f:
|
|
name, seq = None, []
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('>'):
|
|
if name:
|
|
sequences.append((name, ''.join(seq)))
|
|
name = line[1:].split()[0]
|
|
seq = []
|
|
else:
|
|
seq.append(line)
|
|
if name:
|
|
sequences.append((name, ''.join(seq)))
|
|
return sequences
|
|
|
|
def submit_to_eggnog(self, session: requests.Session, fasta_seq: str, db: str = "euk") -> Optional[str]:
|
|
"""提交序列到eggNOG,返回nogname"""
|
|
payload = {"fasta": fasta_seq, "nqueries": 1, "db": db}
|
|
try:
|
|
resp = session.post(EGGNOG_SCAN_URL, json=payload, headers=HEADERS, timeout=60)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
if 'seq_matches' in data and data['seq_matches']:
|
|
result = data['seq_matches'][0]
|
|
if 'hit' in result and 'matches' in result['hit']:
|
|
return result['hit']['matches'][0].get('nogname')
|
|
except Exception as e:
|
|
logger.warning(f"提交错误: {e}")
|
|
return None
|
|
|
|
def get_go_terms(self, session: requests.Session, nogname: str) -> Set[str]:
|
|
"""获取GO terms"""
|
|
if not nogname:
|
|
return set()
|
|
url = f"{EGGNOG_GO_URL}/{nogname}"
|
|
try:
|
|
resp = session.get(url, headers=HEADERS, timeout=30)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
go_ids = set()
|
|
for category, terms in data.get('go_terms', {}).items():
|
|
for term in terms:
|
|
go_id = term[0]
|
|
if go_id.startswith('GO:'):
|
|
go_ids.add(go_id)
|
|
return go_ids
|
|
except Exception as e:
|
|
logger.warning(f"GO获取错误: {e}")
|
|
return set()
|
|
|
|
def run_api_annotation(self, protein_fasta: Path, output_dir: Path,
|
|
prefix: str = "annotation") -> Dict[str, Path]:
|
|
"""运行API注释"""
|
|
params = self.config["parameters"]["api"]
|
|
delay = params.get("delay", 1.0)
|
|
db = params.get("db", "euk")
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 读取序列
|
|
sequences = self.read_fasta(protein_fasta)
|
|
total = len(sequences)
|
|
logger.info(f"[API] 共 {total} 条序列")
|
|
|
|
if total > 1000:
|
|
logger.warning(f"序列数较多({total}),API模式可能较慢,建议使用本地emapper")
|
|
|
|
session = requests.Session()
|
|
gene2go = defaultdict(set)
|
|
failed_genes = []
|
|
|
|
for i, (gene_id, seq) in enumerate(sequences):
|
|
logger.info(f"[{i+1}/{total}] {gene_id}")
|
|
|
|
fasta = f">{gene_id}\n{seq}"
|
|
|
|
# 1. 获取nogname
|
|
nogname = self.submit_to_eggnog(session, fasta, db)
|
|
if not nogname:
|
|
logger.warning(f" 获取nogname失败: {gene_id}")
|
|
failed_genes.append(gene_id)
|
|
time.sleep(delay)
|
|
continue
|
|
|
|
# 2. 获取GO terms
|
|
go_ids = self.get_go_terms(session, nogname)
|
|
if go_ids:
|
|
gene2go[gene_id] = go_ids
|
|
logger.info(f" 找到 {len(go_ids)} 个GO terms")
|
|
|
|
time.sleep(delay)
|
|
|
|
session.close()
|
|
|
|
# 保存结果
|
|
annot_file = output_dir / f"{prefix}.gene2go.txt"
|
|
with open(annot_file, 'w') as f:
|
|
for gene_id in sorted(gene2go.keys()):
|
|
for go_id in sorted(gene2go[gene_id]):
|
|
f.write(f"{gene_id}\t{go_id}\n")
|
|
|
|
self._record_file(annot_file)
|
|
|
|
# 统计
|
|
all_go_terms = set()
|
|
for gos in gene2go.values():
|
|
all_go_terms.update(gos)
|
|
|
|
self.stats = {
|
|
"total_genes": total,
|
|
"annotated_genes": len(gene2go),
|
|
"failed_genes": len(failed_genes),
|
|
"go_term_count": len(all_go_terms),
|
|
}
|
|
|
|
return {
|
|
"gene2go": annot_file,
|
|
}
|
|
|
|
def generate_report(self, output_dir: Path) -> Path:
|
|
"""生成报告"""
|
|
report_file = output_dir / "report.md"
|
|
|
|
report_content = f"""# 功能注释分析报告 (API模式)
|
|
|
|
## 基本信息
|
|
|
|
- **分析方式**: eggNOG API (在线)
|
|
- **分析时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
- **API延迟**: {self.config["parameters"]["api"].get("delay", 1.0)}s
|
|
|
|
## 统计结果
|
|
|
|
| 指标 | 数量 |
|
|
|------|------|
|
|
| 总基因数 | {self.stats.get('total_genes', 0)} |
|
|
| 成功注释 | {self.stats.get('annotated_genes', 0)} |
|
|
| 失败基因 | {self.stats.get('failed_genes', 0)} |
|
|
| 独特GO terms | {self.stats.get('go_term_count', 0)} |
|
|
|
|
## 输出文件
|
|
|
|
| 文件 | 描述 |
|
|
|------|------|
|
|
| *.gene2go.txt | 基因-GO对应表 |
|
|
|
|
## 注意事项
|
|
|
|
API模式适合小数据集,大数据集建议使用本地eggNOG-mapper。
|
|
"""
|
|
|
|
with open(report_file, "w") as f:
|
|
f.write(report_content)
|
|
|
|
self._record_file(report_file)
|
|
return report_file
|
|
|
|
def run(self, protein_fasta: Path, output_dir: Path, prefix: str = "annotation") -> Dict:
|
|
"""运行完整流程"""
|
|
logger.info(f"Starting API annotation for {protein_fasta}")
|
|
|
|
self._record_file(protein_fasta, "input")
|
|
|
|
results = self.run_api_annotation(protein_fasta, output_dir, prefix)
|
|
|
|
report = self.generate_report(output_dir)
|
|
|
|
logger.info(f"API annotation completed!")
|
|
logger.info(f" - Annotated: {self.stats['annotated_genes']}/{self.stats['total_genes']}")
|
|
logger.info(f" - GO terms: {self.stats['go_term_count']}")
|
|
|
|
return {
|
|
"status": "success",
|
|
"output_dir": str(output_dir),
|
|
"report": str(report),
|
|
"gene2go": str(results["gene2go"]),
|
|
"stats": self.stats,
|
|
}
|
|
|
|
def run_demo(self, output_dir: Path) -> Dict:
|
|
"""Demo模式"""
|
|
logger.info("Running API demo mode")
|
|
|
|
demo_dir = output_dir / "demo_data"
|
|
demo_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 创建模拟蛋白文件
|
|
protein_fasta = demo_dir / "demo_proteins.fa"
|
|
protein_fasta.write_text(">Gene001\nMKTLLLPLLLLVLLLLLVQGSISAQAAAAAA\n"
|
|
">Gene002\nMKTLLLPLLLLVLLLLLVQGSISAQBBBBBB\n")
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 模拟结果
|
|
gene2go_file = output_dir / "annotation.gene2go.txt"
|
|
gene2go_file.write_text("Gene001\tGO:0005524\nGene001\tGO:0004674\nGene002\tGO:0003677\n")
|
|
|
|
self.stats = {
|
|
"total_genes": 2,
|
|
"annotated_genes": 2,
|
|
"failed_genes": 0,
|
|
"go_term_count": 3,
|
|
}
|
|
|
|
report = self.generate_report(output_dir)
|
|
|
|
return {
|
|
"status": "success",
|
|
"output_dir": str(output_dir),
|
|
"report": str(report),
|
|
"gene2go": str(gene2go_file),
|
|
"stats": self.stats,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Functional Annotation Skill (API Mode)")
|
|
parser.add_argument("--input", "-i", type=Path, help="蛋白序列FASTA文件")
|
|
parser.add_argument("--output", "-o", type=Path, required=True, help="输出目录")
|
|
parser.add_argument("--prefix", type=str, default="annotation", help="输出文件前缀")
|
|
parser.add_argument("--delay", type=float, default=1.0, help="API请求间隔(秒)")
|
|
parser.add_argument("--demo", action="store_true", help="运行Demo模式")
|
|
|
|
args = parser.parse_args()
|
|
|
|
skill = AnnotationAPISkill()
|
|
skill.config["parameters"]["api"]["delay"] = args.delay
|
|
|
|
if args.demo:
|
|
result = skill.run_demo(args.output)
|
|
else:
|
|
if not args.input:
|
|
parser.error("--input is required (unless using --demo)")
|
|
result = skill.run(args.input, args.output, args.prefix)
|
|
|
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
|
|
return 0 if result["status"] == "success" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|