Skip to content

hrcp

Main module exporting the public API.

Usage

from hrcp import ResourceTree, Resource, PropagationMode, get_value, Provenance

Classes

Resource

A node in the HRCP configuration tree.

Each Resource represents a single configurable entity with: - A unique name within its parent's children - A dictionary of configuration attributes - Optional parent reference (None for root) - Dictionary of child Resources

Example

root = Resource(name="region", attributes={"provider": "aws"}) dc = Resource(name="us-east-1") root.add_child(dc) dc.path '/region/us-east-1'

Source code in src/hrcp/core.py
class Resource:
    """A node in the HRCP configuration tree.

    Each Resource represents a single configurable entity with:
    - A unique name within its parent's children
    - A dictionary of configuration attributes
    - Optional parent reference (None for root)
    - Dictionary of child Resources

    Example:
        >>> root = Resource(name="region", attributes={"provider": "aws"})
        >>> dc = Resource(name="us-east-1")
        >>> root.add_child(dc)
        >>> dc.path
        '/region/us-east-1'
    """

    def __init__(
        self,
        name: str,
        attributes: dict[str, Any] | None = None,
    ) -> None:
        """Create a new Resource.

        Args:
            name: Unique identifier for this resource within its parent.
                  Cannot be empty or contain '/'.
            attributes: Initial configuration key-value pairs.

        Raises:
            ValueError: If name is empty or contains '/'.
        """
        if not name:
            msg = "name cannot be empty"
            raise ValueError(msg)
        if "/" in name:
            msg = "name cannot contain '/'"
            raise ValueError(msg)

        self._name = name
        self._parent: Resource | None = None
        self._children: dict[str, Resource] = {}
        self._attributes: dict[str, Any] = dict(attributes) if attributes else {}

    @property
    def name(self) -> str:
        """The resource's name (unique within parent)."""
        return self._name

    @property
    def attributes(self) -> dict[str, Any]:
        """The resource's configuration attributes."""
        return self._attributes

    @property
    def parent(self) -> Resource | None:
        """The parent Resource, or None if this is a root."""
        return self._parent

    @property
    def children(self) -> dict[str, Resource]:
        """Dictionary of child Resources keyed by name."""
        return self._children

    @property
    def path(self) -> str:
        """The full path from root to this Resource.

        Returns:
            Path string like '/region/datacenter/host'.
        """
        if self._parent is None:
            return f"/{self._name}"
        return f"{self._parent.path}/{self._name}"

    def add_child(self, child: Resource) -> None:
        """Add a child Resource.

        Args:
            child: The Resource to add as a child.

        Raises:
            ValueError: If a child with the same name already exists.
        """
        if child.name in self._children:
            msg = f"Child '{child.name}' already exists"
            raise ValueError(msg)

        self._children[child.name] = child
        child._parent = self

    def remove_child(self, name: str) -> Resource:
        """Remove and return a child Resource by name.

        Args:
            name: The name of the child to remove.

        Returns:
            The removed Resource.

        Raises:
            KeyError: If no child with that name exists.
        """
        child = self._children.pop(name)
        child._parent = None
        return child

    def get_child(self, name: str) -> Resource | None:
        """Get a child Resource by name.

        Args:
            name: The name of the child to retrieve.

        Returns:
            The child Resource, or None if not found.
        """
        return self._children.get(name)

    def set_attribute(self, key: str, value: Any) -> None:
        """Set an attribute value.

        Args:
            key: The attribute name.
            value: The attribute value.
        """
        self._attributes[key] = value

    def get_attribute(self, key: str, default: Any = None) -> Any:
        """Get an attribute value.

        Args:
            key: The attribute name.
            default: Value to return if attribute doesn't exist.

        Returns:
            The attribute value, or default if not found.
        """
        return self._attributes.get(key, default)

    def delete_attribute(self, key: str) -> None:
        """Delete an attribute.

        Args:
            key: The attribute name to delete.

        Raises:
            KeyError: If the attribute doesn't exist.
        """
        del self._attributes[key]

    def __repr__(self) -> str:
        """Return a string representation of the Resource."""
        return f"Resource(name={self._name!r}, path={self.path!r})"

