Persistent Data Instances
The instance module implements the following classes:
InstanceNode
: Abstract class for instance nodes.RootNode
: Root of the data tree.ObjectMember
: Instance node that is an object member.ArrayEntry
: Instance node that is an array entry.InstanceRoute
: Route into an instance value.
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 anArrayEntry
. The key becomes the last component of thepath
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.
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 parentObjectMember
.
- 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 parentObjectMember
.
An
InstanceNode
structure can be created from scratch, or read from JSON text usingDataModel.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 eitherObjectValue
(subclass ofdict
) orArrayValue
(subclass oflist
), seeinstvalue
module for details. The representation of a scalar value depends on its type (seedatatype
module). Structured values, and some scalar values as well, are not the same as the values provided by the generic JSON parsing functionsjson.load()
andjson.loads()
. Therefore, values read from JSON text need some additional processing, or “cooking”. Yangson methods such asDataModel.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 newInstanceNode
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 theInstanceNode
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 anObjectMember
orArrayEntry
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
orArrayEntry
.This method raises
InstanceValueError
if receiver’s value is not structured, andNonexistentInstance
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 raisesInstanceValueError
.
- 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. itsschema_node
is anInternalNode
. Otherwise returnFalse
.>>> 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, andNonexistentSchemaNode
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
wherekey
is the instance name of a key, andvalue
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 thekey=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, andNonexistentInstance
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, theupdate()
method “cooks” the raw value first into the Python’sdecimal.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 anInstanceRoute
(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 methodsDataModel.parse_resource_id()
andDataModel.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:
InstanceValueError
if iroute isn’t compatible with the schemaNonexistentInstance
if the target instance doesn’t exist in the receiver’s valueNonDataNode
if the target instance represents an RPC operation, action or notification (iroute can come from a RESTCONF resource identifier).
>>> 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 toFalse
.>>> 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 isNone
(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 inInstanceNode
.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, andNonexistentInstance
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 inInstanceNode
.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()
andparse_instance_id()
in theDataModel
class.Public Methods