Spaces:
Sleeping
Sleeping
| # api.py | |
| import os | |
| import re | |
| import tempfile | |
| from datetime import datetime | |
| from typing import Any, Dict, Optional | |
| import requests | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel, Field | |
| # ---------------------------- | |
| # Configuration | |
| # ---------------------------- | |
| ROBOT_HOST = os.getenv("ROBOT_HOST", "127.0.0.1") | |
| ROBOT_PORT_OT2 = int(os.getenv("ROBOT_PORT_OT2", "31950")) | |
| ROBOT_PORT_FLEX = int(os.getenv("ROBOT_PORT_FLEX", "31951")) | |
| OT_API_VERSION = os.getenv("OT_API_VERSION", "3") | |
| HTTP_TIMEOUT = float(os.getenv("HTTP_TIMEOUT", "30")) | |
| # Max bytes of non-JSON text to include in errors | |
| MAX_TEXT_SNIP = int(os.getenv("MAX_TEXT_SNIP", "2000")) | |
| # ---------------------------- | |
| # FastAPI app | |
| # ---------------------------- | |
| app = FastAPI(title="Opentrons Protocol Analyzer API", version="1.0.0") | |
| # ---------------------------- | |
| # Models | |
| # ---------------------------- | |
| class AnalyzeRequest(BaseModel): | |
| protocol_text: str = Field(..., description="Python protocol source code") | |
| filename: str = Field("protocol.py", description="Filename to send to robot-server") | |
| class AnalyzeResponse(BaseModel): | |
| ok: bool | |
| robot_type: str # "ot2" or "flex" | |
| robot_server_url: str | |
| status_code: int | |
| payload: Dict[str, Any] | |
| # ---------------------------- | |
| # Helpers | |
| # ---------------------------- | |
| _REQUIREMENTS_ROBOTTYPE_RE = re.compile( | |
| r"""requirements\s*=\s*{.*?["']robotType["']\s*:\s*["']([^"']+)["'].*?}""", | |
| re.DOTALL, | |
| ) | |
| def detect_robot_type(protocol_text: str) -> str: | |
| """ | |
| Detect robot type from protocol 'requirements' metadata. | |
| Returns: | |
| "flex" if requirements.robotType == "Flex" | |
| otherwise "ot2" (default) | |
| """ | |
| m = _REQUIREMENTS_ROBOTTYPE_RE.search(protocol_text) | |
| if not m: | |
| return "ot2" | |
| val = (m.group(1) or "").strip() | |
| if val == "Flex": | |
| return "flex" | |
| if val == "OT-2": | |
| return "ot2" | |
| # Unknown value -> be conservative | |
| return "ot2" | |
| def robot_server_url(robot_type: str) -> str: | |
| port = ROBOT_PORT_FLEX if robot_type == "flex" else ROBOT_PORT_OT2 | |
| return f"http://{ROBOT_HOST}:{port}" | |
| def post_protocol(protocol_text: str, filename: str, base_url: str) -> requests.Response: | |
| endpoint = f"{base_url}/protocols" | |
| headers = {"Opentrons-Version": OT_API_VERSION} | |
| fd, path = tempfile.mkstemp(prefix="protocol_", suffix=".py") | |
| try: | |
| with os.fdopen(fd, "w", encoding="utf-8") as f: | |
| f.write(protocol_text) | |
| with open(path, "rb") as f: | |
| files = {"files": (filename, f, "text/x-python")} | |
| return requests.post(endpoint, headers=headers, files=files, timeout=HTTP_TIMEOUT) | |
| finally: | |
| try: | |
| os.remove(path) | |
| except OSError: | |
| pass | |
| def safe_json(resp: requests.Response) -> Dict[str, Any]: | |
| try: | |
| data = resp.json() | |
| if isinstance(data, dict): | |
| return data | |
| return {"_non_dict_json": data} | |
| except ValueError: | |
| return {"_non_json": resp.text[:MAX_TEXT_SNIP]} | |
| # ---------------------------- | |
| # Routes | |
| # ---------------------------- | |
| APP_NAME = os.getenv("APP_NAME", "Opentrons Protocol Analyzer API") | |
| APP_VERSION = os.getenv("APP_VERSION", "dev") | |
| BUILD_SHA = os.getenv("BUILD_SHA", "unknown") | |
| def root(): | |
| return { | |
| "name": APP_NAME, | |
| "version": APP_VERSION, | |
| "build": BUILD_SHA, | |
| "status": "ok", | |
| "time": datetime.utcnow().isoformat() + "Z", | |
| } | |
| def health() -> Dict[str, Any]: | |
| return {"ok": True} | |
| def robot_health() -> dict: | |
| out: dict = {"ok": True, "robots": {}} | |
| headers = {"Opentrons-Version": OT_API_VERSION} | |
| for rt in ("ot2", "flex"): | |
| base = robot_server_url(rt) | |
| try: | |
| r = requests.get(f"{base}/health", headers=headers, timeout=5) | |
| out["robots"][rt] = {"ok": r.ok, "status_code": r.status_code, "text": r.text[:2000]} | |
| if not r.ok: | |
| out["ok"] = False | |
| except requests.RequestException as e: | |
| out["robots"][rt] = {"ok": False, "error": str(e)} | |
| out["ok"] = False | |
| return out | |
| def analyze(req: AnalyzeRequest) -> AnalyzeResponse: | |
| protocol_text = (req.protocol_text or "").strip() | |
| if not protocol_text: | |
| raise HTTPException(status_code=400, detail="protocol_text is empty") | |
| rt = detect_robot_type(protocol_text) | |
| base = robot_server_url(rt) | |
| try: | |
| resp = post_protocol(protocol_text, req.filename, base) | |
| except requests.RequestException as e: | |
| raise HTTPException(status_code=503, detail=f"robot-server request failed: {e}") | |
| payload = safe_json(resp) | |
| return AnalyzeResponse( | |
| ok=resp.status_code < 400, | |
| robot_type=rt, | |
| robot_server_url=base, | |
| status_code=resp.status_code, | |
| payload=payload, | |
| ) | |