3 from __future__
import annotations
5 from collections.abc
import Callable, Iterator
7 from io
import StringIO, TextIOWrapper
10 from pathlib
import Path
11 from typing
import Any, TextIO, overload
16 from yaml
import CSafeLoader
as FastestAvailableSafeLoader
22 SafeLoader
as FastestAvailableSafeLoader,
25 from propcache
import cached_property
29 from .const
import SECRET_YAML
30 from .objects
import Input, NodeDictClass, NodeListClass, NodeStrClass
34 JSON_TYPE = list | dict | str
36 _LOGGER = logging.getLogger(__name__)
40 """Raised by load_yaml_dict if top level data is not a dict."""
44 """Store secrets while loading YAML."""
47 """Initialize secrets."""
49 self._cache: dict[Path, dict[str, str]] = {}
51 def get(self, requester_path: str, secret: str) -> str:
52 """Return the value of a secret."""
53 current_path = Path(requester_path)
55 secret_dir = current_path
57 secret_dir = secret_dir.parent
60 secret_dir.relative_to(self.
config_dirconfig_dir)
69 "Secret %s retrieved from secrets.yaml in folder %s",
73 return secrets[secret]
75 raise HomeAssistantError(f
"Secret {secret} not defined")
78 """Load the secrets yaml from path."""
79 if (secret_path := secret_dir / SECRET_YAML)
in self._cache:
80 return self._cache[secret_path]
82 _LOGGER.debug(
"Loading %s", secret_path)
86 if not isinstance(secrets, dict):
87 raise HomeAssistantError(
"Secrets is not a dictionary")
89 if "logger" in secrets:
90 logger =
str(secrets[
"logger"]).lower()
92 _LOGGER.setLevel(logging.DEBUG)
96 "Error in secrets.yaml: 'logger: debug' expected, but"
101 del secrets[
"logger"]
102 except FileNotFoundError:
105 self._cache[secret_path] = secrets
111 """Mixin class with extensions for YAML loader."""
118 """Get the name of the loader."""
123 """Get the name of the stream."""
124 return getattr(self.stream,
"name",
"")
128 """The fastest available safe loader, either C or Python."""
130 def __init__(self, stream: Any, secrets: Secrets |
None =
None) ->
None:
131 """Initialize a safe line loader."""
135 if isinstance(stream, str):
136 self.
namename =
"<unicode string>"
137 elif isinstance(stream, bytes):
138 self.
namename =
"<byte string>"
140 self.
namename = getattr(stream,
"name",
"<file>")
147 """Python safe loader."""
149 def __init__(self, stream: Any, secrets: Secrets |
None =
None) ->
None:
150 """Initialize a safe line loader."""
155 type LoaderType = FastSafeLoader | PythonSafeLoader
159 fname: str | os.PathLike[str], secrets: Secrets |
None =
None
160 ) -> JSON_TYPE |
None:
163 If opening the file raises an OSError it will be wrapped in a HomeAssistantError,
164 except for FileNotFoundError which will be re-raised.
167 with open(fname, encoding=
"utf-8")
as conf_file:
169 except UnicodeDecodeError
as exc:
170 _LOGGER.error(
"Unable to read file %s: %s", fname, exc)
171 raise HomeAssistantError(exc)
from exc
172 except FileNotFoundError:
174 except OSError
as exc:
175 raise HomeAssistantError(exc)
from exc
179 fname: str | os.PathLike[str], secrets: Secrets |
None =
None
181 """Load a YAML file and ensure the top level is a dict.
183 Raise if the top level is not a dict.
184 Return an empty dict if the file is empty.
187 if loaded_yaml
is None:
189 if not isinstance(loaded_yaml, dict):
190 raise YamlTypeError(f
"YAML file {fname} does not contain a dict")
195 content: str | TextIO | StringIO, secrets: Secrets |
None =
None
197 """Parse YAML with the fastest available loader."""
201 return _parse_yaml(FastSafeLoader, content, secrets)
202 except yaml.YAMLError:
205 if isinstance(content, (StringIO, TextIO, TextIOWrapper)):
212 content: str | TextIO | StringIO, secrets: Secrets |
None =
None
214 """Parse YAML with the python loader (this is very slow)."""
216 return _parse_yaml(PythonSafeLoader, content, secrets)
217 except yaml.YAMLError
as exc:
218 _LOGGER.error(
str(exc))
219 raise HomeAssistantError(exc)
from exc
223 loader: type[FastSafeLoader | PythonSafeLoader],
224 content: str | TextIO,
225 secrets: Secrets |
None =
None,
227 """Load a YAML file."""
228 return yaml.load(content, Loader=
lambda stream: loader(stream, secrets))
233 obj: list | NodeListClass, loader: LoaderType, node: yaml.nodes.Node
234 ) -> NodeListClass: ...
239 obj: str | NodeStrClass, loader: LoaderType, node: yaml.nodes.Node
240 ) -> NodeStrClass: ...
245 obj: dict | NodeDictClass, loader: LoaderType, node: yaml.nodes.Node
246 ) -> NodeDictClass: ...
250 obj: dict | list | str | NodeDictClass | NodeListClass | NodeStrClass,
252 node: yaml.nodes.Node,
253 ) -> NodeDictClass | NodeListClass | NodeStrClass:
254 """Add file reference information to an object."""
255 if isinstance(obj, list):
256 obj = NodeListClass(obj)
257 elif isinstance(obj, str):
258 obj = NodeStrClass(obj)
259 elif isinstance(obj, dict):
260 obj = NodeDictClass(obj)
266 obj: NodeListClass, loader: LoaderType, node: yaml.nodes.Node
267 ) -> NodeListClass: ...
272 obj: NodeStrClass, loader: LoaderType, node: yaml.nodes.Node
273 ) -> NodeStrClass: ...
278 obj: NodeDictClass, loader: LoaderType, node: yaml.nodes.Node
279 ) -> NodeDictClass: ...
283 obj: NodeDictClass | NodeListClass | NodeStrClass,
285 node: yaml.nodes.Node,
286 ) -> NodeDictClass | NodeListClass | NodeStrClass:
287 """Add file reference information to a node class object."""
289 obj.__config_file__ = loader.get_name
290 obj.__line__ = node.start_mark.line + 1
291 except AttributeError:
296 def _raise_if_no_value[NodeT: yaml.nodes.Node, _R](
297 func: Callable[[LoaderType, NodeT], _R],
298 ) -> Callable[[LoaderType, NodeT], _R]:
299 def wrapper(loader: LoaderType, node: NodeT) -> _R:
301 raise HomeAssistantError(
302 f
"{node.start_mark}: {node.tag} needs an argument."
304 return func(loader, node)
311 """Load another YAML file and embed it using the !include tag.
314 device_tracker: !include device_tracker.yaml
317 fname = os.path.join(os.path.dirname(loader.get_name), node.value)
319 loaded_yaml =
load_yaml(fname, loader.secrets)
320 if loaded_yaml
is None:
321 loaded_yaml = NodeDictClass()
323 except FileNotFoundError
as exc:
324 raise HomeAssistantError(
325 f
"{node.start_mark}: Unable to read file {fname}"
330 """Decide if a file is valid."""
331 return not name.startswith(
".")
335 """Recursively load files in a directory."""
336 for root, dirs, files
in os.walk(directory, topdown=
True):
338 for basename
in sorted(files):
339 if _is_file_valid(basename)
and fnmatch.fnmatch(basename, pattern):
340 filename = os.path.join(root, basename)
346 """Load multiple files from directory as a dictionary."""
347 mapping = NodeDictClass()
348 loc = os.path.join(os.path.dirname(loader.get_name), node.value)
350 filename = os.path.splitext(os.path.basename(fname))[0]
351 if os.path.basename(fname) == SECRET_YAML:
353 loaded_yaml =
load_yaml(fname, loader.secrets)
354 if loaded_yaml
is None:
357 loaded_yaml = NodeDictClass()
358 mapping[filename] = loaded_yaml
364 loader: LoaderType, node: yaml.nodes.Node
366 """Load multiple files from directory as a merged dictionary."""
367 mapping = NodeDictClass()
368 loc = os.path.join(os.path.dirname(loader.get_name), node.value)
370 if os.path.basename(fname) == SECRET_YAML:
372 loaded_yaml =
load_yaml(fname, loader.secrets)
373 if isinstance(loaded_yaml, dict):
374 mapping.update(loaded_yaml)
380 loader: LoaderType, node: yaml.nodes.Node
381 ) -> list[JSON_TYPE]:
382 """Load multiple files from directory as a list."""
383 loc = os.path.join(os.path.dirname(loader.get_name), node.value)
387 if os.path.basename(f) != SECRET_YAML
388 and (loaded_yaml :=
load_yaml(f, loader.secrets))
is not None
394 loader: LoaderType, node: yaml.nodes.Node
396 """Load multiple files from directory as a merged list."""
397 loc: str = os.path.join(os.path.dirname(loader.get_name), node.value)
398 merged_list: list[JSON_TYPE] = []
400 if os.path.basename(fname) == SECRET_YAML:
402 loaded_yaml =
load_yaml(fname, loader.secrets)
403 if isinstance(loaded_yaml, list):
404 merged_list.extend(loaded_yaml)
409 loader: LoaderType, node: yaml.nodes.MappingNode
411 """Load YAML mappings into an ordered dictionary to preserve key order."""
412 loader.flatten_mapping(node)
413 nodes = loader.construct_pairs(node)
416 for (key, _), (child_node, _)
in zip(nodes, node.value, strict=
False):
417 line = child_node.start_mark.line
421 except TypeError
as exc:
422 fname = loader.get_stream_name
423 raise yaml.MarkedYAMLError(
424 context=f
'invalid key: "{key}"',
425 context_mark=yaml.Mark(
436 fname = loader.get_stream_name
438 'YAML file %s contains duplicate key "%s". Check lines %d and %d',
450 """Add line number and file name to Load YAML sequence."""
451 (obj,) = loader.construct_yaml_seq(node)
456 loader: LoaderType, node: yaml.nodes.ScalarNode
457 ) -> str | int | float |
None:
458 """Add line number and file name to Load YAML sequence."""
460 if not isinstance(obj, str):
466 """Load environment variables and embed it into the configuration YAML."""
467 args = node.value.split()
471 return os.getenv(args[0],
" ".join(args[1:]))
472 if args[0]
in os.environ:
473 return os.environ[args[0]]
474 _LOGGER.error(
"Environment variable %s not defined", node.value)
475 raise HomeAssistantError(node.value)
478 def secret_yaml(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE:
479 """Load secrets and embed it into the configuration YAML."""
480 if loader.secrets
is None:
481 raise HomeAssistantError(
"Secrets not supported in this YAML file")
483 return loader.secrets.get(loader.get_name, node.value)
487 """Add to constructor to all loaders."""
488 for yaml_loader
in (FastSafeLoader, PythonSafeLoader):
489 yaml_loader.add_constructor(tag, constructor)
493 add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _handle_mapping_tag)
494 add_constructor(yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, _handle_scalar_tag)
495 add_constructor(yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq)
499 add_constructor(
"!include_dir_merge_list", _include_dir_merge_list_yaml)
501 add_constructor(
"!include_dir_merge_named", _include_dir_merge_named_yaml)
None __init__(self, Any stream, Secrets|None secrets=None)
None __init__(self, Any stream, Secrets|None secrets=None)
dict[str, str] _load_secret_yaml(self, Path secret_dir)
str get(self, str requester_path, str secret)
None __init__(self, Path config_dir)
str get_stream_name(self)
None open(self, **Any kwargs)
JSON_TYPE _parse_yaml_python(str|TextIO|StringIO content, Secrets|None secrets=None)
None add_constructor(Any tag, Any constructor)
bool _is_file_valid(str name)
NodeDictClass _handle_mapping_tag(LoaderType loader, yaml.nodes.MappingNode node)
JSON_TYPE _include_dir_merge_list_yaml(LoaderType loader, yaml.nodes.Node node)
str|int|float|None _handle_scalar_tag(LoaderType loader, yaml.nodes.ScalarNode node)
Iterator[str] _find_files(str directory, str pattern)
str _env_var_yaml(LoaderType loader, yaml.nodes.Node node)
NodeListClass _add_reference_to_node_class(NodeListClass obj, LoaderType loader, yaml.nodes.Node node)
JSON_TYPE|None load_yaml(str|os.PathLike[str] fname, Secrets|None secrets=None)
JSON_TYPE parse_yaml(str|TextIO|StringIO content, Secrets|None secrets=None)
dict load_yaml_dict(str|os.PathLike[str] fname, Secrets|None secrets=None)
list[JSON_TYPE] _include_dir_list_yaml(LoaderType loader, yaml.nodes.Node node)
NodeDictClass _include_dir_merge_named_yaml(LoaderType loader, yaml.nodes.Node node)
JSON_TYPE _construct_seq(LoaderType loader, yaml.nodes.Node node)
NodeDictClass _include_dir_named_yaml(LoaderType loader, yaml.nodes.Node node)
JSON_TYPE _include_yaml(LoaderType loader, yaml.nodes.Node node)
JSON_TYPE _parse_yaml(type[FastSafeLoader|PythonSafeLoader] loader, str|TextIO content, Secrets|None secrets=None)
JSON_TYPE secret_yaml(LoaderType loader, yaml.nodes.Node node)
NodeListClass _add_reference(list|NodeListClass obj, LoaderType loader, yaml.nodes.Node node)