Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Submodels #68

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions yang-modules/test/test-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"test:llistB": ["::1", "127.0.0.1"],
"test:leafX": 53531,
"test:contA": {
"leafB": 9,
"listA": [{
"leafE": "C0FFEE",
"leafF": true,
"contD": {
"leafG": "foo1-bar",
"contE": {
"leafJ": [null],
"leafP": 10
}
}
},
{
"leafE": "ABBA",
"leafW": 9,
"leafF": false
}],
"testb:leafS":
"/test:contA/listA[leafE='C0FFEE'][leafF='true']/contD/contE/leafP",
"testb:leafR": "C0FFEE",
"testb:leafT": "test:CC-BY",
"testb:leafV": 99,
"testb:leafN": "hi!"
},
"test:contT": {
"bits": "dos cuatro",
"decimal64": 4.50,
"enumeration": "Hearts"
}
}
13 changes: 13 additions & 0 deletions yang-modules/test/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import json
from yangson import DataModel
from yangson.enumerations import ContentType
import xml.etree.ElementTree as ET

dm = DataModel.from_file(
'yang-library.json', ['.', '../ietf'])
with open('test-data.json') as infile:
ri = json.load(infile)
inst = dm.from_raw(ri)
root = inst.to_xml()

print(ET.tostring(root))
59 changes: 56 additions & 3 deletions yangson/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
from typing import Optional, Tuple
import xml.etree.ElementTree as ET
from .enumerations import ContentType
from .exceptions import BadYangLibraryData
from .exceptions import BadYangLibraryData, BadRootNode
from .instance import (InstanceRoute, InstanceIdParser, ResourceIdParser,
RootNode)
from .schemadata import SchemaData, SchemaContext
from .schemanode import DataNode, SchemaTreeNode, RawObject, SchemaNode
from .schemanode import DataNode, SchemaTreeNode, RawObject, SchemaNode, ContainerNode
from .typealiases import DataPath, SchemaPath


