Yan Wang commited on
Commit
8ee051c
·
1 Parent(s): c7c73c3

Switch to Docker runtime with FastAPI backend

Browse files
Files changed (3) hide show
  1. Dockerfile +28 -0
  2. requirements.txt +2 -0
  3. server.py +122 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # 安装 Node(用于构建前端)
4
+ RUN apt-get update && apt-get install -y curl gnupg && \
5
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
6
+ apt-get install -y nodejs && \
7
+ rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+
11
+ # 前端依赖优先装
12
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* .npmrc* ./
13
+ RUN if [ -f package-lock.json ]; then npm ci; \
14
+ elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
15
+ elif [ -f pnpm-lock.yaml ]; then npm i -g pnpm && pnpm i --frozen-lockfile; \
16
+ else npm i; fi
17
+
18
+ # 复制全部代码(包含 src/, index.html, server.py 等)
19
+ COPY . .
20
+
21
+ # 构建前端(Vite -> dist/)
22
+ RUN npm run build
23
+
24
+ # Python 依赖
25
+ RUN pip install --no-cache-dir -r requirements.txt
26
+
27
+ EXPOSE 7860
28
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
server.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.responses import JSONResponse, HTMLResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.staticfiles import StaticFiles
5
+ from pydantic import BaseModel
6
+ from datetime import datetime
7
+ import os, json, uuid, pathlib
8
+
9
+ app = FastAPI()
10
+
11
+ # 允许前端访问(你也可以改成你的域名)
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"],
15
+ allow_headers=["*"],
16
+ allow_methods=["*"],
17
+ )
18
+
19
+ DATA_DIR = pathlib.Path("./data")
20
+ DATA_DIR.mkdir(exist_ok=True)
21
+ RUN_DIR = DATA_DIR / "runs"
22
+ RUN_DIR.mkdir(parents=True, exist_ok=True)
23
+
24
+ DIST_DIR = pathlib.Path("./dist") # Vite build 输出目录
25
+
26
+ # ========== 数据集:上传与获取 ==========
27
+
28
+ @app.post("/api/upload")
29
+ async def upload_dataset(file: UploadFile = File(...)):
30
+ if not file.filename.endswith(".json"):
31
+ raise HTTPException(status_code=400, detail="Only .json allowed")
32
+ try:
33
+ raw = await file.read()
34
+ payload = json.loads(raw.decode("utf-8"))
35
+
36
+ keys = list(payload.keys())
37
+ if not keys:
38
+ raise ValueError("Empty dataset")
39
+ first = payload[keys[0]]
40
+ if not isinstance(first, list) or not first or "date" not in first[0] or "close" not in first[0]:
41
+ raise ValueError("Invalid series format (need { TICKER: [{date, close}, ...] })")
42
+
43
+ # 生成数据集 ID,并保存为只读源
44
+ dsid = uuid.uuid4().hex[:12]
45
+ out_path = DATA_DIR / f"{dsid}.json"
46
+ out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
47
+ return {"id": dsid}
48
+ except Exception as e:
49
+ raise HTTPException(status_code=400, detail=str(e))
50
+
51
+ @app.get("/api/dataset/{dsid}")
52
+ def get_dataset(dsid: str):
53
+ path = DATA_DIR / f"{dsid}.json"
54
+ if not path.exists():
55
+ raise HTTPException(status_code=404, detail="Not found")
56
+ return JSONResponse(json.loads(path.read_text(encoding="utf-8")))
57
+
58
+ # ========== 运行结果(每人一份,不覆盖数据集) ==========
59
+
60
+ class RunPayload(BaseModel):
61
+ dataset_id: str
62
+ email: str | None = None
63
+ note: str | None = None
64
+ selections: list[dict]
65
+ portfolio: list[dict]
66
+ stats: dict
67
+ meta: dict | None = None
68
+
69
+ @app.post("/api/submit_run")
70
+ def submit_run(payload: RunPayload):
71
+ ds_path = DATA_DIR / f"{payload.dataset_id}.json"
72
+ if not ds_path.exists():
73
+ raise HTTPException(status_code=400, detail="dataset_id not found")
74
+
75
+ run_id = uuid.uuid4().hex[:12]
76
+ run_path = RUN_DIR / payload.dataset_id
77
+ run_path.mkdir(parents=True, exist_ok=True)
78
+
79
+ record = {
80
+ "run_id": run_id,
81
+ "dataset_id": payload.dataset_id,
82
+ "created_at": datetime.utcnow().isoformat() + "Z",
83
+ "payload": payload.model_dump(),
84
+ }
85
+ out = run_path / f"{run_id}.json"
86
+ out.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
87
+ return {"run_id": run_id}
88
+
89
+ @app.get("/api/list_runs/{dataset_id}")
90
+ def list_runs(dataset_id: str):
91
+ path = RUN_DIR / dataset_id
92
+ if not path.exists():
93
+ return {"dataset_id": dataset_id, "runs": []}
94
+ items = []
95
+ for p in sorted(path.glob("*.json")):
96
+ try:
97
+ obj = json.loads(p.read_text(encoding="utf-8"))
98
+ items.append({
99
+ "run_id": obj.get("run_id"),
100
+ "created_at": obj.get("created_at"),
101
+ "email": obj.get("payload", {}).get("email"),
102
+ "N": obj.get("payload", {}).get("stats", {}).get("N"),
103
+ "cumRet": obj.get("payload", {}).get("stats", {}).get("cumRet"),
104
+ })
105
+ except Exception:
106
+ pass
107
+ return {"dataset_id": dataset_id, "runs": items}
108
+
109
+ @app.get("/api/run/{dataset_id}/{run_id}")
110
+ def get_run(dataset_id: str, run_id: str):
111
+ p = RUN_DIR / dataset_id / f"{run_id}.json"
112
+ if not p.exists():
113
+ raise HTTPException(status_code=404, detail="Not found")
114
+ return JSONResponse(json.loads(p.read_text(encoding="utf-8")))
115
+
116
+ # ========== 静态资源(前端) ==========
117
+ if DIST_DIR.exists():
118
+ app.mount("/", StaticFiles(directory=str(DIST_DIR), html=True), name="static")
119
+ else:
120
+ @app.get("/")
121
+ def hello():
122
+ return HTMLResponse("<h3>Build not found. Please run: npm run build</h3>")