name property

The resource's name (unique within parent).

path property

The full path from root to this Resource.

Returns:

Type Description
str

Path string like '/region/datacenter/host'.

parent property

The parent Resource, or None if this is a root.

children property

Dictionary of child Resources keyed by name.

attributes property

The resource's configuration attributes.

__init__(name, attributes=None)

Create a new Resource.

Parameters:

Name Type Description Default
name str

Unique identifier for this resource within its parent. Cannot be empty or contain '/'.

required
attributes dict[str, Any] | None

Initial configuration key-value pairs.

None

Raises:

Type Description
ValueError

If name is empty or contains '/'.

Source code in src/hrcp/core.py
def __init__(
    self,
    name: str,
    attributes: dict[str, Any] | None = None,
) -> None:
    """Create a new Resource.

    Args:
        name: Unique identifier for this resource within its parent.
              Cannot be empty or contain '/'.
        attributes: Initial configuration key-value pairs.

    Raises:
        ValueError: If name is empty or contains '/'.
    """
    if not name:
        msg = "name cannot be empty"
        raise ValueError(msg)
    if "/" in name:
        msg = "name cannot contain '/'"
        raise ValueError(msg)

    self._name = name
    self._parent: Resource | None = None
    self._children: dict[str, Resource] = {}
    self._attributes: dict[str, Any] = dict(attributes) if attributes else {}

add_child(child)

Add a child Resource.

Parameters:

Name Type Description Default
child Resource

The Resource to add as a child.

required

Raises:

Type Description
ValueError

If a child with the same name already exists.

Source code in src/hrcp/core.py
def add_child(self, child: Resource) -> None:
    """Add a child Resource.

    Args:
        child: The Resource to add as a child.

    Raises:
        ValueError: If a child with the same name already exists.
    """
    if child.name in self._children:
        msg = f"Child '{child.name}' already exists"
        raise ValueError(msg)

    self._children[child.name] = child
    child._parent = self

remove_child(name)

Remove and return a child Resource by name.

Parameters:

Name Type Description Default
name str

The name of the child to remove.

required

Returns:

Type Description
Resource

The removed Resource.

Raises:

Type Description
KeyError

If no child with that name exists.

Source code in src/hrcp/core.py
def remove_child(self, name: str) -> Resource:
    """Remove and return a child Resource by name.

    Args:
        name: The name of the child to remove.

    Returns:
        The removed Resource.

    Raises:
        KeyError: If no child with that name exists.
    """
    child = self._children.pop(name)
    child._parent = None
    return child

get_child(name)

Get a child Resource by name.

Parameters:

Name Type Description Default
name str

The name of the child to retrieve.

required

Returns:

Type Description
Resource | None

The child Resource, or None if not found.

Source code in src/hrcp/core.py
def get_child(self, name: str) -> Resource | None:
    """Get a child Resource by name.

    Args:
        name: The name of the child to retrieve.

    Returns:
        The child Resource, or None if not found.
    """
    return self._children.get(name)

set_attribute(key, value)

Set an attribute value.

Parameters:

Name Type Description Default
key str

The attribute name.

required
value Any

The attribute value.

required
Source code in src/hrcp/core.py
def set_attribute(self, key: str, value: Any) -> None:
    """Set an attribute value.

    Args:
        key: The attribute name.
        value: The attribute value.
    """
    self._attributes[key] = value

get_attribute(key, default=None)

Get an attribute value.

Parameters:

Name Type Description Default
key str

The attribute name.

required
default Any

Value to return if attribute doesn't exist.

None

Returns:

Type Description
Any

The attribute value, or default if not found.

