aboutsummaryrefslogtreecommitdiffstats
path: root/netmodel/model
diff options
context:
space:
mode:
Diffstat (limited to 'netmodel/model')
-rw-r--r--netmodel/model/__init__.py0
-rw-r--r--netmodel/model/attribute.py262
-rw-r--r--netmodel/model/field_names.py396
-rw-r--r--netmodel/model/filter.py397
-rw-r--r--netmodel/model/mapper.py20
-rw-r--r--netmodel/model/object.py231
-rw-r--r--netmodel/model/predicate.py306
-rw-r--r--netmodel/model/query.py147
-rw-r--r--netmodel/model/result_value.py185
-rw-r--r--netmodel/model/sql_parser.py221
-rw-r--r--netmodel/model/type.py89
11 files changed, 2254 insertions, 0 deletions
diff --git a/netmodel/model/__init__.py b/netmodel/model/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/netmodel/model/__init__.py
diff --git a/netmodel/model/attribute.py b/netmodel/model/attribute.py
new file mode 100644
index 00000000..b69ee1bf
--- /dev/null
+++ b/netmodel/model/attribute.py
@@ -0,0 +1,262 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import abc
+import copy
+import logging
+import operator
+import types
+
+from netmodel.model.mapper import ObjectSpecification
+from netmodel.model.type import is_type
+from netmodel.util.meta import inheritors
+from netmodel.util.misc import is_iterable
+from vicn.core.sa_collections import InstrumentedList, _list_decorators
+
+log = logging.getLogger(__name__)
+instance_dict = operator.attrgetter('__dict__')
+
+class NEVER_SET: None
+
+#------------------------------------------------------------------------------
+# Attribute Multiplicity
+#------------------------------------------------------------------------------
+
+class Multiplicity:
+ _1_1 = '1_1'
+ _1_N = '1_N'
+ _N_1 = 'N_1'
+ _N_N = 'N_N'
+
+
+ @staticmethod
+ def reverse(value):
+ reverse_map = {
+ Multiplicity._1_1: Multiplicity._1_1,
+ Multiplicity._1_N: Multiplicity._N_1,
+ Multiplicity._N_1: Multiplicity._1_N,
+ Multiplicity._N_N: Multiplicity._N_N,
+ }
+ return reverse_map[value]
+
+
+# Default attribute properties values (default to None)
+DEFAULT = {
+ 'multiplicity' : Multiplicity._1_1,
+ 'mandatory' : False,
+}
+
+#------------------------------------------------------------------------------
+# Attribute
+#------------------------------------------------------------------------------
+
+class Attribute(abc.ABC, ObjectSpecification):
+ properties = [
+ 'name',
+ 'type',
+ 'description',
+ 'default',
+ 'choices',
+ 'mandatory',
+ 'multiplicity',
+ 'ro',
+ 'auto',
+ 'func',
+ 'requirements',
+ 'reverse_name',
+ 'reverse_description',
+ 'reverse_auto'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ for key in Attribute.properties:
+ value = kwargs.pop(key, NEVER_SET)
+ setattr(self, key, value)
+
+ if len(args) == 1:
+ self.type, = args
+ elif len(args) == 2:
+ self.name, self.type = args
+ assert is_type(self.type)
+
+ self.is_aggregate = False
+
+ self._reverse_attributes = list()
+
+ #--------------------------------------------------------------------------
+ # Display
+ #--------------------------------------------------------------------------
+
+ def __repr__(self):
+ return '<Attribute {}>'.format(self.name)
+
+ __str__ = __repr__
+
+ #--------------------------------------------------------------------------
+ # Descriptor protocol
+ #
+ # see. https://docs.python.org/3/howto/descriptor.html
+ #--------------------------------------------------------------------------
+
+ def __get__(self, instance, owner=None):
+ if instance is None:
+ return self
+
+ value = instance_dict(instance).get(self.name, NEVER_SET)
+
+ # Case : collection attribute
+ if self.is_collection:
+ if value is NEVER_SET:
+ if isinstance(self.default, types.FunctionType):
+ default = self.default(instance)
+ else:
+ default = self.default
+ value = InstrumentedList(default)
+ value._attribute = self
+ value._instance = instance
+ self.__set__(instance, value)
+ return value
+ return value
+
+ # Case : scalar attribute
+
+ if value in (None, NEVER_SET) and self.auto not in (None, NEVER_SET):
+ # Automatic instanciation
+ if not self.requirements in (None, NEVER_SET) and \
+ self.requirements:
+ log.warning('Ignored requirement {}'.format(self.requirements))
+ value = instance.auto_instanciate(self)
+ self.__set__(instance, value)
+ return value
+
+ if value is NEVER_SET:
+ if isinstance(self.default, types.FunctionType):
+ value = self.default(instance)
+ else:
+ value = copy.deepcopy(self.default)
+ self.__set__(instance, value)
+ return value
+
+ return value
+
+ def __set__(self, instance, value):
+ if instance is None:
+ return
+
+ if self.is_collection:
+ if not isinstance(value, InstrumentedList):
+ value = InstrumentedList(value)
+ value._attribute = self
+ value._instance = instance
+
+ instance_dict(instance)[self.name] = value
+ if hasattr(instance, '_state'):
+ instance._state.attr_dirty.add(self.name)
+ instance._state.dirty = True
+
+ def __delete__(self, instance):
+ raise NotImplementedError
+
+ #--------------------------------------------------------------------------
+ # Accessors
+ #--------------------------------------------------------------------------
+
+ def __getattribute__(self, name):
+ value = super().__getattribute__(name)
+ if value is NEVER_SET:
+ if name == 'default':
+ return list() if self.is_collection else None
+ return DEFAULT.get(name, None)
+ return value
+
+ # Shortcuts
+
+ def has_reverse_attribute(self):
+ return self.reverse_name and self.multiplicity
+
+ @property
+ def is_collection(self):
+ return self.multiplicity in (Multiplicity._1_N, Multiplicity._N_N)
+
+ def is_set(self, instance):
+ return self.name in instance_dict(instance)
+
+ #--------------------------------------------------------------------------
+ # Operations
+ #--------------------------------------------------------------------------
+
+ def merge(self, parent):
+ for prop in Attribute.properties:
+ # NOTE: we cannot use getattr otherwise we get the default value,
+ # and we never override
+ value = vars(self).get(prop, NEVER_SET)
+ if value is not NEVER_SET and not is_iterable(value):
+ continue
+
+ parent_value = vars(parent).get(prop, NEVER_SET)
+ if parent_value is NEVER_SET:
+ continue
+
+ if parent_value:
+ if is_iterable(value):
+ value.extend(parent_value)
+ else:
+ setattr(self, prop, parent_value)
+
+ #--------------------------------------------------------------------------
+ # Attribute values
+ #--------------------------------------------------------------------------
+
+ def _handle_getitem(self, instance, item):
+ return item
+
+ def _handle_add(self, instance, item):
+ instance._state.dirty = True
+ instance._state.attr_dirty.add(self.name)
+ print('marking', self.name, 'as dirty')
+ return item
+
+ def _handle_remove(self, instance, item):
+ instance._state.dirty = True
+ instance._state.attr_dirty.add(self.name)
+ print('marking', self.name, 'as dirty')
+
+ def _handle_before_remove(self, instance):
+ pass
+
+ #--------------------------------------------------------------------------
+ # Attribute values
+ #--------------------------------------------------------------------------
+
+class Relation(Attribute):
+ properties = Attribute.properties[:]
+ properties.extend([
+ 'reverse_name',
+ 'reverse_description',
+ 'multiplicity',
+ ])
+
+class SelfRelation(Relation):
+ def __init__(self, *args, **kwargs):
+ if args:
+ if not len(args) == 1:
+ raise ValueError('Bad initialized for SelfRelation')
+ name, = args
+ super().__init__(name, None, *args, **kwargs)
+ else:
+ super().__init__(None, *args, **kwargs)
diff --git a/netmodel/model/field_names.py b/netmodel/model/field_names.py
new file mode 100644
index 00000000..82881998
--- /dev/null
+++ b/netmodel/model/field_names.py
@@ -0,0 +1,396 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FIELD_SEPARATOR = '.'
+DEFAULT_IS_STAR = False
+
+class FieldNames(list):
+ """
+ A FieldNames instance gather a set of field_names or represents *.
+ THIS IS NOT a set(Field).
+
+ The distinction between parent and children fields is based on the
+ FieldNames.FIELD_SEPARATOR character.
+ """
+
+ #--------------------------------------------------------------------------
+ # Constructor
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ """
+ Constructor.
+ """
+ star = kwargs.pop('star', DEFAULT_IS_STAR)
+ list.__init__(self, *args, **kwargs)
+ size = len(self)
+ if star and size != 0:
+ raise ValueError("Inconsistent parameter (star = %s size = %s)" % \
+ (star, size))
+ # self._star == False and len(self) == 0 occurs when we create
+ # FieldNames() (to use later |=) and must behaves as FieldNames(star =
+ # False)
+ self._star = star
+
+ def __repr__(self):
+ """
+ Returns:
+ The %r representation of this FieldNames instance.
+ """
+ if self.is_star():
+ return "<FieldNames *>"
+ else:
+ return "<FieldNames %r>" % [x for x in self]
+
+ def __hash__(self):
+ return hash((self._star,) + tuple(self))
+
+ def is_star(self):
+ """
+ Returns:
+ True iif this FieldNames instance correspond to "*".
+ Example : SELECT * FROM foo
+ """
+ try:
+ return self._star
+ except:
+ # This is due to a bug in early versions of Python 2.7 which are
+ # present on PlanetLab. During copy.deepcopy(), the object is
+ # reconstructed using append before the state (self.__dict__ is
+ # reconstructed). Hence the object has no _star when append is
+ # called and this raises a crazy Exception:
+ # I could not reproduce in a smaller example
+ # http://pastie.org/private/5nf15jg0qcvd05pbmnrp8g
+ return False
+
+ def set_star(self):
+ """
+ Update this FieldNames instance to make it corresponds to "*"
+ """
+ self._star = True
+ self.clear()
+
+ def unset_star(self, field_names):
+ """
+ Update this FieldNames instance to make it corresponds to a set of
+ FieldNames
+
+ Args:
+ field_names: A FieldNames instance or a set of String instances
+ (field names)
+ """
+ assert len(field_names) > 0
+ self._star = False
+ if field_names:
+ self |= field_names
+
+ def is_empty(self):
+ """
+ Returns:
+ True iif FieldNames instance designates contains least one field
+ name.
+ """
+ return not self.is_star() and len(self) == 0
+
+ def copy(self):
+ """
+ Returns:
+ A copy of this FieldNames instance.
+ """
+ return FieldNames(self[:])
+
+ #--------------------------------------------------------------------------
+ # Iterators
+ #--------------------------------------------------------------------------
+
+ def iter_field_subfield(self):
+ for f in self:
+ field, _, subfield = f.partition(FIELD_SEPARATOR)
+ yield (field, subfield)
+
+ #--------------------------------------------------------------------------
+ # Overloaded set internal functions
+ #--------------------------------------------------------------------------
+
+ def __or__(self, fields):
+ """
+ Compute the union of two FieldNames instances.
+ Args:
+ fields: a set of String (corresponding to field names) or a
+ FieldNames instance.
+ Returns:
+ The union of the both FieldNames instance.
+ """
+ if self.is_star() or fields.is_star():
+ return FieldNames(star = True)
+ else:
+ l = self[:]
+ l.extend([x for x in fields if x not in l])
+ return FieldNames(l)
+
+ def __ior__(self, fields):
+ """
+ Compute the union of two FieldNames instances.
+ Args:
+ fields: a set of Field instances or a FieldNames instance.
+ Returns:
+ The updated FieldNames instance.
+ """
+ if fields.is_star():
+ self.set_star()
+ return self
+ else:
+ self.extend([x for x in fields if x not in self])
+ return self
+
+ def __and__(self, fields):
+ """
+ Compute the intersection of two FieldNames instances.
+ Args:
+ fields: a set of Field instances or a FieldNames instance.
+ Returns:
+ The intersection of the both FieldNames instances.
+ """
+ if self.is_star():
+ return fields.copy()
+ elif isinstance(fields, FieldNames) and fields.is_star():
+ return self.copy()
+ else:
+ return FieldNames([x for x in self if x in fields])
+
+ def __iand__(self, fields):
+ """
+ Compute the intersection of two FieldNames instances.
+ Args:
+ fields: a set of Field instances or a FieldNames instance.
+ Returns:
+ The updated FieldNames instance.
+ """
+ if self.is_star():
+ self.unset_star(fields)
+ elif fields.is_star():
+ pass
+ else:
+ self[:] = [x for x in self if x in fields]
+ return self
+
+ def __nonzero__(self):
+ return self.is_star() or bool(list(self))
+
+ # Python>=3
+ __bool__ = __nonzero__
+
+ __add__ = __or__
+
+ def __sub__(self, fields):
+ if fields.is_star():
+ return FieldNames(star = False)
+ else:
+ if self.is_star():
+ # * - x,y,z = ???
+ return FieldNames(star = True)
+ else:
+ return FieldNames([x for x in self if x not in fields])
+
+ def __isub__(self, fields):
+ raise NotImplemented
+
+ def __iadd__(self, fields):
+ raise NotImplemented
+
+ #--------------------------------------------------------------------------
+ # Overloaded set comparison functions
+ #--------------------------------------------------------------------------
+
+ def __eq__(self, other):
+ """
+ Test whether this FieldNames instance corresponds to another one.
+ Args:
+ other: The FieldNames instance compared to self.
+ Returns:
+ True if the both FieldNames instance matches.
+ """
+ return self.is_star() and other.is_star() or set(self) == set(other)
+
+ def __le__(self, other):
+ """
+ Test whether this FieldNames instance in included in
+ (or equal to) another one.
+ Args:
+ other: The FieldNames instance compared to self or
+ Returns:
+ True if the both FieldNames instance matches.
+ """
+ assert isinstance(other, FieldNames),\
+ "Invalid other = %s (%s)" % (other, type(other))
+
+ return (self.is_star() and other.is_star())\
+ or (not self.is_star() and other.is_star())\
+ or (set(self) <= set(other)) # list.__le__(self, other)
+
+ # Defined with respect of previous functions
+
+ def __ne__(self, other):
+ """
+ Test whether this FieldNames instance differs to another one.
+ Args:
+ other: The FieldNames instance compared to self.
+ Returns:
+ True if the both FieldNames instance differs.
+ """
+ return not self == other
+
+ def __lt__(self, other):
+ """
+ Test whether this FieldNames instance in strictly included in
+ another one.
+ Args:
+ other: The FieldNames instance compared to self.
+ Returns:
+ True if self is strictly included in other.
+ """
+ return self <= other and self != other
+
+ def __ge__(self, other):
+ return other.__le__(self)
+
+ def __gt__(self, other):
+ return other.__lt__(self)
+
+ #--------------------------------------------------------------------------
+ # Overloaded set functions
+ #--------------------------------------------------------------------------
+
+ def add(self, field_name):
+ # DEPRECATED
+ assert isinstance(field_name, str)
+ self.append(field_name)
+
+ def set(self, field_names):
+ assert isinstance(field_names, FieldNames)
+ if field_names.is_star():
+ self.set_star()
+ return
+ assert len(field_names) > 0
+ self._star = False
+ self.clear()
+ self |= field_names
+
+ def append(self, field_name):
+ if not isinstance(field_name, str):
+ raise TypeError("Invalid field_name %s (string expected, got %s)" \
+ % (field_name, type(field_name)))
+
+ if not self.is_star():
+ list.append(self, field_name)
+
+ def clear(self):
+ self._star = True
+ del self[:]
+
+ def rename(self, aliases):
+ """
+ Rename all the field names involved in self according to a dict.
+ Args:
+ aliases: A {String : String} mapping the old field name and
+ the new field name.
+ Returns:
+ The updated FieldNames instance.
+ """
+ s = self.copy()
+ for element in s:
+ if element in aliases:
+ s.remove(element)
+ s.add(aliases[element])
+ self.clear()
+ self |= s
+ return self
+
+ @staticmethod
+ def join(field, subfield):
+ return "%s%s%s" % (field, FIELD_SEPARATOR, subfield)
+
+ @staticmethod
+ def after_path(field, path, allow_shortcuts = True):
+ """
+ Returns the part of the field after path
+
+ Args:
+ path (list):
+ allow_shortcuts (bool): Default to True.
+ """
+ if not path:
+ return (field, None)
+ last = None
+ field_parts = field.split(FIELD_SEPARATOR)
+ for path_element in path[1:]:
+ if path_element == field_parts[0]:
+ field_parts.pop(0)
+ last = None
+ else:
+ last = path_element
+ return (FIELD_SEPARATOR.join(field_parts), last)
+
+ def split_subfields(self, include_parent = True, current_path = None,
+ allow_shortcuts = True):
+ """
+ Args:
+ include_parent (bool): is the parent field included in the list of
+ returned FieldNames (1st part of the tuple).
+ current_path (list): the path of fields that will be skipped at the
+ beginning
+ path_shortcuts (bool): do we allow shortcuts in the path
+
+ Returns: A tuple made of 4 operands:
+ fields:
+ map_method_subfields:
+ map_original_field:
+ rename:
+
+ Example path = ROOT.A.B
+ split_subfields(A.B.C.D, A.B.C.D', current_path=[ROOT,A,B]) =>
+ (FieldNames(), { C: [D, D'] })
+ split_subfields(A.E.B.C.D, A.E.B.C.D', current_path=[ROOT,A,B]) =>
+ (FieldNames(), { C: [D, D'] })
+ """
+ field_names = FieldNames()
+ map_method_subfields = dict()
+ map_original_field = dict()
+ rename = dict()
+
+ for original_field in self:
+ # The current_path can be seen as a set of fields that have to be
+ # passed through before we can consider a field
+ field, last = FieldNames.after_path(original_field, current_path,
+ allow_shortcuts)
+
+ field_name, _, subfield = field.partition(FIELD_SEPARATOR)
+
+ if not subfield:
+ field_names.add(field_name)
+ else:
+ if include_parent:
+ field_names.add(field_name)
+ if not field_name in map_method_subfields:
+ map_method_subfields[field_name] = FieldNames()
+ map_method_subfields[field_name].add(subfield)
+
+ map_original_field[field_name] = original_field
+ rename[field_name] = last
+
+ return (field_names, map_method_subfields, map_original_field, rename)
diff --git a/netmodel/model/filter.py b/netmodel/model/filter.py
new file mode 100644
index 00000000..d0790e3e
--- /dev/null
+++ b/netmodel/model/filter.py
@@ -0,0 +1,397 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import copy
+
+from netmodel.model.field_names import FieldNames
+from netmodel.model.predicate import Predicate, eq, included
+from netmodel.util.misc import is_iterable
+
+class Filter(set):
+ """
+ A Filter is a set of Predicate instances
+ """
+
+ @staticmethod
+ def from_list(l):
+ """
+ Create a Filter instance by using an input list.
+ Args:
+ l: A list of Predicate instances.
+ """
+ f = Filter()
+ try:
+ for element in l:
+ f.add(Predicate(*element))
+ except Exception as e:
+ #print("Error in setting Filter from list", e)
+ return None
+ return f
+
+ @staticmethod
+ def from_dict(d):
+ """
+ Create a Filter instance by using an input dict.
+ Args:
+ d: A dict {key : value} instance where each
+ key-value pair leads to a Predicate.
+ 'key' could start with the operator to be
+ used in the predicate, otherwise we use
+ '=' by default.
+ """
+ f = Filter()
+ for key, value in d.items():
+ if key[0] in Predicate.operators.keys():
+ f.add(Predicate(key[1:], key[0], value))
+ else:
+ f.add(Predicate(key, '=', value))
+ return f
+
+ def to_list(self):
+ """
+ Returns:
+ The list corresponding to this Filter instance.
+ """
+ ret = list()
+ for predicate in self:
+ ret.append(predicate.to_list())
+ return ret
+
+ @staticmethod
+ def from_clause(clause):
+ """
+ NOTE: We can only handle simple clauses formed of AND fields.
+ """
+ raise NotImplementedError
+
+ @staticmethod
+ def from_string(string):
+ """
+ """
+ from netmodel.model.sql_parser import SQLParser
+ p = SQLParser()
+ ret = p.filter.parseString(string, parseAll=True)
+ return ret[0] if ret else None
+
+ def filter_by(self, predicate):
+ """
+ Update this Filter by adding a Predicate.
+ Args:
+ predicate: A Predicate instance.
+ Returns:
+ The resulting Filter instance.
+ """
+ assert isinstance(predicate, Predicate),\
+ "Invalid predicate = %s (%s)" % (predicate, type(predicate))
+ self.add(predicate)
+ return self
+
+ def unfilter_by(self, *args):
+ assert len(args) == 1 or len(args) == 3, \
+ "Invalid expression for filter"
+
+ if not self.is_empty():
+ if len(args) == 1:
+ # we got a Filter, or a set, or a list, or a tuple or None.
+ filters = args[0]
+ if filters != None:
+ if not isinstance(filters, (set, list, tuple, Filter)):
+ filters = [filters]
+ for predicate in set(filters):
+ self.remove(predicate)
+ elif len(args) == 3:
+ # we got three args: (field_name, op, value)
+ predicate = Predicate(*args)
+ self.remove(predicate)
+
+ assert isinstance(self, Filter),\
+ "Invalid filters = %s" % (self, type(self))
+ return self
+
+ def add(self, predicate_or_filter):
+ """
+ Adds a predicate or a filter (a set of predicate) -- or a list thereof
+ -- to the current filter.
+ """
+ if is_iterable(predicate_or_filter):
+ map(self.add, predicate_or_filter)
+ return
+
+ assert isinstance(predicate_or_filter, Predicate)
+ set.add(self, predicate_or_filter)
+
+ def is_empty(self):
+ """
+ Tests whether this Filter is empty or not.
+ Returns:
+ True iif this Filter is empty.
+ """
+ return len(self) == 0
+
+ def __str__(self):
+ """
+ Returns:
+ The '%s' representation of this Filter.
+ """
+ if self.is_empty():
+ return "<empty filter>"
+ else:
+ return " AND ".join([str(pred) for pred in self])
+
+ def __repr__(self):
+ """
+ Returns:
+ The '%r' representation of this Filter.
+ """
+ return '<Filter: %s>' % self
+
+ def __key(self):
+ return tuple([hash(pred) for pred in self])
+
+ def __hash__(self):
+ return hash(self.__key())
+
+ def __additem__(self, value):
+ if not isinstance(value, Predicate):
+ raise TypeError("Element of class Predicate expected, received %s"\
+ % value.__class__.__name__)
+ set.__additem__(self, value)
+
+
+ def copy(self):
+ return copy.deepcopy(self)
+
+ def keys(self):
+ """
+ Returns:
+ A set of String corresponding to each field name
+ involved in this Filter.
+ """
+ return set([x.key for x in self])
+
+ def has(self, key):
+ for x in self:
+ if x.key == key:
+ return True
+ return False
+
+ def has_op(self, key, op):
+ for x in self:
+ if x.key == key and x.op == op:
+ return True
+ return False
+
+ def has_eq(self, key):
+ return self.has_op(key, eq)
+
+ def get(self, key):
+ ret = []
+ for x in self:
+ if x.key == key:
+ ret.append(x)
+ return ret
+
+ def delete(self, key):
+ to_del = []
+ for x in self:
+ if x.key == key:
+ to_del.append(x)
+ for x in to_del:
+ self.remove(x)
+
+ def get_op(self, key, op):
+ if isinstance(op, (list, tuple, set)):
+ for x in self:
+ if x.key == key and x.op in op:
+ return x.value
+ else:
+ for x in self:
+ if x.key == key and x.op == op:
+ return x.value
+ return None
+
+ def get_eq(self, key):
+ return self.get_op(key, eq)
+
+ def set_op(self, key, op, value):
+ for x in self:
+ if x.key == key and x.op == op:
+ x.value = value
+ return
+ raise KeyError(key)
+
+ def set_eq(self, key, value):
+ return self.set_op(key, eq, value)
+
+ def get_predicates(self, key):
+ ret = []
+ for x in self:
+ if x.key == key:
+ ret.append(x)
+ return ret
+
+ def match(self, dic, ignore_missing=True):
+ for predicate in self:
+ if not predicate.match(dic, ignore_missing):
+ return False
+ return True
+
+ def filter(self, l):
+ output = []
+ for x in l:
+ if self.match(x):
+ output.append(x)
+ return output
+
+ def get_field_names(self):
+ field_names = FieldNames()
+ for predicate in self:
+ field_names |= predicate.get_field_names()
+ return field_names
+
+ def grep(self, fun):
+ return Filter([x for x in self if fun(x)])
+
+ def rgrep(self, fun):
+ return Filter([x for x in self if not fun(x)])
+
+ def split(self, fun, true_only = False):
+ true_filter, false_filter = Filter(), Filter()
+ for predicate in self:
+ if fun(predicate):
+ true_filter.add(predicate)
+ else:
+ false_filter.add(predicate)
+ if true_only:
+ return true_filter
+ else:
+ return (true_filter, false_filter)
+
+
+ def split_fields(self, fields, true_only = False):
+ return self.split(lambda predicate: predicate.get_key() in fields,
+ true_only)
+
+ def provides_key_field(self, key_fields):
+ # No support for tuples
+ for field in key_fields:
+ if not self.has_op(field, eq) and not self.has_op(field, included):
+ # Missing key fields in query filters
+ return False
+ return True
+
+ def rename(self, aliases):
+ for predicate in self:
+ predicate.rename(aliases)
+ return self
+
+ def get_field_values(self, field):
+ """
+ This function returns the values that are determined by the filters for
+ a given field, or None is the filter is not *setting* determined values.
+
+ Returns: list : a list of fields
+ """
+ value_list = list()
+ for predicate in self:
+ key, op, value = predicate.get_tuple()
+
+ if key == field:
+ extract_tuple = False
+ elif key == (field, ):
+ extract_tuple = True
+ else:
+ continue
+
+ if op == eq:
+ if extract_tuple:
+ value = value[0]
+ value_list.append(value)
+ elif op == included:
+ if extract_tuple:
+ value = [x[0] for x in value]
+ value_list.extend(value)
+ else:
+ continue
+
+ return list(set(value_list))
+
+ def update_field_value_eq(self, field, value):
+ for predicate in self:
+ p_field, p_op, p_value = predicate.get_tuple()
+ if p_field == field:
+ predicate.set_op(eq)
+ predicate.set_value(value)
+ break # assuming there is a single predicate with field/op
+
+ def __and__(self, other):
+ # Note: we assume the predicates in self and other are already in
+ # minimal form, eg. not the same fields twice... We could break after
+ # a predicate with the same key is found btw...
+ s = self.copy()
+ for o_predicate in other:
+ o_key, o_op, o_value = o_predicate.get_tuple()
+
+ key_found = False
+ for predicate in s:
+ key, op, value = predicate.get_tuple()
+ if key != o_key:
+ continue
+
+ # We already have a predicate with the same key
+ key_found = True
+
+ if op == eq:
+ if o_op == eq:
+ # Similar filters...
+ if value != o_value:
+ # ... with different values
+ return None
+ else:
+ # ... with same values
+ pass
+ elif o_op == included:
+ # Inclusion
+ if value not in o_value:
+ # no overlap
+ return None
+ else:
+ # We already have the more restrictive predicate...
+ pass
+
+ elif op == included:
+ if o_op == eq:
+ if o_value not in value:
+ return None
+ else:
+ # One value overlaps... update the initial predicate
+ # with the more restrictive one
+ predicate.set_op(eq)
+ predicate.set_value(value)
+ elif o_op == included:
+ intersection = set(o_value) & set(value)
+ if not set(o_value) & set(value):
+ return None
+ else:
+ predicate.set_value(tuple(intersection))
+
+ # No conflict found, we can add the predicate to s
+ if not key_found:
+ s.add(o_predicate)
+
+ return s
diff --git a/netmodel/model/mapper.py b/netmodel/model/mapper.py
new file mode 100644
index 00000000..9be46a14
--- /dev/null
+++ b/netmodel/model/mapper.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class ObjectSpecification:
+ pass
diff --git a/netmodel/model/object.py b/netmodel/model/object.py
new file mode 100644
index 00000000..32d3a833
--- /dev/null
+++ b/netmodel/model/object.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from abc import ABCMeta
+
+from netmodel.model.attribute import Attribute
+from netmodel.model.type import BaseType
+from netmodel.model.mapper import ObjectSpecification
+
+# Warning and error messages
+
+E_UNK_RES_NAME = 'Unknown resource name for attribute {} in {} ({}) : {}'
+
+class ObjectMetaclass(ABCMeta):
+ """
+ Object metaclass allowing non-uniform attribute declaration.
+ """
+
+ def __init__(cls, class_name, parents, attrs):
+ """
+ Args:
+ cls: The class type we're registering.
+ class_name: A String containing the class_name.
+ parents: The parent class types of 'cls'.
+ attrs: The attribute (members) of 'cls'.
+ """
+ super().__init__(class_name, parents, attrs)
+ cls._sanitize()
+
+class Object(BaseType, metaclass = ObjectMetaclass):
+
+ def __init__(self, **kwargs):
+ """
+ Object constructor.
+
+ Args:
+ kwargs: named arguments consisting in object attributes to be
+ initialized at construction.
+ """
+ mandatory = { a.name for a in self.iter_attributes() if a.mandatory }
+
+ for key, value in kwargs.items():
+ attribute = self.get_attribute(key)
+ if issubclass(attribute.type, Object):
+ if attribute.is_collection:
+ new_value = list()
+ for x in value:
+ if isinstance(x, str):
+ resource = self._state.manager.by_name(x)
+ elif isinstance(x, UUID):
+ resource = self._state.manager.by_uuid(x)
+ else:
+ resource = x
+ if not resource:
+ raise LurchException(E_UNK_RES_NAME.format(key,
+ self.name, self.__class__.__name__, x))
+ new_value.append(resource._state.uuid)
+ value = new_value
+ else:
+ if isinstance(value, str):
+ resource = self._state.manager.by_name(value)
+ elif isinstance(value, UUID):
+ resource = self._state.manager.by_uuid(value)
+ else:
+ resource = value
+ if not resource:
+ raise LurchException(E_UNK_RES_NAME.format(key,
+ self.name, self.__class__.__name__, value))
+ value = resource._state.uuid
+ setattr(self, key, value)
+ mandatory -= { key }
+
+ # Check that all mandatory atttributes have been set
+ # Mandatory resource attributes will be marked as pending since they
+ # might be discovered
+ # Eventually, their absence will be discovered at runtime
+ if mandatory:
+ raise Exception('Mandatory attributes not set: %r' % (mandatory,))
+
+ # Assign backreferences (we need attribute to be initialized, so it has
+ # to be done at the end of __init__
+ for other_instance, attribute in self.iter_backrefs():
+ if attribute.is_collection:
+ collection = getattr(other_instance, attribute.name)
+ collection.append(self)
+ else:
+ setattr(other_instance, attribute.name, self)
+
+ #--------------------------------------------------------------------------
+ # Object model
+ #--------------------------------------------------------------------------
+
+ @classmethod
+ def get_attribute(cls, key):
+ return getattr(cls, key)
+
+ @classmethod
+ def _sanitize(cls):
+ """Sanitize the object model to accomodate for multiple declaration
+ styles
+
+ In particular, this method:
+ - set names to all attributes
+ """
+ cls._reverse_attributes = dict()
+ cur_reverse_attributes = dict()
+ for name, obj in vars(cls).items():
+ if not isinstance(obj, ObjectSpecification):
+ continue
+ if isinstance(obj, Attribute):
+ obj.name = name
+
+ # Remember whether a reverse_name is defined before loading
+ # inherited properties from parent
+ has_reverse = bool(obj.reverse_name)
+
+ # Handle overloaded attributes
+ # By recursion, it is sufficient to look into the parent
+ for base in cls.__bases__:
+ if hasattr(base, name):
+ parent_attribute = getattr(base, name)
+ obj.merge(parent_attribute)
+ assert obj.type
+
+ # Handle reverse attribute
+ #
+ # NOTE: we need to do this after merging to be sure we get all
+ # properties inherited from parent (eg. multiplicity)
+ if has_reverse:
+ a = {
+ 'name' : obj.reverse_name,
+ 'description' : obj.reverse_description,
+ 'multiplicity' : Multiplicity.reverse(obj.multiplicity),
+ 'auto' : obj.reverse_auto,
+ }
+ reverse_attribute = Attribute(cls, **a)
+ reverse_attribute.is_aggregate = True
+
+ cur_reverse_attributes[obj.type] = reverse_attribute
+
+ if not obj in cls._reverse_attributes:
+ cls._reverse_attributes[obj] = list()
+ cls._reverse_attributes[obj].append(reverse_attribute)
+
+ for cls, a in cur_reverse_attributes.items():
+ setattr(cls, a.name, a)
+
+ @classmethod
+ def iter_attributes(cls, aggregates = False):
+ for name in dir(cls):
+ attribute = getattr(cls, name)
+ if not isinstance(attribute, Attribute):
+ continue
+ if attribute.is_aggregate and not aggregates:
+ continue
+
+ yield attribute
+
+ def get_attributes(self, aggregates = False):
+ return list(self.iter_attributes(aggregates = aggregates))
+
+ def get_attribute_names(self, aggregates = False):
+ return set(a.name for a in self.iter_attributes(aggregates = \
+ aggregates))
+
+ def get_attribute_dict(self, field_names = None, aggregates = False,
+ uuid = True):
+ assert not field_names or field_names.is_star()
+ attributes = self.get_attributes(aggregates = aggregates)
+
+ ret = dict()
+ for a in attributes:
+ if not a.is_set(self):
+ continue
+ value = getattr(self, a.name)
+ if a.is_collection:
+ ret[a.name] = list()
+ for x in value:
+ if uuid and isinstance(x, Object):
+ x = x._state.uuid._uuid
+ ret[a.name].append(x)
+ else:
+ if uuid and isinstance(value, Object):
+ value = value._state.uuid._uuid
+ ret[a.name] = value
+ return ret
+
+ def get_tuple(self):
+ return (self.__class__, self._get_attribute_dict())
+
+ def format(self, fmt):
+ return fmt.format(**self.get_attribute_dict(uuid = False))
+
+ def iter_backrefs(self):
+ for base in self.__class__.mro():
+ if not hasattr(base, '_reverse_attributes'):
+ continue
+ for attr, rattrs in base._reverse_attributes.items():
+ instances = getattr(self, attr.name)
+ if not attr.is_collection:
+ instances = [instances]
+ for instance in instances:
+ # - instance = node
+ if instance in (None, NEVER_SET):
+ continue
+ for rattr in rattrs:
+ yield instance, rattr
+
+ #--------------------------------------------------------------------------
+ # Accessors
+ #--------------------------------------------------------------------------
+
+ @classmethod
+ def has_attribute(cls, name):
+ return name in [a.name for a in cls.attributes()]
+
diff --git a/netmodel/model/predicate.py b/netmodel/model/predicate.py
new file mode 100644
index 00000000..08ed956a
--- /dev/null
+++ b/netmodel/model/predicate.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import copy
+
+from netmodel.model.field_names import FieldNames, FIELD_SEPARATOR
+
+from operator import (
+ and_, or_, inv, add, mul, sub, mod, truediv, lt, le, ne, gt, ge, eq, neg
+)
+
+# Define the inclusion operators
+class contains(type): pass
+class included(type): pass
+
+class Predicate:
+
+ operators = {
+ '==' : eq,
+ '!=' : ne,
+ '<' : lt,
+ '<=' : le,
+ '>' : gt,
+ '>=' : ge,
+ 'CONTAINS' : contains,
+ 'INCLUDED' : included
+ }
+
+ operators_short = {
+ '=' : eq,
+ '~' : ne,
+ '<' : lt,
+ '[' : le,
+ '>' : gt,
+ ']' : ge,
+ '}' : contains,
+ '{' : included
+ }
+
+ def __init__(self, *args, **kwargs):
+ """
+ Build a Predicate instance.
+ Args:
+ kwargs: You can pass:
+ - 3 args (left, operator, right)
+ left: The left operand (it may be a String instance or a
+ tuple)
+ operator: See Predicate.operators, this is the binary
+ operator involved in this Predicate.
+ right: The right value (it may be a String instance
+ or a literal (String, numerical value, tuple...))
+ - 1 argument (list or tuple), containing three arguments
+ (variable, operator, value)
+ """
+ if len(args) == 3:
+ key, op, value = args
+ elif len(args) == 1 and isinstance(args[0], (tuple, list)) and \
+ len(args[0]) == 3:
+ key, op, value = args[0]
+ elif len(args) == 1 and isinstance(args[0], Predicate):
+ key, op, value = args[0].get_tuple()
+ else:
+ raise Exception("Bad initializer for Predicate (args = %r)" % args)
+
+ assert not isinstance(value, (frozenset, dict, set)), \
+ "Invalid value type (type = %r)" % type(value)
+ if isinstance(value, list):
+ value = tuple(value)
+
+ self.key = key
+ if isinstance(op, str):
+ op = op.upper()
+ if op in self.operators.keys():
+ self.op = self.operators[op]
+ elif op in self.operators_short.keys():
+ self.op = self.operators_short[op]
+ else:
+ self.op = op
+
+ if isinstance(value, list):
+ self.value = tuple(value)
+ else:
+ self.value = value
+
+ def __str__(self):
+ """
+ Returns:
+ The '%s' representation of this Predicate.
+ """
+ return repr(self)
+
+ def __repr__(self):
+ """
+ Returns:
+ The '%r' representation of this Predicate.
+ """
+ key, op, value = self.get_str_tuple()
+ if isinstance(value, (tuple, list, set, frozenset)):
+ value = [repr(v) for v in value]
+ value = "(%s)" % ", ".join(value)
+ return "%s %s %r" % (key, op, value)
+
+ def __hash__(self):
+ """
+ Returns:
+ The hash of this Predicate (this allows to define set of
+ Predicate instances).
+ """
+ return hash(self.get_tuple())
+
+ def __eq__(self, predicate):
+ """
+ Returns:
+ True iif self == predicate.
+ """
+ if not predicate:
+ return False
+ return self.get_tuple() == predicate.get_tuple()
+
+ def copy(self):
+ return copy.deepcopy(self)
+
+ def get_key(self):
+ """
+ Returns:
+ The left operand of this Predicate. It may be a String
+ or a tuple of Strings.
+ """
+ return self.key
+
+ def set_key(self, key):
+ """
+ Set the left operand of this Predicate.
+ Params:
+ key: The new left operand.
+ """
+ self.key = key
+
+ def update_key(self, function):
+ self.set_key(function(self.get_key()))
+
+ def get_op(self):
+ return self.op
+
+ def set_op(self, op):
+ self.op = op
+
+ def get_value(self):
+ return self.value
+
+ def set_value(self, value):
+ self.value = value
+
+ def get_tuple(self):
+ return (self.key, self.op, self.value)
+
+ def get_tuple_ext(self):
+ key, op, value = self.get_tuple()
+ key_field, _, key_subfield = key.partition(FIELD_SEPARATOR)
+ return (key_field, key_subfield, op, value)
+
+ def get_str_op(self):
+ op_str = [s for s, op in self.operators.items() if op == self.op]
+ return op_str[0]
+
+ def get_str_tuple(self):
+ return (self.key, self.get_str_op(), self.value,)
+
+ def to_list(self):
+ return list(self.get_str_tuple())
+
+ def match(self, dic, ignore_missing=False):
+ # Can we match ?
+ if self.key not in dic:
+ return ignore_missing
+
+ if self.op == eq:
+ if isinstance(self.value, list):
+ return (dic[self.key] in self.value)
+ else:
+ return (dic[self.key] == self.value)
+ elif self.op == ne:
+ if isinstance(self.value, list):
+ return (dic[self.key] not in self.value)
+ else:
+ return (dic[self.key] != self.value)
+ elif self.op == lt:
+ if isinstance(self.value, str):
+ # prefix match
+ return dic[self.key].startswith('%s.' % self.value)
+ else:
+ return (dic[self.key] < self.value)
+ elif self.op == le:
+ if isinstance(self.value, str):
+ return dic[self.key] == self.value or \
+ dic[self.key].startswith('%s.' % self.value)
+ else:
+ return (dic[self.key] <= self.value)
+ elif self.op == gt:
+ if isinstance(self.value, str):
+ # prefix match
+ return self.value.startswith('%s.' % dic[self.key])
+ else:
+ return (dic[self.key] > self.value)
+ elif self.op == ge:
+ if isinstance(self.value, str):
+ # prefix match
+ return dic[self.key] == self.value or \
+ self.value.startswith('%s.' % dic[self.key])
+ else:
+ return (dic[self.key] >= self.value)
+ elif self.op == and_:
+ return (dic[self.key] & self.value)
+ elif self.op == or_:
+ return (dic[self.key] | self.value)
+ elif self.op == contains:
+ try:
+ method, subfield = self.key.split('.', 1)
+ return not not [ x for x in dic[method] \
+ if x[subfield] == self.value]
+ except ValueError: # split has failed
+ return self.value in dic[self.key]
+ elif self.op == included:
+ return dic[self.key] in self.value
+ else:
+ raise Exception("Unexpected table format: %r" % dic)
+
+ def filter(self, dic):
+ """
+ Filter dic according to the current predicate.
+ """
+
+ if '.' in self.key:
+ # users.hrn
+ method, subfield = self.key.split('.', 1)
+ if not method in dic:
+ return None
+
+ if isinstance(dic[method], dict):
+ subpred = Predicate(subfield, self.op, self.value)
+ match = subpred.match(dic[method])
+ return dic if match else None
+
+ elif isinstance(dic[method], (list, tuple)):
+ # 1..N relationships
+ match = False
+ if self.op == contains:
+ return dic if self.match(dic) else None
+ else:
+ subpred = Predicate(subfield, self.op, self.value)
+ dic[method] = subpred.filter(dic[method])
+ return dic
+ else:
+ raise Exception("Unexpected table format: %r", dic)
+
+
+ else:
+ # Individual field operations
+ return dic if self.match(dic) else None
+
+ def get_field_names(self):
+ if isinstance(self.key, (list, tuple, set, frozenset)):
+ return FieldNames(self.key)
+ else:
+ return FieldNames([self.key])
+
+ def get_value_names(self):
+ if isinstance(self.value, (list, tuple, set, frozenset)):
+ return FieldNames(self.value)
+ else:
+ return FieldNames([self.value])
+
+ def has_empty_value(self):
+ if isinstance(self.value, (list, tuple, set, frozenset)):
+ return not any(self.value)
+ else:
+ return not self.value
+
+ def is_composite(self):
+ """
+ Returns:
+ True iif this Predicate instance involves
+ a tuple key (and tuple value).
+ """
+ return isinstance(self.get_key(), tuple)
+
+ def rename(self, aliases):
+ if self.is_composite():
+ raise NotImplemented
+ if self.key in aliases:
+ self.key = aliases[self.key]
diff --git a/netmodel/model/query.py b/netmodel/model/query.py
new file mode 100644
index 00000000..c182cb45
--- /dev/null
+++ b/netmodel/model/query.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.model.filter import Filter
+from netmodel.model.field_names import FieldNames
+
+ACTION_INSERT = 1
+ACTION_SELECT = 2
+ACTION_UPDATE = 3
+ACTION_DELETE = 4
+ACTION_EXECUTE = 5
+ACTION_SUBSCRIBE = 6
+ACTION_UNSUBSCRIBE = 7
+
+ACTION2STR = {
+ ACTION_INSERT : 'insert',
+ ACTION_SELECT : 'select',
+ ACTION_UPDATE : 'update',
+ ACTION_DELETE : 'delete',
+ ACTION_EXECUTE : 'execute',
+ ACTION_SUBSCRIBE : 'subscribe',
+ ACTION_UNSUBSCRIBE : 'unsubscribe',
+}
+STR2ACTION = dict((v, k) for k, v in ACTION2STR.items())
+
+FUNCTION_SUM = 1
+
+FUNCTION2STR = {
+ FUNCTION_SUM : 'sum'
+}
+STR2FUNCTION = dict((v, k) for k, v in FUNCTION2STR.items())
+
+class Query:
+ def __init__(self, action, object_name, filter = None, params = None,
+ field_names = None, aggregate = None, last = False, reply = False):
+ self.action = action
+ self.object_name = object_name
+
+ if filter:
+ if isinstance(filter, Filter):
+ self.filter = filter
+ else:
+ self.filter = Filter.from_list(filter)
+ else:
+ self.filter = Filter()
+
+ self.params = params
+
+ if field_names:
+ if isinstance(field_names, FieldNames):
+ self.field_names = field_names
+ else:
+ self.field_names = FieldNames(field_names)
+ else:
+ self.field_names = FieldNames()
+
+ self.aggregate = aggregate
+
+ self.last = last
+ self.reply = reply
+
+ def to_dict(self):
+ aggregate = FUNCTION2STR[self.aggregate] if self.aggregate else None
+ return {
+ 'action': ACTION2STR[self.action],
+ 'object_name': self.object_name,
+ 'filter': self.filter.to_list(),
+ 'params': self.params,
+ 'field_names': self.field_names,
+ 'aggregate': aggregate,
+ 'reply': self.reply,
+ 'last': self.last
+ }
+
+ @staticmethod
+ def from_dict(dic):
+ action = STR2ACTION[dic.get('action').lower()]
+ object_name = dic.get('object_name')
+ filter = dic.get('filter', None)
+ params = dic.get('params', None)
+ field_names = dic.get('field_names', None)
+ aggregate = STR2FUNCTION[dic.get('aggregate').lower()] \
+ if dic.get('aggregate') else None
+ if field_names == '*':
+ field_names = FieldNames(star = True)
+ last = dic.get('last', False)
+ reply = dic.get('reply', False)
+ return Query(action, object_name, filter, params, field_names,
+ aggregate, last)
+
+ def to_sql(self, multiline = False):
+ """
+ Args:
+ platform: A String corresponding to a namespace (or platform name)
+ multiline: A boolean indicating whether the String could contain
+ carriage return.
+ Returns:
+ The String representing this Query.
+ """
+ get_params_str = lambda : ", ".join(["%s = %r" % (k, v) \
+ for k, v in self.params.items()])
+
+ object_name = self.object_name
+ field_names = self.field_names
+ field_names_str = ('*' if field_names.is_star() \
+ else ', '.join([field for field in field_names]))
+ select = "SELECT %s" % ((FUNCTION2STR[self.aggregate] + "(%s)") \
+ if self.aggregate else '%s') % field_names_str
+ filter = "WHERE %s" % self.filter if self.filter else ''
+ #at = "AT %s" % self.get_timestamp() if self.get_timestamp() else ""
+ at = ''
+ params = "SET %s" % get_params_str() if self.params else ''
+
+ sep = " " if not multiline else "\n "
+
+ strmap = {
+ ACTION_SELECT : "%(select)s%(sep)s%(at)s%(sep)sFROM %(object_name)s%(sep)s%(filter)s",
+ ACTION_UPDATE : "UPDATE %(object_name)s%(sep)s%(params)s%(sep)s%(filter)s%(sep)s%(select)s",
+ ACTION_INSERT : "INSERT INTO %(object_name)s%(sep)s%(params)s",
+ ACTION_DELETE : "DELETE FROM %(object_name)s%(sep)s%(filter)s",
+ ACTION_SUBSCRIBE : "SUBSCRIBE : %(select)s%(sep)s%(at)s%(sep)sFROM %(object_name)s%(sep)s%(filter)s",
+ ACTION_UNSUBSCRIBE : "UNSUBSCRIBE : %(select)s%(sep)s%(at)s%(sep)sFROM %(object_name)s%(sep)s%(filter)s",
+ ACTION_EXECUTE : "EXECUTE : %(select)s%(sep)s%(at)s%(sep)sFROM %(object_name)s%(sep)s%(filter)s",
+ }
+
+ return strmap[self.action] % locals()
+
+ def __str__(self):
+ return self.to_sql()
+
+ def __repr__(self):
+ return self.to_sql()
diff --git a/netmodel/model/result_value.py b/netmodel/model/result_value.py
new file mode 100644
index 00000000..1812d5c4
--- /dev/null
+++ b/netmodel/model/result_value.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import pprint
+import time
+import traceback
+
+from netmodel.network.packet import ErrorPacket
+from netmodel.model.query import Query as Record
+
+# type
+SUCCESS = 0
+WARNING = 1
+ERROR = 2
+
+# origin
+CORE = 0
+GATEWAY = 1
+
+class ResultValue(dict):
+
+ ALLOWED_FIELDS = set(["origin", "type", "code", "value", "description",
+ "traceback", "ts"])
+
+ def __init__(self, *args, **kwargs):
+ if args:
+ if kwargs:
+ raise Exception("Bad initialization for ResultValue")
+
+ if len(args) == 1 and isinstance(args[0], dict):
+ kwargs = args[0]
+
+ given = set(kwargs.keys())
+ cstr_success = set(["code", "origin", "value"]) <= given
+ cstr_error = set(["code", "type", "origin", "description"]) <= given
+ assert given <= self.ALLOWED_FIELDS, \
+ "Wrong fields in ResultValue constructor: %r" % \
+ (given - self.ALLOWED_FIELDS)
+ assert cstr_success or cstr_error, \
+ "Incomplete set of fields in ResultValue constructor: %r" % given
+
+ dict.__init__(self, **kwargs)
+
+ # Set missing fields to None
+ for field in self.ALLOWED_FIELDS - given:
+ self[field] = None
+ if not "ts" in self:
+ self["ts"] = time.time()
+
+ def get_code(self):
+ """
+ Returns:
+ The code transported in this ResultValue instance/
+ """
+ return self["code"]
+
+ @classmethod
+ def get(self, records, errors):
+ num_errors = len(errors)
+
+ if num_errors == 0:
+ return ResultValue.success(records)
+ elif records:
+ return ResultValue.warning(records, errors)
+ else:
+ return ResultValue.errors(errors)
+
+ @classmethod
+ def success(self, result):
+ return ResultValue(
+ code = SUCCESS,
+ type = SUCCESS,
+ origin = [CORE, 0],
+ value = result
+ )
+
+ @staticmethod
+ def warning(result, errors):
+ return ResultValue(
+ code = ERROR,
+ type = WARNING,
+ origin = [CORE, 0],
+ value = result,
+ description = errors
+ )
+
+ @staticmethod
+ def error(description, code = ERROR):
+ assert isinstance(description, str),\
+ "Invalid description = %s (%s)" % (description, type(description))
+ assert isinstance(code, int),\
+ "Invalid code = %s (%s)" % (code, type(code))
+
+ return ResultValue(
+ type = ERROR,
+ code = code,
+ origin = [CORE, 0],
+ description = [ErrorPacket(type = ERROR, code = code,
+ message = description, traceback = None)]
+ )
+
+ @staticmethod
+ def errors(errors):
+ """
+ Make a ResultValue corresponding to an error and
+ gathering a set of ErrorPacket instances.
+ Args:
+ errors: A list of ErrorPacket instances.
+ Returns:
+ The corresponding ResultValue instance.
+ """
+ assert isinstance(errors, list),\
+ "Invalid errors = %s (%s)" % (errors, type(errors))
+
+ return ResultValue(
+ type = ERROR,
+ code = ERROR,
+ origin = [CORE, 0],
+ description = errors
+ )
+
+ def is_warning(self):
+ return self["type"] == WARNING
+
+ def is_success(self):
+ return self["type"] == SUCCESS and self["code"] == SUCCESS
+
+ def get_all(self):
+ """
+ Retrieve the Records embedded in this ResultValue.
+ Raises:
+ RuntimeError: in case of failure.
+ Returns:
+ A Records instance.
+ """
+ if not self.is_success() and not self.is_warning():
+ raise RuntimeError("Error executing query: %s" % \
+ (self["description"]))
+ try:
+ records = self["value"]
+ if len(records) > 0 and not isinstance(records[0], Record):
+ raise TypeError("Please put Record instances in ResultValue")
+ return records
+ except AttributeError as e:
+ raise RuntimeError(e)
+
+ def get_one(self):
+ """
+ Retrieve the only Record embeded in this ResultValue.
+ Raises:
+ RuntimeError: if there is 0 or more that 1 Record in
+ this ResultValue.
+ Returns:
+ A list of Records (and not of dict).
+ """
+ records = self.get_all()
+ num_records = len(records)
+ if num_records != 1:
+ raise RuntimeError('Cannot call get_one() with multiple records')
+ return records.get_one()
+
+ def get_error_message(self):
+ return "%r" % self["description"]
+
+ @staticmethod
+ def to_html(raw_dict):
+ return pprint.pformat(raw_dict).replace("\\n","<br/>")
+
+ def to_dict(self):
+ return dict(self)
diff --git a/netmodel/model/sql_parser.py b/netmodel/model/sql_parser.py
new file mode 100644
index 00000000..862c0a54
--- /dev/null
+++ b/netmodel/model/sql_parser.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+import sys
+import pyparsing as pp
+
+from netmodel.model.query import Query
+from netmodel.model.filter import Filter
+from netmodel.model.predicate import Predicate
+
+DEBUG = False
+
+def debug(args):
+ if DEBUG: print(args)
+
+class SQLParser(object):
+
+ def __init__(self):
+ """
+ Our simple BNF:
+ SELECT [fields[*] FROM table WHERE clause
+ """
+
+ integer = pp.Combine(pp.Optional(pp.oneOf("+ -")) +
+ pp.Word(pp.nums)).setParseAction(lambda t:int(t[0]))
+ floatNumber = pp.Regex(r'\d+(\.\d*)?([eE]\d+)?')
+ point = pp.Literal(".")
+ e = pp.CaselessLiteral("E")
+
+ kw_store = pp.CaselessKeyword('=')
+ kw_select = pp.CaselessKeyword('select')
+ kw_subscribe = pp.CaselessKeyword('subscribe')
+ kw_update = pp.CaselessKeyword('update')
+ kw_insert = pp.CaselessKeyword('insert')
+ kw_delete = pp.CaselessKeyword('delete')
+ kw_execute = pp.CaselessKeyword('execute')
+
+ kw_from = pp.CaselessKeyword('from')
+ kw_into = pp.CaselessKeyword('into')
+ kw_where = pp.CaselessKeyword('where')
+ kw_at = pp.CaselessKeyword('at')
+ kw_set = pp.CaselessKeyword('set')
+ kw_true = pp.CaselessKeyword('true').setParseAction(lambda t: 1)
+ kw_false = pp.CaselessKeyword('false').setParseAction(lambda t: 0)
+ kw_with = pp.CaselessKeyword('with')
+
+ sum_function = pp.CaselessLiteral('sum')
+
+ # Regex string representing the set of possible operators
+ # Example : ">=|<=|!=|>|<|="
+ OPERATOR_RX = "(?i)%s" % '|'.join([re.sub('\|', '\|', o) \
+ for o in Predicate.operators.keys()])
+
+ # predicate
+ field = pp.Word(pp.alphanums + '_' + '.')
+ operator = pp.Regex(OPERATOR_RX).setName("operator")
+ variable = pp.Literal('$').suppress() + pp.Word(pp.alphanums \
+ + '_' + '.').setParseAction(lambda t: "$%s" % t[0])
+ filename = pp.Regex('([a-z]+?://)?(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
+
+ obj = pp.Forward()
+ value = obj | pp.QuotedString('"') | pp.QuotedString("'") | \
+ kw_true | kw_false | integer | variable
+
+ def handle_value_list(s, l, t):
+ t = t.asList()
+ new_t = tuple(t)
+ debug("[handle_value_list] s = %(s)s ** l = %(l)s ** t = %(t)s" \
+ % locals())
+ debug(" new_t = %(new_t)s" % locals())
+ return new_t
+
+ value_list = value \
+ | (pp.Literal("[").suppress() + pp.Literal("]").suppress()) \
+ .setParseAction(lambda s, l, t: [[]]) \
+ | pp.Literal("[").suppress() \
+ + pp.delimitedList(value).setParseAction(handle_value_list) \
+ + pp.Literal("]") \
+ .suppress()
+
+ table = pp.Word(pp.alphanums + ':_-/').setResultsName('object_name')
+ field_list = pp.Literal("*") | pp.delimitedList(field).setParseAction(lambda tokens: set(tokens))
+
+ assoc = (field + pp.Literal(":").suppress() + value_list).setParseAction(lambda tokens: [tokens.asList()])
+ obj << pp.Literal("{").suppress() \
+ + pp.delimitedList(assoc).setParseAction(lambda t: dict(t.asList())) \
+ + pp.Literal("}").suppress()
+
+ # PARAMETER (SET)
+ # X = Y --> t=(X, Y)
+ def handle_param(s, l, t):
+ t = t.asList()
+ assert len(t) == 2
+ new_t = tuple(t)
+ debug("[handle_param] s = %(s)s ** l = %(l)s ** t = %(t)s" % locals())
+ debug(" new_t = %(new_t)s" % locals())
+ return new_t
+
+ param = (field + pp.Literal("=").suppress() + value_list) \
+ .setParseAction(handle_param)
+
+ # PARAMETERS (SET)
+ # PARAMETER[, PARAMETER[, ...]] --> dict()
+ def handle_parameters(s, l, t):
+ t = t.asList()
+ new_t = dict(t) if t else dict()
+ debug("[handle_parameters] s = %(s)s ** l = %(l)s ** t = %(t)s" % locals())
+ debug(" new_t = %(new_t)s" % locals())
+ return new_t
+
+ parameters = pp.delimitedList(param) \
+ .setParseAction(handle_parameters)
+
+ predicate = (field + operator + value_list).setParseAction(self.handlePredicate)
+
+ # For the time being, we only support simple filters and not full clauses
+ filter = pp.delimitedList(predicate, delim='&&').setParseAction(lambda tokens: Filter(tokens.asList()))
+
+ datetime = pp.Regex(r'....-..-.. ..:..:..')
+
+ timestamp = pp.CaselessKeyword('now') | datetime
+
+ store_elt = (variable.setResultsName("variable") + kw_store.suppress())
+ fields_elt = field_list.setResultsName('field_names')
+ aggregate_elt = sum_function.setResultsName('aggregate') + pp.Literal("(").suppress() + fields_elt + pp.Literal(")").suppress()
+ select_elt = (kw_select.suppress() + fields_elt)
+ subscribe_elt = (kw_subscribe.suppress() + fields_elt)
+ where_elt = (kw_where.suppress() + filter.setResultsName('filter'))
+ set_elt = (kw_set.suppress() + parameters.setResultsName('params'))
+ at_elt = (kw_at.suppress() + timestamp.setResultsName('timestamp'))
+ into_elt = (kw_into.suppress() + filename.setResultsName('receiver'))
+
+ # SELECT [SUM(]*|field_list[)] [AT timestamp] FROM table [WHERE clause]
+ select = (
+ pp.Optional(store_elt)\
+ + kw_select.suppress() \
+ + pp.Optional(into_elt) \
+ + (aggregate_elt | fields_elt)\
+ + pp.Optional(at_elt)\
+ + kw_from.suppress()\
+ + table\
+ + pp.Optional(where_elt)
+ ).setParseAction(lambda args: self.action(args, 'select'))
+
+ subscribe = (
+ pp.Optional(store_elt) \
+ + kw_subscribe.suppress() \
+ + pp.Optional(into_elt) \
+ + (aggregate_elt | fields_elt) \
+ + pp.Optional(at_elt)\
+ + kw_from.suppress()\
+ + table\
+ + pp.Optional(where_elt)
+ + pp.Optional(set_elt)
+ ).setParseAction(lambda args: self.action(args, 'subscribe'))
+
+ # UPDATE table SET parameters [WHERE clause] [SELECT *|field_list]
+ update = (
+ kw_update \
+ + table \
+ + set_elt \
+ + pp.Optional(where_elt) \
+ + pp.Optional(select_elt)
+ ).setParseAction(lambda args: self.action(args, 'update'))
+
+ # INSERT INTO table SET parameters [SELECT *|field_list]
+ insert = (
+ kw_insert + kw_into + table
+ + set_elt
+ + pp.Optional(select_elt)
+ ).setParseAction(lambda args: self.action(args, 'insert'))
+
+ # DELETE FROM table [WHERE clause]
+ delete = (
+ kw_delete \
+ + kw_from \
+ + table \
+ + pp.Optional(where_elt)
+ ).setParseAction(lambda args: self.action(args, 'delete'))
+
+ #
+ execute = (
+ kw_execute + kw_from + table
+ + set_elt
+ + pp.Optional(where_elt)
+ ).setParseAction(lambda args: self.action(args, 'execute'))
+
+ annotation = pp.Optional(kw_with + \
+ parameters.setResultsName('annotation'))
+
+ self.bnf = (select | update | insert | delete | subscribe | execute) \
+ + annotation
+
+ # For reusing parser:
+ self.filter = filter
+
+ def action(self, args, action):
+ args['action'] = action
+
+ def handlePredicate(self, args):
+ return Predicate(*args)
+
+ def parse(self, string):
+ result = self.bnf.parseString(string, parseAll = True)
+ return dict(result.items())
diff --git a/netmodel/model/type.py b/netmodel/model/type.py
new file mode 100644
index 00000000..20dc2580
--- /dev/null
+++ b/netmodel/model/type.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from netmodel.util.meta import inheritors
+
+class BaseType:
+ @staticmethod
+ def name():
+ return self.__class__.__name__.lower()
+
+class String(BaseType):
+ def __init__(self, *args, **kwargs):
+ self._min_size = kwargs.pop('min_size', None)
+ self._max_size = kwargs.pop('max_size', None)
+ self._ascii = kwargs.pop('ascii', False)
+ self._forbidden = kwargs.pop('forbidden', None)
+ super().__init__()
+
+class Integer(BaseType):
+ def __init__(self, *args, **kwargs):
+ self._min_value = kwargs.pop('min_value', None)
+ self._max_value = kwargs.pop('max_value', None)
+ super().__init__()
+
+class Double(BaseType):
+ def __init__(self, *args, **kwargs):
+ self._min_value = kwargs.pop('min_value', None)
+ self._max_value = kwargs.pop('max_value', None)
+ super().__init__()
+
+class Bool(BaseType):
+ pass
+
+class Dict(BaseType):
+ pass
+
+class Self(BaseType):
+ """Self-reference
+ """
+
+class Type:
+ BASE_TYPES = (String, Integer, Double, Bool)
+ _registry = dict()
+
+ @staticmethod
+ def from_string(type_name, raise_exception=True):
+ """Returns a type corresponding to the type name.
+
+ Params:
+ type_name (str) : Name of the type
+
+ Returns
+ Type : Type class of the requested type name
+ """
+ type_cls = [t for t in Type.BASE_TYPES if t.name == type_name]
+ if type_cls:
+ return type_cls[0]
+
+ type_cls = Type._registry.get(type_name, None)
+ if not type_cls:
+ raise Exception("No type found: {}".format(type_name))
+ return type_cls
+
+ @staticmethod
+ def is_base_type(type_cls):
+ return type_cls in Type.BASE_TYPES
+
+ @staticmethod
+ def exists(typ):
+ return (isinstance(typ, type) and typ in inheritors(BaseType)) \
+ or isinstance(typ, BaseType)
+
+is_base_type = Type.is_base_type
+is_type = Type.exists