Persistent Data Instances

The instance module implements the following classes:

Doctest snippets for this module use the data model and instance document from Example 2.

>>> dm = DataModel.from_file('yang-library-ex2.json',
... [".", "../../../yang-modules/ietf"])
>>> with open('example-data.json') as infile:
...   ri = json.load(infile)
>>> inst = dm.from_raw(ri)
class yangson.instance.InstanceNode(key: InstanceKey, value: Value, parinst: InstanceNode | None, schema_node: DataNode, timestamp: datetime.datetime)

The key argument is the key of the instance in the parent structure, i.e. either instance name for an ObjectMember or integer index for an ArrayEntry. The key becomes the last component of the path attribute. Other constructor arguments contain values for instance attributes of the same name.

This class and its subclasses implement the zipper interface for instance data along the lines of Gérard Huet’s original paper [Hue97], only adapted for the specifics of JSON-like structures. An important property of the zipper interface is that it makes the underlying data structure persistent: any changes to the data realized through the methods of the InstanceNode class return an updated copy of the original instance without changing the latter. As much as possible, the data are shared between the original instance and the updated copy.

Whilst the zipper interface slightly complicates access to instance data, it provides the advantages of persistent structures that are known from functional programming languages:

  • The structures are thread-safe.

  • It is easy to edit the data and then return to the original version, for example if new version isn’t valid according to the data model.

  • Staging datastores, such as candidate in NETCONF (sec. 8.3 in [RFC6241]) can be implemented in a space-efficient way.

Instance Attributes

parinst

Parent instance node, or None for the root node.

schema_node

Data node in the schema corresponding to the instance node.

timestamp

The date and time when the instance node was last modified.

value

Scalar or structured value of the node, see module instvalue.

The arguments of the InstanceNode constructor provide values for instance attributes of the same name.

Properties

name

The instance name of the receiver. For an ArrayEntry instance it is by definition the same as the qualified name of the parent ObjectMember.

namespace

The namespace identifier of the instance node.

path

Path of the instance in the data tree: a tuple containing keys of the ancestor nodes and the instance itself.

qual_name

The qualified name of the receiver. For an ArrayEntry instance it is by definition the same as the qualified name of the parent ObjectMember.

An InstanceNode structure can be created from scratch, or read from JSON text using DataModel.from_raw() (see the doctest snippet above).

The internal representation of InstanceNode values is very similar to the JSON encoding of data modelled with YANG [RFC7951]. In particular, member names have to be in the form specified in sec. 4 of that document:

member-name ::=  [identifier ":"] identifier

where the first identifier is a module name and the second is a data node name. The longer (namespace-qualified) form is used if and only if the member is defined in a different YANG module than its parent.

>>> inst.value["example-2:bag"]["bar"]
True

A structured InstanceNode value is represented as either ObjectValue (subclass of dict) or ArrayValue (subclass of list), see instvalue module for details. The representation of a scalar value depends on its type (see datatype module). Structured values, and some scalar values as well, are not the same as the values provided by the generic JSON parsing functions json.load() and json.loads(). Therefore, values read from JSON text need some additional processing, or “cooking”. Yangson methods such as DataModel.from_raw() take care of this step.

>>> type(inst.value)
<class 'yangson.instvalue.ObjectValue'>

Inside the larger structure of a data tree, an InstanceNode represents “focus” on a particular node of the structure. The focus can be moved to a neighbour instance node (parent, child, sibling) and the value of an instance node can be created, deleted and updated by using the methods described below. Each of the methods returns a new InstanceNode that shares, as much as possible, portions of the surrounding data tree with the original instance node. However, any modifications to the new instance node – if performed through the methods of the InstanceNode class and its subclasses – leave other instance nodes intact.

Most methods for moving the focus inside the zipper structure and updating the value of an instance node are defined in the InstanceNode, additional methods that are specific to an ObjectMember or ArrayEntry are defined in the respective class.

Public Methods

__str__() str

