Source code for action0.celery_sched.yaml_loader

"""
The YAML loader for schedule files: PyYAML's ``SafeLoader``, plus two things.

- The ``!ENV`` tag substitutes environment variables into a scalar (see
  :py:mod:`~action0.celery_sched.envvars`).
- Duplicate keys are an error. Plain YAML parsers silently keep the *last*
  of two equal keys, so a copy-pasted entry that kept its name would quietly
  replace the original. Keys brought in through a merge (``<<: *base``) may
  still be overridden — that is what merging is for.

Being a ``SafeLoader``, it cannot instantiate arbitrary Python objects.
:py:func:`load_yaml` is the entry point the loaders use.
"""

from typing import Any

import yaml

from action0.celery_sched.envvars import substitute_env
from action0.celery_sched.errors import DefinitionError
from action0.celery_sched.errors import DuplicateEntryError

_MERGE_TAG = "tag:yaml.org,2002:merge"


[docs] class ScheduleLoader(yaml.SafeLoader): """ ``SafeLoader`` with the ``!ENV`` tag and duplicate-key detection. >>> yaml.load("a: 1\\na: 2", Loader=ScheduleLoader) Traceback (most recent call last): ... action0.celery_sched.errors.DuplicateEntryError: duplicate entry 'a' on line 2 (first on line 1) """ def __init__(self, stream: Any) -> None: """ :param stream: the YAML text or stream """ super().__init__(stream) # the nodes whose keys were checked; see flatten_mapping() self._checked: set[int] = set() self._root: yaml.Node | None = None
[docs] def construct_document(self, node: yaml.Node) -> Any: """Remember the root, so a duplicate *entry* can be told from a duplicate key.""" self._root = node return super().construct_document(node)
[docs] def flatten_mapping(self, node: yaml.MappingNode) -> None: """ Check a mapping's own keys for duplicates, then resolve its merges. This is the one hook PyYAML calls on every mapping *before* it rewrites the node's pairs to include the merged ones. A node can be flattened more than once (as a merge source, then again when constructed), so each is checked only on its first, untouched visit. """ if id(node) not in self._checked: self._checked.add(id(node)) self._check_duplicates(node) super().flatten_mapping(node)
def _check_duplicates(self, node: yaml.MappingNode) -> None: """Raise on the first scalar key that appears twice in the node.""" first_lines: dict[tuple[str, str], int] = {} for key_node, _value_node in node.value: if not isinstance(key_node, yaml.ScalarNode) or key_node.tag == _MERGE_TAG: continue key = (key_node.tag, key_node.value) line = key_node.start_mark.line + 1 if key in first_lines: at_root = node is self._root error_type = DuplicateEntryError if at_root else DefinitionError raise error_type( f"duplicate {'entry' if at_root else 'key'} {key_node.value!r} " f"on line {line} (first on line {first_lines[key]})" ) first_lines[key] = line
[docs] def load_yaml(text: str) -> Any: """ Parse YAML text with the :py:class:`ScheduleLoader`. >>> load_yaml("'Poll feed': {schedule: {every: 5m}}") {'Poll feed': {'schedule': {'every': '5m'}}} :param text: the YAML document :returns: the parsed document (``None`` for an empty one) :raises DefinitionError: on a syntax error, a duplicate key, or an unset environment variable """ try: return yaml.load(text, Loader=ScheduleLoader) except yaml.YAMLError as error: raise DefinitionError(f"invalid YAML: {error}") from error
def _env_constructor(loader: yaml.SafeLoader, node: yaml.Node) -> str: """Construct an ``!ENV``-tagged scalar by substituting environment variables.""" line = node.start_mark.line + 1 if not isinstance(node, yaml.ScalarNode): raise DefinitionError(f"!ENV only applies to scalars (line {line})") try: return substitute_env(loader.construct_scalar(node)) except DefinitionError as error: # the entry and key aren't known while parsing, but the line is raise DefinitionError(f"{error.reason} on line {line}") from None ScheduleLoader.add_constructor("!ENV", _env_constructor)