Coverage for src/extratools_core/config.py: 96%
50 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-26 03:52 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-26 03:52 -0700
1from __future__ import annotations
3from collections.abc import Callable, Iterable
4from datetime import timedelta
5from enum import IntEnum
6from os import environ
7from pathlib import Path
8from typing import Any, NamedTuple
10from cachetools import TTLCache, cached
11from sortedcontainers import SortedList
13from .json import JsonDict, get_by_path, merge_json, read_json_from, set_by_path
16class ConfigLevel(IntEnum):
17 DEFAULT = 0
18 CONFIG_FILE = 1
19 ENV = 2
20 DYNAMIC = 3
21 ADHOC = 4
24class ConfigSource(NamedTuple):
25 name: str
26 level: ConfigLevel
27 data: Callable[[], JsonDict] | JsonDict
29 @staticmethod
30 def sort_key(t: ConfigSource) -> tuple[ConfigLevel, str]:
31 return (t.level, t.name)
34class Config:
35 def __init__(
36 self,
37 *,
38 register_env: bool = True,
39 config_files: Iterable[Path | str] | None = None,
40 ttl: timedelta = timedelta(minutes=10),
41 ) -> None:
42 self.sources: SortedList = SortedList(key=ConfigSource.sort_key)
44 self.__cache = TTLCache(maxsize=1, ttl=ttl.total_seconds())
46 @cached(cache=self.__cache)
47 def raw() -> JsonDict:
48 return merge_json(*[
49 (
50 source.data() if isinstance(source.data, Callable)
51 else source.data
52 )
53 for source in self.sources
54 ])
56 self.raw: Callable[[], JsonDict] = raw
58 for config_file in config_files or []:
59 config_file = Path(config_file).expanduser()
61 self.register_source(ConfigSource(
62 name=config_file.name,
63 level=ConfigLevel.CONFIG_FILE,
64 data=read_json_from(config_file),
65 ))
67 if register_env:
68 self.register_source(ConfigSource(
69 name="env",
70 level=ConfigLevel.ENV,
71 data={
72 "env": environ.copy(),
73 },
74 ))
76 self.adhoc: JsonDict = {}
77 self.register_source(ConfigSource(
78 name="adhoc",
79 level=ConfigLevel.ADHOC,
80 data=self.adhoc,
81 ))
83 def register_source(
84 self,
85 source: ConfigSource,
86 ) -> None:
87 self.sources.add(source)
89 self.__cache.clear()
91 def get(self, path: str) -> Any:
92 if path.lstrip('.') == path:
93 path = '.' + path
95 return get_by_path(self.raw(), path)
97 def set_in_adhoc(self, path: str, value: Any) -> None:
98 if path.lstrip('.') == path:
99 path = '.' + path
101 set_by_path(self.adhoc, path, value)
103 self.__cache.clear()