Scaffold backend (FastAPI/uv) and frontend (SvelteKit/bun)
Backend: FastAPI app with health check, AppError hierarchy, stderr logging via LOG_LEVEL, ruff+pyright(strict) config, pytest passing. Frontend: SvelteKit (TS) with prettier, eslint, vitest (unit + component), adapter-node for self-hosted deployment on Zeus. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
06df9df339
commit
e5b4f54cd8
33 changed files with 517 additions and 4 deletions
1
backend/.python-version
Normal file
1
backend/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.12
|
||||
17
backend/README.md
Normal file
17
backend/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# sagaphone-backend
|
||||
|
||||
FastAPI backend for Sagaphone: generates the family tree, calls the Haiku API
|
||||
for narration, streams Piper TTS audio to the frontend.
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run uvicorn app.main:app --reload
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
See `secrets.toml.example` for required config — copy to `secrets.toml`
|
||||
(git-ignored) and fill in real values.
|
||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
41
backend/app/errors.py
Normal file
41
backend/app/errors.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""Base exception hierarchy for the Sagaphone backend.
|
||||
|
||||
Errors propagate normally; only the API edge (see main.py's exception
|
||||
handler) catches them and translates them into an HTTP response.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class AppError(Exception):
|
||||
"""Base class for all application errors."""
|
||||
|
||||
status_code: int = 500
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class NotFoundError(AppError):
|
||||
"""Requested resource does not exist."""
|
||||
|
||||
status_code = 404
|
||||
|
||||
|
||||
class ConfigError(AppError):
|
||||
"""Missing or invalid configuration (e.g. secrets.toml)."""
|
||||
|
||||
status_code = 500
|
||||
|
||||
|
||||
class NarrationError(AppError):
|
||||
"""The narration (Haiku) call failed or returned something unusable."""
|
||||
|
||||
status_code = 502
|
||||
|
||||
|
||||
class TTSError(AppError):
|
||||
"""The TTS (Piper) synthesis step failed."""
|
||||
|
||||
status_code = 502
|
||||
25
backend/app/logging_config.py
Normal file
25
backend/app/logging_config.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""Logging setup for the backend.
|
||||
|
||||
Follows the project's output contract: all log output goes to stderr,
|
||||
level controlled via the LOG_LEVEL env var (default INFO). This is server
|
||||
logging, not the request/response payload — that stays in FastAPI's
|
||||
responses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
level_name = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||
level = getattr(logging, level_name, logging.INFO)
|
||||
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-5s %(name)s: %(message)s"))
|
||||
|
||||
root = logging.getLogger()
|
||||
root.setLevel(level)
|
||||
root.handlers = [handler]
|
||||
33
backend/app/main.py
Normal file
33
backend/app/main.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Sagaphone FastAPI entrypoint.
|
||||
|
||||
Routes are added as features land (see ../SPEC.md and ../spec/*.md).
|
||||
This currently only wires up app creation, logging, and a health check
|
||||
so the scaffold is runnable end to end.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.errors import AppError
|
||||
from app.logging_config import configure_logging
|
||||
|
||||
configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="Sagaphone", version="0.1.0")
|
||||
|
||||
|
||||
@app.exception_handler(AppError)
|
||||
async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse:
|
||||
"""Edge-catch: translate AppError subclasses into clean HTTP responses."""
|
||||
logger.error("request failed: %s", exc.message)
|
||||
return JSONResponse(status_code=exc.status_code, content={"error": exc.message})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
50
backend/pyproject.toml
Normal file
50
backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
[project]
|
||||
name = "sagaphone-backend"
|
||||
version = "0.1.0"
|
||||
description = "Sagaphone backend — family-tree simulation, narration, and TTS streaming API."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"uvicorn[standard]>=0.32",
|
||||
"anthropic>=0.40",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.8",
|
||||
"pyright>=1.1.390",
|
||||
"pytest>=8.3",
|
||||
"pytest-asyncio>=0.24",
|
||||
"httpx>=0.27",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||
|
||||
[tool.pyright]
|
||||
include = ["app", "tests"]
|
||||
typeCheckingMode = "strict"
|
||||
pythonVersion = "3.12"
|
||||
|
||||
[[tool.pyright.executionEnvironments]]
|
||||
root = "tests"
|
||||
reportUnknownMemberType = false
|
||||
reportUnknownVariableType = false
|
||||
reportUnknownParameterType = false
|
||||
reportMissingParameterType = false
|
||||
reportMissingTypeStubs = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["app"]
|
||||
9
backend/secrets.toml.example
Normal file
9
backend/secrets.toml.example
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Copy to secrets.toml and fill in real values. secrets.toml is git-ignored.
|
||||
|
||||
[anthropic]
|
||||
api_key = "sk-ant-..." # Haiku API key, server-side only, never sent to the client
|
||||
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
log_level = "INFO" # DEBUG | INFO | WARN | ERROR
|
||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
11
backend/tests/test_health.py
Normal file
11
backend/tests/test_health.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_health() -> None:
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue