Quick Start
Installation
Yardl is a single executable file. The installation steps are:
- Head over to the latest release page.
- Download the right archive for your platform.
- Extract the archive and find the
yardl
executable. Copy it to a directory in yourPATH
environment variable.
You should now be able to run yardl --version
.
Dependencies
The generated Python code requires Python 3.9 or newer and you need to have NumPy version 1.22.0 or later installed.
Getting our Feet Wet
Note
Yardl is currently based on YAML. If you are new to YAML, you can get an overview here.
To get started, create a new empty directory and cd
into it. Then run:
yardl init playground
yardl init playground
This creates the initial structure and files for our project:
$ tree .
.
└── model
├── model.yml
└── _package.yml
$ tree .
.
└── model
├── model.yml
└── _package.yml
The Yardl model package is in the model
directory.
_package.yml
is the package's manifest.
namespace: Playground
cpp:
sourcesOutputDir: ../cpp/generated
python:
outputDir: ../python
namespace: Playground
cpp:
sourcesOutputDir: ../cpp/generated
python:
outputDir: ../python
It specifies the package's namespace along with code generation settings. The python.outputDir
property specifies where the generated python package should go. If you are not interested in generating C++ code, you can remove the cpp
property from the file:
namespace: Playground
cpp:
sourcesOutputDir: ../cpp/generated
python:
outputDir: ../python
namespace: Playground
cpp:
sourcesOutputDir: ../cpp/generated
python:
outputDir: ../python
All other .yml
and .yaml
files in the directory are assumed to be yardl model files. The contents of model.yml
look like this:
# This is an example protocol, which is defined as a Header value
# followed by a stream of zero or more Sample values
MyProtocol: !protocol
sequence:
# A Header value
header: Header
# A stream of Samples
samples: !stream
items: Sample
# Header is a record with a single string field
Header: !record
fields:
subject: string
# Sample is a record made up of a datetime and
# a vector of integers
Sample: !record
fields:
# The time the sample was taken
timestamp: datetime
# A vector of integers
data: int*
# This is an example protocol, which is defined as a Header value
# followed by a stream of zero or more Sample values
MyProtocol: !protocol
sequence:
# A Header value
header: Header
# A stream of Samples
samples: !stream
items: Sample
# Header is a record with a single string field
Header: !record
fields:
subject: string
# Sample is a record made up of a datetime and
# a vector of integers
Sample: !record
fields:
# The time the sample was taken
timestamp: datetime
# A vector of integers
data: int*
!protocol
, !stream
and !record
are custom YAML tags, which describe the type of the YAML node that follows.
MyProtocol
is a protocol, which is a defined sequence of values that can be written to or read from a file or binary stream (e.g. over a network connection). This example protocol says that there will be one Header
value followed by an unknown number of Sample
s. Header
and Sample
are records.
To generate Python code for this model, cd
into the model
directory and run:
yardl generate
yardl generate
This will generate a Python package in the outputDir
directory:
$ tree .
.
└── playground
├── _binary.py
├── binary.py
├── _dtypes.py
├── __init__.py
├── _ndjson.py
├── ndjson.py
├── protocols.py
├── types.py
└── yardl_types.py
$ tree .
.
└── playground
├── _binary.py
├── binary.py
├── _dtypes.py
├── __init__.py
├── _ndjson.py
├── ndjson.py
├── protocols.py
├── types.py
└── yardl_types.py
yardl_types.py
contains definitions of primitive data types. types.py
contains the definitions of the non-protocol types defined in our model (in this case, Header
and Sample
). protocols.py
contains abstract protocol reader and writer classes, from which concrete implementations inherit from in binary.py
and ndjson.py
.
Ok, let's write some code! in our python
directory (containing the generated playground
directory), create run_playground
that looks like this:
from playground import (
BinaryMyProtocolWriter,
BinaryMyProtocolReader,
Header,
Sample,
DateTime,
)
def generate_samples():
yield Sample(timestamp=DateTime.now(), data=[1, 2, 3])
yield Sample(timestamp=DateTime.now(), data=[4, 5, 6])
path = "playground.bin"
with BinaryMyProtocolWriter(path) as w:
w.write_header(Header(subject="Me"))
w.write_samples(generate_samples())
with BinaryMyProtocolReader(path) as r:
print(r.read_header())
for sample in r.read_samples():
print(sample)
from playground import (
BinaryMyProtocolWriter,
BinaryMyProtocolReader,
Header,
Sample,
DateTime,
)
def generate_samples():
yield Sample(timestamp=DateTime.now(), data=[1, 2, 3])
yield Sample(timestamp=DateTime.now(), data=[4, 5, 6])
path = "playground.bin"
with BinaryMyProtocolWriter(path) as w:
w.write_header(Header(subject="Me"))
w.write_samples(generate_samples())
with BinaryMyProtocolReader(path) as r:
print(r.read_header())
for sample in r.read_samples():
print(sample)
You can inspect the binary file our code produced with:
hexdump -C playground.bin
hexdump -C playground.bin
Note that the binary file contains a JSON representation of the protocol's schema. This allows code that was not previously aware of this protocol to deserialize the contents.
In addition to the compact binary format, we can write the protocol out to an NDJSON file. This requires only a few modifications to our code:
from playground import (
BinaryMyProtocolWriter,
NDJsonMyProtocolWriter,
BinaryMyProtocolReader,
NDJsonMyProtocolReader,
Header,
Sample,
DateTime,
)
def generate_samples():
yield Sample(timestamp=DateTime.now(), data=[1, 2, 3])
yield Sample(timestamp=DateTime.now(), data=[4, 5, 6])
path = "playground.bin"
path = "playground.ndjson"
with BinaryMyProtocolWriter(path) as w:
with NDJsonMyProtocolWriter(path) as w:
w.write_header(Header(subject="Me"))
w.write_samples(generate_samples())
with BinaryMyProtocolReader(path) as r:
with NDJsonMyProtocolReader(path) as r:
print(r.read_header())
for sample in r.read_samples():
print(sample)
from playground import (
BinaryMyProtocolWriter,
NDJsonMyProtocolWriter,
BinaryMyProtocolReader,
NDJsonMyProtocolReader,
Header,
Sample,
DateTime,
)
def generate_samples():
yield Sample(timestamp=DateTime.now(), data=[1, 2, 3])
yield Sample(timestamp=DateTime.now(), data=[4, 5, 6])
path = "playground.bin"
path = "playground.ndjson"
with BinaryMyProtocolWriter(path) as w:
with NDJsonMyProtocolWriter(path) as w:
w.write_header(Header(subject="Me"))
w.write_samples(generate_samples())
with BinaryMyProtocolReader(path) as r:
with NDJsonMyProtocolReader(path) as r:
print(r.read_header())
for sample in r.read_samples():
print(sample)
Static Type Checking
The generated Python code that Yardl generates uses type hints extensively. If you would like to use a static type checker with your project that uses the generated code, we recommend Pyright.