Standard Library

The standard library provides a default set of components that can be used in every pyromaniac configuration. It contains components for configuration rendering, loading and rendering templates, and adding file system nodes.

Create merge fields for inline, local, and/or remote configs

std.merge(
    *configs: str | Path | URL | dict,  # configurations to create merge for
    headers: dict = {},                 # headers to add for URLs
)

The result is intended to be added as the ignition.config.merge field.

For strings, paths, and URLs, the result of passing them to the std.contents component is added to the merge. For dicts, they are rendered into a string using Butane after all composite keys are expanded. Empty dicts are skipped. If the dict has a “for” field, it will be embedded into the Butane hierarchy accordingly using the std.py.hierarchy component. The headers dict is passed to the contents component only for URLs.

Example:

  • Merge inline Butane config with remote ignition file using authentication: std.merge({'storage.files[0].path': "/var/file.txt"}, URL("https://example.com/config.ign"), headers={"Authorization": "..."})

Load file from disk and render it using jinja if variables are supplied

std.load(
    path: Path,   # path to file
    /,
    **vars: Any,  # variables to pass to jinja renderer
)

Jinja is only invoked if at least one variable is passed as keyword argument.

You may use the shell filter to serialize (lists of) shell arguments, as in {{ ["echo", "hello world"] | shell }}. The extra series and ellipsis tests are also available.

Example:

  • Load and render text file: std.load(_/"file.txt", name="Alice")

Load JSON from disk injecting variables using jinja

std.load.json(
    path: Path,   # path to file
    /,
    **vars: Any,  # variables to pass to jinja renderer
)

Jinja is only invoked if at least one variable is passed as keyword argument.

Jinja expressions will be serialized before inserting them into the document. You can therefore safely inject structured data into your JSON document without worrying about breaking JSON syntax. To insert raw strings into your document, use the raw filter as in {"greeting": "Hello, {{ name | raw }}!"}. You may use the shell filter to serialize (lists of) shell arguments, as in {"code": {{ ["echo", "hello world"] | shell }}}. The extra series and ellipsis tests are also available.

Example:

  • Load and inject: std.load.json(_/"file.json", name="Alice")

Load YAML from disk injecting variables using jinja

std.load.yaml(
    path: Path,   # path to file
    /,
    **vars: Any,  # variables to pass to jinja renderer
)

Jinja is only invoked if at least one variable is passed as keyword argument.

Jinja expressions will be serialized before inserting them into the document. You can therefore safely inject structured data into your YAML document without worrying about breaking YAML syntax. To insert raw strings into your document, use the raw filter as in greeting: "Hello, {{ name | raw }}!". You may use the shell filter to serialize (lists of) shell arguments, as in code: {{ ["echo", "hello world"] | shell }}. The extra series and ellipsis tests are also available.

Example:

  • Load and inject: std.load.yaml(_/"file.yml", name="Alice")

Load TOML from disk injecting variables using jinja

std.load.toml(
    path: Path,   # path to file
    /,
    **vars: Any,  # variables to pass to jinja renderer
)

Jinja is only invoked if at least one variable is passed as keyword argument.

Jinja expressions will be serialized before inserting them into the document. You can therefore safely inject structured data into your TOML document without worrying about breaking TOML syntax. To insert raw strings into your document, use the raw filter as in greeting = "Hello, {{ name | raw }}!". You may use the shell filter to serialize (lists of) shell arguments, as in code = {{ ["echo", "hello world"] | shell }}. The extra series and ellipsis tests are also available.

Example:

  • Load and inject: std.load.toml(_/"file.toml", name="Alice")

Wrap value in magic type for convenient member access and default handling

std.magic(
    value: Any,  # any value to wrap
)

Recursively wraps dicts and lists to allow access to dict members via dot notation.

When encountering non-existent array or dict keys, a Nothing object will be returned that in turn returns itself when indexed. The Nothing object is falsy and therefore allows specifying default values using or DEFAULT.

Beware that everything other than dicts, lists, and non-existent keys are returned as is: std.magic({"foo": 42}).foo.bar will still result in an AttributeError because 42 has no attribute bar.

Examples:

  • Dot notation: std.magic({"foo": [{"bar": "baz"}]}).foo[0].bar
  • Default handling: std.magic({"foo": "bar"}).baz.qux or "default"

Create file fields with specified content and file ownership

std.file(
    path: Path,                                  # path to create the file at in the final system
    content: str | Path | URL | dict = None,     # string, path, or URL source or a custom dict
    user: int | str = None,                      # user ID or name
    group: int | str | None = ...,               # group ID or name (defaults to the same as user)
    headers: dict = {},                          # map of request headers for URL contents
    append: list[str | Path | URL | dict] = [],  # list of contents to append
    **fields: Any,                               # additional fields to take over as they are
)

The result is intended to be added as an element to the storage.files list.

The path must be absolute unless the user name is specified, in which case relative paths will be interpreted relative to the user’s default home directory.

The content argument and each element of the append list are passed through the std.contents component along with the headers argument. The user and group arguments are passed through the std.ownership component. See their documentation for further details.

Examples:

  • Add inline file for root user: std.file("/var/file.txt", "foo")
  • Add file from disk for “core” user in its home directory: std.file("file.txt", _/"file.txt", "core")
std.link(
    path: Path,                     # path to create the link at in the final system
    target: Path,                   # path to link to
    user: int | str = None,         # user ID or name
    group: int | str | None = ...,  # group ID or name (defaults to the same as user)
    **fields: Any,                  # additional fields to take over as they are
)

The result is intended to be added as an element to the storage.links list.

The path must be absolute unless the user name is specified, in which case relative paths will be interpreted relative to the user’s default home directory.

