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

1"""Config read/update handlers for the HTTP server.""" 

2 

3from __future__ import annotations 

4 

5import copy 

6import functools 

7from typing import Any 

8 

9from pydantic_core import PydanticUndefined 

10 

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 

23 

24 

25async def update_config(updates: dict[str, Any]) -> ConfigUpdateResponse: 

26 """Partial update of writable config fields. 

27 

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) 

36 

37 

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) 

43 

44 

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 

58 

59 

60async def get_config_defaults() -> ConfigResponse: 

61 """Return canonical defaults for every public config field. 

62 

63 Covers writable fields (resettable via PATCH /api/config) and the 

64 model-role fields (resettable via PUT /api/models/<role>). 

65 

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()))