Yan Wang commited on
Commit
0ea25d9
·
1 Parent(s): 983f32e

revised dockerfile, package, server,app_new, and add api

Browse files
Files changed (4) hide show
  1. Dockerfile +9 -23
  2. package.json +11 -13
  3. server.py +91 -106
  4. src/api.ts +46 -0
Dockerfile CHANGED
@@ -1,28 +1,14 @@
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"]
 
1
+ FROM node:18 AS build
 
 
 
 
 
 
 
2
  WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm install
 
 
 
 
 
 
 
5
  COPY . .
 
 
6
  RUN npm run build
7
 
8
+ FROM python:3.10
9
+ WORKDIR /app
10
+ COPY --from=build /app/dist ./dist
11
+ COPY server.py ./
12
+ RUN pip install fastapi uvicorn huggingface_hub
13
+ ENV PORT=7860
14
  CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
package.json CHANGED
@@ -1,23 +1,21 @@
1
  {
2
- "name": "asset-choice-space",
 
3
  "private": true,
4
- "version": "0.0.2",
5
- "type": "module",
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "vite build",
9
- "preview": "vite preview --port 7860 --strictPort"
10
  },
11
  "dependencies": {
12
- "react": "^18.3.1",
13
- "react-dom": "^18.3.1",
14
- "recharts": "^2.12.7"
15
  },
16
  "devDependencies": {
17
- "typescript": "^5.4.0",
18
- "vite": "^5.2.0",
19
- "@vitejs/plugin-react": "^4.2.0",
20
- "@types/react": "^18.2.0",
21
- "@types/react-dom": "^18.2.0"
22
  }
23
- }
 
1
  {
2
+ "name": "hf-asset-annotator",
3
+ "version": "1.0.0",
4
  "private": true,
 
 
5
  "scripts": {
6
  "dev": "vite",
7
  "build": "vite build",
8
+ "preview": "vite preview"
9
  },
10
  "dependencies": {
11
+ "react": "^18.2.0",
12
+ "react-dom": "^18.2.0",
13
+ "recharts": "^2.7.2"
14
  },
15
  "devDependencies": {
16
+ "typescript": "^5.3.3",
17
+ "vite": "^5.0.0",
18
+ "@types/react": "^18.2.17",
19
+ "@types/react-dom": "^18.2.7"
 
20
  }
21
+ }
server.py CHANGED
@@ -1,122 +1,107 @@
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>")
 
1
+ import os
2
+ import io
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+ from fastapi import FastAPI, Query, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
 
7
  from pydantic import BaseModel
8
+ from datetime import datetime, timezone
9
+ from huggingface_hub import HfApi, upload_file, hf_hub_download, create_repo
10
 
11
+ # === Configuration ===
12
+ REPO_ID = os.environ.get("STORE_REPO", "your-org/asset-annotator-store")
13
+ HF_TOKEN = os.environ.get("HF_TOKEN")
14
+
15
+ api = HfApi(token=HF_TOKEN)
16
  app = FastAPI()
17
 
18
+ # Allow local frontend access
19
  app.add_middleware(
20
  CORSMiddleware,
21
  allow_origins=["*"],
22
+ allow_credentials=True,
23
  allow_methods=["*"],
24
+ allow_headers=["*"],
25
  )
26
 
 
 
 
 
27
 
28
+ def ensure_repo():
29
+ try:
30
+ create_repo(REPO_ID, repo_type="dataset", exist_ok=True, token=HF_TOKEN)
31
+ except Exception:
32
+ pass
33
 
 
34
 
