YAML Templating Language
Project description
Summary
YATL is a templating language in which both the input and output are YAML. This solves the common problem of wanting to have a template that produces YAML files, but are usually solved by using an templating framework (Go templates, Jinja2, etc.), thus making the input not YAML. This means you can no longer lint your input, or load it in an IDE without confusing it. It also means that your template is probably tied to the specific language in which your toolchain is written.
YATL aims to be both a standard YAML-in, YAML-out templating language, and a library to load files. This codebase is a Python implementation, but the plan is to make a core library with bindings for many languages.
This is a work in progress. See the status section below for details.
Installation
$ pip install PyYATL
Usage
>>> import yatl
>>> yatl.load("""
... hosts:
... - .for (host in west_hosts):
... .(host)
... - .for (host in east_hosts):
... .(host)
... """, {"west_hosts": ["west-1", "west-2"], "east_hosts": ["east-1", "east-2"]})
{'hosts': ['west-1', 'west-2', 'east-1', 'east-2']}
The YATL Language
This section gives an overview of the YATL syntax. For more details, see the complete documentation (coming soon).
All YATL directives start with a .
.
Interpolation
When .(p)
is seen in a value, it is replaced with the parameter value of p
.
Example:
environment: .(env)
deployment_name: .(service_name)-.(env)
If env = production
and service_name = foo
, then the output would be:
environment: production
deployment_name: foo-production
Conditionals
Example:
deployment_type: canary
.if (is_production):
alert-email: page_me@example.com
If is_production = true
, then the output is:
deployment_type: canary
alert-email: page_me@example.com
You can also have .elif
and .else
:
.if (is_production):
slack-channel: "#production"
.elif (is_staging):
slack-channel: "#staging"
.else:
slack-channel: "#development"
You can also use .if
in lists. This is a special case where the value within the .if
will extend the outer list:
hosts:
- west-1
- west-2
- .if (multi_data_center):
- east-1
- east-2
Assuming multi_data_center = true
, this would output:
hosts:
- west-1
- west-2
- east-1
- east-2
If you actually want a list within a list when using .if
, you need to add an extra list wrapping the .if
.
For Loops
For loops allow you to loop over values:
hosts:
.for (host in hosts):
.(host)
If hosts = ["west-1", "west-2"]
, then the output would be:
hosts:
- west-1
- west-2
For loops always return lists, so the syntax is a bit loose. The following are both equivalent:
hosts:
.for (host in hosts):
.(host)
hosts:
- .for (host in hosts):
.(host)
Like .if
, they extend the outer list, so you can combine for loops into a single list:
hosts:
- .for (host in west_hosts):
.(host)
- .for (host in east_hosts):
.(host)
Assuming the obvious assignments, this outputs:
hosts:
- west-1
- west-2
- east-1
- east-2
Loading Files
YATL allows including files, to make it easier to organize otherwise large YAML files.
The basic idea is that if you load a YATL file like this:
foo: bar
.load: some-file.yaml
And if some-file.yaml
looks like this:
baz: quux
Then you'll get this:
foo: bar
baz: quux
If you want to load more than one file in the same object, you can also load lists of files:
.load:
- defs.yaml
- resource_types.yaml
- resources.yaml
Loaded files can also load other files recursively.
If files contain the same fields as the object they're loaded into, then whatever field is seen last will be the
one used in the output. There is no deep merging of nested objects done with .load
. You can however load deeply
nested objects and merge specific nested fields with .load_defaults_from
.
Files loaded with .load_defaults_from
are always considered defaults. Hence, if a file has fields in common
with loaded defaults, then the file doing the loading always wins out. Otherwise objects are merged. For example,
say we have this in a file called config.yaml
:
outer:
.load_defaults_from: some-file.yaml
inner:
foo: bar
If some-file.yaml
looks like this:
inner:
foo: baz
Then the result will be this (fields in both config.yaml
and some-file.yaml
are taken from config.yaml
, because
loads are always defaults):
outer:
inner:
foo: bar
If some-file.yaml
looks like this instead:
inner:
baz: quux
Then the result would be this (fields in objects are merged):
outer:
inner:
foo: bar
baz: quux
If inner
was not an object (e.g., it's a list) in either file, then no merging will happen, and whatever is in
config.yaml
will be the result.
Lastly, if a file loads two or more files which both have defaults for the same field, then whichever is loaded at the highest nesting level will win. For example, if we have:
outer:
.load_defaults_from: file1.yaml
inner:
load_defaults_from: file2.yaml
If both file1.yaml
and file2.yaml
have defaults for the same field (which would have to be inside inner
), then the
defaults from file2.yaml
will take precendence.
Definitions
Definitions in YATL are an improvement over anchors in YAML. They're a bit like a function:
.def email_on_failure(email):
.if (is_production):
on-failure:
alert-email: .(email)
tasks:
- test:
command: run_tests.sh
.use email_on_failure: tests@example.com
- deploy:
command: do_deploy.sh
.use email_on_failure: deploys@example.com
If is_production = true
, then the output will be:
tasks:
- test:
command: run_tests.sh
on-failure:
alert-email: tests@example.com
- deploy:
command: do_deploy.sh
on-failure:
alert-email: deploys@example.com
If is_production = false
then the on-failure
parts will be left out.
Definitions are more powerful than anchors because you can parameterize them. They're also cleaner because, unlike anchors, the definition doesn't remain in the output. Only the usages are in the output.
Definitions can have zero to any number of arguments. If they have zero arguments, then pass an empty string, list, or object as the argument when using it (this is just so the syntax is valid YAML):
.def replicas:
.if (is_production): 3
.else: 1
services:
.for (s in services):
name: .(s)
replicas:
.use replicas: {}
If is_production = true
and services = ["foo", "bar"]
, then the output will be:
services:
- name: foo
replicas: 3
- name: bar
replicas: 3
If there are multiple arguments, they can be passed as an object or list:
.def task(name, command):
name: .(name)
command: .(command)
container: ubuntu
.if (is_production):
on-failure:
alert-email: errors@example.com
tasks:
- .use task: # Pass args as an object
name: build
command: build.sh
- .use task: # Pass args as a list
- test
- test.sh
- .use task: [deploy, deploy.sh] # Shorthand list
If is_production = false
, the output will be:
tasks:
- name: build
command: build.sh
container: ubuntu
- name: test
command: test.sh
container: ubuntu
- name: deploy
command: deploy.sh
container: ubuntu
Status
⚠️ The language spec is likely to change at least slightly.
- Proof of concept
- Support safe expressions
- Polish (allow escaping, etc.)
- Complete documentation
- Include line number with error messages and don't stop at the first error
- Support Python versions other than CPython 3.6 and Python 3.7+ (because of dict ordering)
- Support other programming languages
This software should be considered beta.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file PyYATL-0.7.0.tar.gz
.
File metadata
- Download URL: PyYATL-0.7.0.tar.gz
- Upload date:
- Size: 12.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.0.9 CPython/3.7.0 Linux/4.19.121-linuxkit
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2db16bd551c440a052b2b9934aee357586a3d38df36bc49b1fcffed7d80375dc |
|
MD5 | 44d8c0949345050054199038bb4bd0c1 |
|
BLAKE2b-256 | 26d133b2b0f5ca58ceaed7e17e142a647d0109eeb1518c360393bae6679f2188 |
File details
Details for the file PyYATL-0.7.0-py3-none-any.whl
.
File metadata
- Download URL: PyYATL-0.7.0-py3-none-any.whl
- Upload date:
- Size: 9.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.0.9 CPython/3.7.0 Linux/4.19.121-linuxkit
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | b15be33ee5b185ceafc83b594f2571e0c6cd88072239e78f2efb2e2639f0cc1e |
|
MD5 | 22ff073b2896ce26d200c1913883bd37 |
|
BLAKE2b-256 | 68c418a870356f6829c84c80627ceacff90d5de0bb3bf3dc20e97ef678be149a |