Coverage for src / lilbee / server / handlers / config.py: 100%
30 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
1"""Config read/update handlers for the HTTP server."""
3from __future__ import annotations
5import copy
6import functools
7from typing import Any
9from pydantic_core import PydanticUndefined
11from lilbee.app.settings import apply_settings_update
12from lilbee.config_meta import (
13 MODEL_ROLE_FIELDS as _MODEL_ROLE_FIELDS,
14)
15from lilbee.config_meta import (
16 PUBLIC_CONFIG_FIELDS as _PUBLIC_CONFIG_FIELDS,
17)
18from lilbee.config_meta import (
19 WRITABLE_CONFIG_FIELDS,
20)
21from lilbee.core.config import Config, cfg
22from lilbee.server.models import ConfigResponse, ConfigUpdateResponse
25async def update_config(updates: dict[str, Any]) -> ConfigUpdateResponse:
26 """Partial update of writable config fields.
28 Delegates validation, snapshot/rollback, persistence, and cache
29 invalidation to ``app.settings.apply_settings_update`` so HTTP, MCP,
30 CLI, and the TUI share one write boundary. Model role writes are
31 refused at this surface because PUT /api/models/<role> already
32 handles them with an install-availability check.
33 """
34 result = apply_settings_update(updates, allow_model_roles=False)
35 return ConfigUpdateResponse(updated=result.updated, reindex_required=result.reindex_required)
38async def get_config() -> ConfigResponse:
39 """Return all user-facing configuration values."""
40 dumped = cfg.model_dump()
41 result = {k: v for k, v in dumped.items() if k in _PUBLIC_CONFIG_FIELDS}
42 return ConfigResponse(**result)
45@functools.cache
46def _compute_config_defaults() -> dict[str, Any]:
47 """Materialize Config defaults once per process."""
48 defaults: dict[str, Any] = {}
49 for name, info in Config.model_fields.items():
50 is_writable_public = name in WRITABLE_CONFIG_FIELDS and name in _PUBLIC_CONFIG_FIELDS
51 if not is_writable_public and name not in _MODEL_ROLE_FIELDS:
52 continue
53 value = info.get_default(call_default_factory=True)
54 if value is PydanticUndefined: # pragma: no cover
55 continue
56 defaults[name] = value
57 return defaults
60async def get_config_defaults() -> ConfigResponse:
61 """Return canonical defaults for every public config field.
63 Covers writable fields (resettable via PATCH /api/config) and the
64 model-role fields (resettable via PUT /api/models/<role>).
66 Deepcopies the cached dict so callers that mutate the response
67 (list-valued fields like ``crawl_exclude_patterns``) cannot poison
68 subsequent calls.
69 """
70 return ConfigResponse(**copy.deepcopy(_compute_config_defaults()))