Return string representation of the receiver’s value.

If the receiver’s value is a scalar, then the result is the canonical representation of the value, if it is defined for the value’s type (see sec. 9 in [RFC7950]). Otherwise, the result is the value returned by Python standard function str.

__getitem__(key: InstanceKey) InstanceNode

This method allows for selecting receiver’s member or entry using square brackets as it is usual for other Python sequence types. The argument key is

  • an integer index, if the receiver’s value is an array (negative indices are also supported), or

  • an instance name, if the receiver’s value is an object.

The value returned by this method is either an ObjectMember or ArrayEntry.

This method raises InstanceValueError if receiver’s value is not structured, and NonexistentInstance if the member or entry identified by key doesn’t exist in the actual receiver’s value.

>>> bag = inst['example-2:bag']
>>> foo = bag['foo']
>>> foo.path
('example-2:bag', 'foo')
>>> bag['baz']
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/example-2:bag} member 'baz'
>>> foo6 = foo[0]
>>> foo6.value['number']
6
>>> fool = foo[-1]
>>> fool.value['in-words']
'eight'
>>> foo[4]
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/example-2:bag/foo} entry 4
__iter__()

Return receiver’s iterator.

If the receiver’s value is an object, then this method returns simply the value’s iterator that can be used exactly as a Python dictionary iterator:

>>> sorted([m for m in bag])
['bar', 'foo']

However, if the receiver’s value is an array, the returned iterator yields successive ArrayEntry instances:

>>> [e.json_pointer() for e in foo]
['/example-2:bag/foo/0', '/example-2:bag/foo/1', '/example-2:bag/foo/2', '/example-2:bag/foo/3']

An attempt to iterate over an InstanceNode that has a scalar value raises InstanceValueError.

json_pointer() JSONPointer

Return JSON Pointer [RFC6901] of the receiver.

>>> fool.json_pointer()
'/example-2:bag/foo/3'
instance_route() InstanceRoute

Return InstanceRoute of the receiver.

>>> str(fool.instance_route())
'/example-2:bag/foo[number="8"]'
is_internal() bool

Return True if the receiver is an instance of an internal schema node, i.e. its schema_node is an InternalNode. Otherwise return False.

>>> inst.is_internal()
True
put_member(name: InstanceName, value: RawValue | Value, raw: bool = False) InstanceNode

Return receiver’s member name with a new value specified by the value argument. The raw flag has to be set to True if value is a raw value.

If member name doesn’t exist in the receiver’s value, it is created (provided that the schema permits it).

This method raises InstanceValueError if the receiver’s value is not an object, and NonexistentSchemaNode if the schema doesn’t permit member name.

>>> nbar = bag.put_member('bar', False)
>>> nbar.value
False
>>> bag.value['bar']  # bag is unchanged
True
>>> e2bag = bag.put_member('baz', 3.1415926).up()  # baz is created
>>> sorted(e2bag.value.keys())
['bar', 'baz', 'foo']
>>> bag.put_member('quux', 0)
Traceback (most recent call last):
...
yangson.exceptions.NonexistentSchemaNode: quux under example-2:bag
delete_item(key: InstanceKey) InstanceNode

Return a new instance node that is an exact copy of the receiver, except that item key is deleted from its value.

This method raises InstanceValueError if the receiver’s value is a scalar, and:exc:~.NonexistentInstance if the item isn’t present in the actual receiver’s value.

>>> xbag = e2bag.delete_item('baz')
>>> sorted(xbag.value.keys())
['bar', 'foo']
>>> sorted(e2bag.value.keys())  # e2bag is unvchanged
['bar', 'baz', 'foo']
>>> xfoo = foo.delete_item(0)
>>> len(xfoo.value)
3
>>> len(foo.value)   # foo is unchanged
4
look_up(raw: bool = False, /, **keys: Dict[InstanceName, ScalarValue]) ArrayEntry

Return an instance node corresponding to the receiver’s entry with specified keys. The receiver must be a YANG list.