Source code in src/hrcp/core.py
def get_attribute(self, key: str, default: Any = None) -> Any:
    """Get an attribute value.

    Args:
        key: The attribute name.
        default: Value to return if attribute doesn't exist.

    Returns:
        The attribute value, or default if not found.
    """
    return self._attributes.get(key, default)

delete_attribute(key)

Delete an attribute.

Parameters:

Name Type Description Default
key str

The attribute name to delete.

required

Raises:

Type Description
KeyError

If the attribute doesn't exist.

Source code in src/hrcp/core.py
def delete_attribute(self, key: str) -> None:
    """Delete an attribute.

    Args:
        key: The attribute name to delete.

    Raises:
        KeyError: If the attribute doesn't exist.
    """
    del self._attributes[key]

ResourceTree

Container and manager for an HRCP resource hierarchy.

The ResourceTree provides a convenient interface for working with a tree of Resources, including path-based access, creation, and traversal operations.

Example

tree = ResourceTree(root_name="infrastructure") tree.create("/infrastructure/us-east/dc1", attributes={"region": "us-east-1"}) host = tree.get("/infrastructure/us-east/dc1") host.attributes

Source code in src/hrcp/core.py
class ResourceTree:
    """Container and manager for an HRCP resource hierarchy.

    The ResourceTree provides a convenient interface for working with
    a tree of Resources, including path-based access, creation, and
    traversal operations.

    Example:
        >>> tree = ResourceTree(root_name="infrastructure")
        >>> tree.create("/infrastructure/us-east/dc1", attributes={"region": "us-east-1"})
        >>> host = tree.get("/infrastructure/us-east/dc1")
        >>> host.attributes
        {'region': 'us-east-1'}
    """

    def __init__(self, root_name: str = "root") -> None:
        """Create a new ResourceTree.

        Args:
            root_name: Name for the root Resource.
        """
        self._root = Resource(name=root_name)

    @property
    def root(self) -> Resource:
        """The root Resource of the tree."""
        return self._root

    def get(self, path: str) -> Resource | None:
        """Get a Resource by its path.

        Args:
            path: The path to the Resource (e.g., '/root/child/grandchild').
                  Use '/' to get the root.

        Returns:
            The Resource at the path, or None if not found.
        """
        if path == "/":
            return self._root

        # Remove leading slash and split
        parts = path.lstrip("/").split("/")

        # First part should match root name
        if not parts or parts[0] != self._root.name:
            return None

        # Traverse from root
        current = self._root
        for part in parts[1:]:
            child = current.get_child(part)
            if child is None:
                return None
            current = child

        return current

    def create(
        self,
        path: str,
        attributes: dict[str, Any] | None = None,
    ) -> Resource:
        """Create a Resource at the specified path.

        Creates any intermediate Resources needed to build the path.

        Args:
            path: The path where the Resource should be created.
            attributes: Initial attributes for the new Resource.

        Returns:
            The newly created Resource.

        Raises:
            ValueError: If path doesn't start with root name or
                       if Resource already exists at path.
        """
        parts = path.lstrip("/").split("/")

        # Validate path starts with root
        if not parts or parts[0] != self._root.name:
            msg = f"Path must start with '/{self._root.name}'"
            raise ValueError(msg)

        # Check if target already exists
        if self.get(path) is not None:
            msg = f"Resource already exists at '{path}'"
            raise ValueError(msg)

        # Traverse/create path
        current = self._root
        for i, part in enumerate(parts[1:], start=1):
            child = current.get_child(part)
            if child is None:
                # Create intermediate or final resource
                is_final = i == len(parts) - 1
                child = Resource(
                    name=part,
                    attributes=attributes if is_final else None,
                )
                current.add_child(child)
            current = child

        return current

    def delete(self, path: str) -> Resource:
        """Delete a Resource and its subtree.

        Args:
            path: The path to the Resource to delete.

        Returns:
            The deleted Resource.

        Raises:
            ValueError: If attempting to delete the root.
            KeyError: If no Resource exists at the path.
        """
        parts = path.lstrip("/").split("/")

        # Check if trying to delete root
        if len(parts) == 1 and parts[0] == self._root.name:
            msg = "cannot delete root"
            raise ValueError(msg)

        # Find the resource and its parent
        resource = self.get(path)
        if resource is None:
            msg = f"No resource at '{path}'"
            raise KeyError(msg)

        # Remove from parent
        parent = resource.parent
        if parent is not None:
            return parent.remove_child(resource.name)

        msg = f"No resource at '{path}'"
        raise KeyError(msg)

    def walk(self, start_path: str = "/") -> Iterator[Resource]:
        """Iterate over all Resources in the tree (depth-first).

        Args:
            start_path: Path to start walking from (default: root).

        Yields:
            Each Resource in depth-first order.

        Raises:
            KeyError: If start_path doesn't exist.
        """
        if start_path == "/":
            start: Resource = self._root
        else:
            maybe_start = self.get(start_path)
            if maybe_start is None:
                msg = f"No resource at '{start_path}'"
                raise KeyError(msg)
            start = maybe_start

        yield from self._walk_resource(start)

    def _walk_resource(self, resource: Resource) -> Iterator[Resource]:
        """Recursively walk a Resource and its children."""
        yield resource
        for child in resource.children.values():
            yield from self._walk_resource(child)

    def __len__(self) -> int:
        """Return the total number of Resources in the tree."""
        return sum(1 for _ in self.walk())

    def query(self, pattern: str) -> list[Resource]:
        """Query resources matching a wildcard pattern.

        Supports:
        - Single wildcard (*): matches any single path segment
        - Double wildcard (**): matches any number of segments (including zero)

        Args:
            pattern: Path pattern like '/root/*/child' or '/root/**/leaf'

        Returns:
            List of matching Resources.
        """
        return [
            resource
            for resource in self.walk()
            if match_pattern(resource.path, pattern)
        ]

    def query_values(
        self,
        pattern: str,
        key: str,
        mode: PropagationMode,
    ) -> list[Any]:
        """Get attribute values from resources matching a pattern.

        Args:
            pattern: Path pattern for matching resources.
            key: The attribute key to retrieve.
            mode: Propagation mode for value resolution.

        Returns:
            List of values from matching resources (excludes None values).
        """
        results: list[Any] = []
        for resource in self.query(pattern):
            value = get_value(resource, key, mode)
            if mode == PropagationMode.UP:
                # UP returns a list, extend if not empty
                if value and isinstance(value, list):
                    results.extend(value)
            elif value is not None:
                results.append(value)
        return results

    def to_dict(self) -> dict[str, Any]:
        """Serialize the tree to a dictionary.

        Returns:
            A dict representation of the tree that can be serialized to JSON.
        """
        return tree_to_dict(self)

    def _resource_to_dict(self, resource: Resource) -> dict[str, Any]:
        """Recursively serialize a Resource to a dict."""
        return resource_to_dict(resource)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ResourceTree:
        """Create a ResourceTree from a dictionary.

        Args:
            data: A dict representation of the tree.

        Returns:
            A new ResourceTree with the data.
        """
        return tree_from_dict(data)

    @classmethod
    def _load_children(
        cls,
        tree: ResourceTree,
        parent: Resource,
        children_data: dict[str, dict[str, Any]],
    ) -> None:
        """Recursively load children from dict data."""
        load_children(tree, parent, children_data)

    def to_json(self, path: str, indent: int = 2) -> None:
        """Save the tree to a JSON file.

        Args:
            path: Path to the output JSON file.
            indent: Indentation level for human-readable output.
        """
        tree_to_json(self, path, indent)

    @classmethod
    def from_json(cls, path: str) -> ResourceTree:
        """Load a ResourceTree from a JSON file.

        Args:
            path: Path to the JSON file.

        Returns:
            A new ResourceTree loaded from the file.
        """
        return tree_from_json(path)

    def __repr__(self) -> str:
        """Return a string representation of the ResourceTree."""
        return f"ResourceTree(root={self._root.name!r}, size={len(self)})"

