Coverage for src/extratools_core/config.py: 91%
50 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-25 04:13 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-25 04:13 -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 []: 58 ↛ 59line 58 didn't jump to line 59 because the loop on line 58 never started
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: 67 ↛ 76line 67 didn't jump to line 76 because the condition on line 67 was always true
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: 98 ↛ 101line 98 didn't jump to line 101 because the condition on line 98 was always true
99 path = '.' + path
101 set_by_path(self.adhoc, path, value)
103 self.__cache.clear()