From 50db453ec979753c008489352b2477df4ed831e4 Mon Sep 17 00:00:00 2001 From: openclaw Date: Mon, 23 Mar 2026 16:06:58 +0000 Subject: [PATCH] =?UTF-8?q?feat(project):=20add=20REST=20API=20=E2=80=94?= =?UTF-8?q?=20CRUD=20endpoints=20with=20FastAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- backend/app/main.py | 47 +++++++++++++++ .../modules/project/interfaces/http/router.py | 60 +++++++++++++++++++ backend/tests/test_api_project.py | 44 ++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 backend/tests/test_api_project.py diff --git a/backend/app/main.py b/backend/app/main.py index e69de29..bb52e04 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -0,0 +1,47 @@ +import os +from pathlib import Path + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from app.shared.kernel.exceptions import NotFoundError, ValidationError +from app.shared.infrastructure.config import Settings +from app.modules.project.infrastructure.json_repository import JsonProjectRepository +from app.modules.project.application.services import ProjectService +from app.modules.project.interfaces.http.router import router as project_router, init_router as init_project_router + + +def create_app() -> FastAPI: + app = FastAPI(title="Arch Design Dashboard API", version="0.1.0") + + # Settings + registry_path = Path(os.environ.get("REGISTRY_PATH", str(Settings().registry_path))) + + # Wire Project module + project_repo = JsonProjectRepository(registry_path) + project_service = ProjectService(project_repo) + init_project_router(project_service) + + # Register routers + app.include_router(project_router, prefix="/api") + + # Health check + @app.get("/api/health") + def health(): + return {"status": "ok"} + + # Exception handlers + @app.exception_handler(NotFoundError) + async def not_found_handler(request: Request, exc: NotFoundError): + return JSONResponse(status_code=404, content={"detail": str(exc)}) + + @app.exception_handler(ValidationError) + async def validation_handler(request: Request, exc: ValidationError): + return JSONResponse(status_code=400, content={"detail": exc.message}) + + return app + + +# For uvicorn: use `uvicorn app.main:create_app --factory` +# or for simple usage: +app = create_app() diff --git a/backend/app/modules/project/interfaces/http/router.py b/backend/app/modules/project/interfaces/http/router.py index e69de29..75e7a8b 100644 --- a/backend/app/modules/project/interfaces/http/router.py +++ b/backend/app/modules/project/interfaces/http/router.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, Response +from pydantic import BaseModel + +from app.modules.project.application.services import ProjectService + +router = APIRouter(prefix="/projects", tags=["project"]) + +_service: ProjectService | None = None + + +def init_router(service: ProjectService) -> None: + global _service + _service = service + + +class CreateProjectRequest(BaseModel): + name: str + design_dir: str + code_dir: str | None = None + + +class ProjectResponse(BaseModel): + id: str + name: str + design_dir: str + code_dir: str | None + created_at: str + + +def _to_response(p) -> dict: + return { + "id": p.id, + "name": p.name, + "design_dir": p.design_dir, + "code_dir": p.code_dir, + "created_at": p.created_at.isoformat(), + } + + +@router.get("") +def list_projects(): + return [_to_response(p) for p in _service.list_projects()] + + +@router.post("", status_code=201) +def create_project(req: CreateProjectRequest): + p = _service.create_project(req.name, req.design_dir, req.code_dir) + return _to_response(p) + + +@router.get("/{project_id}") +def get_project(project_id: str): + p = _service.get_project(project_id) + return _to_response(p) + + +@router.delete("/{project_id}", status_code=204) +def delete_project(project_id: str): + _service.delete_project(project_id) + return Response(status_code=204) diff --git a/backend/tests/test_api_project.py b/backend/tests/test_api_project.py new file mode 100644 index 0000000..314e9d6 --- /dev/null +++ b/backend/tests/test_api_project.py @@ -0,0 +1,44 @@ +import pytest + + +def test_health(client): + r = client.get("/api/health") + assert r.status_code == 200 + assert r.json()["status"] == "ok" + + +def test_list_projects_empty(client): + r = client.get("/api/projects") + assert r.status_code == 200 + assert r.json() == [] + + +def test_create_and_get_project(client, design_dir): + r = client.post("/api/projects", json={"name": "test", "design_dir": str(design_dir)}) + assert r.status_code == 201 + pid = r.json()["id"] + + r = client.get(f"/api/projects/{pid}") + assert r.status_code == 200 + assert r.json()["name"] == "test" + + +def test_create_project_invalid_dir(client): + r = client.post("/api/projects", json={"name": "test", "design_dir": "/nonexistent"}) + assert r.status_code == 400 + + +def test_delete_project(client, design_dir): + r = client.post("/api/projects", json={"name": "test", "design_dir": str(design_dir)}) + pid = r.json()["id"] + + r = client.delete(f"/api/projects/{pid}") + assert r.status_code == 204 + + r = client.get(f"/api/projects/{pid}") + assert r.status_code == 404 + + +def test_get_nonexistent_project(client): + r = client.get("/api/projects/nonexistent") + assert r.status_code == 404