Expand Down Expand Up @@ -80,14 +80,51 @@ def __init__(self, yltxt: str, mod_path: Tuple[str] = (".",),
self.yang_library = json.loads(yltxt)
except json.JSONDecodeError as e:
raise BadYangLibraryData(str(e)) from None
self.schema_data = SchemaData(self.yang_library, mod_path)
self.schema_data = SchemaData(self.yang_library, list(mod_path))
self.schema = SchemaTreeNode(self.schema_data)
self.schema._ctype = ContentType.all
self._build_schema()
self.schema.description = description if description else (
"Data model ID: " +
self.yang_library["ietf-yang-library:modules-state"]
["module-set-id"])
self.subschema = {}

def add_submodel(self, container: ContainerNode, submodel: "DataModel"):
if container.schema_root() != self.schema:
raise BadRootNode(container.iname())

# update yang library
yl_modules = self.yang_library['ietf-yang-library:modules-state']['module']
existing = list()
for module in yl_modules:
existing.append((module['name'], module['revision']))

sm_modules = submodel.yang_library['ietf-yang-library:modules-state']['module']
for module in sm_modules:
if (module['name'], module['revision']) not in existing:
yl_modules.append(module)

# update schema data
self.schema_data.add(submodel.schema_data)

# update container
for subchild in submodel.schema.children:
container.children.append(subchild)
subchild.parent = container

self.schema.subschema[(subchild.name, subchild.ns)] = subchild.data_path()
if subchild.mandatory:
container._mandatory_children.add(subchild)

if self.schema.description.startswith('Data model ID: '):
self.schema.description = (
"Data model ID: " +
self.yang_library["ietf-yang-library:modules-state"]
["module-set-id"])

# rebuild schema patterns
self.schema._make_schema_patterns()

def module_set_id(self) -> str:
"""Compute unique id of YANG modules comprising the data model.
Expand Down Expand Up @@ -174,9 +211,25 @@ def clear_val_counters(self):
self.schema.clear_val_counters()

def parse_instance_id(self, text: str) -> InstanceRoute:
split = text.split('/')
ns, sep, name = split[1].partition(':')

if (name, ns) in self.schema.subschema:
text = self.schema.subschema[(name, ns)]
if len(split) >= 2:
text = text + '/' + '/'.join(split[2:])

return InstanceIdParser(text).parse()

def parse_resource_id(self, text: str) -> InstanceRoute:
split = text.split('/')
ns, sep, name = split[1].partition(':')

if (name, ns) in self.schema.subschema:
text = self.schema.subschema[(name, ns)]
if len(split) >= 2:
text = text + '/' + '/'.join(split[2:])

return ResourceIdParser(text, self.schema).parse()

def schema_digest(self) -> str:
Expand Down
4 changes: 4 additions & 0 deletions yangson/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ def __str__(self) -> str:
return f"{prefix}{self.name} under {super().__str__()}"


class BadRootNode(SchemaNodeException):
'''Wrong root node of schema node'''
pass

class BadSchemaNodeType(SchemaNodeException):
"""A schema node is of a wrong type."""

Expand Down
63 changes: 50 additions & 13 deletions yangson/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@

__all__ = ["InstanceNode", "RootNode", "ObjectMember", "ArrayEntry",
"InstanceIdParser", "ResourceIdParser", "InstanceRoute",
"InstanceException", "InstanceValueError", "NonexistentInstance"]
"InstanceException", "InstanceValueError", "NonexistentInstance",
"OutputFilter"]


class OutputFilter:
Expand Down Expand Up @@ -236,7 +237,8 @@ def ita():
return ita()
if isinstance(self.value, ObjectValue):
return iter(self._member_names())
raise InstanceValueError(self.json_pointer(),
raise InstanceValueError(
self.json_pointer(),
"{} is a scalar instance".format(str(type(self.value))))

def is_internal(self) -> bool:
Expand Down Expand Up @@ -340,19 +342,27 @@ def goto(self, iroute: "InstanceRoute") -> "InstanceNode":
inst = sel.goto_step(inst)
return inst

def peek(self, iroute: "InstanceRoute") -> Optional[Value]:
"""Return a value within the receiver's subtree.
def full_peek(self, iroute: "InstanceRoute") -> Optional[Tuple]:
"""Return a value and schema within the receiver's subtree.

Args:
iroute: Instance route (relative to the receiver).
"""
val = self.value
sn = self.schema_node
for sel in iroute:
val, sn = sel.peek_step(val, sn)
if val is None:
return None
return val
return (None, None)
val, sn = sel.peek_step(val, sn)
return (val, sn)

def peek(self, iroute: "InstanceRoute") -> Optional[Value]:
"""Return a value within the receiver's subtree.

Args:
iroute: Instance route (relative to the receiver).
"""
return self.full_peek(iroute)[0]

def validate(self, scope: ValidationScope = ValidationScope.all,
ctype: ContentType = ContentType.config) -> None:
Expand Down Expand Up @@ -486,9 +496,10 @@ def to_xml(self, filter: OutputFilter = OutputFilter(), elem: ET.Element = None)
if elem is None:
element = ET.Element(self.schema_node.name)

module = self.schema_data.modules_by_name.get(self.schema_node.ns)
ns = self.schema_node.ns
module = self.schema_data.modules_by_name.get(ns)
if not module:
raise MissingModuleNamespace(self.schema_node.ns)
raise MissingModuleNamespace(ns if ns else 'None')
element.attrib['xmlns'] = module.xml_namespace
else:
element = elem
Expand Down Expand Up @@ -565,9 +576,10 @@ def _member_names(self) -> List[InstanceName]:
return [m for m in self.value if not m.startswith("@")]

def _member(self, name: InstanceName) -> "ObjectMember":
pts = name.partition(":")
if pts[1] and pts[0] == self.namespace:
name = pts[2]
if not type(self) is RootNode:
pts = name.partition(":")
if pts[1] and pts[0] == self.namespace:
name = pts[2]
sibs = self.value.copy()
try:
return ObjectMember(
Expand Down Expand Up @@ -678,6 +690,7 @@ def __init__(self, value: Value, schema_node: "DataNode",
schema_data: "SchemaData", timestamp: datetime):
super().__init__("/", value, None, schema_node, timestamp)
self.schema_data = schema_data
"""Dictionary of subschema root qnames to subschema paths"""

def up(self) -> None:
"""Override the superclass method.
Expand Down Expand Up @@ -1005,6 +1018,20 @@ def peek_step(self, val: ObjectValue,
val: Current value (object).
sn: Current schema node.
"""
qn = (self.name, self.namespace)
if isinstance(sn, SchemaTreeNode) and qn in sn.subschema:
path = sn.subschema.get((self.name, self.namespace))
c_schema = sn
c_value = val
try:
for dp in path.split('/'):
name, sep, ns = dp.partition(':')
c_schema = c_schema.get_data_child(name, ns if ns else None)
c_value = c_value[c_schema.iname()]
return c_value, c_schema
except (IndexError, KeyError, TypeError):
return (None, c_schema)

cn = sn.get_data_child(self.name, self.namespace)
try:
return (val[cn.iname()], cn)
Expand All @@ -1017,6 +1044,15 @@ def goto_step(self, inst: InstanceNode) -> InstanceNode:
Args:
inst: Current instance.
"""
sn = inst.schema_node
qn = (self.name, self.namespace)

if isinstance(sn, SchemaTreeNode)and qn in sn.subschema:
path = sn.subschema.get((self.name, self.namespace))
child = inst
for dp in path[1:].split('/'):
child = child[dp]
return child
return inst[self.iname()]


Expand Down Expand Up @@ -1323,4 +1359,5 @@ def _key_predicates(self) -> EntryKeys:
AnyContentNode,AnydataNode, CaseNode,
ChoiceNode, DataNode,
InternalNode, LeafNode, LeafListNode, ListNode,
RpcActionNode, SequenceNode, TerminalNode)
RpcActionNode, SchemaTreeNode, SequenceNode, TerminalNode,
)
13 changes: 12 additions & 1 deletion yangson/schemadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,22 @@ def __init__(self, yang_lib: Dict[str, Any], mod_path: List[str]) -> None:
"""Dictionary of module data."""
self.modules_by_name = {} # type: Dict[str, ModuleData]
"""Dictionary of module data by module name."""
self.modules_by_ns = {}
self.modules_by_ns = {} # type: Dict[str, ModuleData]
"""Dictionary of module data by xml namespace."""
self._module_sequence = [] # type: List[ModuleId]
"""List that defines the order of module processing."""
self._from_yang_library(yang_lib)

def add(self, child: "SchemaData"):
"""Add the schemadata of a subschema to this one"""
self.identity_adjs = {**self.identity_adjs, **child.identity_adjs}
self.implement = {**self.implement, **child.implement}
self.module_search_path.extend(child.module_search_path)
self.modules = {**self.modules, **child.modules}
self.modules_by_name = {**self.modules_by_name, **child.modules_by_name}
self.modules_by_ns = {**self.modules_by_ns, **child.modules_by_ns}
self._module_sequence.extend(child._module_sequence)

def _from_yang_library(self, yang_lib: Dict[str, Any]) -> None:
"""Set the schema structures from YANG library data.

Expand Down
10 changes: 6 additions & 4 deletions yangson/schemanode.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,7 @@ def from_raw(self, rval: RawObject, jptr: JSONPointer = "", allow_nodata: bool =
res[qn] = self._process_metadata(rval[qn], jptr)
else:
cn = self._iname2qname(qn)
if allow_nodata:
ch = self.get_child(*cn)
else:
ch = self.get_data_child(*cn)
ch = self.get_data_child(*cn)
npath = jptr + "/" + qn
if ch is None:
raise RawMemberError(npath)
Expand Down Expand Up @@ -799,11 +796,15 @@ def __init__(self, schemadata: "SchemaData" = None):
super().__init__()
self.annotations: Dict[QualName, Annotation] = {}
self.schema_data = schemadata
self.subschema: Dict[QualName, "InstanceRoute"] = {}

def data_parent(self) -> InternalNode:
"""Override the superclass method."""
return self.parent

def add_subschema(self, container: SchemaNode):
self.subschema[container.qual_name] = container.data_path()

def _annotation_stmt(self, stmt: Statement, sctx: SchemaContext) -> None:
"""Handle annotation statement."""
if not sctx.schema_data.if_features(stmt, sctx.text_mid):
Expand Down Expand Up @@ -1137,6 +1138,7 @@ def _process_xmlarray_child(
res.append(child)
else:
child = None

return child

def entry_from_raw(self, rval: RawEntry,
Expand Down