hyera
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. See the
documentation site for the full guide,
and Differences from Puppet for where hyera
intentionally diverges.
Features
- Hiera 5 configuration -- full
hiera.yamlversion 5 schema validation, with Puppet's own error messages. - Hiera 1-4 configs, too -- a versionless or
version: 3hiera.yaml(Hiera 1, 2 and 3's own dialect) andversion: 4module/environment configs are read the way Puppet reads them. - Global, environment and module layers --
environmentpath/basemodulepath/modulepathreproduce Puppet's own layer stack, including a module'sdefault_hierarchy. - Every lookup form --
lookup/__call__/h[...]/in, plusdig/get/getvarnavigation andexplain(). - Puppet's merge strategies --
first,unique,hashanddeep(withknockout_prefix,sort_merged_arrays,merge_hash_arrays), driven by an explicitmerge=or by data-declaredlookup_options. convert_to-- Puppet'snew()type conversion, including a redactingSensitivewrapper.- YAML, JSON and HOCON backends, plus
eyaml_lookup_key(PKCS7) and asops_databackend Puppet itself does not have. - A CLI that mirrors
puppet lookup's own flags, exit codes and--render-asoutput, runnable ashyera,python -m hyera, or as an MCP tool (HYERA_MCP=stdio). - 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>=0.6.0,<0.7 |
the hyera command / python -m hyera |
hocon |
pip install "hyera[hocon]" |
pyhocon>=0.3.62,<0.4 |
hocon_data hierarchy levels |
eyaml |
pip install "hyera[eyaml]" |
cryptography>=50.0,<51 |
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
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.
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.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, mirroring puppet lookup's own 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) |
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.
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 hocon_includes: false on the hierarchy entry/defaults -- hyera's own extension, not
Puppet vocabulary) 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), - captured stderr surfaced in a
BackendError, - a clear error when the
sopsbinary is not onPATH, - the data file is passed to
sopsas an absolute path after a literal--, so a level or scope value that starts with-can never be read as asopsoption, - the
sopsfound onPATHis the one executed, by its full resolved path; asops.bat/sops.cmdshim is refused (cmd.exere-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_varandpkcs7_b64_private_key_env_varfollow 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_keyresolves against the process's current working directory, exactly like hiera-eyaml itself -- notbase_pathand 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.
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 accepts puppet lookup's own flags:
hyera [options] KEY [KEY ...]
hyera --hiera_config hiera.yaml --facts facts.yaml --node web01.example.com ntp::servers
hyera --merge deep --knock-out-prefix=-- --render-as json profile::settings
hyera --explain ntp::servers
python -m hyera --hiera_config hiera.yaml --facts 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-arraysand--merge-hash-arrays(only with--merge deep);--type(asserts the found value and--defaultagainst 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 nopuppet lookupcounterpart). - settings:
--hiera_config PATH(default./hiera.yamlif 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(defaultwarning). - output:
--render-as s|json|yaml(defaultyaml, orswhile 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.
Errors and exit codes
Everything raised derives from HieraError (.path names the file
concerned, where there is one):
ConfigError--hiera.yamlis missing, unreadable, unparsable, or violates Puppet's version 5 schema..linenames the 1-based line in.paththe problem was found at, when known.BackendError-- a data file could not be read or parsed;.pathnames it.HieraLookupError-- a failure while resolving a key, with subclassesInterpolationError(an unknown interpolation method, a misplaced%{alias(...)}, a recursive lookup, or an undefined variable understrict="error"),MergeError(an unknown or invalid merge strategy), andKeyNotFoundError(also aKeyError) --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 bad config or data
file, an unrenderable value, or a reader that closes the output early
(also silent: nothing on stderr). 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, printing nothing for the error
case; hyera's CLI tells the two apart.
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.
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 |
Supported | 1 and 2 are Hiera 3's own dialect to Puppet, read the same way; any other version raises "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; see Backends. (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 |
Not supported | |
--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. |
Differences from Puppet
hyera aims to resolve exactly like 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 whenpathdoes not exist, and the CLI's--hiera_configbehaves the same way for a named file that is missing; Puppet then falls back to its built-in default configuration. Ask for that explicitly withHiera(None, base_path=...), or omit--hiera_configso./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 newHierato 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 anenvironmentpath, so an environment name it cannot find always raises. Pass a realenvironmentpathto 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 abackends:/hiera3_backend:name it cannot run; hyera cannot run a Ruby Hiera 3 backend at all, so register a third-party Pythonhyera.Backendunder that name instead, or drop it frombackends:. (id:v3-ruby-backend-unavailable) codedirdefaults to Puppet's AIO system location for the platform (%ALLUSERSPROFILE%\PuppetLabs\codeon Windows,/etc/puppetlabs/codeelsewhere) -- never the per-user~/.puppetlabs/etc/codedefault or a value discovered frompuppet.conf. Passcodedir=/--codedirexplicitly to match a differently-configured Puppet install. (id:codedir-aio-default)sops_data(alsosops, andsops_yaml/sops_json/sops_ini/sops_dotenvto force the format) -- adata_hashbackend 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'sinclude file("*.conf")globs. hyera lets pyhocon's own resolution run for real, which expands a glob in afile(...)argument and includes every match; Puppet's ownhocon_datanever expands such a glob (it contributes nothing). There is no opt-in that reproduces Puppet's non-globbingfile(...)exactly, thoughhocon_includes=False(orhocon_includes: falseon the entry/defaults) is available as a stricter, non-resolving alternative for every include form. Every otherincludeform matches Puppet exactly (see Backends). (id:hocon-include-glob)eyaml_lookup_keysupports 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 raisesHieraLookupError("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_prefixthat Python'sremodule cannot compile raisesMergeError. Ruby accepts a prefix like**(with a warning about a redundant nested repeat operator) and uses it as a regex; choose aknockout_prefixthat is valid in both regex dialects to avoid the difference. (id:knockout-prefix-not-python-regex) convert_to(Puppet'snew()) does not support every type Puppet does. SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raisehyera.HieraLookupError("hiera does not support new() for the Puppet type '...'") instead of converting -- these are types whose values are not plain data. A type alias other thanData/RichDatais also unsupported (parse_typeresolves 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 tostrict="error", which fails the lookup. PassScope(strict="error")(or.scoped(strict="error")) to match Puppet's own default. Hierarchy locations are lenient in every mode, 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 yamlprintsSensitivevalues 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 sprints 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=VALUEsets node parameters, whichpuppet lookuptakes from the node classifier instead; use--scopefor 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 lookupalso reads the local node's facter facts or PuppetDB-stored facts when--factsis omitted; hyera always requires an explicit facts source (a--factsfile with no facts is rejected, as in Puppet). (id:facts-from-file-only) $server_factsholds onlyserverversion(8.10.0) andenvironment. A real Puppet server populates several more; pass the missing ones throughScope(server_facts=...)directly if a hierarchy needs them. (id:server-facts-minimal)environment.confis not read;--compileand--trustedare not supported. Configureenvironmentpath/modulepath/basemodulepathexplicitly instead of relying onenvironment.confdiscovery, and there is no catalog-compilation mode to fall back to. (id:environment-conf-compile-trusted-unsupported)
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.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hyera-0.0.0.tar.gz | 665.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hyera-0.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 965.8 kB
Release files / hyera-0.0.0.tar.gz
| Download URL | hyera-0.0.0.tar.gz |
|---|---|
| Size | 665.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8e9d741ecbad9f2f17067a9183a2e08b3ce8fb583dcf9d263c2e0166fc14361e
|
|
BLAKE2b-256 checksum How to use checksums |
a41005d14e6f4f6e6e6bc1af5c942db0312ec6ab130def4d879986acfebfd136
|
| 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 1, 2026.
Transparency logRelease files / hyera-0.0.0-py3-none-any.whl
| Download URL | hyera-0.0.0-py3-none-any.whl |
|---|---|
| Size | 300.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ac17a9f809bccf3dd02a3f847fc40313ad8b4a0132d95b8699fd7b0453a4010f
|
|
BLAKE2b-256 checksum How to use checksums |
22d5cbee674f4d100a303add5f720b53c5279a92753c57a7f16c907cb7759a9c
|
| 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 1, 2026.
Transparency log