Initial commit: BtToxin Pipeline project structure
This commit is contained in:
21
.env.example
Normal file
21
.env.example
Normal file
@@ -0,0 +1,21 @@
|
||||
# Application
|
||||
APP_NAME=BtToxin Pipeline
|
||||
DEBUG=false
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:password@localhost:5432/bttoxin
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# S3/MinIO
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
S3_ACCESS_KEY=minioadmin
|
||||
S3_SECRET_KEY=minioadmin
|
||||
S3_BUCKET=bttoxin-results
|
||||
|
||||
# Docker
|
||||
DOCKER_IMAGE=quay.io/biocontainers/bttoxin_digger:1.0.10--hdfd78af_0
|
||||
|
||||
# NCBI
|
||||
NCBI_EMAIL=your_email@example.com
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
*.egg-info/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# System
|
||||
.DS_Store
|
||||
|
||||
# Environment
|
||||
.env
|
||||
*.log
|
||||
|
||||
# Data
|
||||
uploads/
|
||||
results/
|
||||
logs/
|
||||
tests/test_data/genomes/*.fna
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
15
.woodpecker/test.yml
Normal file
15
.woodpecker/test.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
when:
|
||||
event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
prepare:
|
||||
image: bttoxin-digger:latest
|
||||
commands:
|
||||
- echo "Preparing environment..."
|
||||
- BtToxin_Digger --version
|
||||
|
||||
test:
|
||||
image: python:3.10
|
||||
commands:
|
||||
- pip install pytest httpx
|
||||
- pytest tests/
|
||||
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
||||
.PHONY: help dev build test clean
|
||||
|
||||
help:
|
||||
@echo "BtToxin Pipeline Commands:"
|
||||
@echo " make dev - Start development environment"
|
||||
@echo " make build - Build Docker images"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make clean - Clean up"
|
||||
|
||||
dev:
|
||||
docker compose -f config/docker-compose.yml up -d
|
||||
|
||||
build:
|
||||
docker compose -f config/docker-compose.yml build
|
||||
|
||||
test:
|
||||
cd backend && pytest
|
||||
|
||||
clean:
|
||||
docker compose -f config/docker-compose.yml down -v
|
||||
rm -rf uploads/ results/ logs/
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# BtToxin Pipeline
|
||||
|
||||
Automated Bacillus thuringiensis toxin mining system with CI/CD integration.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker / Podman
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
|
||||
### Development Setup
|
||||
```bash
|
||||
# 1. Clone and setup
|
||||
git clone <your-repo>
|
||||
cd bttoxin-pipeline
|
||||
|
||||
# 2. Install dependencies
|
||||
cd backend && pip install -r requirements.txt
|
||||
cd ../frontend && npm install
|
||||
|
||||
# 3. Start services
|
||||
docker compose -f config/docker-compose.yml up -d
|
||||
|
||||
# 4. Run backend
|
||||
cd backend
|
||||
uvicorn app.main:app --reload
|
||||
|
||||
# 5. Run frontend
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Frontend (Vue 3) → Backend (FastAPI) → Celery → Docker (BtToxin_Digger)
|
||||
↓
|
||||
PostgreSQL + Redis
|
||||
↓
|
||||
S3/MinIO
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API Documentation](docs/api.md)
|
||||
- [Deployment Guide](docs/deployment.md)
|
||||
- [Usage Guide](docs/usage.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
2
backend/app/__init__.py
Normal file
2
backend/app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""BtToxin Pipeline Backend Application"""
|
||||
__version__ = "1.0.0"
|
||||
0
backend/app/api/v1/__init__.py
Normal file
0
backend/app/api/v1/__init__.py
Normal file
98
backend/app/api/v1/jobs.py
Normal file
98
backend/app/api/v1/jobs.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""任务管理 API"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
from ...database import get_db
|
||||
from ...models.job import Job, JobStatus
|
||||
from ...schemas.job import JobResponse
|
||||
from ...workers.tasks import run_bttoxin_analysis
|
||||
from ...config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
UPLOAD_DIR = Path(settings.UPLOAD_DIR)
|
||||
RESULTS_DIR = Path(settings.RESULTS_DIR)
|
||||
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||
RESULTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
@router.post("/create", response_model=JobResponse)
|
||||
async def create_job(
|
||||
files: List[UploadFile] = File(...),
|
||||
sequence_type: str = "nucl",
|
||||
scaf_suffix: str = ".fna",
|
||||
threads: int = 4,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建新任务"""
|
||||
job_id = str(uuid.uuid4())
|
||||
|
||||
job_input_dir = UPLOAD_DIR / job_id
|
||||
job_output_dir = RESULTS_DIR / job_id
|
||||
job_input_dir.mkdir(parents=True)
|
||||
job_output_dir.mkdir(parents=True)
|
||||
|
||||
uploaded_files = []
|
||||
for file in files:
|
||||
file_path = job_input_dir / file.filename
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
uploaded_files.append(file.filename)
|
||||
|
||||
job = Job(
|
||||
id=job_id,
|
||||
status=JobStatus.PENDING,
|
||||
input_files=uploaded_files,
|
||||
sequence_type=sequence_type,
|
||||
scaf_suffix=scaf_suffix,
|
||||
threads=threads
|
||||
)
|
||||
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
task = run_bttoxin_analysis.delay(
|
||||
job_id=job_id,
|
||||
input_dir=str(job_input_dir),
|
||||
output_dir=str(job_output_dir),
|
||||
sequence_type=sequence_type,
|
||||
scaf_suffix=scaf_suffix,
|
||||
threads=threads
|
||||
)
|
||||
|
||||
job.celery_task_id = task.id
|
||||
db.commit()
|
||||
|
||||
return job
|
||||
|
||||
@router.get("/{job_id}", response_model=JobResponse)
|
||||
async def get_job(job_id: str, db: Session = Depends(get_db)):
|
||||
"""获取任务详情"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
return job
|
||||
|
||||
@router.get("/{job_id}/progress")
|
||||
async def get_job_progress(job_id: str, db: Session = Depends(get_db)):
|
||||
"""获取任务进度"""
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
if job.celery_task_id:
|
||||
from ...core.celery_app import celery_app
|
||||
task = celery_app.AsyncResult(job.celery_task_id)
|
||||
|
||||
return {
|
||||
'job_id': job_id,
|
||||
'status': job.status,
|
||||
'celery_state': task.state,
|
||||
'progress': task.info if task.state == 'PROGRESS' else None
|
||||
}
|
||||
|
||||
return {'job_id': job_id, 'status': job.status}
|
||||
8
backend/app/api/v1/results.py
Normal file
8
backend/app/api/v1/results.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""结果查询 API"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def results_info():
|
||||
return {"message": "Results endpoint"}
|
||||
8
backend/app/api/v1/upload.py
Normal file
8
backend/app/api/v1/upload.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""文件上传 API"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def upload_info():
|
||||
return {"message": "Upload endpoint"}
|
||||
47
backend/app/config.py
Normal file
47
backend/app/config.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""应用配置"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用设置"""
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = "BtToxin Pipeline"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = False
|
||||
|
||||
# API 配置
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL: str = "postgresql://postgres:password@localhost:5432/bttoxin"
|
||||
|
||||
# Redis
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
# S3/MinIO
|
||||
S3_ENDPOINT: Optional[str] = None
|
||||
S3_ACCESS_KEY: str = ""
|
||||
S3_SECRET_KEY: str = ""
|
||||
S3_BUCKET: str = "bttoxin-results"
|
||||
S3_REGION: str = "us-east-1"
|
||||
|
||||
# Docker
|
||||
DOCKER_IMAGE: str = "quay.io/biocontainers/bttoxin_digger:1.0.10--hdfd78af_0"
|
||||
|
||||
# 文件路径
|
||||
UPLOAD_DIR: str = "uploads"
|
||||
RESULTS_DIR: str = "results"
|
||||
|
||||
# Celery
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS: list = ["http://localhost:3000", "http://localhost:5173"]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
settings = Settings()
|
||||
21
backend/app/core/celery_app.py
Normal file
21
backend/app/core/celery_app.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Celery 配置"""
|
||||
from celery import Celery
|
||||
from ..config import settings
|
||||
|
||||
celery_app = Celery(
|
||||
"bttoxin_worker",
|
||||
broker=settings.CELERY_BROKER_URL,
|
||||
backend=settings.CELERY_RESULT_BACKEND,
|
||||
include=['app.workers.tasks']
|
||||
)
|
||||
|
||||
celery_app.conf.update(
|
||||
task_serializer='json',
|
||||
accept_content=['json'],
|
||||
result_serializer='json',
|
||||
timezone='UTC',
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_time_limit=7200,
|
||||
worker_prefetch_multiplier=1,
|
||||
)
|
||||
72
backend/app/core/docker_client.py
Normal file
72
backend/app/core/docker_client.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Docker 客户端管理"""
|
||||
import docker
|
||||
from typing import Dict, Any
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DockerManager:
|
||||
"""Docker 容器管理器"""
|
||||
|
||||
def __init__(self, image: str = None):
|
||||
from ..config import settings
|
||||
self.client = docker.from_env()
|
||||
self.image = image or settings.DOCKER_IMAGE
|
||||
|
||||
def ensure_image(self) -> bool:
|
||||
"""确保镜像存在"""
|
||||
try:
|
||||
self.client.images.get(self.image)
|
||||
return True
|
||||
except docker.errors.ImageNotFound:
|
||||
logger.info(f"Pulling image {self.image}...")
|
||||
self.client.images.pull(self.image)
|
||||
return True
|
||||
|
||||
def run_bttoxin_digger(
|
||||
self,
|
||||
input_dir: Path,
|
||||
output_dir: Path,
|
||||
sequence_type: str = "nucl",
|
||||
scaf_suffix: str = ".fna",
|
||||
threads: int = 4
|
||||
) -> Dict[str, Any]:
|
||||
"""运行 BtToxin_Digger"""
|
||||
self.ensure_image()
|
||||
|
||||
volumes = {
|
||||
str(input_dir.absolute()): {'bind': '/data', 'mode': 'ro'},
|
||||
str(output_dir.absolute()): {'bind': '/results', 'mode': 'rw'}
|
||||
}
|
||||
|
||||
command = [
|
||||
"/usr/local/env-execute", "BtToxin_Digger",
|
||||
"--SeqPath", "/data",
|
||||
"--SequenceType", sequence_type,
|
||||
"--Scaf_suffix", scaf_suffix,
|
||||
"--threads", str(threads)
|
||||
]
|
||||
|
||||
try:
|
||||
container = self.client.containers.run(
|
||||
self.image,
|
||||
command=command,
|
||||
volumes=volumes,
|
||||
platform="linux/amd64",
|
||||
detach=True,
|
||||
remove=False
|
||||
)
|
||||
|
||||
result = container.wait()
|
||||
logs = container.logs().decode('utf-8')
|
||||
container.remove()
|
||||
|
||||
return {
|
||||
'success': result['StatusCode'] == 0,
|
||||
'logs': logs,
|
||||
'exit_code': result['StatusCode']
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
24
backend/app/database.py
Normal file
24
backend/app/database.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""数据库连接"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from .config import settings
|
||||
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
"""获取数据库会话"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
49
backend/app/main.py
Normal file
49
backend/app/main.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""FastAPI 主应用"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from .config import settings
|
||||
from .api.v1 import jobs, upload, results
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时
|
||||
print("🚀 Starting BtToxin Pipeline API...")
|
||||
yield
|
||||
# 关闭时
|
||||
print("👋 Shutting down BtToxin Pipeline API...")
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="Automated Bacillus thuringiensis toxin mining pipeline",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 路由
|
||||
app.include_router(jobs.router, prefix=f"{settings.API_V1_STR}/jobs", tags=["jobs"])
|
||||
app.include_router(upload.router, prefix=f"{settings.API_V1_STR}/upload", tags=["upload"])
|
||||
app.include_router(results.router, prefix=f"{settings.API_V1_STR}/results", tags=["results"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"status": "healthy"
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
32
backend/app/models/job.py
Normal file
32
backend/app/models/job.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""任务模型"""
|
||||
from sqlalchemy import Column, String, Integer, DateTime, JSON, Enum, Text
|
||||
from sqlalchemy.sql import func
|
||||
import enum
|
||||
|
||||
from ..database import Base
|
||||
|
||||
class JobStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
class Job(Base):
|
||||
__tablename__ = "jobs"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
celery_task_id = Column(String, nullable=True)
|
||||
status = Column(Enum(JobStatus), default=JobStatus.PENDING)
|
||||
|
||||
input_files = Column(JSON)
|
||||
sequence_type = Column(String, default="nucl")
|
||||
scaf_suffix = Column(String, default=".fna")
|
||||
threads = Column(Integer, default=4)
|
||||
|
||||
result_url = Column(String, nullable=True)
|
||||
logs = Column(Text, nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
31
backend/app/schemas/job.py
Normal file
31
backend/app/schemas/job.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""任务 Schema"""
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
class JobCreate(BaseModel):
|
||||
input_files: List[str]
|
||||
sequence_type: str = "nucl"
|
||||
scaf_suffix: str = ".fna"
|
||||
threads: int = 4
|
||||
|
||||
class JobResponse(BaseModel):
|
||||
id: str
|
||||
status: JobStatus
|
||||
input_files: List[str]
|
||||
sequence_type: str
|
||||
threads: int
|
||||
result_url: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
created_at: datetime
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
64
backend/app/workers/tasks.py
Normal file
64
backend/app/workers/tasks.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Celery 任务"""
|
||||
from celery import Task
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from ..core.celery_app import celery_app
|
||||
from ..core.docker_client import DockerManager
|
||||
from ..database import SessionLocal
|
||||
from ..models.job import Job, JobStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def run_bttoxin_analysis(
|
||||
self,
|
||||
job_id: str,
|
||||
input_dir: str,
|
||||
output_dir: str,
|
||||
sequence_type: str = "nucl",
|
||||
scaf_suffix: str = ".fna",
|
||||
threads: int = 4
|
||||
):
|
||||
"""执行分析任务"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
job = db.query(Job).filter(Job.id == job_id).first()
|
||||
job.status = JobStatus.RUNNING
|
||||
db.commit()
|
||||
|
||||
self.update_state(
|
||||
state='PROGRESS',
|
||||
meta={'current': 20, 'total': 100, 'status': 'Running analysis...'}
|
||||
)
|
||||
|
||||
docker_manager = DockerManager()
|
||||
result = docker_manager.run_bttoxin_digger(
|
||||
input_dir=Path(input_dir),
|
||||
output_dir=Path(output_dir),
|
||||
sequence_type=sequence_type,
|
||||
scaf_suffix=scaf_suffix,
|
||||
threads=threads
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
job.status = JobStatus.COMPLETED
|
||||
job.logs = result.get('logs', '')
|
||||
else:
|
||||
job.status = JobStatus.FAILED
|
||||
job.error_message = result.get('error', 'Analysis failed')
|
||||
|
||||
db.commit()
|
||||
|
||||
return {'job_id': job_id, 'status': job.status}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Task failed: {e}")
|
||||
job.status = JobStatus.FAILED
|
||||
job.error_message = str(e)
|
||||
db.commit()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
38
backend/requirements.txt
Normal file
38
backend/requirements.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
# Web 框架
|
||||
fastapi==0.115.5
|
||||
uvicorn[standard]==0.32.1
|
||||
python-multipart==0.0.20
|
||||
|
||||
# 任务队列
|
||||
celery==5.4.0
|
||||
redis==5.2.1
|
||||
flower==2.0.1
|
||||
|
||||
# 容器管理
|
||||
docker==7.1.0
|
||||
|
||||
# 数据库
|
||||
sqlalchemy==2.0.36
|
||||
alembic==1.14.0
|
||||
psycopg2-binary==2.9.10
|
||||
|
||||
# 对象存储
|
||||
boto3==1.35.78
|
||||
minio==7.2.11
|
||||
|
||||
# 数据处理
|
||||
biopython==1.84
|
||||
pandas==2.2.3
|
||||
|
||||
# 工具
|
||||
pydantic==2.10.4
|
||||
pydantic-settings==2.6.1
|
||||
python-dotenv==1.0.1
|
||||
aiofiles==24.1.0
|
||||
|
||||
# 监控
|
||||
prometheus-client==0.21.0
|
||||
|
||||
# 测试
|
||||
pytest==8.3.4
|
||||
httpx==0.28.1
|
||||
75
config/docker-compose.yml
Normal file
75
config/docker-compose.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: bttoxin
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/worker/Dockerfile
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ../backend:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:password@postgres:5432/bttoxin
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
celery:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/worker/Dockerfile
|
||||
command: celery -A app.core.celery_app worker --loglevel=info
|
||||
volumes:
|
||||
- ../backend:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:password@postgres:5432/bttoxin
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/frontend/Dockerfile
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
46
docker/digger/Dockerfile
Normal file
46
docker/digger/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
# BtToxin_Digger Docker 镜像
|
||||
FROM continuumio/miniconda3:4.10.3
|
||||
|
||||
LABEL maintainer="your-email@example.com" \
|
||||
description="BtToxin_Digger for Pipeline" \
|
||||
version="1.0.10"
|
||||
|
||||
ENV LANG=C.UTF-8 \
|
||||
PATH=/opt/conda/envs/bttoxin/bin:$PATH \
|
||||
CONDA_DEFAULT_ENV=bttoxin \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# 配置 conda
|
||||
RUN conda config --add channels defaults && \
|
||||
conda config --add channels bioconda && \
|
||||
conda config --add channels conda-forge && \
|
||||
conda config --set channel_priority flexible && \
|
||||
conda install -n base -c conda-forge mamba -y
|
||||
|
||||
# 安装 BtToxin_Digger
|
||||
RUN mamba install -y \
|
||||
python=3.7.10 \
|
||||
perl=5.26.2 && \
|
||||
mamba install -y \
|
||||
bttoxin_digger=1.0.10 && \
|
||||
conda clean -afy
|
||||
|
||||
# 安装额外工具
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl wget jq git && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 创建工作目录
|
||||
RUN mkdir -p /workspace/input /workspace/output /workspace/logs
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# 复制入口脚本
|
||||
COPY entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD BtToxin_Digger --version || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
CMD ["--help"]
|
||||
84
docker/digger/entrypoint.sh
Executable file
84
docker/digger/entrypoint.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
init() {
|
||||
log_info "Initializing BtToxin_Digger environment..."
|
||||
|
||||
if ! BtToxin_Digger --version &>/dev/null; then
|
||||
log_error "BtToxin_Digger not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "BtToxin_Digger $(BtToxin_Digger --version)"
|
||||
log_info "Python $(python --version)"
|
||||
log_info "Perl $(perl --version | head -2 | tail -1)"
|
||||
}
|
||||
|
||||
update_db() {
|
||||
log_info "Updating BtToxin_Digger database..."
|
||||
if BtToxin_Digger --update-db; then
|
||||
log_info "Database updated successfully"
|
||||
else
|
||||
log_warn "Database update failed, using existing database"
|
||||
fi
|
||||
}
|
||||
|
||||
run_analysis() {
|
||||
log_info "Starting toxin mining analysis..."
|
||||
log_info "Input: $INPUT_PATH"
|
||||
log_info "Type: $SEQUENCE_TYPE"
|
||||
log_info "Threads: $THREADS"
|
||||
|
||||
BtToxin_Digger \
|
||||
--SeqPath "$INPUT_PATH" \
|
||||
--SequenceType "$SEQUENCE_TYPE" \
|
||||
--Scaf_suffix "$SCAF_SUFFIX" \
|
||||
--threads "$THREADS" \
|
||||
2>&1 | tee /workspace/logs/digger.log
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "Analysis completed successfully"
|
||||
else
|
||||
log_error "Analysis failed!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
INPUT_PATH="${INPUT_PATH:-/workspace/input}"
|
||||
SEQUENCE_TYPE="${SEQUENCE_TYPE:-nucl}"
|
||||
SCAF_SUFFIX="${SCAF_SUFFIX:-.fna}"
|
||||
THREADS="${THREADS:-4}"
|
||||
UPDATE_DB="${UPDATE_DB:-false}"
|
||||
|
||||
init
|
||||
|
||||
if [ "$UPDATE_DB" = "true" ]; then
|
||||
update_db
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
exec BtToxin_Digger "$@"
|
||||
else
|
||||
run_analysis
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
18
docker/frontend/Dockerfile
Normal file
18
docker/frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
24
docker/frontend/nginx.conf
Normal file
24
docker/frontend/nginx.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
25
docker/worker/Dockerfile
Normal file
25
docker/worker/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Docker CLI
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
|
||||
sh get-docker.sh && \
|
||||
rm get-docker.sh
|
||||
|
||||
# 复制依赖文件
|
||||
COPY backend/requirements.txt .
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY backend/ .
|
||||
|
||||
CMD ["celery", "-A", "app.core.celery_app", "worker", "--loglevel=info"]
|
||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BtToxin Pipeline</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "bttoxin-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"pinia": "^2.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"naive-ui": "^2.40.1",
|
||||
"@vicons/ionicons5": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
41
frontend/src/App.vue
Normal file
41
frontend/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<n-layout>
|
||||
<n-layout-header bordered>
|
||||
<n-space align="center" justify="space-between" style="padding: 16px">
|
||||
<n-h2>BtToxin Pipeline</n-h2>
|
||||
<n-menu mode="horizontal" :options="menuOptions" />
|
||||
</n-space>
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout-content style="padding: 24px">
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
|
||||
<n-layout-footer bordered style="padding: 16px; text-align: center">
|
||||
BtToxin Pipeline © 2025
|
||||
</n-layout-footer>
|
||||
</n-layout>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { h } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/' }, { default: () => 'Home' }),
|
||||
key: 'home'
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/upload' }, { default: () => 'Upload' }),
|
||||
key: 'upload'
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/jobs' }, { default: () => 'Jobs' }),
|
||||
key: 'jobs'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
14
frontend/src/main.js
Normal file
14
frontend/src/main.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import naive from 'naive-ui'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
app.mount('#app')
|
||||
31
frontend/src/router.js
Normal file
31
frontend/src/router.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('./views/Home.vue')
|
||||
},
|
||||
{
|
||||
path: '/upload',
|
||||
name: 'Upload',
|
||||
component: () => import('./views/Upload.vue')
|
||||
},
|
||||
{
|
||||
path: '/jobs',
|
||||
name: 'Jobs',
|
||||
component: () => import('./views/Jobs.vue')
|
||||
},
|
||||
{
|
||||
path: '/jobs/:id',
|
||||
name: 'JobDetail',
|
||||
component: () => import('./views/JobDetail.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
22
frontend/src/services/api.js
Normal file
22
frontend/src/services/api.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
export default {
|
||||
createJob(formData) {
|
||||
return api.post('/jobs/create', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
|
||||
getJob(jobId) {
|
||||
return api.get(`/jobs/${jobId}`)
|
||||
},
|
||||
|
||||
getJobProgress(jobId) {
|
||||
return api.get(`/jobs/${jobId}/progress`)
|
||||
}
|
||||
}
|
||||
7
frontend/src/views/Home.vue
Normal file
7
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<n-space vertical size="large">
|
||||
<n-card title="Welcome to BtToxin Pipeline">
|
||||
<p>Automated Bacillus thuringiensis toxin mining system</p>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
5
frontend/src/views/JobDetail.vue
Normal file
5
frontend/src/views/JobDetail.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<n-card title="Job Details">
|
||||
<p>Job ID: {{ $route.params.id }}</p>
|
||||
</n-card>
|
||||
</template>
|
||||
5
frontend/src/views/Jobs.vue
Normal file
5
frontend/src/views/Jobs.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<n-card title="Job List">
|
||||
<n-empty description="No jobs yet" />
|
||||
</n-card>
|
||||
</template>
|
||||
7
frontend/src/views/Upload.vue
Normal file
7
frontend/src/views/Upload.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<n-card title="Upload Genome Files">
|
||||
<n-upload multiple>
|
||||
<n-button>Select Files</n-button>
|
||||
</n-upload>
|
||||
</n-card>
|
||||
</template>
|
||||
21
frontend/vite.config.js
Normal file
21
frontend/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
56
scripts/download_bpprc_data.py
Executable file
56
scripts/download_bpprc_data.py
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 BPPRC/NCBI 下载测试数据"""
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from Bio import Entrez, SeqIO
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Entrez.email = "your_email@example.com"
|
||||
|
||||
TEST_GENOMES = {
|
||||
'Bacillus_thuringiensis_HD-73': 'NZ_CP004069.1',
|
||||
'Bacillus_thuringiensis_YBT-1520': 'NZ_CP003889.1',
|
||||
'Bacillus_thuringiensis_BMB171': 'NC_014171.1',
|
||||
}
|
||||
|
||||
def download_genome(accession, output_file):
|
||||
"""下载基因组"""
|
||||
try:
|
||||
logger.info(f"Downloading {accession}...")
|
||||
handle = Entrez.efetch(
|
||||
db="nucleotide",
|
||||
id=accession,
|
||||
rettype="fasta",
|
||||
retmode="text"
|
||||
)
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(handle.read())
|
||||
|
||||
handle.close()
|
||||
logger.info(f"✓ Downloaded: {output_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--output-dir', default='tests/test_data/genomes')
|
||||
parser.add_argument('--email', required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
Entrez.email = args.email
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for name, accession in TEST_GENOMES.items():
|
||||
output_file = output_dir / f"{name}.fna"
|
||||
download_genome(accession, output_file)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user