baxin's picture
Update api.py
b1e3a85 verified
# 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")
@app.get("/")
def root():
return {
"name": APP_NAME,
"version": APP_VERSION,
"build": BUILD_SHA,
"status": "ok",
"time": datetime.utcnow().isoformat() + "Z",
}
@app.get("/health")
def health() -> Dict[str, Any]:
return {"ok": True}
@app.get("/robot/health")
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
@app.post("/analyze", response_model=AnalyzeResponse)
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,
)