Team Ai
Apppublic

MeMoModelling/gene-expression-bootstrapper

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
main.py196 linesDownload Raw Back to backend
1import asyncio2import io3import json4import os5import sys6import tempfile7import uuid8import zipfile9from contextlib import redirect_stdout10from pathlib import Path11 12from fastapi import FastAPI, File, Form, UploadFile13from fastapi.responses import FileResponse, JSONResponse, StreamingResponse14from fastapi.staticfiles import StaticFiles15 16app = FastAPI()17 18# In-memory job store19jobs: dict = {}20 21 22@app.post("/api/process")23async def process(24    model_files: list[UploadFile] = File(...),25    mapping_files: list[UploadFile] = File(...),26    expr_file: UploadFile = File(...),27    batch_count: int = Form(1000),28):29    job_id = str(uuid.uuid4())30    jobs[job_id] = {31        "status": "running",32        "progress": 0,33        "messages": [],34        "output_zip": None,35        "error": None,36        "file_count": batch_count,37        "species_count": len(model_files),38    }39 40    # Save uploaded files to temp dir41    tmp = tempfile.mkdtemp()42 43    model_paths = []44    for f in model_files:45        content = await f.read()46        path = os.path.join(tmp, f.filename)47        with open(path, "wb") as out:48            out.write(content)49        model_paths.append(path)50 51    mapping_paths = []52    for f in mapping_files:53        content = await f.read()54        path = os.path.join(tmp, f.filename)55        with open(path, "wb") as out:56            out.write(content)57        mapping_paths.append(path)58 59    expr_content = await expr_file.read()60    expr_path = os.path.join(tmp, expr_file.filename)61    with open(expr_path, "wb") as out:62        out.write(expr_content)63 64    species_prefixes = [Path(f.filename).stem.split("_")[0] for f in model_files]65 66    asyncio.create_task(67        run_bootstrap(job_id, model_paths, mapping_paths, expr_path, species_prefixes, batch_count, tmp)68    )69 70    return {"job_id": job_id}71 72 73async def run_bootstrap(job_id, model_paths, mapping_paths, expr_path, species_prefixes, batch_count, tmp):74    job = jobs[job_id]75 76    def add_msg(text, type_=""):77        job["messages"].append({"text": text, "type": type_})78 79    try:80        add_msg(f"Starting bootstrap for: {', '.join(species_prefixes)}", "info")81        job["progress"] = 1082 83        output_dir = os.path.join(tmp, "output")84        os.makedirs(output_dir, exist_ok=True)85 86        captured = io.StringIO()87 88        def run_sync():89            sys.path.insert(0, "/app")90            from utils.bootstrap_genes import bootstrap_genes91            with redirect_stdout(captured):92                bootstrap_genes(93                    model_pre_filenames=model_paths,94                    mapping_filenames=mapping_paths,95                    species_prefixes=species_prefixes,96                    combined_geneExpr_filename=expr_path,97                    geneExpr_folder=output_dir,98                    batch_count=batch_count,99                )100 101        job["progress"] = 20102        loop = asyncio.get_event_loop()103        await loop.run_in_executor(None, run_sync)104        job["progress"] = 80105 106        # Relay only key status lines — skip all Warning lines107        for line in captured.getvalue().strip().split("\n"):108            line = line.strip()109            if not line or line.startswith("Warning"):110                continue111            if "Write to" in line:112                add_msg("Writing output files…", "ok")113            elif line.startswith("Read") or line.startswith("Species") or line.startswith("Bootstrap"):114                add_msg(line, "info")115 116        add_msg("Compressing output files…", "info")117        job["progress"] = 90118 119        zip_path = os.path.join(tmp, "geneExpr_bootstrapped.zip")120        output_files = sorted([f for f in os.listdir(output_dir) if f.endswith(".csv")])121        with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:122            for fname in output_files:123                zf.write(os.path.join(output_dir, fname), arcname=fname)124 125        job["output_zip"] = zip_path126        job["file_count"] = batch_count127        job["species_count"] = len(species_prefixes)128        job["progress"] = 100129        job["status"] = "done"130        add_msg(f"Done! {batch_count} files generated.", "ok")131 132    except Exception as e:133        import traceback134        job["status"] = "error"135        job["error"] = str(e)136        add_msg(f"Error: {e}", "err")137        print(traceback.format_exc(), flush=True)138 139 140@app.get("/api/progress/{job_id}")141async def progress_stream(job_id: str):142    async def event_stream():143        last_idx = 0144        while True:145            job = jobs.get(job_id)146            if not job:147                yield f"data: {json.dumps({'status': 'error', 'error': 'Job not found'})}\n\n"148                break149 150            new_msgs = job["messages"][last_idx:]151            last_idx = len(job["messages"])152 153            for msg in new_msgs:154                payload = {155                    "message": msg["text"],156                    "type": msg["type"],157                    "progress": job["progress"],158                    "label": msg["text"],159                }160                yield f"data: {json.dumps(payload)}\n\n"161 162            if job["status"] == "done":163                yield f"data: {json.dumps({'status': 'done', 'progress': 100, 'file_count': job.get('file_count'), 'species_count': job.get('species_count')})}\n\n"164                break165            elif job["status"] == "error":166                yield f"data: {json.dumps({'status': 'error', 'error': job.get('error', 'Unknown error')})}\n\n"167                break168 169            if not new_msgs:170                yield f"data: {json.dumps({'progress': job['progress'], 'label': 'Processing…'})}\n\n"171 172            await asyncio.sleep(1)173 174    return StreamingResponse(175        event_stream(),176        media_type="text/event-stream",177        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},178    )179 180 181@app.get("/api/download/{job_id}")182async def download(job_id: str):183    job = jobs.get(job_id, {})184    zip_path = job.get("output_zip")185    if not zip_path or not os.path.exists(zip_path):186        return JSONResponse({"error": "File not found"}, status_code=404)187    return FileResponse(188        zip_path,189        filename="geneExpr_bootstrapped.zip",190        media_type="application/zip",191    )192 193 194# Serve frontend — must be last195app.mount("/", StaticFiles(directory="/app/frontend", html=True), name="frontend")196