The user and group arguments are passed through the std.ownership component. See its documentation for further details.

Examples:

  • Add absolute hard link for root user: std.link("/var/file.txt", "/var/other.txt", hard=True)
  • Add relative soft link for “core” user in its home directory: std.link("bin", ".local/bin", "core")

Create directory fields with specified ownership

std.directory(
    path: Path,                     # path to create the directory at in the final system
    user: int | str = None,         # user ID or name
    group: int | str | None = ...,  # group ID or name (defaults to the same as user)
    **fields: Any,                  # additional fields to take over as they are
)

The result is intended to be added as an element to the storage.directories list.

The path must be absolute unless the user name is specified, in which case relative paths will be interpreted relative to the user’s default home directory.

The user and group arguments are passed through the std.ownership component. See its documentation for further details.

Examples:

  • Add absolute directory for root user: std.directory("/var/dir")
  • Add read-only directory for “core” user in its home directory: std.directory("dir", "core", mode=0o550)

Create directory with parents with specified ownership

std.directories(
    base: Path,                     # path to create directories under in the final system
    path: Path,                     # path of directories to create
    user: int | str = None,         # user ID or name
    group: int | str | None = ...,  # group ID or name (defaults to the same as user)
    **fields: Any,                  # additional fields to take over as they are
)

The result is intended to be added as the storage.directories field.

The base must be absolute unless the user name is specified, in which case relative paths will be interpreted relative to the user’s default home directory. The path will be interpreted relative to the base.

Only the members of the path that are children of base will be created, not base itself.

The user and group arguments are passed through the std.ownership component. See its documentation for further details.

Example:

  • Add user unit directory for “core” user: std.directories(".", ".config/systemd/user", "core")

Add local directory tree with specified ownership

std.tree(
    path: Path,                     # path to create the directory at in the final system
    local: Path,                    # local directory to copy files from
    user: int | str = None,         # user ID or name
    group: int | str | None = ...,  # group ID or name (defaults to the same as user)
    mode: bool = False,             # whether to copy permission bits from the original files
    overwrite: bool = False,        # whether to set the `overwrite` field on all nodes
)

The result is intended to be added as the storage field.

The path must be absolute unless the user name is specified, in which case relative paths will be interpreted relative to the user’s default home directory.

The user and group arguments are passed through the std.ownership component. See its documentation for further details.

If mode is True, file permissions will be copied from the original files.

Example:

  • Copy config directory to “core” user’s home directory preserving permissions: std.tree(".config", _/"config", "core", mode=True)

Create storage fields for combining file system objects

std.storage(
    *objects: dict,  # file system objects to merge
)

The result is intended to be added as the storage field.

Combines a list of storage objects based on their for field. The field is required and represents the path the object should be placed at in the Butane structure. The storage-specific standard library components return objects with the for field correctly set.

If multiple entries with the same path exist in the files, directories, links, or trees list respectively, they will be merged into one entry per unique path.

Example:

  • Combine a file with a file tree: std.storage(std.file('/var/file.txt'), std.tree('config', _/'config', user='core'))

Create contents dict as required for files and in several other places

std.contents(
    content: str | Path | URL | dict,  # string, path, or URL source or a custom dict
    headers: dict = {},                # map of request headers to add
    **fields: Any,                     # additional fields to take over as they are
)

Will contain an “inline”, “local”, or “source” field depending on whether the content argument is a string, path, or URL. If content is a dict, all key value pairs from it will be copied into the result instead, overriding other fields.

The header dict will be converted into a list and inserted as “http_headers” field, if the contents specify a remote source.

Examples:

  • Specify contents inline: std.contents("foo")
  • Specify local file: std.contents(Path("/path/to/file.txt"))
  • Specify remote file: std.contents(URL("http://..."), headers={"Accept": "..."})

Parse content string into inline, path or URL value based on its format

std.contents.parse(
    content: str,  # path, URL, or inline content as string
)

Values starting with “/” or “./” will be returned as path objects. Values starting with one of Butane’s supported protocol names followed by “://” will be returned as URL objects. Everything else will be returned as is.

Examples:

  • Inline content: std.contents.parse("foo")
  • Local file: std.contents.parse("./bar.txt")
  • Remote file: std.contents.parse("https://example.com/baz.txt")

Create ownership fields as required for file system nodes

std.ownership(
    user: int | str = None,         # user ID or name
    group: int | str | None = ...,  # group ID or name
)

Integers are interpreted as user/group IDs and strings as user/group names. The group defaults to be the same as the user. Explicitly passing None disables setting the field in the result.

Examples:

  • Set user and group name to “core”: std.ownership("core")
  • Set only user ID to 1000: std.ownership(1000, None)

Create recursive merge of arbitrary python values

std.py.merge(
    base: Any,                  # base value to merge with
    *values: Any,               # values to merge into base
    merge_lists: bool = False,  # whether to merge lists
)

For anything but dicts and lists the last value is taken. The same is true for lists, unless merge_lists is set to True. Dicts are merged into each other with these rules applied recursively to each member.

Example:

  • Merge two dicts including a list: std.py.merge({"a": {"b": 13, "c": 69}, "users": ["root"]}, {"a": {"b": 42}, "users": ["core"]}, merge_lists=True)

Embed object in data structure according to specified path

std.py.hierarchy(
    path: str,  # path to embed object at
    obj: Any,   # object to embed
)

The path represents the dot-separated path the object should be placed at in the resulting structure. If it ends with “[]”, the object will be placed inside an array.

Example:

  • Wrap an object as in {"storage": {"files": [obj]}}: std.py.hierarchy("storage.files[]", obj)