Dinesh4311's picture
Add application code
aa752f5 verified
Raw
History Blame Contribute Delete
5.45 kB
"""FastAPI application: REST + WebSocket API and HTMX-rendered frontend."""
import asyncio
from io import StringIO
from pathlib import Path
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord
from Bio import SeqIO
from core import SPECIES_PROFILES, DnaAnalyzer, species_choices, mechanism_for
from core.codons import CODON_USAGE_TABLES
from .schemas import DesignRequest, AnalyzeRequest, GrnaRequest, CAS_TO_PAM
from .service import run_design, analyze_sequence
BASE_DIR = Path(__file__).resolve().parent.parent
templates = Jinja2Templates(directory=str(BASE_DIR / "web" / "templates"))
app = FastAPI(title="Plant DNA Designer", version="2.0")
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "web" / "static")), name="static")
# --- pages -----------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
def index(request: Request):
return templates.TemplateResponse(request, "index.html", {
"species": species_choices(),
"profiles": SPECIES_PROFILES,
"cas_types": list(CAS_TO_PAM.keys()),
"codon_tables": list(CODON_USAGE_TABLES.keys()),
})
@app.get("/traits/{species}", response_class=HTMLResponse)
def traits_partial(request: Request, species: str):
"""HTMX swap: trait checkboxes for the selected species."""
prof = SPECIES_PROFILES.get(species)
if not prof:
raise HTTPException(404, "Unknown species")
return templates.TemplateResponse(request, "partials/traits.html", {
"traits": list(prof['traits'].keys()),
"gc_range": prof['gc_range'], "codon_table": prof['codon_table'],
})
@app.get("/api/mechanism/{trait}")
def api_mechanism(trait: str):
"""Trait → Mechanism → Effector protein record (for the UI target layer)."""
m = mechanism_for(trait)
if not m:
raise HTTPException(404, f"No known mechanism for trait: {trait}")
return JSONResponse(m)
# --- JSON API --------------------------------------------------------------
@app.post("/api/design")
def api_design(req: DesignRequest):
try:
return run_design(req)
except ValueError as e:
raise HTTPException(400, str(e))
@app.post("/api/analyze")
def api_analyze(req: AnalyzeRequest):
return analyze_sequence(req.dna.strip().upper(), req.codon_table)
@app.post("/api/grna")
def api_grna(req: GrnaRequest):
from core import design_grna
pam = CAS_TO_PAM.get(req.cas_type, "NGG")
grnas = design_grna(req.dna.strip().upper(), pam_type=pam, grna_num=req.grna_num)
return [{"grna": g, "position": p, "on_target": round(s["on_target"], 3),
"off_target": round(s["off_target"], 3)} for g, p, s in grnas]
# --- exports ---------------------------------------------------------------
@app.get("/api/export/{fmt}")
def api_export(fmt: str, dna: str, name: str = "synthetic"):
dna = dna.strip().upper()
ana = DnaAnalyzer(dna)
if fmt == "fasta":
return PlainTextResponse(f">{name}_{ana.sha()}\n{dna}\n",
headers={"Content-Disposition": f"attachment; filename={ana.sha()}.fasta"})
if fmt == "sbol":
return PlainTextResponse(ana.generate_sbol(),
headers={"Content-Disposition": f"attachment; filename={ana.sha()}.sbol"})
if fmt == "genbank":
record = SeqRecord(Seq(dna), id=ana.sha(), description=f"Synthetic DNA ({name})")
record.annotations["molecule_type"] = "DNA"
buf = StringIO()
SeqIO.write(record, buf, "genbank")
return PlainTextResponse(buf.getvalue(),
headers={"Content-Disposition": f"attachment; filename={ana.sha()}.gb"})
raise HTTPException(400, f"Unknown format: {fmt}")
# --- WebSocket: live design with streaming progress ------------------------
@app.websocket("/ws/design")
async def ws_design(ws: WebSocket):
await ws.accept()
loop = asyncio.get_running_loop()
try:
payload = await ws.receive_json()
req = DesignRequest(**payload)
queue: asyncio.Queue = asyncio.Queue()
def progress_cb(fraction: float):
loop.call_soon_threadsafe(queue.put_nowait, fraction)
async def pump():
while True:
frac = await queue.get()
await ws.send_json({"type": "progress", "value": round(frac, 4)})
pump_task = asyncio.create_task(pump())
try:
result = await loop.run_in_executor(None, lambda: run_design(req, progress_cb))
await asyncio.sleep(0) # let the final progress frame flush
await ws.send_json({"type": "result", "data": result})
finally:
pump_task.cancel()
except WebSocketDisconnect:
return
except Exception as e: # surface validation/runtime errors to the client
await ws.send_json({"type": "error", "message": str(e)})
finally:
await ws.close()
@app.get("/api/health")
def health():
return JSONResponse({"status": "ok", "species": len(SPECIES_PROFILES)})