|
|
"""
|
|
|
FastAPI Main Application
|
|
|
Backend API for Cancer@Home
|
|
|
"""
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from fastapi.responses import HTMLResponse
|
|
|
from strawberry.fastapi import GraphQLRouter
|
|
|
from pathlib import Path
|
|
|
import uvicorn
|
|
|
|
|
|
from backend.neo4j.graphql_schema import schema
|
|
|
from backend.neo4j.db_manager import DatabaseManager
|
|
|
from backend.boinc.client import BOINCClient, BOINCTaskManager
|
|
|
from backend.gdc.client import GDCClient
|
|
|
from backend.pipeline import (
|
|
|
FASTQProcessor,
|
|
|
BLASTRunner,
|
|
|
VariantCaller
|
|
|
)
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
title="Cancer@Home v2",
|
|
|
description="Distributed cancer genomics research platform",
|
|
|
version="2.0.0"
|
|
|
)
|
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
CORSMiddleware,
|
|
|
allow_origins=["*"],
|
|
|
allow_credentials=True,
|
|
|
allow_methods=["*"],
|
|
|
allow_headers=["*"],
|
|
|
)
|
|
|
|
|
|
|
|
|
graphql_app = GraphQLRouter(schema)
|
|
|
app.include_router(graphql_app, prefix="/graphql")
|
|
|
|
|
|
|
|
|
frontend_path = Path("frontend/dist")
|
|
|
if frontend_path.exists():
|
|
|
app.mount("/static", StaticFiles(directory=str(frontend_path)), name="static")
|
|
|
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
async def root():
|
|
|
"""Serve main dashboard"""
|
|
|
html_file = Path("frontend/index.html")
|
|
|
if html_file.exists():
|
|
|
with open(html_file, 'r') as f:
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
return """
|
|
|
<!DOCTYPE html>
|
|
|
<html>
|
|
|
<head>
|
|
|
<title>Cancer@Home v2</title>
|
|
|
<style>
|
|
|
body {
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
color: white;
|
|
|
}
|
|
|
.container {
|
|
|
max-width: 1200px;
|
|
|
margin: 0 auto;
|
|
|
padding: 40px 20px;
|
|
|
}
|
|
|
h1 {
|
|
|
font-size: 3em;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
.card {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
border-radius: 10px;
|
|
|
padding: 30px;
|
|
|
margin: 20px 0;
|
|
|
backdrop-filter: blur(10px);
|
|
|
}
|
|
|
.links {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
gap: 20px;
|
|
|
margin-top: 30px;
|
|
|
}
|
|
|
.link-card {
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
border-radius: 8px;
|
|
|
padding: 20px;
|
|
|
text-decoration: none;
|
|
|
color: white;
|
|
|
transition: transform 0.2s;
|
|
|
}
|
|
|
.link-card:hover {
|
|
|
transform: translateY(-5px);
|
|
|
background: rgba(255, 255, 255, 0.25);
|
|
|
}
|
|
|
.link-card h3 {
|
|
|
margin-top: 0;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<h1>🧬 Cancer@Home v2</h1>
|
|
|
<div class="card">
|
|
|
<h2>Welcome to Cancer@Home</h2>
|
|
|
<p>A distributed computing platform for cancer genomics research</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="links">
|
|
|
<a href="/api/docs" class="link-card">
|
|
|
<h3>📚 API Documentation</h3>
|
|
|
<p>Interactive API docs with Swagger UI</p>
|
|
|
</a>
|
|
|
<a href="/graphql" class="link-card">
|
|
|
<h3>🔍 GraphQL Playground</h3>
|
|
|
<p>Query cancer data with GraphQL</p>
|
|
|
</a>
|
|
|
<a href="http://localhost:7474" class="link-card">
|
|
|
<h3>📊 Neo4j Browser</h3>
|
|
|
<p>Visualize graph database</p>
|
|
|
</a>
|
|
|
<a href="/api/health" class="link-card">
|
|
|
<h3>💚 Health Check</h3>
|
|
|
<p>Check system status</p>
|
|
|
</a>
|
|
|
</div>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
|
"""
|
|
|
|
|
|
|
|
|
@app.get("/api/health")
|
|
|
async def health_check():
|
|
|
"""Health check endpoint"""
|
|
|
db = DatabaseManager()
|
|
|
try:
|
|
|
db.execute_query("RETURN 1")
|
|
|
neo4j_status = "healthy"
|
|
|
except Exception as e:
|
|
|
neo4j_status = f"unhealthy: {str(e)}"
|
|
|
finally:
|
|
|
db.close()
|
|
|
|
|
|
return {
|
|
|
"status": "healthy",
|
|
|
"neo4j": neo4j_status,
|
|
|
"version": "2.0.0"
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/boinc/tasks")
|
|
|
async def get_boinc_tasks(status: str = None):
|
|
|
"""Get BOINC tasks"""
|
|
|
client = BOINCClient()
|
|
|
tasks = client.list_tasks(status=status)
|
|
|
return {"tasks": [vars(t) for t in tasks]}
|
|
|
|
|
|
|
|
|
@app.post("/api/boinc/submit")
|
|
|
async def submit_boinc_task(workunit_type: str, input_file: str):
|
|
|
"""Submit new BOINC task"""
|
|
|
manager = BOINCTaskManager()
|
|
|
|
|
|
if workunit_type == "variant_calling":
|
|
|
task_id = manager.submit_variant_calling(input_file)
|
|
|
elif workunit_type == "blast_search":
|
|
|
task_id = manager.submit_blast_search(input_file)
|
|
|
else:
|
|
|
task_id = manager.client.submit_task(workunit_type, input_file)
|
|
|
|
|
|
return {"task_id": task_id, "status": "submitted"}
|
|
|
|
|
|
|
|
|
@app.get("/api/boinc/statistics")
|
|
|
async def get_boinc_statistics():
|
|
|
"""Get BOINC statistics"""
|
|
|
client = BOINCClient()
|
|
|
stats = client.get_statistics()
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/gdc/projects")
|
|
|
async def get_gdc_projects():
|
|
|
"""Get available GDC projects"""
|
|
|
projects = [
|
|
|
{"id": "TCGA-BRCA", "name": "Breast Cancer", "cases": 1098},
|
|
|
{"id": "TCGA-LUAD", "name": "Lung Adenocarcinoma", "cases": 585},
|
|
|
{"id": "TCGA-COAD", "name": "Colon Adenocarcinoma", "cases": 461},
|
|
|
{"id": "TCGA-GBM", "name": "Glioblastoma", "cases": 617},
|
|
|
{"id": "TARGET-AML", "name": "Acute Myeloid Leukemia", "cases": 238},
|
|
|
]
|
|
|
return {"projects": projects}
|
|
|
|
|
|
|
|
|
@app.get("/api/gdc/files/{project_id}")
|
|
|
async def search_gdc_files(project_id: str, limit: int = 10):
|
|
|
"""Search GDC files for a project"""
|
|
|
client = GDCClient()
|
|
|
files = client.get_project_files(project_id, limit=limit)
|
|
|
return {"files": [vars(f) for f in files]}
|
|
|
|
|
|
|
|
|
@app.post("/api/gdc/download")
|
|
|
async def download_gdc_file(file_id: str):
|
|
|
"""Download a file from GDC"""
|
|
|
client = GDCClient()
|
|
|
file_path = client.download_file(file_id)
|
|
|
|
|
|
if file_path:
|
|
|
return {"status": "success", "file_path": str(file_path)}
|
|
|
else:
|
|
|
raise HTTPException(status_code=500, detail="Download failed")
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/pipeline/fastq/qc")
|
|
|
async def run_fastq_qc(file_path: str):
|
|
|
"""Run FASTQ quality control"""
|
|
|
processor = FASTQProcessor()
|
|
|
stats = processor.calculate_statistics(Path(file_path))
|
|
|
return {"statistics": stats}
|
|
|
|
|
|
|
|
|
@app.post("/api/pipeline/blast")
|
|
|
async def run_blast(query_file: str):
|
|
|
"""Run BLAST search"""
|
|
|
runner = BLASTRunner()
|
|
|
output_file = runner.run_blastn(Path(query_file))
|
|
|
|
|
|
if output_file:
|
|
|
hits = runner.parse_results(output_file)
|
|
|
return {
|
|
|
"status": "success",
|
|
|
"output_file": str(output_file),
|
|
|
"total_hits": len(hits),
|
|
|
"hits": hits[:10]
|
|
|
}
|
|
|
else:
|
|
|
raise HTTPException(status_code=500, detail="BLAST search failed")
|
|
|
|
|
|
|
|
|
@app.post("/api/pipeline/variants")
|
|
|
async def call_variants(alignment_file: str, reference_genome: str):
|
|
|
"""Call variants from alignment"""
|
|
|
caller = VariantCaller()
|
|
|
vcf_file = caller.call_variants(
|
|
|
Path(alignment_file),
|
|
|
Path(reference_genome)
|
|
|
)
|
|
|
|
|
|
variants = caller.filter_variants(vcf_file)
|
|
|
|
|
|
return {
|
|
|
"status": "success",
|
|
|
"vcf_file": str(vcf_file),
|
|
|
"total_variants": len(variants),
|
|
|
"variants": [vars(v) for v in variants]
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/neo4j/summary")
|
|
|
async def get_database_summary():
|
|
|
"""Get database summary statistics"""
|
|
|
db = DatabaseManager()
|
|
|
|
|
|
query = """
|
|
|
MATCH (g:Gene) WITH count(g) as genes
|
|
|
MATCH (m:Mutation) WITH genes, count(m) as mutations
|
|
|
MATCH (p:Patient) WITH genes, mutations, count(p) as patients
|
|
|
MATCH (c:CancerType) WITH genes, mutations, patients, count(c) as cancer_types
|
|
|
RETURN genes, mutations, patients, cancer_types
|
|
|
"""
|
|
|
|
|
|
result = db.execute_query(query)
|
|
|
db.close()
|
|
|
|
|
|
return result[0] if result else {}
|
|
|
|
|
|
|
|
|
@app.get("/api/neo4j/genes/{symbol}")
|
|
|
async def get_gene_info(symbol: str):
|
|
|
"""Get gene information"""
|
|
|
db = DatabaseManager()
|
|
|
from backend.neo4j.db_manager import GeneRepository
|
|
|
|
|
|
repo = GeneRepository(db)
|
|
|
gene = repo.get_gene_by_symbol(symbol)
|
|
|
|
|
|
if gene:
|
|
|
mutations = repo.get_gene_mutations(gene['gene_id'])
|
|
|
db.close()
|
|
|
return {
|
|
|
"gene": gene,
|
|
|
"mutations": mutations,
|
|
|
"mutation_count": len(mutations)
|
|
|
}
|
|
|
|
|
|
db.close()
|
|
|
raise HTTPException(status_code=404, detail="Gene not found")
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
uvicorn.run(app, host="0.0.0.0", port=5000)
|
|
|
|