root property

The root Resource of the tree.

__init__(root_name='root')

Create a new ResourceTree.

Parameters:

Name Type Description Default
root_name str

Name for the root Resource.

'root'
Source code in src/hrcp/core.py
def __init__(self, root_name: str = "root") -> None:
    """Create a new ResourceTree.

    Args:
        root_name: Name for the root Resource.
    """
    self._root = Resource(name=root_name)

create(path, attributes=None)

Create a Resource at the specified path.

Creates any intermediate Resources needed to build the path.

Parameters:

Name Type Description Default
path str

The path where the Resource should be created.

required
attributes dict[str, Any] | None

Initial attributes for the new Resource.

None

Returns:

Type Description
Resource

The newly created Resource.

Raises:

Type Description
ValueError

If path doesn't start with root name or if Resource already exists at path.

Source code in src/hrcp/core.py
def create(
    self,
    path: str,
    attributes: dict[str, Any] | None = None,
) -> Resource:
    """Create a Resource at the specified path.

    Creates any intermediate Resources needed to build the path.

    Args:
        path: The path where the Resource should be created.
        attributes: Initial attributes for the new Resource.

    Returns:
        The newly created Resource.

    Raises:
        ValueError: If path doesn't start with root name or
                   if Resource already exists at path.
    """
    parts = path.lstrip("/").split("/")

    # Validate path starts with root
    if not parts or parts[0] != self._root.name:
        msg = f"Path must start with '/{self._root.name}'"
        raise ValueError(msg)

    # Check if target already exists
    if self.get(path) is not None:
        msg = f"Resource already exists at '{path}'"
        raise ValueError(msg)

    # Traverse/create path
    current = self._root
    for i, part in enumerate(parts[1:], start=1):
        child = current.get_child(part)
        if child is None:
            # Create intermediate or final resource
            is_final = i == len(parts) - 1
            child = Resource(
                name=part,
                attributes=attributes if is_final else None,
            )
            current.add_child(child)
        current = child

    return current

