diff options
Diffstat (limited to 'netmodel/model')
-rw-r--r-- | netmodel/model/__init__.py | 0 | ||||
-rw-r--r-- | netmodel/model/attribute.py | 262 | ||||
-rw-r--r-- | netmodel/model/field_names.py | 396 | ||||
-rw-r--r-- | netmodel/model/filter.py | 397 | ||||
-rw-r--r-- | netmodel/model/mapper.py | 20 | ||||
-rw-r--r-- | netmodel/model/object.py | 231 | ||||
-rw-r--r-- | netmodel/model/predicate.py | 306 | ||||
-rw-r--r-- | netmodel/model/query.py | 147 | ||||
-rw-r--r-- | netmodel/model/result_value.py | 185 | ||||
-rw-r--r-- | netmodel/model/sql_parser.py | 221 | ||||
-rw-r--r-- | netmodel/model/type.py | 89 |
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 |