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
« 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.
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"""
12from __future__ import annotations
14from contextlib import contextmanager
15from contextvars import ContextVar
16from typing import TYPE_CHECKING
18from pydantic import ValidationError
20from lilbee.core.config.model import cfg
22if TYPE_CHECKING:
23 from collections.abc import Iterator
25 from lilbee.core.config.model import Config
27_active: ContextVar[Config | None] = ContextVar("lilbee_active_config", default=None)
30def active_config() -> Config:
31 """Return the scoped Config if one is active, else the process-global ``cfg``."""
32 return _active.get() or cfg
35def validate_ocr_timeout(ocr_timeout: float | None) -> None:
36 """Raise ``ValueError`` unless *ocr_timeout* satisfies the config field's own bound.
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
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)