get(path)

Get a Resource by its path.

Parameters:

Name Type Description Default
path str

The path to the Resource (e.g., '/root/child/grandchild'). Use '/' to get the root.

required

Returns:

Type Description
Resource | None

The Resource at the path, or None if not found.

Source code in src/hrcp/core.py
def get(self, path: str) -> Resource | None:
    """Get a Resource by its path.

    Args:
        path: The path to the Resource (e.g., '/root/child/grandchild').
              Use '/' to get the root.

    Returns:
        The Resource at the path, or None if not found.
    """
    if path == "/":
        return self._root

    # Remove leading slash and split
    parts = path.lstrip("/").split("/")

    # First part should match root name
    if not parts or parts[0] != self._root.name:
        return None

    # Traverse from root
    current = self._root
    for part in parts[1:]:
        child = current.get_child(part)
        if child is None:
            return None
        current = child

    return current

delete(path)

Delete a Resource and its subtree.

Parameters:

Name Type Description Default
path str

The path to the Resource to delete.

required

Returns:

Type Description
Resource

The deleted Resource.

Raises:

Type Description
ValueError

If attempting to delete the root.

KeyError

If no Resource exists at the path.

Source code in src/hrcp/core.py
def delete(self, path: str) -> Resource:
    """Delete a Resource and its subtree.

    Args:
        path: The path to the Resource to delete.

    Returns:
        The deleted Resource.

    Raises:
        ValueError: If attempting to delete the root.
        KeyError: If no Resource exists at the path.
    """
    parts = path.lstrip("/").split("/")

    # Check if trying to delete root
    if len(parts) == 1 and parts[0] == self._root.name:
        msg = "cannot delete root"
        raise ValueError(msg)

    # Find the resource and its parent
    resource = self.get(path)
    if resource is None:
        msg = f"No resource at '{path}'"
        raise KeyError(msg)

    # Remove from parent
    parent = resource.parent
    if parent is not None:
        return parent.remove_child(resource.name)

    msg = f"No resource at '{path}'"
    raise KeyError(msg)

