Coverage for src/lilbee/core/config/context.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-28 17:20 +0000

1"""Per-scope active config: how the library API runs against its own Config. 

2 

3The process-global ``cfg`` singleton backs the CLI, TUI, and HTTP daemon. The 

4library API (:class:`lilbee.Lilbee`) instead binds a caller-supplied Config for 

5the duration of each public method via :func:`config_scope`, and the ingest path 

6reads it through :func:`active_config` instead of the global. Scoping is a 

7ContextVar, so it stays isolated to the entering task and propagates into the 

8``to_ingest_thread`` workers (they copy the calling context), without mutating 

9the process-global cfg that other clients share. 

10""" 

11 

12from __future__ import annotations 

13 

14from contextlib import contextmanager 

15from contextvars import ContextVar 

16from typing import TYPE_CHECKING 

17 

18from pydantic import ValidationError 

19 

20from lilbee.core.config.model import cfg 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Iterator 

24 

25 from lilbee.core.config.model import Config 

26 

27_active: ContextVar[Config | None] = ContextVar("lilbee_active_config", default=None) 

28 

29 

30def active_config() -> Config: 

31 """Return the scoped Config if one is active, else the process-global ``cfg``.""" 

32 return _active.get() or cfg 

33 

34 

35def validate_ocr_timeout(ocr_timeout: float | None) -> None: 

36 """Raise ``ValueError`` unless *ocr_timeout* satisfies the config field's own bound. 

37 

38 ``None`` always passes (it means "keep the current setting"). Assigns into 

39 a throwaway copy of the active config, so the ``ocr_timeout`` field's own 

40 ``validate_assignment`` rule decides, the same rule the CLI already 

41 triggers through a direct ``cfg.ocr_timeout = value`` assignment. 

42 """ 

43 if ocr_timeout is None: 

44 return 

45 scratch = active_config().model_copy() 

46 try: 

47 scratch.ocr_timeout = ocr_timeout 

48 except ValidationError as exc: 

49 raise ValueError(f"ocr_timeout: {exc.errors()[0]['msg']}") from exc 

50 

51 

52@contextmanager 

53def config_scope(config: Config) -> Iterator[None]: 

54 """Bind *config* as the active config for the duration of the block.""" 

55 token = _active.set(config) 

56 try: 

57 yield 

58 finally: 

59 _active.reset(token)