Coverage for src / lilbee / server / routes / general.py: 100%
49 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
1"""General routes — health, status, config."""
3from __future__ import annotations
5from pathlib import Path
6from typing import Any
8from litestar import Response, get, patch
9from pydantic import ValidationError
11from lilbee.server import handlers
12from lilbee.server.auth import read_only
13from lilbee.server.models import (
14 ConfigResponse,
15 ConfigUpdateResponse,
16 HealthResponse,
17 SourceContentResponse,
18 StatusResponse,
19)
22@get("/api/health")
23@read_only
24async def health_route() -> HealthResponse:
25 """Service health check returning server version and uptime status."""
26 return await handlers.health()
29@get("/api/status")
30@read_only
31async def status_route() -> StatusResponse:
32 """Current configuration, indexed document sources, and chunk counts."""
33 return await handlers.status()
36@get("/api/config")
37@read_only
38async def config_route() -> ConfigResponse:
39 """Return all user-facing configuration values."""
40 return await handlers.get_config()
43@get("/api/config/defaults")
44@read_only
45async def config_defaults_route() -> ConfigResponse:
46 """Return canonical defaults for every writable, public configuration field."""
47 return await handlers.get_config_defaults()
50@patch("/api/config")
51async def config_update_route(data: dict[str, Any]) -> ConfigUpdateResponse:
52 """Partial update of writable configuration fields."""
53 try:
54 return await handlers.update_config(data)
55 except (ValueError, ValidationError) as exc:
56 from litestar.exceptions import ValidationException
58 raise ValidationException(str(exc)) from exc
61@get("/api/source")
62@read_only
63async def source_content_route(
64 source: str, raw: bool = False
65) -> SourceContentResponse | Response[bytes]:
66 """Return stored source file as JSON (``raw=0``) or raw bytes (``raw=1``)."""
67 from litestar.exceptions import NotFoundException
69 try:
70 result = await handlers.get_source_content(source, raw=raw)
71 except FileNotFoundError as exc:
72 raise NotFoundException(f"source not found: {source}") from exc
73 except ValueError as exc:
74 from litestar.exceptions import ValidationException
76 raise ValidationException(str(exc)) from exc
78 # ``raw=True`` returns ``(bytes, content_type)``; narrow via ``isinstance``
79 # so mypy sees the tuple branch without leaning on ``type: ignore``.
80 if isinstance(result, tuple):
81 body, content_type = result
82 # nosniff blocks browser MIME-sniffing fallbacks; attachment forces a
83 # download for any type the handler degraded to octet-stream so
84 # attacker-named files don't render inline anywhere.
85 headers = {"X-Content-Type-Options": "nosniff"}
86 if content_type == "application/octet-stream":
87 headers["Content-Disposition"] = f'attachment; filename="{Path(source).name}"'
88 return Response(content=body, media_type=content_type, status_code=200, headers=headers)
89 return result