walk(start_path='/')

Iterate over all Resources in the tree (depth-first).

Parameters:

Name Type Description Default
start_path str

Path to start walking from (default: root).

'/'

Yields:

Type Description
Resource

Each Resource in depth-first order.

Raises:

Type Description
KeyError

If start_path doesn't exist.

Source code in src/hrcp/core.py
def walk(self, start_path: str = "/") -> Iterator[Resource]:
    """Iterate over all Resources in the tree (depth-first).

    Args:
        start_path: Path to start walking from (default: root).

    Yields:
        Each Resource in depth-first order.

    Raises:
        KeyError: If start_path doesn't exist.
    """
    if start_path == "/":
        start: Resource = self._root
    else:
        maybe_start = self.get(start_path)
        if maybe_start is None:
            msg = f"No resource at '{start_path}'"
            raise KeyError(msg)
        start = maybe_start

    yield from self._walk_resource(start)

query(pattern)

Query resources matching a wildcard pattern.

Supports: - Single wildcard (): matches any single path segment - Double wildcard (*): matches any number of segments (including zero)

Parameters:

Name Type Description Default
pattern str

Path pattern like '/root//child' or '/root/*/leaf'

required

Returns:

Type Description
list[Resource]

List of matching Resources.

Source code in src/hrcp/core.py
def query(self, pattern: str) -> list[Resource]:
    """Query resources matching a wildcard pattern.

    Supports:
    - Single wildcard (*): matches any single path segment
    - Double wildcard (**): matches any number of segments (including zero)

    Args:
        pattern: Path pattern like '/root/*/child' or '/root/**/leaf'

    Returns:
        List of matching Resources.
    """
    return [
        resource
        for resource in self.walk()
        if match_pattern(resource.path, pattern)
    ]

query_values(pattern, key, mode)

Get attribute values from resources matching a pattern.

Parameters:

Name Type Description Default
pattern str

Path pattern for matching resources.

required
key str

The attribute key to retrieve.

required
mode PropagationMode

Propagation mode for value resolution.

required

Returns:

Type Description
list[Any]

List of values from matching resources (excludes None values).

Source code in src/hrcp/core.py
def query_values(
    self,
    pattern: str,
    key: str,
    mode: PropagationMode,
) -> list[Any]:
    """Get attribute values from resources matching a pattern.

    Args:
        pattern: Path pattern for matching resources.
        key: The attribute key to retrieve.
        mode: Propagation mode for value resolution.

    Returns:
        List of values from matching resources (excludes None values).
    """
    results: list[Any] = []
    for resource in self.query(pattern):
        value = get_value(resource, key, mode)
        if mode == PropagationMode.UP:
            # UP returns a list, extend if not empty
            if value and isinstance(value, list):
                results.extend(value)
        elif value is not None:
            results.append(value)
    return results

to_dict()

Serialize the tree to a dictionary.

Returns:

Type Description
dict[str, Any]

A dict representation of the tree that can be serialized to JSON.

Source code in src/hrcp/core.py
def to_dict(self) -> dict[str, Any]:
    """Serialize the tree to a dictionary.

    Returns:
        A dict representation of the tree that can be serialized to JSON.
    """
    return tree_to_dict(self)

from_dict(data) classmethod

Create a ResourceTree from a dictionary.

Parameters:

Name Type Description Default
data dict[str, Any]

A dict representation of the tree.

required

Returns:

Type Description
ResourceTree

A new ResourceTree with the data.

