- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
"""S.H.O.N.A.R. FastAPI application entrypoint.
|
|
|
|
Run (dev): uvicorn shonar.main:app --reload --port 8000
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
from shonar import __version__
|
|
from shonar.api.v1 import api_router
|
|
from shonar.core.config import get_settings
|
|
from shonar.core.ratelimit import limiter
|
|
from shonar.db.session import dispose_engine, get_engine
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger("shonar")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
settings = get_settings()
|
|
get_engine() # validate URL parses; connections are lazy
|
|
for warning in settings.validate_production():
|
|
logger.warning("CONFIG: %s", warning)
|
|
if settings.auto_migrate:
|
|
from shonar.db.migrate import upgrade_head
|
|
|
|
await upgrade_head(settings.database_url)
|
|
inline = settings.queue_backend.strip().lower() == "inline"
|
|
if inline:
|
|
from shonar.services import inline_queue
|
|
|
|
await inline_queue.start()
|
|
yield
|
|
if inline:
|
|
from shonar.services import inline_queue
|
|
|
|
await inline_queue.stop()
|
|
await dispose_engine()
|
|
|
|
|
|
app = FastAPI(
|
|
title="SHONAR API",
|
|
description=(
|
|
"SHONAR — Self-hosted Oral Notes and Audio Recorder. REST API. "
|
|
"All data stays on your server."
|
|
),
|
|
version=__version__,
|
|
lifespan=lifespan,
|
|
# Interactive docs at the conventional paths.
|
|
docs_url="/docs",
|
|
redoc_url="/redoc",
|
|
openapi_url="/openapi.json",
|
|
)
|
|
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def unhandled_exception_handler(request: Request, exc: Exception):
|
|
"""Safe error surface: log details server-side, never leak internals."""
|
|
logger.exception("Unhandled error on %s %s", request.method, request.url.path)
|
|
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
|
|
|
|
|
app.include_router(api_router)
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {"app": "SHONAR", "docs": "/docs", "health": "/api/v1/healthz"}
|