The keys are passed to this method as a sequence of Python keyword arguments in the form key=value where key is the instance name of a key, and value is the corresponding key value.

>>> foo8 = foo.look_up(number=8)
>>> foo8.json_pointer()
'/example-2:bag/foo/3'

Keyword arguments won’t work for keys with namespace-qualified names such as yangmod:index. In this case, the keys and values have to be packed in a dictionary and passed to the method as follows:

mylist.look_up(**{'yangmod:index': 42})

By default, the values of all keys are expected to be passed as cooked values. If raw values are passed instead, the raw flag has to be set to True. In this case, this flag must appear as the first positional argument so as to avoid interference with the remaining arguments that are specified in the key=value form:

>>> foo.look_up(True, number='8').json_pointer()
'/example-2:bag/foo/3'

Whilst this method is mainly intended for use with YANG list keys (as the number leaf in the example above) but, with a bit of caution, it can be used with any child nodes of the receiver:

>>> foo.look_up(prime=True)['number'].value
3

The first list entry that satisfies the look-up criteria is returned.

Note

Default values of leaves are always ignored by the look-up procedure. Therefore, if you need to take defaults into account, populate the receiver first with default values by using the add_defaults() method.

This method raises InstanceValueError if the receiver is not a YANG list, and NonexistentInstance if no entry with matching keys exists.

up() InstanceNode

Return an instance node corresponding to the receiver’s parent.

This method raises NonexistentInstance if the receiver is the root of the data tree and thus has no parent.

>>> foo.up().name
'example-2:bag'
>>> inst.up()
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/} up of top
top() InstanceNode

Return an instance node corresponding to the root of the data tree.

>>> e2inst = e2bag.top()
>>> e2inst.value['example-2:bag']['baz']
3.1415926
update(value: RawValue | Value, raw: bool = False) InstanceNode

Return a new instance node that is a copy of the receiver with a value specified by the value argument. The raw flag has to be set to True if value is a raw value.

>>> ebar = bag['bar'].update(False)
>>> ebar.value
False

In the following example, the string '2.7182818' is an acceptable raw value for the baz leaf whose type is decimal64 (see sec. 6.1 in [RFC7951]). Since the raw flag is set, the update() method “cooks” the raw value first into the Python’s decimal.Decimal type.

>>> e3baz = e2bag['baz'].update('2.7182818', raw=True)
>>> e3baz.value
Decimal('2.7182818')
>>> e2bag['foo'][0]['in-words'].update(66, raw=True)
Traceback (most recent call last):
...
yangson.exceptions.RawTypeError: {/example-2:bag/foo/0/in-words} expected string value
merge(value: RawValue | Value, raw: bool = False) InstanceNode

Return a new instance node whose value is the receiver’s value merged with the value argument, in the sense of the merge operation in [RFC8072]. The raw flag has to be set to True if value is a raw value.

The method uses (recursively) the following rules for merging instance values, as specified in [RFC7950], sections 7.5.8, 7.6.7, 7.7.9 and 7.8.6.

  • If the receiver is a leaf or anydata instance, the result is the same as for update(), i.e. the receiver’s value is overwritten by value.

  • Otherwise, receiver’s entries or members are matched against the entries or members in value using list keys, leaf-list values or member names. Matching entries/members are merged, and non-matching entries/members are copied from both sources into the result.

  • If the receiver is a list or leaf-list instance, then the non-matching entries from value are appended at the end. This is also true for lists/leaf-lists ordered by user (see section 7.7.7 in [RFC7950]).

>>> mfoo = foo.merge([{'number': '8', 'in-words': 'acht'},
... {'number': '9', 'in-words': 'nine'},
... {'number': '6', 'in-words': 'sechs'},
... {'number': '11', 'prime': True, 'in-words': 'eleven'}],
... raw = True)
>>> [en['in-words'].value for en in mfoo]
['sechs', 'three', 'seven', 'acht', 'nine', 'eleven']
>>> [en['in-words'].value for en in foo] # original instance doesn't change
['six', 'three', 'seven', 'eight']
goto(iroute: InstanceRoute) InstanceNode

