141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
from __future__ import annotations
|
|
import argparse
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
import json
|
|
from pathlib import Path
|
|
from .config import load_config
|
|
from .review_bridge import ReviewWorkspaceBridge
|
|
from .workspace_manager import WorkspaceManager
|
|
|
|
def json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
|
|
body = json.dumps(payload, indent=2).encode("utf-8")
|
|
handler.send_response(status)
|
|
handler.send_header("Content-Type", "application/json")
|
|
handler.send_header("Content-Length", str(len(body)))
|
|
handler.send_header("Access-Control-Allow-Origin", "*")
|
|
handler.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
|
handler.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
handler.end_headers()
|
|
handler.wfile.write(body)
|
|
|
|
class ReviewBridgeHandler(BaseHTTPRequestHandler):
|
|
reviewer: str = "Unknown Reviewer"
|
|
workspace_manager: WorkspaceManager = None # type: ignore
|
|
active_bridge: ReviewWorkspaceBridge | None = None
|
|
active_workspace_id: str | None = None
|
|
|
|
@classmethod
|
|
def set_active_workspace(cls, workspace_id: str) -> bool:
|
|
meta = cls.workspace_manager.touch_recent(workspace_id)
|
|
if meta is None:
|
|
return False
|
|
cls.active_workspace_id = workspace_id
|
|
cls.active_bridge = ReviewWorkspaceBridge(meta.path, reviewer=cls.reviewer)
|
|
return True
|
|
|
|
def do_OPTIONS(self):
|
|
json_response(self, 200, {"ok": True})
|
|
|
|
def do_GET(self):
|
|
if self.path == "/api/workspaces":
|
|
reg = self.workspace_manager.list_workspaces()
|
|
json_response(self, 200, reg.model_dump())
|
|
return
|
|
if self.path == "/api/load":
|
|
if self.active_bridge is None:
|
|
json_response(self, 400, {"error": "no active workspace"})
|
|
return
|
|
session = self.active_bridge.load_session()
|
|
json_response(self, 200, {"workspace_id": self.active_workspace_id, "session": session.model_dump()})
|
|
return
|
|
json_response(self, 404, {"error": "not found"})
|
|
|
|
def do_POST(self):
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
raw = self.rfile.read(length) if length else b"{}"
|
|
payload = json.loads(raw.decode("utf-8") or "{}")
|
|
|
|
if self.path == "/api/workspaces/create":
|
|
meta = self.workspace_manager.create_workspace(
|
|
workspace_id=payload["workspace_id"],
|
|
title=payload["title"],
|
|
notes=payload.get("notes", "")
|
|
)
|
|
self.set_active_workspace(meta.workspace_id)
|
|
json_response(self, 200, {"ok": True, "workspace": meta.model_dump()})
|
|
return
|
|
|
|
if self.path == "/api/workspaces/open":
|
|
ok = self.set_active_workspace(payload["workspace_id"])
|
|
if not ok:
|
|
json_response(self, 404, {"error": "workspace not found"})
|
|
return
|
|
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id})
|
|
return
|
|
|
|
if self.path == "/api/workspaces/import-preview":
|
|
preview = self.workspace_manager.preview_import(
|
|
source_dir=payload["source_dir"],
|
|
workspace_id=payload["workspace_id"]
|
|
)
|
|
json_response(self, 200, preview.model_dump())
|
|
return
|
|
|
|
if self.path == "/api/workspaces/import":
|
|
try:
|
|
meta = self.workspace_manager.import_draft_pack(
|
|
source_dir=payload["source_dir"],
|
|
workspace_id=payload["workspace_id"],
|
|
title=payload.get("title"),
|
|
notes=payload.get("notes", ""),
|
|
allow_overwrite=bool(payload.get("allow_overwrite", False)),
|
|
)
|
|
except FileNotFoundError as exc:
|
|
json_response(self, 404, {"ok": False, "error": str(exc)})
|
|
return
|
|
except FileExistsError as exc:
|
|
json_response(self, 409, {"ok": False, "error": str(exc)})
|
|
return
|
|
except ValueError as exc:
|
|
json_response(self, 400, {"ok": False, "error": str(exc)})
|
|
return
|
|
self.set_active_workspace(meta.workspace_id)
|
|
json_response(self, 200, {"ok": True, "workspace": meta.model_dump()})
|
|
return
|
|
|
|
if self.active_bridge is None:
|
|
json_response(self, 400, {"error": "no active workspace"})
|
|
return
|
|
|
|
if self.path == "/api/save":
|
|
session = self.active_bridge.apply_actions(payload.get("actions", []))
|
|
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id, "session": session.model_dump()})
|
|
return
|
|
|
|
if self.path == "/api/export":
|
|
session = self.active_bridge.export_promoted()
|
|
json_response(self, 200, {"ok": True, "promoted_pack_dir": str(self.active_bridge.promoted_pack_dir), "workspace_id": self.active_workspace_id, "session": session.model_dump()})
|
|
return
|
|
|
|
json_response(self, 404, {"error": "not found"})
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Didactopus local review bridge server with semantic QA")
|
|
parser.add_argument("--config", default="configs/config.example.yaml")
|
|
return parser
|
|
|
|
def main() -> None:
|
|
args = build_parser().parse_args()
|
|
config = load_config(Path(args.config))
|
|
ReviewBridgeHandler.reviewer = config.review.default_reviewer
|
|
ReviewBridgeHandler.workspace_manager = WorkspaceManager(
|
|
registry_path=config.bridge.registry_path,
|
|
default_workspace_root=config.bridge.default_workspace_root
|
|
)
|
|
server = HTTPServer((config.bridge.host, config.bridge.port), ReviewBridgeHandler)
|
|
print(f"Didactopus review bridge listening on http://{config.bridge.host}:{config.bridge.port}")
|
|
server.serve_forever()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|