File size: 6,573 Bytes
62a70e4
6520ac8
 
865ff98
6520ac8
 
865ff98
6520ac8
 
 
62a70e4
 
6520ac8
 
 
 
62a70e4
 
6520ac8
 
865ff98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6520ac8
 
865ff98
 
 
 
6520ac8
 
865ff98
6520ac8
 
 
 
 
865ff98
 
 
 
 
6520ac8
 
865ff98
6520ac8
 
 
865ff98
6520ac8
 
 
 
 
62a70e4
865ff98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62a70e4
865ff98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62a70e4
865ff98
 
 
 
 
 
 
62a70e4
865ff98
 
 
62a70e4
865ff98
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import io
import os
import traceback
import zipfile
from fastapi import FastAPI, HTTPException, status
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from opentrons.simulate import simulate, format_runlog

app = FastAPI()

class Protocol(BaseModel):
    name: str
    content: str

@app.get("/")
async def root():
    return {"message": "Opentrons simulation API is running."}

@app.get("/protocols")
async def list_saved_protocols():
    """
    storageディレクトリに保存されているプロトコルファイルの一覧を返します。
    """
    storage_dir = "storage"
    if not os.path.isdir(storage_dir):
        # ディレクトリが存在しない場合は空のリストを返す
        return {"protocols": []}
    
    try:
        # os.listdirはブロッキングI/Oなのでスレッドプールで実行
        file_list = await run_in_threadpool(os.listdir, storage_dir)
        return {"protocols": sorted(file_list)}
    except Exception as e:
        print(f"Error listing files: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="An error occurred while listing the protocols."
        )

@app.get("/protocols/download")
async def download_all_protocols():
    """
    storageディレクトリ内のすべてのプロトコルをzipファイルにまとめてダウンロードします。
    """
    storage_dir = "storage"
    
    if not os.path.isdir(storage_dir) or not os.listdir(storage_dir):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No protocols found to download."
        )

    # メモリ上でzipファイルを作成
    zip_io = io.BytesIO()
    
    with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as temp_zip:
        # run_in_threadpoolの中でファイルI/Oを実行
        def create_zip_in_thread():
            for root, _, files in os.walk(storage_dir):
                for file in files:
                    file_path = os.path.join(root, file)
                    # zipファイル内でのパスをルートからの相対パスに設定
                    temp_zip.write(file_path, os.path.relpath(file_path, storage_dir))
        
        await run_in_threadpool(create_zip_in_thread)

    # BytesIOのポインタを先頭に戻す
    zip_io.seek(0)

    return StreamingResponse(
        content=zip_io,
        media_type="application/zip",
        headers={"Content-Disposition": "attachment; filename=protocols.zip"}
    )

@app.post("/simulate")
async def simulate_protocol(protocol: Protocol):
    """
    プロトコルの内容を受け取り、シミュレーションのみを実行します。
    ファイルへの保存は行いません。
    """
    try:
        protocol_file = io.StringIO(protocol.content)
        
        run_log, _ = await run_in_threadpool(
            simulate,
            protocol_file=protocol_file,
            file_name=protocol.name
        )
        
        return {
            "protocol_name": protocol.name,
            "run_status": "success",
            "run_log": format_runlog(run_log)
        }
    except Exception as e:
        print(f"Simulation Error: {e}")
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "message": "Failed to simulate protocol.",
                "error_type": type(e).__name__,
                "error_details": str(e),
                "traceback": traceback.format_exc()
            }
        )

@app.post("/protocols")
async def save_and_simulate_protocol(protocol: Protocol):
    """
    プロトコルをファイルに保存し、続けてシミュレーションを実行します。
    保存とシミュレーション両方の結果を返します。
    """
    # --- 1. プロトコルファイルを保存 ---
    storage_dir = "storage"
    os.makedirs(storage_dir, exist_ok=True)
    file_path = os.path.join(storage_dir, protocol.name)
    
    try:
        # スレッドプールでファイルを保存し、実際に保存されたパスを受け取る
        saved_path = await run_in_threadpool(save_protocol_file, protocol.content, file_path)
        save_message = f"Protocol '{os.path.basename(saved_path)}' saved successfully."
    except Exception as e:
        print(f"File saving error: {e}")
        # ファイル保存に失敗した場合は、ここで処理を中断してエラーを返す
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="An error occurred while saving the file."
        )

    # --- 2. シミュレーションを実行 ---
    try:
        protocol_file = io.StringIO(protocol.content)
        
        # スレッドプールでシミュレーションを実行
        run_log, _ = await run_in_threadpool(
            simulate,
            protocol_file=protocol_file,
            file_name=protocol.name
        )
        
        # 保存とシミュレーション両方の成功を返す
        return {
            "save_status": "success",
            "save_message": save_message,
            "protocol_name": protocol.name,
            "run_status": "success",
            "run_log": format_runlog(run_log)
        }
    except Exception as e:
        # ファイル保存は成功したが、シミュレーションでエラーが発生した場合
        print(f"Simulation Error after save: {e}")
        # 保存が成功したことを伝えつつ、シミュレーションのエラー情報を返す
        return {
            "save_status": "success",
            "save_message": save_message,
            "protocol_name": protocol.name,
            "run_status": "failure",
            "error_details": str(e),
            "traceback": traceback.format_exc()
        }

def save_protocol_file(content: str, file_path: str) -> str:
    """
    指定されたパスにテキストコンテンツを保存します。
    ファイル名が重複する場合は、連番を付与して新しいパスに保存します。
    実際に保存したファイルのパスを返します。
    """
    base, ext = os.path.splitext(file_path)
    counter = 1
    new_path = file_path
    while os.path.exists(new_path):
        new_path = f"{base}_{counter}{ext}"
        counter += 1
        
    with open(new_path, 'w') as f:
        f.write(content)
    return new_path