Return an InstanceNode corresponding to a target instance arbitrarily deep inside the receiver’s value. The argument iroute is an InstanceRoute (relative to the receiver) that identifies the target instance.

The easiest way for obtaining an InstanceRoute is to parse it either from a resource identifier or instance identifier using methods DataModel.parse_resource_id() and DataModel.parse_instance_id(), respectively.

>>> irt = dm.parse_resource_id('/example-2:bag/foo=3/in-words')
>>> irt2 = dm.parse_instance_id('/example-2:bag/baz')

This method may raise the following exceptions:

>>> inst.goto(irt).value
'three'
>>> inst.goto(irt2)
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/example-2:bag} member 'baz'
peek(iroute: InstanceRoute) Value | None

Return the value of a target instance arbitrarily deep inside the receiver’s value. The argument iroute is an InstanceRoute (relative to the receiver) that identifies the target instance. None is returned if the target instance doesn’t exist.

>>> inst.peek(irt)
'three'

Caution

This method doesn’t create a new instance, so the access to the returned value should in general be read-only. Any modifications of the returned value also affect the receiver, as shown in the next example. This means that the persistence property for the receiver is lost.

>>> irt3 = dm.parse_resource_id('/example-2:bag/foo=3')
>>> e2inst.peek(irt3)['in-words'] = 'tres'
>>> e2inst.value['example-2:bag']['foo'][1]['in-words'] # changed!
'tres'
validate(scope: ValidationScope = ValidationScope.all, ctype: ContentType = ContentType.config) None

Perform validation on the receiver’s value. The scope argument determines the validation scope. The options are as follows:

  • ValidationScope.syntax – verifies schema constraints (taking into account if-feature and when statements, if present) and data types.

  • ValidationScope.semantics – verifies must constraints, uniqueness of list keys, unique constraints in list nodes, and integrity of leafref references.

  • ValidationScope.all – performs all checks from both items above.

The value of the ctype argument belongs to the ContentType enumeration and specifies whether the receiver’s value is to be validated as configuration (Content.config) or as both configuration and state data (Content.all).

The method returns None if the validation succeeds, otherwise one of the following exceptions is raised:

  • SchemaError – if the value doesn’t conform to the schema,

  • SemanticError – if the value violates a semantic constraint.

  • YangTypeError – if the value is a scalar of incorrect data type.

>>> inst.validate(ctype=ContentType.all) # no output means OK
>>> badinst = bag.put_member('baz', 'ILLEGAL').top()
>>> badinst.validate(ctype=ContentType.all)
Traceback (most recent call last):
...
yangson.exceptions.YangTypeError: {/example-2:bag/baz} invalid-type: expected decimal64

In the following example, member baz is not allowed because it is a conditional leaf and its when constraint evaluates to False.

>>> e2foo6 = e2bag['foo'][0]
>>> bad2 = e2foo6.update(
... {'number': '42', 'in-words': 'forty-two'}, raw=True).top()
>>> bad2.validate(ctype=ContentType.all)
Traceback (most recent call last):
...
yangson.exceptions.SchemaError: {/example-2:bag} member-not-allowed: baz
add_defaults(ctype: ContentType = None) InstanceNode

Return a new instance node that is a copy of the receiver extended with default values specified the data model. Only default values that are “in use” are added, see sections 7.6.1 and 7.7.2 in [RFC7950].

The argument ctype restricts the content type of data nodes whose default values will be added. For example, setting it to ContentType.config means that only default values of configuration nodes will be added. If ctype is None (default), the content type of added defaults will be the same as the content type of the receiver.

>>> wd = inst.add_defaults()
>>> wd.value['example-2:bag']['baz']
Decimal('0E-7')
>>> wd.value['example-2:bag']['foo'][0]['prime']
False
raw_value() RawValue