Source code in src/hrcp/core.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ResourceTree:
    """Create a ResourceTree from a dictionary.

    Args:
        data: A dict representation of the tree.

    Returns:
        A new ResourceTree with the data.
    """
    return tree_from_dict(data)

to_json(path, indent=2)

Save the tree to a JSON file.

Parameters:

Name Type Description Default
path str

Path to the output JSON file.

required
indent int

Indentation level for human-readable output.

2
Source code in src/hrcp/core.py
def to_json(self, path: str, indent: int = 2) -> None:
    """Save the tree to a JSON file.

    Args:
        path: Path to the output JSON file.
        indent: Indentation level for human-readable output.
    """
    tree_to_json(self, path, indent)

from_json(path) classmethod

Load a ResourceTree from a JSON file.

Parameters:

Name Type Description Default
path str

Path to the JSON file.

required

Returns:

Type Description
ResourceTree

A new ResourceTree loaded from the file.

Source code in src/hrcp/core.py
@classmethod
def from_json(cls, path: str) -> ResourceTree:
    """Load a ResourceTree from a JSON file.

    Args:
        path: Path to the JSON file.

    Returns:
        A new ResourceTree loaded from the file.
    """
    return tree_from_json(path)

PropagationMode

Bases: Enum

Defines how attribute values propagate through the resource hierarchy.

Attributes:

Name Type Description
NONE

No propagation - only the resource's own value is used.

INHERIT

Inheritance - values propagate from ancestors to descendants. The closest ancestor with the value wins.

AGGREGATE

Aggregation - values are collected from all descendants. Returns a list of all values found in the subtree.

MERGE

Deep merge - dict values are recursively merged from ancestors, with descendant values taking precedence.

REQUIRE_PATH

All ancestors (including self) must have a truthy value. Returns the local value if ALL nodes from self to root have the attribute set to a truthy value, else None.

COLLECT_ANCESTORS

Collect values from self up to root into a list. Useful for custom AND/OR logic across the path.

Source code in src/hrcp/propagation.py
class PropagationMode(Enum):
    """Defines how attribute values propagate through the resource hierarchy.

    Attributes:
        NONE: No propagation - only the resource's own value is used.
        INHERIT: Inheritance - values propagate from ancestors to descendants.
                 The closest ancestor with the value wins.
        AGGREGATE: Aggregation - values are collected from all descendants.
                   Returns a list of all values found in the subtree.
        MERGE: Deep merge - dict values are recursively merged from
               ancestors, with descendant values taking precedence.
        REQUIRE_PATH: All ancestors (including self) must have a truthy value.
                      Returns the local value if ALL nodes from self to root
                      have the attribute set to a truthy value, else None.
        COLLECT_ANCESTORS: Collect values from self up to root into a list.
                           Useful for custom AND/OR logic across the path.
    """

    NONE = auto()
    INHERIT = auto()
    AGGREGATE = auto()
    MERGE = auto()
    REQUIRE_PATH = auto()
    COLLECT_ANCESTORS = auto()

    # Aliases for backward compatibility (deprecated)
    DOWN = INHERIT
    UP = AGGREGATE
    MERGE_DOWN = MERGE

INHERIT = auto() class-attribute instance-attribute

AGGREGATE = auto() class-attribute instance-attribute

MERGE = auto() class-attribute instance-attribute

REQUIRE_PATH = auto() class-attribute instance-attribute

COLLECT_ANCESTORS = auto() class-attribute instance-attribute

NONE = auto() class-attribute instance-attribute

Provenance dataclass

Records the origin and resolution path of a configuration value.

Attributes:

Name Type Description
value Any

The resolved configuration value.

source_path str

The path of the resource that provided the value. For INHERIT/NONE, this is the single source. For AGGREGATE, this is the root of the aggregation. For MERGE, this is the deepest resource.

mode PropagationMode

The propagation mode used to resolve the value.

