.. testcleanup:: os.chdir("../..")
This section contains a series of hands-on examples that illustrate basic ways of using the Yangson library. The snippets below should be enough to get a moderately experienced Python programmer going. The examples use the YANG data model and instance document from :ref:`sec-ex2`. Of course, the reader is encouraged to continue experimenting on his or her own.
First, let's import the necessary Python modules and symbols, and go to the appropriate directory:
>>> import os
>>> import json
>>> from yangson import DataModel
>>> os.chdir("examples/ex2")
Next, we need to initialize the YANG data model. It can be done easily by reading YANG library data [RFC7895]_ from a file:
>>> dm = DataModel.from_file('yang-library-ex2.json',
... [".", "../../../yang-modules/ietf"])
>>> dm.schema.description
'Data model ID: 9a9b7d2d28d4d78fa42e12348346990e3fb1c1b9'
Note
YANG library data can be created by running yangson-mkylib.
Here is an ASCII art depicting the schema tree:
>>> print(dm.ascii_tree(), end='')
+--rw example-2:bag
+--rw bar <boolean>
+--rw baz? <decimal64>
+--rw foo* [number]
+--rw in-words? <string>
+--rw number <uint8>
+--rw prime? <boolean>
Now, we can load a JSON-encoded instance document
>>> with open('example-data.json') as infile:
... ri = json.load(infile)
>>> inst = dm.from_raw(ri)
and validate it against the data model:
>>> inst.validate()
No output means that the validation was successful.
We can now print the ASCII tree again, this time without showing the types but instead displaying validation counters that indicate how many times each schema node has been used for validating instances. This is useful for assessing the coverage of instance data with respect to the schema.
>>> print(dm.ascii_tree(no_types=True, val_count=True), end='')
+--rw example-2:bag {1}
+--rw bar {1}
+--rw baz? {0}
+--rw foo* [number] {4}
+--rw in-words? {4}
+--rw number {4}
+--rw prime? {2}
We can move around the instance data tree, either step by step or directly to any location by using an :class:`~.instance.InstanceRoute`. One way to obtain the latter is to parse it from a RESTCONF :term:`resource identifier`:
>>> irt = dm.parse_resource_id('/example-2:bag/foo=3/in-words')
>>> type(irt)
<class 'yangson.instance.InstanceRoute'>
No we can go straight to the desired spot, see that we are really there, and inspect the value of that instance:
>>> inw = inst.goto(irt)
>>> inw.json_pointer()
'/example-2:bag/foo/1/in-words'
>>> inw.value
'three'
We can also change the value:
>>> inw2 = inw.update('forty-two')
>>> inw2.value
'forty-two'
Instance data is represented as a data structure
called zipper [Hue97]_. This structure is persistent, which means
that invoking the :meth:`~.InstanceNode.update` method on the inw
instance results in a new instance, and inw hasn't changed at
all – it contains the value of three
as before:
>>> inw.value
'three'
We can move from the new instance inw2 back to the top, thus obtaining an edited version of the original data tree:
>>> inst2 = inw2.top()
We expect the two data trees to differ in the value of in-words leaf that we modified. To verify it, we can once again use the :class:`~.instance.InstanceRoute` irt that we compiled previously, this time with the :meth:`~.InstanceNode.peek` method:
>>> inst.peek(irt)
'three'
>>> inst2.peek(irt)
'forty-two'
Another nice property of the zipper structure is that the two data trees share their contents to the maximum possible extent – it's kind of copy on write.
So, the new data tree differs from the original but it is nevertheless still valid:
>>> inst2.validate()
We can also add default values as specified in the data model to both data trees:
>>> iwd = inst.add_defaults()
>>> i2wd = inst2.add_defaults()
Again, it is worth noting that we get new instances whilst the original ones (inst and inst2) haven't been touched.
The YANG module example-2 defines a default value of 0
for
the baz leaf:
leaf baz {
when "not(../foo/in-words = 'forty-two')";
type decimal64 {
fraction-digits "7";
}
default "0";
}
So let's see if that default value is in place:
>>> iwd['example-2:bag']['baz'].value
Decimal('0E-7')
Indeed it is – Decimal('0E-7')
is just a fancy way of writing
decimal zero.
However, if we try the same for the other data tree, we don't find the baz instance:
>>> i2wd['example-2:bag']['baz'].value
Traceback (most recent call last):
...
yangson.instance.NonexistentInstance: [/example-2:bag] member 'baz'
Why is that? The reason is also hidden in the above definition of
the baz leaf: due to the edit that we made, the when expression
becomes False
, the baz leaf isn't therefore valid, and so the
default value doesn't apply.
Let's also try to violate the data model schema in various ways. First, we modify the name sibling of our inw instance, which happens to be the key of the foo list:
>>> broken1 = inw.sibling('number').update(6).top()
>>> broken1.validate()
Traceback (most recent call last):
...
yangson.schemanode.SemanticError: [/example-2:bag/foo] non-unique-key: 6
Correct! Both entries of the foo list now have the same key, namely 6
.
Other thing that YANG doesn't permit is to install a leaf value that doesn't conform to the leaf's type, as in the following example:
>>> inw.update('INFINITY').validate()
Traceback (most recent call last):
...
yangson.schemanode.YangTypeError: [/example-2:bag/foo/1/in-words] invalid-type: must be number in words
This is again correct because the new value INFINITY
doesn't match
the regular expression pattern in the definition of the in-words
leaf. Note that the traceback displays the custom error message that
is defined for the pattern.
And note also that validation needn't be performed only on entire data
trees, it can start from any instance node (inw
in this case) and
check just its subtree.
And finally, we delete a leaf that's defined as mandatory in the data model:
>>> broken2 = inw.up().up().up().delete_item('bar').top()
>>> broken2.validate()
Traceback (most recent call last):
...
yangson.schemanode.SchemaError: [/example-2:bag] missing-data: expected 'bar'