feat: add FastAPI backend with PostgreSQL and start script fixes
- Add server/ directory with FastAPI backend - Fix server/start.sh to properly handle venv on Windows/Git Bash - Add alembic migrations and pyproject.toml - Add server tests Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1
server/src/app/api/__init__.py
Normal file
1
server/src/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["router"]
|
||||
13
server/src/app/api/deps.py
Normal file
13
server/src/app/api/deps.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_session_factory
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = get_session_factory()()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
6
server/src/app/api/router.py
Normal file
6
server/src/app/api/router.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.router import router as v1_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(v1_router)
|
||||
1
server/src/app/api/v1/__init__.py
Normal file
1
server/src/app/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["router"]
|
||||
1
server/src/app/api/v1/endpoints/__init__.py
Normal file
1
server/src/app/api/v1/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["health", "employees", "reimbursements"]
|
||||
19
server/src/app/api/v1/endpoints/bootstrap.py
Normal file
19
server/src/app/api/v1/endpoints/bootstrap.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.core.bootstrap import build_bootstrap_state, persist_bootstrap_config
|
||||
from app.core.config import get_settings
|
||||
from app.schemas.bootstrap import BootstrapSetupPayload, BootstrapStateRead
|
||||
|
||||
router = APIRouter(prefix="/bootstrap")
|
||||
|
||||
|
||||
@router.get("", response_model=BootstrapStateRead)
|
||||
def get_bootstrap_state() -> BootstrapStateRead:
|
||||
return build_bootstrap_state(get_settings())
|
||||
|
||||
|
||||
@router.post("", response_model=BootstrapStateRead, status_code=status.HTTP_201_CREATED)
|
||||
def initialize_bootstrap(payload: BootstrapSetupPayload) -> BootstrapStateRead:
|
||||
return persist_bootstrap_config(payload, get_settings())
|
||||
31
server/src/app/api/v1/endpoints/employees.py
Normal file
31
server/src/app/api/v1/endpoints/employees.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.schemas.employee import EmployeeCreate, EmployeeRead
|
||||
from app.services.employee import EmployeeService
|
||||
|
||||
router = APIRouter()
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
@router.get("", response_model=list[EmployeeRead])
|
||||
def list_employees(db: DbSession) -> list[EmployeeRead]:
|
||||
return EmployeeService(db).list_employees()
|
||||
|
||||
|
||||
@router.post("", response_model=EmployeeRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_employee(payload: EmployeeCreate, db: DbSession) -> EmployeeRead:
|
||||
return EmployeeService(db).create_employee(payload)
|
||||
|
||||
|
||||
@router.get("/{employee_id}", response_model=EmployeeRead)
|
||||
def get_employee(employee_id: str, db: DbSession) -> EmployeeRead:
|
||||
employee = EmployeeService(db).get_employee(employee_id)
|
||||
if employee is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Employee not found")
|
||||
return employee
|
||||
34
server/src/app/api/v1/endpoints/health.py
Normal file
34
server/src/app/api/v1/endpoints/health.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.db.session import get_engine
|
||||
|
||||
router = APIRouter(prefix="/health")
|
||||
|
||||
|
||||
@router.get("")
|
||||
def health_check() -> dict[str, object]:
|
||||
settings = get_settings()
|
||||
database_ok = False
|
||||
database_error = None
|
||||
|
||||
if settings.setup_completed:
|
||||
try:
|
||||
with get_engine().connect() as connection:
|
||||
connection.execute(text("SELECT 1"))
|
||||
database_ok = True
|
||||
except Exception as exc: # pragma: no cover - runtime connectivity branch
|
||||
database_error = str(exc)
|
||||
|
||||
return {
|
||||
"status": "ok" if database_ok else "degraded",
|
||||
"database": {
|
||||
"configured": settings.setup_completed,
|
||||
"ok": database_ok,
|
||||
"error": database_error,
|
||||
},
|
||||
"redis": {"configured": bool(settings.redis_url), "enabled": bool(settings.redis_url)},
|
||||
}
|
||||
31
server/src/app/api/v1/endpoints/reimbursements.py
Normal file
31
server/src/app/api/v1/endpoints/reimbursements.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.schemas.reimbursement import ReimbursementCreate, ReimbursementRead
|
||||
from app.services.reimbursement import ReimbursementService
|
||||
|
||||
router = APIRouter()
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
@router.get("", response_model=list[ReimbursementRead])
|
||||
def list_reimbursements(db: DbSession) -> list[ReimbursementRead]:
|
||||
return ReimbursementService(db).list_reimbursements()
|
||||
|
||||
|
||||
@router.post("", response_model=ReimbursementRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_reimbursement(payload: ReimbursementCreate, db: DbSession) -> ReimbursementRead:
|
||||
return ReimbursementService(db).create_reimbursement(payload)
|
||||
|
||||
|
||||
@router.get("/{request_id}", response_model=ReimbursementRead)
|
||||
def get_reimbursement(request_id: str, db: DbSession) -> ReimbursementRead:
|
||||
request = ReimbursementService(db).get_reimbursement(request_id)
|
||||
if request is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Request not found")
|
||||
return request
|
||||
12
server/src/app/api/v1/router.py
Normal file
12
server/src/app/api/v1/router.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
|
||||
from app.api.v1.endpoints.employees import router as employees_router
|
||||
from app.api.v1.endpoints.health import router as health_router
|
||||
from app.api.v1.endpoints.reimbursements import router as reimbursements_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(health_router, tags=["health"])
|
||||
router.include_router(bootstrap_router, tags=["bootstrap"])
|
||||
router.include_router(employees_router, prefix="/employees", tags=["employees"])
|
||||
router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"])
|
||||
Reference in New Issue
Block a user