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