feat: add web layer (FastAPI + worker)
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
pytest==8.0.0
|
pytest==8.0.0
|
||||||
|
fastapi==0.111.0
|
||||||
|
uvicorn==0.30.0
|
||||||
|
python-multipart==0.0.9
|
||||||
|
jinja2==3.1.4
|
||||||
|
|||||||
+80
@@ -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})
|
||||||
@@ -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";
|
||||||
|
});
|
||||||
@@ -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; }
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user