35
+ def upload_json(path_in_repo: str, obj: Any, message: str):
36
+ ensure_repo()
37
+ buf = io.BytesIO(json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8"))
38
+ upload_file(
39
+ path_or_fileobj=buf,
40
+ path_in_repo=path_in_repo,
41
+ repo_id=REPO_ID,
42
+ repo_type="dataset",
43
+ token=HF_TOKEN,
44
+ commit_message=message,
45
+ )
46
+
47
+
48
+ def download_json(path_in_repo: str) -> Optional[Any]:
49
  try:
50
+ local = hf_hub_download(repo_id=REPO_ID, repo_type="dataset", filename=path_in_repo, token=HF_TOKEN)
51
+ with open(local, "r", encoding="utf-8") as f:
52
+ return json.load(f)
53
+ except Exception:
54
+ return None
55
+
56
+
57
+ @app.get("/api/health")
58
+ def health():
59
+ return {"ok": True, "repo": REPO_ID}
60
+
61
+
62
+ class Dataset(BaseModel):
63
+ dataset_id: str
64
+ name: str
65
+ data: Dict[str, List[Dict[str, Any]]]
66
+ assets: List[str]
67
+ dates: List[str]
68
+
69
+
70
+ class Annotation(BaseModel):
 
 
 
 
 
 
 
71
  dataset_id: str
72
+ user_id: str
73
+ selections: List[Dict[str, Any]]
74
+ step: int
75
+ window_len: int
76
+
77
+
78
+ @app.post("/api/dataset/upsert")
79
+ def upsert_dataset(ds: Dataset):
80
+ payload = ds.dict()
81
+ payload["saved_at"] = datetime.now(timezone.utc).isoformat()
82
+ upload_json("datasets/latest.json", payload, f"upsert dataset {ds.dataset_id}")
83
+ return {"ok": True}
84
+
85
+
86
+ @app.get("/api/dataset/latest")
87
+ def get_latest():
88
+ obj = download_json("datasets/latest.json")
89
+ if obj is None:
90
+ raise HTTPException(status_code=404, detail="No dataset yet.")
91
+ return obj
92
+
93
+
94
+ @app.get("/api/annotation/get")
95
+ def get_annotation(dataset_id: str = Query(...), user_id: str = Query(...)):
96
+ obj = download_json(f"annotations/{dataset_id}/{user_id}.json")
97
+ if obj is None:
98
+ return {"user_id": user_id, "dataset_id": dataset_id, "selections": [], "step": 1, "window_len": 7}
99
+ return obj
100
+
101
+
102
+ @app.post("/api/annotation/upsert")
103
+ def upsert_annotation(ann: Annotation):
104
+ payload = ann.dict()
105
+ payload["updated_at"] = datetime.now(timezone.utc).isoformat()
106
+ upload_json(f"annotations/{ann.dataset_id}/{ann.user_id}.json", payload, f"upsert annotation {ann.dataset_id}/{ann.user_id}")
107
+ return {"ok": True}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/api.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type DataDict = Record<string, { date: string; close: number[] }[]>;
2
+
3
+ export async function apiGetLatestDataset() {
4
+ const r = await fetch("/api/dataset/latest");
5
+ if (!r.ok) throw new Error(await r.text());
6
+ return r.json();
7
+ }
8
+
9
+ export async function apiUpsertDataset(body: {
10
+ dataset_id: string;
11
+ name: string;
12
+ data: DataDict;
13
+ assets: string[];
14
+ dates: string[];
15
+ }) {
16
+ const r = await fetch("/api/dataset/upsert", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify(body),
20
+ });
21
+ if (!r.ok) throw new Error(await r.text());
22
+ return r.json();
23
+ }
24
+
25
+ export async function apiGetAnnotation(dataset_id: string, user_id: string) {
26
+ const url = `/api/annotation/get?dataset_id=${dataset_id}&user_id=${user_id}`;
27
+ const r = await fetch(url);
28
+ if (!r.ok) throw new Error(await r.text());
29
+ return r.json();
30
+ }
31
+
32
+ export async function apiUpsertAnnotation(body: {
33
+ dataset_id: string;
34
+ user_id: string;
35
+ selections: any[];
36
+ step: number;
37
+ window_len: number;
38
+ }) {
39
+ const r = await fetch("/api/annotation/upsert", {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(body),
43
+ });
44
+ if (!r.ok) throw new Error(await r.text());
45
+ return r.json();
46
+ }