Skip to main content

hyera

Version Python versions License Docs CI

hyera resolves Puppet Hiera data the way Puppet 8's own lookup does: pip install hyera, import hyera, and run the hyera command. It reads a Hiera base config, walks the hierarchy for a given context, and fully resolves values -- %{...} interpolation, the hiera/lookup/scope/literal/alias functions, and array/hash/deep-hash merging included.

The reference is Puppet 8.10.0 as measured: for the same hiera.yaml, data and scope, a lookup finds, misses or fails where Puppet's does and returns the same value. Error message text, the command's output text and its exit statuses are hyera's own; where hyera deliberately differs, the list under Differences from Puppet says how. The documentation site holds the API reference and the changelog.

Features

  • Hiera 5 configuration -- full hiera.yaml version 5 schema validation: a config Puppet rejects fails here too.
  • Hiera 1-4 configs, too -- a versionless or version: 3 hiera.yaml (Hiera 1, 2 and 3's own dialect) and version: 4 module/environment configs are read the way Puppet reads them.
  • Global, environment and module layers -- environmentpath/ basemodulepath/modulepath reproduce Puppet's own layer stack, including a module's default_hierarchy.
  • Every lookup form -- lookup/__call__/h[...]/in, plus dig/get/getvar navigation and explain().
  • Puppet's merge strategies -- first, unique, hash and deep (with knockout_prefix, sort_merged_arrays, merge_hash_arrays), driven by an explicit merge= or by data-declared lookup_options.
  • convert_to -- Puppet's new() type conversion, including a redacting Sensitive wrapper.
  • YAML, JSON and HOCON backends, plus eyaml_lookup_key (PKCS7) and a sops_data backend Puppet itself does not have.
  • A CLI that takes puppet lookup's flags and renders the value as s, json or yaml, runnable as hyera, python -m hyera, or as an MCP tool (HYERA_MCP=stdio). Its exit statuses are its own: 1 for a miss, 2 for an error.
  • Bounded, revalidating caches -- lookups reuse resolved locations and parsed data across calls, and pick up changed files without restarting.
  • A typed exception hierarchy -- every failure derives from HieraError, so callers can catch precisely.

Installation

pip install hyera
Extra Install Adds Needed for
cli pip install "hyera[cli]" duho the hyera command / python -m hyera
hocon pip install "hyera[hocon]" pyhocon hocon_data hierarchy levels
eyaml pip install "hyera[eyaml]" cryptography eyaml_lookup_key (PKCS7) levels

The sops_data/sops/sops_<format> backend needs the external sops binary on PATH, not a Python extra.

Quick start

version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: "Per node"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Per OS family"
    path: "os/%{facts.os.family}.yaml"
  - name: "Per role"
    path: "roles/%{role}.yaml"
  - name: "Common"
    path: "common.yaml"

This is examples/hiera.yaml; its data lives under examples/data/ and a matching fact file sits at examples/facts.yaml. From the repo root:

>>> from hyera import Hiera, Scope, load_facts
>>> h = Hiera("examples/hiera.yaml", scope=Scope(facts=load_facts("examples/facts.yaml")))
>>> h.lookup("ntp::servers")
['ntp.web.example.com', '0.pool.ntp.org', '1.pool.ntp.org']
>>> h["nginx::workers"]
8
>>> h.dig("users", "alice", "uid")
1001
>>> h.lookup("users")
{'alice': {'shell': '/bin/bash', 'uid': 1001}}
>>> "motd" in h
True
>>> h.keys()
['nginx::workers', 'users', 'packages::manager', 'ntp::servers', 'motd']
>>> h.to_dict()["nginx::workers"]
8
>>> from hyera import lookup
>>> lookup("examples/hiera.yaml", "nginx::workers", facts=load_facts("examples/facts.yaml"))
8

With the cli extra installed:

$ hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --node web01.example.com --render-as json ntp::servers
["ntp.web.example.com","0.pool.ntp.org","1.pool.ntp.org"]

puppet lookup takes the same flags, as examples/README.md shows.

Command line

Install the cli extra to get the command: pip install "hyera[cli]" (without it, hyera/python -m hyera print that hint and exit 2).

hyera takes puppet lookup's flags, apart from those listed as not supported under Hiera coverage:

hyera [options] KEY [KEY ...]

hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --node web01.example.com ntp::servers
hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --merge deep --knock-out-prefix=-- --render-as json users
hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml --explain ntp::servers
python -m hyera --hiera_config examples/hiera.yaml --facts examples/facts.yaml ntp::servers

Options, grouped:

  • lookup: one or more KEYs (the first one found wins); --merge first|unique|hash|deep; --knock-out-prefix, --sort-merged-arrays and --merge-hash-arrays (only with --merge deep); --type (asserts the found value and --default against a Puppet type expression); --default; --explain/--explain-options.
  • facts and scope: --facts FILE (.json/.yaml/.yml, or any other name tried as JSON then YAML); --node NAME (used in messages only, sets no fact); --scope NAME=VALUE/-s (repeatable; VALUE is YAML, a dotted NAME builds a hash -- hyera's one flag with no puppet lookup counterpart).
  • settings: --hiera_config PATH (default ./hiera.yaml if present, else Puppet's built-in default configuration); --environment NAME; --environmentpath/--modulepath/--basemodulepath (each a list of paths separated by the OS path separator); --codedir; --strict off|warning|error (default warning).
  • output: --render-as s|json|yaml (default yaml, or s while explaining).
  • logging: -v/--verbose (repeatable; adds info, then debug), -d/--debug (debug, same as -vv), -q/--quiet (repeatable; drops to error, then critical), --loglevel [NAME:]LEVEL.

Without --merge, the data's lookup_options decides; an explicit --merge, first included, overrides it. See Errors and exit codes below for what each exit status means and what this CLI does not (yet) support.

HYERA_MCP=stdio hyera runs the same command as an MCP server over stdin/stdout, so an MCP client can drive lookups: it exposes one tool, hyera, whose arguments are the command-line fields (keys, hiera_config, facts, scope, merge, ...) and whose result is what the command would print. An option value of exactly -- (the knock-out prefix --) is passed through as the value, as on the command line. A call that finds nothing or fails comes back as an error result whose last line gives the exit status and its meaning, exit code: 1 (No value found for the key).

An MCP caller that controls the tool's arguments can do what a user at the command line can: read any file the process can read as a facts file or a hiera.yaml (an error message can name the keys of a YAML or JSON mapping it finds there, and shows whether a path exists), and read any hiera data on disk. A hierarchy level that names sops_data runs the sops binary. Expose the tool only to a caller you would trust with that access, or bound it with two environment variables read once when the server starts (ignored without HYERA_MCP): HYERA_MCP_ROOT=/srv/hiera requires every path argument (facts, hiera_config, environmentpath, modulepath, basemodulepath, codedir) to resolve, links resolved, inside that directory, and turns on confine_locations for every call; HYERA_MCP_BACKENDS=yaml_data,json_data lets a hierarchy name only those functions. A refused call exits 2 with one line naming the argument; a value that is not usable stops the server at start. Neither is a sandbox for what a backend you allow does. The plain command has no such flags.

API overview

Module Purpose Reference
hyera hyera: a Python implementation of Puppet Hiera data lookup. https://jose-pr.github.io/hyera/api/hyera/
hyera.types Public Puppet type objects (Integer, Optional, Struct, ...). https://jose-pr.github.io/hyera/api/types/
hyera.backends Data backends: a self-registering Backend registry. https://jose-pr.github.io/hyera/api/backends/
hyera.testing Helpers for testing a backend: run its hooks the way the engine does. https://jose-pr.github.io/hyera/api/testing/
hyera.cli Command-line interface for hyera, built on duho. https://jose-pr.github.io/hyera/api/cli/
hyera (command) / python -m hyera Runs a lookup from the command line, taking puppet lookup's flags. #command-line

Guide

Configuration

A standard Hiera 5 hiera.yaml works. Each level names a data_hash, lookup_key or data_dig backend and a source (path, paths, glob, globs, uri, uris, or mapped_paths):

---
version: 5
defaults:
  data_hash: yaml_data
  datadir: data

hierarchy:
  - name: "Per-node"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Per-environment"
    path: "environments/%{environment}.yaml"
  - name: "Per-role"
    mapped_paths: [roles, role, "roles/%{role}.yaml"]
  - name: "Modules"
    globs:
      - "modules/*.yaml"
  - name: "Common"
    path: "common.yaml"

See Backends below for the registered data_hash/lookup_key names and the one non-Puppet backend.

Hiera 3 and 4 configs

A hiera.yaml without a version key, or with version: 3 (Hiera 1, 2 and 3 are the same dialect to Puppet), is read and validated against Puppet's own version 3 schema, then resolved with Puppet's backend-major provider order: one data source per listed backends name, over the whole hierarchy, in list order.

---
:backends:
  - yaml
:yaml:
  :datadir: data
  :extension: yaml
:hierarchy:
  - "nodes/%{::trusted.certname}"
  - common

yaml/json/hocon/eyaml map onto the same YAMLBackend/JSONBackend/ HOCONBackend/EyamlBackend a v5 data_hash: yaml_data etc. would use; any other name must be a third-party hyera.Backend registered under that name in the "v3" namespace (NAMES = {"v3": (...)}), or it raises ConfigError (see Differences from Puppet -- Puppet, with real Hiera 3 installed, would instead skip that backend silently). merge_behavior/deep_merge_options/logger are validated but never applied, matching Puppet: only an explicit merge=/--merge changes how results combine. A relative datadir (including the default, <codedir>/environments/%{::environment}/hieradata) resolves against the process's working directory at construction, never the hiera.yaml directory -- Hiera(..., codedir=...)/hyera --codedir set $codedir (Puppet's own AIO default per platform otherwise).

hiera.yaml version 4 (backend: yaml|json|hocon instead of data_hash:, path/paths defaulting to the entry's own name) is accepted in the environment and module layers only -- Puppet rejects it at the global layer, after validating its schema (a schema-invalid version 4 file at the global layer raises its schema error, never the layer one). Its datadir is joined onto the config root exactly as written, with no interpolation at all -- unlike every other version. A version 3 (or missing-version) hiera.yaml at an environment or module root is likewise still fully schema-validated, then ignored with a warning (or, under Scope(strict="error"), raised) rather than read -- see Layers below. hiera3_backend follows the same backend-name rule as a v3 backends: entry, and is accepted only in the global layer.

A %{lookup()}/%{hiera()}/%{alias()} reached while interpolating a version 3 global layer's own data stays confined to the global layer -- never reaching an environment or module, even for an otherwise-qualified key -- unless the current environment has a real version 5 hiera.yaml (an absent, ignored-version-3, or version 4 environment all count as none).

Layers

hiera.yaml above is the global layer. Puppet also reads an environment layer and, for a module::key-shaped lookup, a module layer -- pass environmentpath/basemodulepath/modulepath to Hiera(...) to enable them:

.
├── hiera.yaml                          # global
├── data/common.yaml
└── environments/
    └── production/
        ├── hiera.yaml                  # environment (scope.environment)
        ├── data/common.yaml
        └── modules/
            └── mymod/
                ├── hiera.yaml          # module (mymod::* keys only)
                └── data/common.yaml
h = Hiera(
    "hiera.yaml",
    environmentpath="./environments",
    basemodulepath="./modules",
)
h.lookup("mymod::setting")  # global, then environment, then mymod's own hiera.yaml

The lookup order, at every level, is global then environment then module -- a merge (merge="unique", merge="deep", ...) spans all three. A key not qualified <module>::... never reaches the module layer at all, and a module's own data that is not qualified with that module's name is dropped (with a warning) rather than leaking into another module's namespace. hiera3_backend is accepted only in the global layer's hiera.yaml. A version-3 (or missing-version) hiera.yaml at an environment or module root is silently ignored (with a warning); puppet lookup's own strict=error raises instead. A version 4 hiera.yaml at an environment or module root is read normally (it is only the global layer that rejects it). See the shipped API header's "Layers" entry for the full discovery and error rules.

A module's own hiera.yaml may also declare a default_hierarchy (default_hierarchy is rejected everywhere else -- global or environment -- with ConfigError):

# modules/mymod/hiera.yaml
version: 5
hierarchy:
  - name: "Common"
    path: "common.yaml"
default_hierarchy:
  - name: "Module defaults"
    path: "module_defaults.yaml"

It is consulted only for that module's own mymod::* keys, and only after every layer (global, environment, the module's own main hierarchy) misses. The caller's merge= does not apply there -- the merge comes from the default hierarchy's own lookup_options instead -- while the main hierarchy's convert_to still applies to whatever value it returns.

Lookups

from hyera import Hiera, Scope

h = Hiera("hiera.yaml", scope=Scope(facts={"os": {"family": "Debian"}}, environment="production"))

# First match wins:
h.lookup("ntp::servers")

# Merge across the whole hierarchy:
h.lookup("classes", merge="unique")          # flatten + dedupe arrays
h.lookup("users", merge="deep")              # deep hash merge

# Missing keys raise KeyNotFoundError (also a KeyError) unless a default is given:
h.lookup("missing", default_value="fallback")
"some::key" in h

# A Hiera is callable, and h[...] takes lookup()'s own arguments:
h("ntp::servers")
h["classes", None, "unique"]

# Bind a derived scope once and reuse -- a view, sharing config and caches:
prod = h.scoped(environment="production")
prod["ntp::servers"]

Coming from hiera()/hiera_array()/hiera_hash() -- Puppet's legacy functions always force a merge, ignoring lookup_options; merge="first" below is that forcing, not merely "the default":

Puppet hyera
hiera('key') h.lookup('key', None, 'first')
hiera('key', 'default') h.lookup('key', None, 'first', 'default')
hiera_array('key') h.lookup('key', None, 'unique')
hiera_hash('key') h.lookup('key', None, 'hash')
hiera_include('key') not supported (applies classes to a catalog)

One-shot lookup

import hyera

hyera.lookup("hiera.yaml", "ntp::servers", facts={"os": {"family": "Debian"}})

hyera.lookup(base_config, name, ..., facts=None, scope=None) builds a Hiera and returns its lookup; every other argument is Hiera.lookup's own. It builds a new instance on every call and caches nothing between calls, so a loop uses the class. facts and scope are exclusive, and no other Hiera option (layers, backends, cache control) is reachable from it.

Listing keys

h.keys()      # every top-level key the data_hash levels hold, in precedence order
h.to_dict()   # {key: h.lookup((key,)) for key in h.keys()}
h.to_dict(merge="deep")   # one merge strategy for every key

keys() lists what the scope's data_hash levels hold: the global hierarchy, the environment's, each module's own (only keys in its namespace), then each module's default_hierarchy. A key appears once; lookup_options is never listed. A lookup_key or data_dig level cannot be listed and adds nothing. to_dict() runs one exact-key lookup per key, so each value is interpolated and converted as lookup returns it, and a key whose lookup misses is left out.

Navigating values

dig/get/getvar are Puppet's own navigation functions, each ported onto Hiera:

h.dig("db", "credentials", "user")            # None if any step is missing
h.get("db.credentials.user", default_value="admin")   # a dotted navigation string
h.getvar("facts.os.family")                   # reads the bound scope, not the data

dig looks up its first argument, then walks the rest of them into the result (a list index or a dict key at a time), returning None the moment a step is missing rather than raising. get takes the same idea as one dotted string and a default_value, plus an optional block that receives a walk error (a non-collection or a non-integer list index) instead of raising. getvar runs get's own navigation over a scope variable instead of a looked-up key.

Explaining a lookup

print(h.explain("ntp::servers").text())

explain takes exactly lookup's own arguments and returns a hyera.ExplainResult: .text() is the indented report puppet lookup --explain prints (every hierarchy entry and path consulted, merges and their results, interpolations, the lookup_options search); .to_hash() is the same tree, keyed the way --render-as json --explain renders it. explain_options=True reports only how lookup_options was assembled.

Type-checked lookups

value_type (on lookup/dig/get/explain/()/[]) takes a Puppet type-expression string, or the equivalent object from hyera.types -- one isinstance-aware class per Puppet type, never a builtin subclass:

from hyera import types

h.lookup("ntp::servers", types.Array[types.String])  # same as "Array[String]"
h.lookup("retries", types.Integer[1, 10])             # same as "Integer[1, 10]"

isinstance(5, types.Integer)          # True
isinstance(5, types.Integer[1, 10])   # True
isinstance(11, types.Integer[1, 10])  # False

types.Integer("42")   # 42 (an int) -- Puppet's new(), same as convert_to
types.Array("ab")      # ["a", "b"]

A bare class (types.Integer) is the unparameterized type; subscripting (types.Integer[1, 10]) builds a parameterized one, equal to parsing the same Puppet text; calling either is Puppet's new(). hyera.types is not re-exported from top-level hyera except Sensitive, already public there as the redacting wrapper (types.Sensitive is the same object).

Merging and lookup_options

Pass merge= to lookup() -- one of Puppet's strategy names, a hyera.Merge member (Merge.DEEP is the same value as "deep", so either spelling works everywhere merge= is accepted), or a hash of deep options:

h.lookup("classes", merge="unique")               # flatten + dedupe arrays
h.lookup("app::name", merge="default")            # explicit first-match
h.lookup("conf", merge="deep")                    # recursive hash merge
h.lookup("conf", merge={"strategy": "deep",       # deep-merge options
                        "knockout_prefix": "--",
                        "sort_merged_arrays": True,
                        "merge_hash_arrays": True})

More idiomatically, declare the strategy (and optional convert_to) in the data under the reserved lookup_options key -- then callers need not pass merge= at all:

# common.yaml
classes:
  - base
lookup_options:
  classes:            { merge: unique }
  "^app::.*":         { merge: { strategy: deep } }   # regex: must start with ^
  port:               { convert_to: Integer }
  db::password:       { convert_to: Sensitive }

A lookup_options key is treated as a regular expression only when it starts with ^ (Hiera 5's rule); every other key is matched literally, so a key containing . or other metacharacters cannot shadow unrelated keys. Patterns use Ruby regex syntax ((?<name>…), \A, \z, \h/\H, a lookbehind) and match by searching from the start of the key, so ^app:: matches app::ports; they are tried in the merged order, lower-priority levels' patterns first. An exact key match always wins over a pattern match. An invalid pattern, or a lookup_options value that is not a hash, raises HieraLookupError for the whole lookup. An entry that is a string applies no options and stops the search (a matching pattern for the same key is never tried); any other non-hash, non-string entry raises.

With layers configured, lookup_options from the global, environment and module data all apply to the same key -- global wins over environment, which wins over module -- and a module's own keys/patterns must start with <module>::.

An explicit merge= argument overrides only the merge lookup_options would have picked; convert_to always applies. convert_to takes a Puppet type string (Integer, Optional[Integer]) or [Type, *args] ([Integer, 16], [String, '%x']) and converts with Puppet's new(): Integer, Float, Numeric, String, Boolean, Array, Hash, Tuple, Struct, Optional, NotUndef and Sensitive (a redacting hyera.Sensitive wrapper). An invalid type or a failed conversion raises hyera.HieraLookupError. A String format is one directive (%d, %5.2f, %x, %p, ...) with Puppet's per-type rules and its own "Illegal format" errors; a format that is not exactly one directive is an error, never ignored.

Scope and facts

Scope is Puppet's top scope: it holds variables (node parameters), facts, trusted data, server_facts, environment and strict, and is what every Hiera lookup runs against. Precedence for a top-scope variable name: an explicit variables entry wins over a fact of the same name, which wins over a server_facts entry; $environment defaults to "production"; $trusted defaults to Puppet's local hash (certname taken from a clientcert variable/fact, else empty). Facts are also reachable as a whole through $facts, and server_facts through $server_facts.

from hyera import Hiera, Scope, Strict, load_facts

scope = Scope(facts=load_facts("facts.yaml"), environment="production", strict=Strict.ERROR)
h = Hiera("hiera.yaml", scope=scope)
h.lookup("ntp::servers")

strict= takes a hyera.Strict member (OFF/WARNING/ERROR) or the plain string it equals ("off"/"warning"/"error"); hyera.Merge, hyera.FunctionKind, hyera.BackendKind and hyera.RenderAs are the same kind of str-mixin enum for the other closed-set arguments described below.

load_facts(path) reads a puppet lookup --facts-style file (JSON for .json, YAML for .yaml/.yml, otherwise JSON then YAML); the result must be a mapping, and hostname/domain/fqdn/clientcert are all-or-nothing. facts_from_facter() runs a bare facter -j instead:

from hyera import facts_from_facter

scope = Scope(facts=facts_from_facter())

h.scoped(**derive_args) returns a Hiera view bound to h.scope.derive(**derive_args), sharing h's config, backends and caches: variables/facts/server_facts shallow-update the parent scope's own (new values win, nothing goes stale); environment/strict/trusted/ node_name replace the parent's when given.

strict ("off", "warning" -- the default -- or "error") controls what happens when an interpolated variable is undefined; see Differences from Puppet.

Backends

Backends (by data_hash name -- Puppet function names only; see Differences from Puppet below for the one exception):

Backend data_hash name Notes
YAMLBackend yaml_data parses YAML the way Puppet's Psych does (types, symbols, BOM), on libyaml when available
JSONBackend json_data
HOCONBackend hocon_data requires pip install "hyera[hocon]"
SopsBackend sops_data (also sops, sops_<yaml|json|ini|dotenv>) decrypts via the sops CLI on the fly

A third-party backend registers itself the same way, by subclassing hyera.Backend and declaring NAMES; Backend.find/.get/.new/.names look a backend up by name, and Hiera(backends=[...]) restricts a lookup to an explicit allow-list of classes.

HOCONBackend resolves include directives exactly as Puppet's own hocon_data does by default: a plain include "file" contributes nothing; include file(…) really reads the file (relative to the process working directory, or absolute); a directive in value position (including inside a [...] array) is kept as literal text; url(…), classpath(…), required(…), package(…) and a case-mismatched keyword all raise BackendError, matching Puppet's own parse/method errors for those forms. Pass hocon_includes=False to HOCONBackend, or set options: {hocon_includes: false} on a hocon_data hierarchy entry (or in defaults: {options: ...}; hyera's own extension, which Puppet rejects -- it refuses every options key on hocon_data), to restore the stricter, pre-fidelity behaviour instead: every form but a plain quoted include raises, include file(…) included. In either mode, pyhocon's own include-resolving methods stay wrapped as a fail-closed backstop, so an undiscovered gap in the text scanner still cannot read a file or reach the network for a form the active mode does not intend to resolve.

sops and unattended runs

SopsBackend (data_hash: sops_data) shells out to sops to decrypt a level on the fly. The format (YAML, JSON, INI or dotenv) is inferred from the file's extension the same way the sops CLI itself picks it (.yaml/.yml/.json/.env/.ini, case-sensitive); any other extension is a clear error, since sops would read that file as binary. It is hardened so an automated lookup never hangs, dies opaquely, or leaks a decrypted secret:

  • a finite subprocess timeout (hyera.backends.SOPS_TIMEOUT, default 30 s; SopsBackend(timeout=...) overrides it for one backend); on expiry sops and its child processes are killed and a BackendTimeoutError (a BackendError and a TimeoutError) is raised,
  • sops runs with its standard input closed, so it can never consume the caller's own input,
  • the last 2,000 characters of its stderr surfaced in a BackendError,
  • a clear error when the sops binary is not on PATH,
  • the data file is passed to sops as an absolute path after a literal --, so a level or scope value that starts with - can never be read as a sops option,
  • the sops found on PATH is the one executed, by its full resolved path; a sops.bat/sops.cmd shim is refused (cmd.exe re-parses a batch file's argument line, which a data-derived path could abuse),
  • a decrypted file that fails to parse reports only the problem and its line/column -- never the decrypted plaintext; a YAML value shaped to quote itself into the error message (!!float, !ruby/object:...) is redacted instead,
  • an INI file is always decrypted through sops's own JSON view, never ini text -- sops's INI writer can otherwise emit a value that a text parser reads as a different key or an injected section.

eyaml_lookup_key

EyamlBackend (lookup_key: eyaml_lookup_key) decrypts hiera-eyaml's ENC[PKCS7,...] values, behind the optional eyaml extra (cryptography). PKCS7 only -- the private key alone is needed, no certificate:

hierarchy:
  - name: "secrets"
    lookup_key: eyaml_lookup_key
    path: "secrets.eyaml"
    options:
      pkcs7_private_key: "keys/private_key.pkcs7.pem"
  • pkcs7_private_key, pkcs7_private_key_env_var and pkcs7_b64_private_key_env_var follow hiera-eyaml's own precedence (env var beats a plain path, base64-env-var beats both); pkcs7_public_key* options are accepted but never read.
  • A relative pkcs7_private_key resolves against the process's current working directory, exactly like hiera-eyaml itself -- not base_path and not the data file's own directory.
  • Other hiera-eyaml encryptors (GPG and third-party plugins) are not supported; a value using one raises the same "cannot load such file" error Puppet itself gives without that plugin installed.

Writing a backend

A package that ships backends needs no import in the user's code: it names the module that defines them in the hyera.backends entry-point group, and hyera imports that module the first time it looks a backend up (never at import hyera). An entry that fails to import is logged and skipped.

[project.entry-points."hyera.backends"]
my_backend = "my_package.backend"

A hook (data_hash(path, options, context), lookup_key(key, options, context) or data_dig(key_segments, options, context)) that raises an exception of a class outside hyera is reported as a BackendError naming the function and the location, with the original as its cause; raise BackendError yourself for a problem with a source. hyera.testing runs a hook without a hierarchy (lookup_key, data_dig, data_hash, with LookupContext.for_testing() as the context) and ships BackendContract, the checks every backend passes:

import json

from hyera.backends import Backend, BackendError
from hyera.testing import BackendContract


class JSONFileBackend(Backend):
    NAMES = {"function": ("json_file_data",)}

    def loads(self, text):
        try:
            return json.loads(text)
        except ValueError as e:
            raise BackendError("invalid JSON: " + e.msg) from None


class TestJSONFileBackend(BackendContract):
    backend = JSONFileBackend

    def write_source(self, directory, data):
        (directory / "a.json").write_text(json.dumps(data), encoding="utf-8")
        return {"path": "a.json"}

Errors and exit codes

What goes wrong at run time derives from HieraError (.path names the file concerned, where there is one). A caller's bad argument is not one: it raises a plain TypeError (wrong type) or ValueError (right type, unusable value), such as h.lookup("k", merge="bogus") or h.lookup("k", "Bogus["). The same strategy or type in a data file's lookup_options or convert_to raises the package error. MergeError and InterpolationError are also ValueError, so test for HieraError or the exact class, not ValueError alone.

  • ConfigError -- hiera.yaml is missing, unreadable, unparsable, or violates Puppet's version 5 schema. .line names the 1-based line in .path the problem was found at, when known.
  • BackendError -- a data file could not be read or parsed; .path names it.
  • HieraLookupError -- a failure while resolving a key, with subclasses InterpolationError (an unknown interpolation method, a misplaced %{alias(...)}, a recursive lookup, or an undefined variable under strict="error"), MergeError (values that cannot be merged, or an unknown or invalid strategy in a data file's lookup_options), and KeyNotFoundError (also a KeyError) -- lookup()'s miss, with no default given.

The CLI's exit codes: 0 found (or --default/--explain printed), 1 the key was not found -- nothing is printed, matching puppet lookup's own silent miss -- 2 any other error: a usage problem, a missing or unreadable facts file, a bad config or data file, an unrenderable value, or a reader that closes the output early or a device that cannot be written (both silent: nothing on stderr) -- and 130 an interrupt (Ctrl-C), with no traceback. A 2 is reported as one stderr line (-v, -d or DUHO_TRACEBACK=1 adds the traceback). puppet lookup exits 1 for both a miss and an error, and prints Error: Could not run: and the message for an error; hyera's CLI tells the two apart, and its message text is its own (see Differences from Puppet).

Caching

Each Hiera (and every .scoped(...) view of it, which shares the same caches) keeps two scope-keyed caches -- resolved hierarchy locations and merged lookup_options -- plus an unbounded cache of parsed data files, bounded like Puppet's own per-environment cache: its size follows the data tree, not the number of scopes seen. Hiera(..., cache_size=256) bounds each scope-keyed cache (least-recently-used entries dropped; None for no bound, 0 to disable); Hiera.clear_cache() drops every cache, including parsed data files. Hiera(..., revalidate=True) (the default): each lookup re-checks the data files and glob listings it uses and re-reads one whose inode, modification time or size changed, as Puppet does between compilations -- files added or removed at path/paths/mapped_paths locations and under globbed directories are seen by the next lookup. revalidate=False keeps every file and glob listing as first read until clear_cache(). Neither mode re-reads hiera.yaml itself; construct a new Hiera to pick up a changed base config.

A lookup_key or data_dig result is kept per top-level key. Under revalidate=True it is dropped when a file the hook read through context.cached_file_data changes (its inode, modification time or size), and a hook that read no file through it is called once per lookup, since hyera cannot know when its source changed. Under revalidate=False every result is kept until clear_cache(). A miss is never kept, and a hook's own context.cache is a separate store.

Untrusted input

Like Puppet, hyera trusts what the hierarchy and the data say: a scope value interpolated into a path can climb out of the datadir with .. or name an absolute path, a YAML document can expand aliases without bound, a glob can expand {a,b}{a,b}... exponentially, and a HOCON ${VAR} reads the process environment. If the scope values or the data come from a caller you do not control, turn on what applies; nothing is on by default.

  • Hiera(..., confine_locations=True) treats a data file location outside its level's datadir (symbolic links resolved) as absent: never opened, shown by explain() as a path not found, logged once at WARNING. A HOCON include file(...) outside it fails. It covers the files a level reads (path, paths, glob, mapped_paths); uri levels and what a lookup_key backend does with a path are not covered.
  • Hiera(..., limits=hyera.Limits(...)) bounds three costs: yaml_alias_nodes, the nodes one YAML document may yield through aliases (the load fails before any node is built); glob_patterns, the patterns one glob may expand to through braces (the lookup fails before any directory is walked); and hocon_substitution_size, the largest value one HOCON ${...} substitution may insert, in characters for a string and in nodes plus characters for a list or object (the load fails before the value is copied). They bound those costs and not memory in general.
  • options: {hocon_env: false} on a hocon_data entry, or HOCONBackend(hocon_env=False), stops a HOCON substitution the document does not define from reading the process environment.

One set to start from, a recommendation and not a measured bound:

hiera = hyera.Hiera(
    "hiera.yaml",
    scope=hyera.Scope(facts=facts_from_the_caller),
    confine_locations=True,
    limits=hyera.Limits(
        yaml_alias_nodes=100_000,
        glob_patterns=1_000,
        hocon_substitution_size=1_000_000,
    ),
)

For the MCP tool, see Command line.

Hiera coverage

hiera.yaml keys

Feature Status Notes
version Supported 5 is fully validated; a versionless or 3 config is read as Hiera 3; 4 only in environment/module layers; any other value raises.
defaults Supported applies to any hierarchy entry lacking its own value; missing/empty/null becomes Puppet's built-in default.
name Supported required, non-empty, unique per hierarchy.
path Supported
paths Supported
glob Partial matched through hyera's own Ruby Dir.glob port; case-sensitive and byte-sorted on every OS, unlike Ruby on Windows. (id: glob-case-sensitive-byte-order)
globs Partial same as glob. (id: glob-case-sensitive-byte-order)
mapped_paths Supported a scope reference iterated as [key, value] pairs; each item is a local scope variable.
uri Supported validated with Ruby's URI() grammar, passed to the entry's function, never fetched.
uris Supported same as uri.
datadir Supported defaults to data, next to hiera.yaml.
options Supported interpolated, passed to the backend with path/uri.
data_hash Supported value must be a real Puppet function name (or the one non-Puppet sops_data name -- see Backends below).
lookup_key Supported called per key and per location with a hyera.LookupContext.
data_dig Supported same calling convention as lookup_key, plus the requested key segments.
hiera3_backend Partial global layer only; an unregistered name raises ConfigError where Puppet, with real Hiera 3 installed, silently contributes nothing. (id: v3-ruby-backend-unavailable)
default_hierarchy Supported module layer only; consulted after every other layer misses.
plan_hierarchy Not supported schema-validated but never consulted -- hyera does not run Puppet Bolt plans, the only context where Puppet applies it.

Features

Feature Status Notes
Hiera 3 and 4 configs Supported version 3 (or missing) resolved with Puppet's backend-major provider order; version 4 accepted in the environment/module layers only.
version 1, 2 and others Not supported a versionless file or 3 is read as Hiera 3; 1, 2 and any other value except 4 (environment/module layers) and 5 raise "This runtime does not support hiera.yaml version N".
Global/environment/module layers Supported Hiera(..., environmentpath=, basemodulepath=, modulepath=).
Interpolation variables (%{x}, %{::x}, %{facts.x}, %{trusted.x}) Supported
Interpolation functions (hiera/lookup/alias/scope/literal) Supported
Undefined variables (strict) Partial default is "warning" (interpolates as "" and logs); Puppet 8 defaults to "error". (id: strict-default-warning)
Merge strategies (first/default/unique/hash/deep) Supported
Deep-merge options (knockout_prefix, sort_merged_arrays, merge_hash_arrays) Partial a knockout_prefix Python's re cannot compile raises, where Ruby accepts it with a warning. (id: knockout-prefix-not-python-regex)
reverse_deep/unconstrained_deep Supported Hiera-3-era deep-merge variants.
lookup_options merge (exact and ^ keys) Supported
convert_to Partial SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raise. (id: convert-to-unsupported-type)
Lookup forms (name list, value_type, default_value, default_values_hash, override, block) Supported
Dotted keys Supported
dig/get/getvar Supported
explain/explain_options Supported
Type expressions Partial a type alias other than Data/RichData is unsupported. (id: convert-to-unsupported-type)
yaml_data Supported
json_data Supported
hocon_data Partial include file("*.conf") globs by default, where Puppet's never does, and pyhocon parses a few constructs differently; see Backends and Differences from Puppet. (id: hocon-include-glob)
eyaml_lookup_key Partial PKCS7 only; other hiera-eyaml encryptors are not supported. (id: eyaml-pkcs7-only)
sops_data Supported the one backend with no Puppet equivalent. (id: sops-backend)
puppet lookup CLI flags Partial every flag except --compile/--trusted and the binary --render-as formats. (id: environment-conf-compile-trusted-unsupported)

Not supported

Feature Status Notes
Running an arbitrary Ruby Hiera 3 backend Not supported a v3/hiera3_backend name must be a Puppet-mapped one or a third-party Python hyera.Backend.
Encrypted-value convert_to beyond Sensitive Not supported
hiera-eyaml encryptors other than PKCS7 Not supported GPG and third-party plugins.
environment.conf's modulepath/environment_data_provider, metadata.json's deprecated data_provider Not supported superseded by the explicit modulepath= keyword.
--render-as binary|msgpack|console|flat|rich_data_json Not supported only s, json and yaml; an empty --render-as is an error.
-V Not supported use --version.
Underscore and hyphen spellings of a flag (--hiera-config, --render_as, --knock_out_prefix) Not supported only the spellings under Command line are accepted, and an option cannot be abbreviated (--expl).
--render-as json float text Not supported floats print in Python's spelling (1e-05, 1000000000000000.0), where Puppet's Ruby prints 0.00001 and 1e+15; a nil hash key prints "null".
An empty --environment Not supported Puppet falls back to its default environment; hyera reports an error.
--compile/--trusted Not supported
calling_class/calling_module Not supported
Facts from PuppetDB or the Puppet server Not supported facts come only from --facts/Scope(facts=...).
puppet.conf discovery Not supported
Type aliases other than Data/RichData; new() for SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type, Object Not supported
hiera()/hiera_array()/hiera_hash()/hiera_include() as methods Not supported use .lookup() -- see the mapping table under Lookups.
The types Iterable, Iterator, Init and Unit in a type expression Not supported a value_type or convert_to naming one raises HieraLookupError.
A Float bound written as a string (Float['1', 2]), a float among a Tuple's element types (Tuple[String, 1.0, 2]), and a Hash used as a key by Hash.new Not supported Puppet accepts the two type expressions and builds the Hash; hyera raises (a Python dict cannot hold a dict as a key).

Differences from Puppet

hyera aims to give the same lookup outcome and value as Puppet 8.10.0's puppet lookup. Every deliberate difference is listed here, tagged with a slug; each is also either a recorded conformance-harness deviation (checked against the real Puppet oracle) or a documented-only difference the harness cannot record a golden for.

  • A missing hiera.yaml raises ConfigError, not Puppet's built-in fallback. Hiera(path) raises when path does not exist, and the CLI's --hiera_config behaves the same way for a named file that is missing; Puppet then falls back to its built-in default configuration. Ask for that explicitly with Hiera(None, base_path=...), or omit --hiera_config so ./hiera.yaml-if-present is tried first. (id: missing-config-raises)
  • The directory holding hiera.yaml is used literally. hyera never interpolates %{...} inside that absolute base directory, and for glob levels treats any glob metacharacter in it as a literal pattern character; Puppet interpolates %{...} there too. There is no opt-in, since the directory is fixed at construction. (id: config-dir-not-interpolated)
  • A changed hiera.yaml is not re-read by an existing Hiera. Puppet re-reads it between compilations; construct a new Hiera to pick up a changed base config (data files and glob listings are re-checked by default; see Caching). (id: config-not-revalidated)
  • environmentpath=None (the default) means no environment directories at all. Every environment name then resolves with no environment layer and no error; Puppet always has an environmentpath, so an environment name it cannot find always raises. Pass a real environmentpath to get Puppet's raising behaviour. (id: environmentpath-none-means-no-layer)
  • An unregistered Hiera 3 backend name raises ConfigError. Puppet, with real Hiera 3 installed, silently contributes nothing for a backends:/hiera3_backend: name it cannot run; hyera cannot run a Ruby Hiera 3 backend at all, so register a third-party Python hyera.Backend under that name instead, or drop it from backends:. (id: v3-ruby-backend-unavailable)
  • codedir defaults to Puppet's AIO system location for the platform (%ALLUSERSPROFILE%\PuppetLabs\code on Windows, /etc/puppetlabs/code elsewhere) -- never the per-user ~/.puppetlabs/etc/code default or a value discovered from puppet.conf. Pass codedir=/--codedir explicitly to match a differently-configured Puppet install. (id: codedir-aio-default)
  • sops_data (also sops, and sops_yaml/sops_json/sops_ini/ sops_dotenv to force the format) -- a data_hash backend with no Puppet equivalent, for decrypting a sops-encrypted data file on the fly. A hierarchy that uses it does not load under real Puppet, and there is no Puppet equivalent to fall back to. (id: sops-backend)
  • hocon_data's include file("*.conf") globs. hyera lets pyhocon's own resolution run for real, which expands a glob in a file(...) argument and includes every match; Puppet's own hocon_data never expands such a glob (it contributes nothing). There is no opt-in that reproduces Puppet's non-globbing file(...) exactly, though hocon_includes=False (or options: {hocon_includes: false} on the entry or in defaults) is available as a stricter, non-resolving alternative for every include form. Every other include form matches Puppet exactly (see Backends). (id: hocon-include-glob)
  • hocon_data is parsed by pyhocon, not Ruby's hocon gem. Quoted keys, null inside a concatenation and unicode escapes match Puppet, but these constructs differ (Puppet, then hyera): list = [1] then list += 2 gives [1,2], then 2; enabled = True is the string "True", then the boolean true; label = true x is "true x", then "Truex"; mode = 010 is 8, then 10; ratio = 1.0 is 1, then 1.0; a key set first to an object and then to a scalar, a quoted-path key such as "x"."y" = 4, the empty-string key "" = 2 and a leading byte-order mark load in Puppet and are parse errors in hyera; [1,, 2] and +1 are errors in Puppet and are accepted by hyera; a backslash-slash escape and a unicode escape with non-hex digits are kept as written; an object's keys come back in a different order. (id: hocon-pyhocon-parser)
  • eyaml_lookup_key supports only the PKCS7 encryptor. hiera-eyaml's other encryptors (GPG, and any third-party plugin) raise the same "cannot load such file" error real Puppet gives without that plugin's gem installed -- this project never adds one, so there is no way to opt into GPG support here. (id: eyaml-pkcs7-only)
  • Navigating a dotted sub-key into an Integer keeps hyera's own error text. Puppet crashes with a raw Ruby NoMethodError ("undefined method 'include?' for an instance of Integer") instead of a designed message; hyera raises HieraLookupError ("Data Provider type mismatch: Got Integer when a hash-like object was expected ..."), the same shape it already uses for a String or Array in this position. There is no opt-in. (id: integer-dotted-navigation-error-text)
  • A deep-merge knockout_prefix that Python's re module cannot compile raises MergeError. Ruby accepts a prefix like ** (with a warning about a redundant nested repeat operator) and uses it as a regex; choose a knockout_prefix that is valid in both regex dialects to avoid the difference. (id: knockout-prefix-not-python-regex)
  • convert_to (Puppet's new()) does not support every type Puppet does. SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raise hyera.HieraLookupError ("hyera does not support new() for the Puppet type '...'") instead of converting -- these are types whose values are not plain data. A type alias other than Data/RichData is also unsupported (parse_type resolves only the five Puppet static-loader aliases; any other capitalized name becomes an unresolved type reference). There is no opt-in. (id: convert-to-unsupported-type)
  • Undefined variables default to strict="warning" (an undefined %{var}/%{scope('var')} interpolates as "" and logs a warning); Puppet 8 defaults to strict="error", which fails the lookup. Pass Scope(strict="error") (or .scoped(strict="error")) to match Puppet's own default. Hierarchy locations of a version 5 hiera.yaml are lenient in every mode, as in Puppet; a version 3 or 4 one fails the lookup under strict="error", as in Puppet. (id: strict-default-warning)
  • Glob wildcards are case-sensitive and results sort by byte order on every OS, as on Puppet's Linux servers; Ruby on Windows matches glob wildcards case-insensitively instead, so a hierarchy authored against a Windows Puppet server may need adjusting. (id: glob-case-sensitive-byte-order)
  • --render-as yaml prints Sensitive values redacted, as the other formats do; Puppet prints the plaintext. There is no opt-in to print the plaintext here. (id: render-yaml-sensitive-redacted)
  • --render-as yaml reads back as the value looked up, but its quoting need not match Puppet's text. hyera quotes a string that a YAML 1.1 reader would take for a number, date or float (1,000, 2001-1-1, .Nan), escapes the line breaks YAML adds to LF and CR, and never writes anchors or aliases. --explain --render-as yaml writes the explain tree with plain string keys. (id: render-yaml-equivalent-not-identical)
  • --render-as s prints hashes in Ruby 3.2's AIO form ({"a"=>1}), as Puppet 8's own packages do; Puppet on Ruby 3.4 or later renders {"a" => 1} (with spaces around =>) instead. There is no opt-in, since hyera targets the AIO packages' own Ruby version. (id: aio-hash-rendering)
  • --scope NAME=VALUE sets node parameters, which puppet lookup takes from the node classifier instead; use --scope for every value a real Puppet run would source from the classifier. (id: scope-flag-sets-node-parameters)
  • Facts come only from --facts/Scope(facts=...). puppet lookup also reads the local node's facter facts or PuppetDB-stored facts when --facts is omitted; hyera always requires an explicit facts source (a --facts file with no facts is rejected, as in Puppet). (id: facts-from-file-only)
  • $server_facts holds only serverversion (8.10.0) and environment. A real Puppet server populates several more; pass the missing ones through Scope(server_facts=...) directly if a hierarchy needs them. (id: server-facts-minimal)
  • environment.conf is not read; --compile and --trusted are not supported. Configure environmentpath/modulepath/basemodulepath explicitly instead of relying on environment.conf discovery, and there is no catalog-compilation mode to fall back to. (id: environment-conf-compile-trusted-unsupported)
  • Hash keys Python cannot tell apart are an error or one key. Ruby keeps 1, 1.0 and true as three keys; Python treats them as one. A YAML mapping whose keys collide only that way ({1: a, 1.0: b}) raises BackendError naming them, rather than silently dropping one entry, and --merge deep joins such keys from different levels into one. Write the keys as strings to avoid it. (id: python-equal-hash-keys)
  • Data that is not valid UTF-8 is rejected as a whole. hyera reads data files and eyaml plaintext as strict UTF-8. A json_data file with one non-UTF-8 byte fails every lookup that reaches it, where Puppet answers the other keys and fails only when it renders that value; an eyaml plaintext that is not UTF-8 raises, where Puppet returns the bytes; a !!binary value whose bytes are not UTF-8 cannot be rendered as s or json. (id: non-utf8-data)
  • Collections nested more than 500 levels deep are a parse error, in YAML and JSON files, --facts files and --scope values, with the message nested too deeply. Puppet reads YAML to about 10,000 levels and stops JSON at 100; the bound keeps a hostile file from ending the interpreter, which libyaml's recursive composer does on Python 3.9. (id: nesting-bound)
  • A few Ruby regex constructs are refused, and POSIX bracket classes are ASCII-only. A Pattern/Regexp type, a convert_to/value_type expression or a lookup_options key using \p{..}, \P{..}, \R, \X, \G, \K, \g<..>, && or a nested class inside [...], a negated shorthand (\D \W \S \H) inside [...], a nested repeat such as a**, or (on Python 3.9 and 3.10) a possessive quantifier or an atomic group, raises HieraLookupError naming the construct, where Ruby accepts it. [[:alpha:]] and the other POSIX classes match ASCII letters only, where Ruby's match Unicode. There is no match-time bound, as in Ruby: a pattern with nested quantifiers can take exponential time on a long subject. (id: ruby-regex-constructs)
  • String formats cover one directive per value, not Puppet's container options. A format given as a type map ({Integer => '%x'}), the # indenting flag on an Array or Hash, and a precision on %a/%A raise HieraLookupError, as does converting a Binary, Timestamp, URI or Object value; Puppet accepts all of them. (id: string-format-subset)
  • A chain of %{lookup()} or %{alias()} interpolations resolves to about 80 hops. Puppet resolves 100; hyera recurses once per hop and never changes Python's recursion limit, so a longer chain, or a value nested past the limit, raises InterpolationError naming the keys being resolved. (id: interpolation-chain-depth)
  • Two interpolation shapes differ. %{::::x} reads as an undefined variable, where Puppet prints the fact it names; and a hash key that interpolates to an Array (%{alias('arr')}) raises InterpolationError ("not hashable"), where Puppet keeps the Array as the key. (id: interpolation-key-shapes)
  • Every error exits 2 where puppet lookup exits 1, and the error line is hyera's own. A miss exits 1 in both. For any other error puppet lookup exits 1 and prints Error: Could not run: and the message; hyera exits 2 and logs one ERROR line. See Errors and exit codes. (id: error-exit-status)
  • Error message text is hyera's own where it differs. The same lookup fails in both, but the wording need not match: hyera's unknown-function error ends with known: ..., a hiera.yaml version: 4 file in the global layer is refused without Puppet's (file: ...) suffix, and --explain of a version 3 config with a relative :datadir: shows absolute paths where Puppet shows them as written. Match on the exception class, not its text. (id: error-message-text)
  • Schema errors of a version 5 hiera.yaml carry (line: N). Puppet never prints a line number; hyera ends each mismatch with the line of the node it points at, and puts the first one's in ConfigError.line. Both list every mismatch. (id: schema-error-line-suffix)
  • Some inputs that crash Puppet 8.10 work in hyera. puppet lookup --type Data k (or any type alias) fails in Puppet and returns the value here; an Integer key in lookup_options or in module data fails every Puppet lookup that reads it and is ignored by hyera; Float.new("0") crashes Puppet and returns 0.0 here. (id: puppet-crashes-hyera-answers)
  • --environment takes the name literally. --environment production/ is accepted by Puppet and names no environment in hyera, which reports the missing environment. (id: environment-trailing-slash)
  • A global hiera.yaml that cannot be loaded fails at construction, even for lookup_options. Hiera(...) reads the global configuration when it is built, so a global layer in version 4, or one failing the schema, raises ConfigError there; Puppet reads it only for a key that needs it and answers a miss for the reserved key lookup_options. An environment or module layer that cannot be loaded is read by the first lookup that needs it, and the reserved key answers a miss before that, as in Puppet. (id: global-config-error-at-construction)
  • Three Ruby Dir.glob behaviours are not matched. A brace group directly after **/ (**/{b,a}.yaml) is matched per directory in sorted order by Ruby, where hyera expands it first and keeps the written order, so the files of such a glob level can be searched in a different order; Ruby keeps a doubled / in a result (a//f.yaml), and hyera collapses it; a brace that expands to an empty pattern ({,a}) also returns the base directory in Ruby, which Puppet then discards as a directory. (id: dir-glob-ruby-quirks)
  • On a case-insensitive filesystem, a literal glob segment matched by case folding is returned in the pattern's spelling, where Ruby returns the on-disk spelling (Dir.glob('Sub/c.yaml') finds sub/c.yaml on APFS). Values are unaffected; --explain and sources() show the pattern's spelling. (id: glob-case-folded-spelling)

Development

python -m venv .venv/3.14-posix-x86_64
.venv/3.14-posix-x86_64/bin/pip install -e ".[dev]"
python -m pytest -q
python -m black --check src/ tests/ benchmarks/ examples/

Windows uses Scripts\python instead of bin/python; venvs are named <version>-<os>-<arch>. Build the docs with pip install -e ".[docs]", then python -m mkdocs build --strict. See the root AGENTS.md for the conformance recorder and the full contributor guide.

Releasing

This project follows Semantic Versioning and keeps a CHANGELOG.md. Pushing a tag matching v* runs release.yml: the test gate, then build (which checks the tag names the version actually built), then a strict docs build (docs-gate), then the GitHub release, then publishing to PyPI through Trusted Publishing. A pre-release tag (v1.0.0-rc.1) is published as a PyPI pre-release, which pip install hyera skips unless you ask for it (--pre or an exact version pin); only a final tag also dispatches docs.yml to redeploy the docs at that tag.

License

MIT, for this project's own code. Several modules port code translated from Puppet (Apache-2.0), the deep_merge gem (MIT), Psych (MIT), Ruby's YAML library, and Ruby's uri library (2-clause BSDL); those files carry their own notice. See NOTICE and LICENSES/.

Metadata

Release files for hyera 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hyera 0.1.0
File Size Uploaded
hyera-0.1.0.tar.gz 976.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hyera 0.1.0
File Interpreter ABI Platform
hyera-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / hyera-0.1.0.tar.gz

Download URL hyera-0.1.0.tar.gz
Size 976.3 kB
Tags Source
SHA-256 checksum
How to use checksums
4fa5f35eaf5fe3c73b978e7ee3c0e6bb6be8d4ecf53aa5d895714b040c779661
BLAKE2b-256 checksum
How to use checksums
66fc1452b93a9bd1f233d366e4f92e7c45fd70baa49d20203dc4d90ff5c5b3c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / hyera-0.1.0-py3-none-any.whl

Download URL hyera-0.1.0-py3-none-any.whl
Size 397.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fde941483ee05ab077d2c85a4371d26989b6b3c456e473e4fcd1e7b65adaee48
BLAKE2b-256 checksum
How to use checksums
2b38ddaae9b38bc097a353a313e022cd6558e9495dd5e308adda55bb3fba4c52
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page