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

Add map access feature for lists #36

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
19 changes: 16 additions & 3 deletions yangson/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import hashlib
import json
from typing import Optional, Tuple
import xml.etree.ElementTree as ET
from .enumerations import ContentType
from .exceptions import BadYangLibraryData
from .instance import (InstanceRoute, InstanceIdParser, ResourceIdParser,
Expand Down Expand Up @@ -75,13 +76,13 @@ def __init__(self, yltxt: str, mod_path: Tuple[str] = (".",),
ModuleNotFound: If a YANG module wasn't found in any of the
directories specified in `mod_path`.
"""
self.schema = SchemaTreeNode()
self.schema._ctype = ContentType.all
try:
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 = SchemaTreeNode(self.schema_data)
self.schema._ctype = ContentType.all
self._build_schema()
self.schema.description = description if description else (
"Data model ID: " +
Expand All @@ -107,7 +108,19 @@ def from_raw(self, robj: RawObject) -> RootNode:
Root instance node.
"""
cooked = self.schema.from_raw(robj)
return RootNode(cooked, self.schema, cooked.timestamp)
return RootNode(cooked, self.schema, self.schema_data, cooked.timestamp)

def from_xml(self, root: ET.Element) -> RootNode:
"""Create an instance node from a raw data tree.

Args:
robj: Dictionary representing a raw data tree.

Returns:
Root instance node.
"""
cooked = self.schema.from_xml(root)
return RootNode(cooked, self.schema, self.schema_data, cooked.timestamp)

def get_schema_node(self, path: SchemaPath) -> Optional[SchemaNode]:
"""Return the schema node addressed by a schema path.
Expand Down
43 changes: 41 additions & 2 deletions yangson/datatype.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@
import base64
import decimal
import numbers
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING

from .constraint import Intervals, Pattern
from .exceptions import (
InvalidArgument, ParserException, ModuleNotRegistered, UnknownPrefix,
InvalidLeafrefPath)
InvalidLeafrefPath, MissingModuleNamespace)
from .schemadata import SchemaContext
from .instance import InstanceNode, InstanceIdParser, InstanceRoute
from .statement import Statement
Expand Down Expand Up @@ -97,10 +98,25 @@ def from_raw(self, raw: RawScalar) -> Optional[ScalarValue]:
if isinstance(raw, str):
return raw

def from_xml(self, xml: ET.Element) -> Optional[ScalarValue]:
"""Return a cooked value of the received XML type.

Args:
xml: Text of the XML node
"""
return self.from_raw(xml.text)

def to_raw(self, val: ScalarValue) -> Optional[RawScalar]:
"""Return a raw value ready to be serialized in JSON."""
return val

def to_xml(self, val: ScalarValue) -> Optional[str]:
"""Return XML text value ready to be serialized in XML."""
value = self.to_raw(val)
if value is not None:
return str(value)
return None

def parse_value(self, text: str) -> Optional[ScalarValue]:
"""Parse value of the receiver's type.

Expand Down Expand Up @@ -229,6 +245,13 @@ def from_raw(self, raw: RawScalar) -> Optional[Tuple[None]]:
if raw == [None]:
return (None,)

def from_xml(self, xml: str) -> Optional[Tuple[None]]:
if xml == '':
return (None,)

def to_xml(self, val: Tuple[None]) -> None:
return None


class BitsType(DataType):
"""Class representing YANG "bits" type."""
Expand Down Expand Up @@ -582,6 +605,22 @@ def from_raw(self, raw: RawScalar) -> Optional[QualName]:
return None
return (i2, i1) if s else (i1, self.sctx.default_ns)

def from_xml(self, xml: ET.Element) -> Optional[QualName]:
try:
i1, s, i2 = xml.text.partition(":")
except AttributeError:
return None
if not i1:
return (i2, self.sctx.default_ns)

ns_url = xml.attrib.get('xmlns:'+i1)
if not ns_url:
raise MissingModuleNamespace(ns_url)
module = self.sctx.schema_data.modules_by_ns.get(ns_url)
if not module:
raise MissingModuleNamespace(ns_url)
return (i2, module.main_module[0])

def __contains__(self, val: QualName) -> bool:
for b in self.bases:
if not self.sctx.schema_data.is_derived_from(val, b):
Expand Down Expand Up @@ -719,7 +758,7 @@ def parse_value(self, text: str) -> Optional[int]:
return None

def from_raw(self, raw: RawScalar) -> Optional[int]:
if not isinstance(raw, int) or isinstance(raw, bool):
if not isinstance(raw, (int, bool, str)):
return None
try:
return int(raw)
Expand Down
10 changes: 10 additions & 0 deletions yangson/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ def __str__(self) -> str:
return self.name


class MissingModuleNamespace(YangsonException):
"""Abstract exception class – a module is missing."""

def __init__(self, ns: str):
self.ns = ns

def __str__(self) -> str:
return self.ns


class ModuleContentMismatch(YangsonException):
"""Abstract exception class – unexpected module name or revision."""

Expand Down
142 changes: 136 additions & 6 deletions yangson/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@

from datetime import datetime
import json
from typing import Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import unquote
import xml.etree.ElementTree as ET
from .enumerations import ContentType, ValidationScope
from .exceptions import (BadSchemaNodeType, EndOfInput, InstanceException,
InstanceValueError, InvalidKeyValue,
MissingModuleNamespace,
NonexistentInstance, NonDataNode,
NonexistentSchemaNode, UnexpectedInput)
from .instvalue import (ArrayValue, InstanceKey, ObjectValue, Value,
Expand All @@ -49,6 +51,20 @@
"InstanceException", "InstanceValueError", "NonexistentInstance"]


class OutputFilter:
def begin_member(self, parent: "InstanceNode", node: "InstanceNode")->bool:
return True

def end_member(self, parent: "InstanceNode", node: "InstanceNode")->bool:
return True

def begin_element(self, parent: "InstanceNode", node: "InstanceNode")->bool:
return True

def end_element(self, parent: "InstanceNode", node: "InstanceNode")->bool:
return True


class LinkedList:
"""Persistent linked list of instance values."""

Expand Down Expand Up @@ -132,6 +148,8 @@ def __init__(self, key: InstanceKey, value: Value,
"""Parent instance node, or ``None`` for the root node."""
self.schema_node = schema_node # type: DataNode
"""Data node corresponding to the instance node."""
self.schema_data = parinst.schema_data if parinst else None
"""Link to schema data"""
self.timestamp = timestamp # type: datetime
"""Time of the receiver's last modification."""
self.value = value # type: Value
Expand Down Expand Up @@ -178,12 +196,21 @@ def __getitem__(self, key: InstanceKey) -> "InstanceNode":
`name`.
InstanceValueError: If the receiver's value is not an object.
"""
if isinstance(self.value, ArrayValue) and isinstance(key, tuple):
return self._mapentry(key)
if isinstance(self.value, ArrayValue) and isinstance(key, dict):
return self._mapentry(self._map2tuple(key))
if isinstance(self.value, ObjectValue):
return self._member(key)
if isinstance(self.value, ArrayValue):
return self._entry(key)
raise InstanceValueError(self.json_pointer(), "scalar instance")

def __contains__(self, key: InstanceKey) -> bool:
"""Checks if key does exist
"""
return self.get(key) is not None

def __iter__(self):
"""Return receiver's iterator.

Expand All @@ -205,6 +232,14 @@ def ita():
return iter(self._member_names())
raise InstanceValueError(self.json_pointer(), "scalar instance")

def get(self, key: InstanceKey, d=None):
"""Return member or entry with given key, returns default if it does not exist
"""
try:
return self[key]
except (InstanceValueError, NonexistentInstance):
return d

def is_internal(self) -> bool:
"""Return ``True`` if the receiver is an instance of an internal node.
"""
Expand Down Expand Up @@ -364,14 +399,90 @@ def add_defaults(self, ctype: ContentType = None) -> "InstanceNode":
break
return res.up()

def raw_value(self) -> RawValue:
def raw_value(self, filter: OutputFilter = OutputFilter()) -> RawValue:
"""Return receiver's value in a raw form (ready for JSON encoding)."""
if isinstance(self.value, ObjectValue):
return {m: self._member(m).raw_value() for m in self.value}
value = {}
for m in self.value:
member = self[m]
add1 = filter.begin_member(self, member)
if add1:
member_value = member.raw_value(filter)
add2 = filter.end_member(self, member)
if add1 and add2:
value[m] = member_value
return value
if isinstance(self.value, ArrayValue):
return [en.raw_value() for en in self]
value = list()
for en in self:
add1 = filter.begin_element(self, en)
if add1:
member_value = en.raw_value(filter)
add2 = filter.end_element(self, en)
if add1 and add2 and member_value:
value.append(member_value)
return value
return self.schema_node.type.to_raw(self.value)

def to_xml(self, filter: OutputFilter = OutputFilter(), elem: ET.Element = None):
"""put receiver's value into a XML element"""
if elem is None:
element = ET.Element(self.schema_node.name)

module = self.schema_data.modules_by_name.get(self.schema_node.ns)
if not module:
raise MissingModuleNamespace(self.schema_node.ns)
element.attrib['xmlns'] = module.xml_namespace
else:
element = elem

if isinstance(self.value, ObjectValue):
for cname in self:
childs = list()
if cname[:1] == '@':
# ignore annotations for now until they are stored independent of JSON encoding
continue

m = self[cname]
if filter.begin_member(self, m):
sn = m.schema_node
dp = sn.data_parent()

if isinstance(m.schema_node, (ListNode, LeafListNode)):
for en in m:
add1 = filter.begin_element(m, en)
if add1:
child = ET.Element(sn.name)
if not dp or dp.ns != sn.ns:
module = self.schema_data.modules_by_name.get(sn.ns)
if not module:
raise MissingModuleNamespace(sn.ns)
child.attrib['xmlns'] = module.xml_namespace
en.to_xml(filter, child)
add2 = filter.end_element(m, en)
if add1 and add2:
childs.append(child)
else:
child = ET.Element(sn.name)
childs.append(child)
if not dp or dp.ns != sn.ns:
module = self.schema_data.modules_by_name.get(sn.ns)
if not module:
raise MissingModuleNamespace(sn.ns)
child.attrib['xmlns'] = module.xml_namespace
m.to_xml(filter, child)
if filter.end_member(self, m):
for c in childs:
element.append(c)
if elem is None and len(element) == 0:
return None
elif isinstance(self.value, ArrayValue):
# Array outside an Object doesn't make sense
super().to_xml(filter, element)
else:
element.text = self.schema_node.type.to_xml(self.value)
return element

def _member_names(self) -> List[InstanceName]:
if isinstance(self.value, ObjectValue):
return [m for m in self.value if not m.startswith("@")]
Expand All @@ -397,6 +508,24 @@ def _entry(self, index: int) -> "ArrayEntry":
except (IndexError, TypeError):
raise NonexistentInstance(self.json_pointer(), "entry " + str(index)) from None

def _map2tuple(self, key: dict) -> tuple:
"""generate tuple for key"""
keylist = []
for keyit in self.schema_node._key_members:
keylist.append(key[keyit])

return tuple(keylist)

def _mapentry(self, key: tuple) -> "ArrayEntry":
"""iterate over all childs"""
for child in self:
"""generate tuple for key"""
childkey = tuple(child[singlekey].value for singlekey in self.schema_node._key_members)
if key == childkey:
return child

raise NonexistentInstance(self.json_pointer(), f"key '{key}'") from None

def _peek_schema_route(self, sroute: SchemaRoute) -> Value:
irt = InstanceRoute()
sn = self.schema_node
Expand Down Expand Up @@ -484,8 +613,9 @@ class RootNode(InstanceNode):
"""This class represents the root of the instance tree."""

def __init__(self, value: Value, schema_node: "DataNode",
timestamp: datetime):
schema_data: "SchemaData", timestamp: datetime):
super().__init__("/", value, None, schema_node, timestamp)
self.schema_data = schema_data

def up(self) -> None:
"""Override the superclass method.
Expand All @@ -497,7 +627,7 @@ def up(self) -> None:

def _copy(self, newval: Value, newts: datetime = None) -> InstanceNode:
return RootNode(
newval, self.schema_node, newts if newts else newval.timestamp)
newval, self.schema_node, self.schema_data, newts if newts else newval.timestamp)

def _ancestors_or_self(
self, qname: Union[QualName, bool] = None) -> List["RootNode"]:
Expand Down
2 changes: 1 addition & 1 deletion yangson/instvalue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
EntryValue = Union[ScalarValue, "ObjectValue"]
"""Type of the value a list ot leaf-list entry."""

InstanceKey = Union[InstanceName, int]
InstanceKey = Union[InstanceName, int, tuple, dict]
"""Index of an array entry or name of an object member."""

MetadataObject = Dict[PrefName, ScalarValue]
Expand Down
Loading