feat(project): add REST API — CRUD endpoints with FastAPI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ab3dd6da1c
commit
50db453ec9
|
|
@ -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()
|
||||||
|
|
@ -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)
|
||||||
44
backend/tests/test_api_project.py
Normal file
44
backend/tests/test_api_project.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user