Return receiver’s value in a raw form (ready for JSON encoding).

>>> wd['example-2:bag']['baz'].raw_value()
'0.0'
class yangson.instance.RootNode(value: Value, schema_node: SchemaNode, timestamp: datetime.datetime)

Bases: InstanceNode

This class represents the root of the instance tree.

class yangson.instance.ObjectMember(key: InstanceName, siblings: Dict[InstanceName, Value], value: Value, parinst: InstanceNode, schema_node: DataNode, timestamp: datetime.datetime)

This class represents an instance node that is a member of an object. It is a subclass of InstanceNode. The additional constructor arguments name and siblings provide values for instance variables of the same name. Other arguments of the constructor have the same meaning as in InstanceNode.

Instance Attributes

siblings

Dictionary of the receiver’s siblings (other members of the parent object).

Public Methods

sibling(name: InstanceName) ObjectMember

Return the instance node corresponding to sibling member name.

This method raises NonexistentSchemaNode if member name is not permitted by the parent’s schema, and NonexistentInstance if sibling member name doesn’t exist.

>>> foo.sibling('bar').json_pointer()
'/example-2:bag/bar'
class yangson.instance.ArrayEntry(key: int, before: List[Value], after: List[Value], value: Value, parinst: InstanceNode, schema_node: DataNode, timestamp: datetime.datetime)

This class is a subclass of InstanceNode, and represents an instance node that is an entry of an array, i.e. list or leaf-list. The additional constructor arguments before and after provide values for instance variables of the same name. Other arguments have the same meaning as in InstanceNode.

Instance Attributes

before

Entries of the parent array that precede the receiver.

after

Entries of the parent array that follow the receiver.

Properties

index

The receiver’s index within the parent array.

>>> foo6.index
0
>>> foo6.name  # inherited from parent
'foo'

Public Methods

previous() ArrayEntry

Return an instance node corresponding to the previous entry in the parent array.

This method raises NonexistentInstance if the receiver is the first entry of the parent array.

>>> foo8.previous().json_pointer()
'/example-2:bag/foo/2'
>>> foo6.previous()
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/example-2:bag/foo[number="6"]} previous of first
next() ArrayEntry

Return an instance node corresponding to the next entry in the parent array.

This method raises NonexistentInstance if the receiver is the last entry of the parent array.

>>> foo6.next().json_pointer()
'/example-2:bag/foo/1'
>>> foo8.next()
Traceback (most recent call last):
...
yangson.exceptions.NonexistentInstance: {/example-2:bag/foo[number="8"]} next of last
insert_before(value: RawValue | Value, raw: bool = False) ArrayEntry

Insert a new entry before the receiver and return an instance node corresponding to the new entry. The value argument specifies the value of the new entry, and the raw flag has to be set to True if value is a raw value.

>>> foo4 = foo8.insert_before({'number': '4', 'in-words': 'four'}, raw=True)
>>> [en['number'] for en in foo4.up().value]
[6, 3, 7, 4, 8]
insert_after(value: RawValue | Value, raw: bool = False) ArrayEntry

Insert a new entry after the receiver and return an instance node corresponding to the new entry. The value argument specifies the value of the new entry, and the raw flag has to be set to True if value is a raw value.

>>> foo5 = foo4.insert_after({'number': '5', 'in-words': 'five'}, raw=True)
>>> [en['number'] for en in foo5.up().value]
[6, 3, 7, 4, 5, 8]
class yangson.instance.InstanceRoute(iterable=(), /)

Bases: list

This class represents a route into an instance value.

Instances of this class can be conveniently created by using one of the methods parse_resource_id() and parse_instance_id() in the DataModel class.

Public Methods

__str__() str

Return instance-id as the string representation of the receiver.

>>> str(irt)
'/example-2:bag/foo[number="3"]/in-words'
>>> str(irt2)
'/example-2:bag/baz'