feat: add web layer (FastAPI + worker)

This commit is contained in:
hangshuo652
2026-05-24 12:52:20 +08:00
parent 818e81269c
commit 331b38eac1
8 changed files with 218 additions and 0 deletions
+4
View File
@@ -1,3 +1,7 @@
httpx==0.27.0
pyyaml==6.0.1
pytest==8.0.0
fastapi==0.111.0
uvicorn==0.30.0
python-multipart==0.0.9
jinja2==3.1.4
View File
+80
View File
@@ -0,0 +1,80 @@
"""Web API layer — wraps orchestrator with 202+ polling pattern."""
import uuid, json, shutil, sys, os
from pathlib import Path
from datetime import datetime
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import Config
from orchestrator import run_pipeline
app = FastAPI(title="COBOL→Java Verify")
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
TASKS_DIR = Path("tasks")
TASKS_DIR.mkdir(exist_ok=True)
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
MAX_SIZE = 10 * 1024 * 1024 # 10MB
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("upload.html", {"request": request})
@app.post("/verify")
async def verify(
copybook: UploadFile = File(...),
cobol_src: UploadFile = File(...),
java_src: UploadFile = File(...),
mapping: UploadFile = File(...),
runner: str = Form("native"),
):
task_id = str(uuid.uuid4())[:8]
task_dir = UPLOAD_DIR / task_id
task_dir.mkdir(parents=True, exist_ok=True)
for f, name in [(copybook, "copybook.cpy"), (cobol_src, "program.cbl"),
(java_src, "java"), (mapping, "mapping.yaml")]:
content = await f.read()
if len(content) > MAX_SIZE:
raise HTTPException(413, f"{f.filename} exceeds 10MB limit")
dest = task_dir / name
dest.write_bytes(content)
task_file = TASKS_DIR / f"{task_id}.json"
task_file.write_text(json.dumps({
"id": task_id, "status": "queued",
"copybook": str(task_dir / "copybook.cpy"),
"cobol_src": str(task_dir / "program.cbl"),
"java_src": str(task_dir / "java"),
"mapping": str(task_dir / "mapping.yaml"),
"runner": runner, "created": datetime.now().isoformat()
}))
return JSONResponse({"task_id": task_id, "status": "queued"}, status_code=202)
@app.get("/status/{task_id}")
async def status(task_id: str):
tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists():
raise HTTPException(404, "task not found")
data = json.loads(tf.read_text())
return JSONResponse({"task_id": task_id, "status": data.get("status", "unknown"),
"result": data.get("result")})
@app.get("/result/{task_id}", response_class=HTMLResponse)
async def result(request: Request, task_id: str):
tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists():
raise HTTPException(404, "task not found")
data = json.loads(tf.read_text())
return templates.TemplateResponse("result.html", {"request": request, "task": data})
+25
View File
@@ -0,0 +1,25 @@
document.getElementById("verify-form").addEventListener("submit", async (e) => {
e.preventDefault();
const btn = e.target.querySelector("button");
btn.disabled = true;
btn.textContent = "Submitting...";
document.getElementById("status").textContent = "";
document.getElementById("result").innerHTML = "";
const fd = new FormData(e.target);
try {
const r = await fetch("/verify", { method: "POST", body: fd });
const d = await r.json();
if (r.ok) {
document.getElementById("status").textContent = "Task " + d.task_id + " queued";
document.getElementById("result").innerHTML =
'<p><a href="/result/' + d.task_id + '">View result →</a></p>';
} else {
document.getElementById("status").textContent = "Error: " + (d.detail || "unknown");
}
} catch (err) {
document.getElementById("status").textContent = "Upload failed: " + err.message;
}
btn.disabled = false;
btn.textContent = "Verify";
});
+11
View File
@@ -0,0 +1,11 @@
body { font-family: monospace; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; }
h1 { font-size: 1.2rem; }
label { display: block; margin: .75rem 0; }
input, select { display: block; margin-top: .25rem; }
button { margin-top: 1rem; padding: .5rem 1.5rem; cursor: pointer; font-size: 1rem; }
#status { margin-top: 1rem; font-weight: bold; }
#result { margin-top: 1rem; }
pre { background: #f0f0f0; padding: 1rem; border-radius: 4px; }
.pass { border-left: 4px solid #4caf50; }
.fail { border-left: 4px solid #f44336; }
.container a { display: inline-block; margin-top: 1rem; }
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Result - {{ task.id }}</title>
<link rel="stylesheet" href="/static/style.css"></head><body>
<div class="container">
<h1>Verification Result</h1>
{% if task.status == "done" and task.result %}
<pre class="pass">Status: {{ task.result.status }}
Program: {{ task.result.program }}
Matched: {{ task.result.matched }} | Mismatched: {{ task.result.mismatched }}
Runner: {{ task.result.runner }} | Duration: {{ task.result.duration }}s</pre>
{% elif task.status == "error" %}
<pre class="fail">{{ task.result }}</pre>
{% else %}
<div id="poll-status">Status: {{ task.status }} — polling...</div>
<script>
const id = "{{ task.id }}";
setInterval(async () => {
const r = await fetch("/status/" + id);
const d = await r.json();
document.getElementById("poll-status").textContent = "Status: " + d.status;
if (d.status === "done" || d.status === "error") location.reload();
}, 3000);
</script>
{% endif %}
<a href="/">← New verification</a>
</div>
</body></html>
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>COBOL→Java Verify</title>
<link rel="stylesheet" href="/static/style.css"></head><body>
<div class="container">
<h1>COBOL→Java Migration Verification</h1>
<form id="verify-form" enctype="multipart/form-data">
<label>COPYBOOK:<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
<label>COBOL source:<input type="file" name="cobol_src" accept=".cbl" required></label>
<label>Java source (dir with pom.xml):<input type="file" name="java_src" webkitdirectory required></label>
<label>Mapping YAML:<input type="file" name="mapping" accept=".yaml,.yml" required></label>
<label>Runner:<select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
<button type="submit">Verify</button>
</form>
<div id="status"></div>
<div id="result"></div>
</div>
<script src="/static/script.js"></script>
</body></html>
+53
View File
@@ -0,0 +1,53 @@
"""Worker process — polls task queue, executes run_pipeline()."""
import json, sys, time, shutil
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
TASKS_DIR = Path("tasks")
def main():
print("Worker started. Watching tasks/ ...")
while True:
for tf in sorted(TASKS_DIR.glob("*.json")):
try:
data = json.loads(tf.read_text())
if data.get("status") != "queued":
continue
data["status"] = "running"
tf.write_text(json.dumps(data))
from config import Config
from orchestrator import run_pipeline
cfg = Config()
cfg.runner_mode = data.get("runner", "native")
if cfg.runner_mode == "spark" and not shutil.which("spark-submit"):
data["status"] = "blocked"
data["result"] = "spark-submit not installed"
tf.write_text(json.dumps(data))
continue
vr = run_pipeline(cfg, data["copybook"], data["cobol_src"],
data["java_src"], data["mapping"])
data["status"] = "done"
data["result"] = {
"program": vr.program, "status": vr.status,
"matched": vr.fields_matched, "mismatched": vr.fields_mismatched,
"duration": vr.duration_s, "runner": vr.runner,
}
tf.write_text(json.dumps(data))
except Exception as e:
data = json.loads(tf.read_text()) if tf.exists() else {}
data["status"] = "error"
data["result"] = str(e)[:500]
tf.write_text(json.dumps(data))
time.sleep(2)
if __name__ == "__main__":
main()