key_sources dict[str, str]

For MERGE with dict values, maps each key to the path of the resource that provided it. Uses dot notation for nested keys (e.g., "logging.level").

contributing_paths list[str]

For AGGREGATE/COLLECT_ANCESTORS, lists all resource paths that contributed values.

Source code in src/hrcp/provenance.py
@dataclass
class Provenance:
    """Records the origin and resolution path of a configuration value.

    Attributes:
        value: The resolved configuration value.
        source_path: The path of the resource that provided the value.
                     For INHERIT/NONE, this is the single source.
                     For AGGREGATE, this is the root of the aggregation.
                     For MERGE, this is the deepest resource.
        mode: The propagation mode used to resolve the value.
        key_sources: For MERGE with dict values, maps each key
                     to the path of the resource that provided it.
                     Uses dot notation for nested keys (e.g., "logging.level").
        contributing_paths: For AGGREGATE/COLLECT_ANCESTORS, lists all resource
                           paths that contributed values.
    """

    value: Any
    source_path: str
    mode: PropagationMode
    key_sources: dict[str, str] = field(default_factory=dict)
    contributing_paths: list[str] = field(default_factory=list)

value instance-attribute

source_path instance-attribute

mode instance-attribute

key_sources = field(default_factory=dict) class-attribute instance-attribute

contributing_paths = field(default_factory=list) class-attribute instance-attribute

Functions

get_value(resource, key, mode, default=None, *, with_provenance=False)

Get a configuration value with optional provenance information.

This is the unified API for retrieving values based on propagation mode.

Parameters:

Name Type Description Default
resource Resource

The Resource to get the value for.

required
key str

The attribute key.

required
mode PropagationMode

The propagation mode to use.

required
default Any

Default value if attribute not found (not used for AGGREGATE mode or when with_provenance=True).

None
with_provenance bool

If True, return a Provenance object with source tracking. If False, return just the value.

False

Returns:

Type Description
Any | Provenance | None

If with_provenance=False: The effective value based on propagation mode, or the default if not found.

Any | Provenance | None

If with_provenance=True: Provenance object containing the value and its origin information, or None if value doesn't exist (except for AGGREGATE mode which returns Provenance with empty list).

Source code in src/hrcp/provenance.py
def get_value(
    resource: Resource,
    key: str,
    mode: PropagationMode,
    default: Any = None,
    *,
    with_provenance: bool = False,
) -> Any | Provenance | None:
    """Get a configuration value with optional provenance information.

    This is the unified API for retrieving values based on propagation mode.

    Args:
        resource: The Resource to get the value for.
        key: The attribute key.
        mode: The propagation mode to use.
        default: Default value if attribute not found (not used for AGGREGATE
                 mode or when with_provenance=True).
        with_provenance: If True, return a Provenance object with source
                        tracking. If False, return just the value.

    Returns:
        If with_provenance=False: The effective value based on propagation mode,
            or the default if not found.
        If with_provenance=True: Provenance object containing the value and
            its origin information, or None if value doesn't exist (except
            for AGGREGATE mode which returns Provenance with empty list).
    """
    if mode == PropagationMode.NONE:
        prov = _provenance_none(resource, key)
    elif mode == PropagationMode.INHERIT:
        prov = _provenance_inherit(resource, key)
    elif mode == PropagationMode.AGGREGATE:
        prov = _provenance_aggregate(resource, key)
    elif mode == PropagationMode.MERGE:
        prov = _provenance_merge(resource, key)
    elif mode == PropagationMode.REQUIRE_PATH:
        prov = _provenance_require_path(resource, key)
    elif mode == PropagationMode.COLLECT_ANCESTORS:
        prov = _provenance_collect_ancestors(resource, key)
    else:
        msg = f"Unknown propagation mode: {mode}"
        raise ValueError(msg)

    if with_provenance:
        return prov

    if prov is None:
        return default
    return prov.value