Coverage for b4_backup/utils.py: 100%
42 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-18 22:40 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-18 22:40 +0000
1"""A collection of Helper functions."""
3import logging
4import os
5from pathlib import Path, PurePath
7from omegaconf import OmegaConf, SCMode
8from rich.console import Console
9from rich.logging import RichHandler
10from rich.theme import Theme
12from b4_backup.config_schema import DEFAULT, BaseConfig
14log = logging.getLogger("b4_backup.utils")
16DEFAULT_CONFIG = Path(os.getenv("B4_BACKUP_CONFIG", str(BaseConfig.config_path)))
18DEFAULT_THEME = Theme(
19 {
20 "logging.level.debug": "blue",
21 "logging.level.info": "green",
22 "logging.level.warning": "yellow",
23 "logging.level.error": "red",
24 "logging.level.critical": "reverse red",
25 }
26)
27CONSOLE = Console(theme=DEFAULT_THEME)
30# Dynamically imported by logging.config.dictConfig
31def rich_handler() -> RichHandler:
32 """Used in the logging config to use a customized RichHandler."""
33 return RichHandler(console=CONSOLE)
36def resolve_parent_dir(path: str) -> str:
37 """
38 This resolver (parent_dir) can be used in OmegaConf configs to return the parent directory or the directory of a file.
40 Args:
41 path: Name of the file or directory
43 Returns:
44 Parent directory
45 """
46 return str(PurePath(path).parent)
49def resolve_from_file(path: str) -> str:
50 """
51 This resolver (from_file) can be used in OmegaConf configs to use the raw content of a file as an input.
53 Args:
54 path: Input file path
56 Returns:
57 File content
58 """
59 return Path(path).read_text(encoding="utf8").strip()
62def _copy_from_default_retention(config: BaseConfig):
63 for target in config.backup_targets.values():
64 for (
65 retention_name,
66 retention,
67 ) in config.backup_targets[DEFAULT].src_retention.items():
68 if retention_name not in target.src_retention:
69 target.src_retention[retention_name] = retention
71 for (
72 retention_name,
73 retention,
74 ) in config.backup_targets[DEFAULT].dst_retention.items():
75 if retention_name not in target.dst_retention:
76 target.dst_retention[retention_name] = retention
79def load_config(
80 config_path: Path = DEFAULT_CONFIG, overrides: list[str] | None = None
81) -> BaseConfig:
82 """
83 Reads the config file and returns a config dataclass.
85 Args:
86 config_path: Path of the config file
87 overrides:
88 A list of dot list entries, which can override the values in the config.
89 Used for CLI.
91 Returns:
92 Config object
93 """
94 overrides = overrides or []
96 config_path = config_path.expanduser()
97 config_path.parent.mkdir(exist_ok=True, parents=True)
98 _ = config_path.exists() or config_path.touch()
100 if not OmegaConf.has_resolver("from_file"):
101 OmegaConf.register_new_resolver("from_file", resolve_from_file)
102 OmegaConf.register_new_resolver("parent_dir", resolve_parent_dir)
104 base_conf = OmegaConf.merge(
105 OmegaConf.structured(BaseConfig),
106 OmegaConf.load(config_path),
107 OmegaConf.from_dotlist(overrides),
108 )
110 # Templates shouldn't fail, if there is a value missing
111 base_conf.backup_targets[DEFAULT].source = "NONE"
113 # pylance doesn't understand here that it's actually a config_schema.BaseConfig type
114 base_conf_instance: BaseConfig = OmegaConf.to_container( # type: ignore
115 base_conf, structured_config_mode=SCMode.INSTANTIATE, resolve=True
116 )
118 base_conf_instance.config_path = config_path
120 # retention rulesets shouldn't do a nested merge
121 # That's why I do a shallow update here manually
122 _copy_from_default_retention(base_conf_instance)
124 return base_conf_instance
127def contains_path(path: PurePath, sub_path: PurePath) -> bool:
128 """
129 Check if a subpath is included in another path.
131 Args:
132 path: Path you want to check
133 sub_path: Subpath that should be included in path.
135 Returns:
136 True if path contains subpath
137 """
138 return any(
139 slice == sub_path.parts
140 for slice in zip(*[path.parts[i:] for i in range(len(sub_path.parts))])
141 )