aboutsummaryrefslogtreecommitdiffstats
path: root/netmodel/model
diff options
context:
space:
mode:
Diffstat (limited to 'netmodel/model')
-rw-r--r--netmodel/model/attribute.py120
-rw-r--r--netmodel/model/collection.py5
-rw-r--r--netmodel/model/key.py19
-rw-r--r--netmodel/model/mapper.py4
-rw-r--r--netmodel/model/object.py126
-rw-r--r--netmodel/model/query.py13
-rw-r--r--netmodel/model/sa_collections.py265
-rw-r--r--netmodel/model/sa_compat.py270
-rw-r--r--netmodel/model/type.py185
-rw-r--r--netmodel/model/uuid.py51
10 files changed, 940 insertions, 118 deletions
diff --git a/netmodel/model/attribute.py b/netmodel/model/attribute.py
index b69ee1bf..b2fa2331 100644
--- a/netmodel/model/attribute.py
+++ b/netmodel/model/attribute.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 Cisco and/or its affiliates.
@@ -22,11 +22,10 @@ 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
+from netmodel.model.mapper import ObjectSpecification
+from netmodel.model.type import Type, Self
+from netmodel.util.misc import is_iterable
+from netmodel.model.collection import Collection
log = logging.getLogger(__name__)
instance_dict = operator.attrgetter('__dict__')
@@ -38,26 +37,25 @@ class NEVER_SET: None
#------------------------------------------------------------------------------
class Multiplicity:
- _1_1 = '1_1'
- _1_N = '1_N'
- _N_1 = 'N_1'
- _N_N = 'N_N'
-
+ OneToOne = '1_1'
+ OneToMany = '1_N'
+ ManyToOne = 'N_1'
+ ManyToMany = '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,
+ Multiplicity.OneToOne: Multiplicity.OneToOne,
+ Multiplicity.OneToMany: Multiplicity.ManyToOne,
+ Multiplicity.ManyToOne: Multiplicity.OneToMany,
+ Multiplicity.ManyToMany: Multiplicity.ManyToMany,
}
return reverse_map[value]
# Default attribute properties values (default to None)
DEFAULT = {
- 'multiplicity' : Multiplicity._1_1,
+ 'multiplicity' : Multiplicity.OneToOne,
'mandatory' : False,
}
@@ -71,33 +69,36 @@ class Attribute(abc.ABC, ObjectSpecification):
'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:
+ for key in kwargs.keys():
+ if not key in self.properties:
+ raise ValueError("Invalid attribute property {}".format(key))
+ for key in self.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.type is optional since the type can be inherited. Although we
+ # will have to verify the attribute is complete at some point
+ if isinstance(self.type, str):
+ self.type = Type.from_string(self.type)
+ assert self.type is Self or Type.exists(self.type)
self.is_aggregate = False
self._reverse_attributes = list()
-
+
#--------------------------------------------------------------------------
# Display
#--------------------------------------------------------------------------
@@ -107,6 +108,15 @@ class Attribute(abc.ABC, ObjectSpecification):
__str__ = __repr__
+ # The following functions are required to allow comparing attributes, and
+ # using them as dict keys
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __hash__(self):
+ return hash(self.name)
+
#--------------------------------------------------------------------------
# Descriptor protocol
#
@@ -118,7 +128,7 @@ class Attribute(abc.ABC, ObjectSpecification):
return self
value = instance_dict(instance).get(self.name, NEVER_SET)
-
+
# Case : collection attribute
if self.is_collection:
if value is NEVER_SET:
@@ -126,12 +136,12 @@ class Attribute(abc.ABC, ObjectSpecification):
default = self.default(instance)
else:
default = self.default
- value = InstrumentedList(default)
+ value = Collection(default)
value._attribute = self
value._instance = instance
self.__set__(instance, value)
return value
- return value
+ return value
# Case : scalar attribute
@@ -159,8 +169,8 @@ class Attribute(abc.ABC, ObjectSpecification):
return
if self.is_collection:
- if not isinstance(value, InstrumentedList):
- value = InstrumentedList(value)
+ if not isinstance(value, Collection):
+ value = Collection(value)
value._attribute = self
value._instance = instance
@@ -172,6 +182,10 @@ class Attribute(abc.ABC, ObjectSpecification):
def __delete__(self, instance):
raise NotImplementedError
+ def __set_name__(self, owner, name):
+ self.name = name
+ self.owner = owner
+
#--------------------------------------------------------------------------
# Accessors
#--------------------------------------------------------------------------
@@ -184,17 +198,16 @@ class Attribute(abc.ABC, ObjectSpecification):
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)
+ return self.multiplicity in (Multiplicity.OneToMany,
+ Multiplicity.ManyToMany)
def is_set(self, instance):
- return self.name in instance_dict(instance)
+ return instance.is_set(self.name)
#--------------------------------------------------------------------------
# Operations
@@ -217,46 +230,3 @@ class Attribute(abc.ABC, ObjectSpecification):
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/collection.py b/netmodel/model/collection.py
index 21be84d8..01a63299 100644
--- a/netmodel/model/collection.py
+++ b/netmodel/model/collection.py
@@ -16,9 +16,10 @@
# limitations under the License.
#
-from netmodel.model.filter import Filter
+from netmodel.model.sa_collections import InstrumentedList
+from netmodel.model.filter import Filter
-class Collection(list):
+class Collection(InstrumentedList):
"""
A collection corresponds to a list of objects, and includes processing functionalities to
manipulate them.
diff --git a/netmodel/model/key.py b/netmodel/model/key.py
new file mode 100644
index 00000000..bc49af03
--- /dev/null
+++ b/netmodel/model/key.py
@@ -0,0 +1,19 @@
+from netmodel.model.mapper import ObjectSpecification
+
+class Key(ObjectSpecification):
+ def __init__(self, *attributes):
+ self._attributes = attributes
+
+ #--------------------------------------------------------------------------
+ # Descriptor protocol
+ #
+ # see. https://docs.python.org/3/howto/descriptor.html
+ #--------------------------------------------------------------------------
+
+ def __set_name__(self, owner, name):
+ self._name = name
+ self._owner = owner
+
+ def __iter__(self):
+ for attribute in self._attributes:
+ yield attribute
diff --git a/netmodel/model/mapper.py b/netmodel/model/mapper.py
index 9be46a14..856238c7 100644
--- a/netmodel/model/mapper.py
+++ b/netmodel/model/mapper.py
@@ -16,5 +16,7 @@
# limitations under the License.
#
-class ObjectSpecification:
+import sys
+
+class ObjectSpecification:
pass
diff --git a/netmodel/model/object.py b/netmodel/model/object.py
index 32d3a833..99dbe0c2 100644
--- a/netmodel/model/object.py
+++ b/netmodel/model/object.py
@@ -18,7 +18,10 @@
from abc import ABCMeta
-from netmodel.model.attribute import Attribute
+import sys
+
+from netmodel.model.attribute import Attribute, Multiplicity
+from netmodel.model.key import Key
from netmodel.model.type import BaseType
from netmodel.model.mapper import ObjectSpecification
@@ -26,11 +29,21 @@ from netmodel.model.mapper import ObjectSpecification
E_UNK_RES_NAME = 'Unknown resource name for attribute {} in {} ({}) : {}'
-class ObjectMetaclass(ABCMeta):
+class ObjectMetaclass(type):
"""
Object metaclass allowing non-uniform attribute declaration.
"""
+ def __new__(mcls, name, bases, attrs):
+ cls = super(ObjectMetaclass, mcls).__new__(mcls, name, bases, attrs)
+ if (sys.version_info < (3, 6)):
+ # Before Python 3.6, descriptor protocol does not include __set_name__.
+ # We use a metaclass to emulate the functionality.
+ for attr, obj in attrs.items():
+ if isinstance(obj, ObjectSpecification):
+ obj.__set_name__(cls, attr)
+ return cls
+
def __init__(cls, class_name, parents, attrs):
"""
Args:
@@ -67,19 +80,19 @@ class Object(BaseType, metaclass = ObjectMetaclass):
else:
resource = x
if not resource:
- raise LurchException(E_UNK_RES_NAME.format(key,
+ 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)
+ resource = self._state.manager.by_name(value)
elif isinstance(value, UUID):
- resource = self._state.manager.by_uuid(value)
+ resource = self._state.manager.by_uuid(value)
else:
resource = value
if not resource:
- raise LurchException(E_UNK_RES_NAME.format(key,
+ raise LurchException(E_UNK_RES_NAME.format(key,
self.name, self.__class__.__name__, value))
value = resource._state.uuid
setattr(self, key, value)
@@ -111,19 +124,20 @@ class Object(BaseType, metaclass = ObjectMetaclass):
@classmethod
def _sanitize(cls):
- """Sanitize the object model to accomodate for multiple declaration
- styles
+ """
+ This methods performs sanitization of the object declaration.
+
+ More specifically:
+ - it goes over all attributes and sets their name based on the python
+ object attribute name.
+ - it establishes mutual object relationships through reverse attributes.
- 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):
+ if not isinstance(obj, Attribute):
continue
- if isinstance(obj, Attribute):
- obj.name = name
# Remember whether a reverse_name is defined before loading
# inherited properties from parent
@@ -135,30 +149,72 @@ class Object(BaseType, metaclass = ObjectMetaclass):
if hasattr(base, name):
parent_attribute = getattr(base, name)
obj.merge(parent_attribute)
- assert obj.type
+ assert obj.type, "No type for obj={} cls={}, base={}".format(obj, cls, base)
# Handle reverse attribute
#
# NOTE: we need to do this after merging to be sure we get all
# properties inherited from parent (eg. multiplicity)
+ #
+ # See "Reverse attributes" section in BaseResource docstring.
+ #
+ # Continueing with the same example, let's detail how it is handled:
+ #
+ # Original declaration:
+ # >>>
+ # class Group(Resource):
+ # resources = Attribute(Resource, description = 'Resources belonging to the group',
+ # multiplicity = Multiplicity.ManyToMany,
+ # default = [],
+ # reverse_name = 'groups',
+ # reverse_description = 'Groups to which the resource belongs')
+ # <<<
+ #
+ # Local variables:
+ # cls = <class 'vicn.resource.group.Group'>
+ # obj = <Attribute resources>
+ # obj.type = <class 'vicn.core.Resource'>
+ # reverse_attribute = <Attribute groups>
+ #
+ # Result:
+ # 1) Group._reverse_attributes =
+ # { <Attribute resources> : [<Attribute groups>, ...], ...}
+ # 2) Add attribute <Attribute groups> to class Resource
+ # 3) Resource._reverse_attributes =
+ # { <Attribute groups> : [<Attribute resources], ...], ...}
+ #
if has_reverse:
a = {
- 'name' : obj.reverse_name,
- 'description' : obj.reverse_description,
- 'multiplicity' : Multiplicity.reverse(obj.multiplicity),
- 'auto' : obj.reverse_auto,
+ 'name' : obj.reverse_name,
+ 'description' : obj.reverse_description,
+ 'multiplicity' : Multiplicity.reverse(obj.multiplicity),
+ 'reverse_name' : obj.name,
+ 'reverse_description' : obj.description,
+ 'auto' : obj.reverse_auto,
}
- reverse_attribute = Attribute(cls, **a)
+
+ # We need to use the same class as the Attribute !
+ reverse_attribute = obj.__class__(cls, **a)
reverse_attribute.is_aggregate = True
+ # 1) Store the reverse attributes to be later inserted in the
+ # remote class, at the end of the function
+ # TODO : clarify the reasons to perform this in two steps
cur_reverse_attributes[obj.type] = reverse_attribute
+ # 2)
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)
+ # 3)
+ if not reverse_attribute in obj.type._reverse_attributes:
+ obj.type._reverse_attributes[reverse_attribute] = list()
+ obj.type._reverse_attributes[reverse_attribute].append(obj)
+
+ # Insert newly created reverse attributes in the remote class
+ for kls, a in cur_reverse_attributes.items():
+ setattr(kls, a.name, a)
@classmethod
def iter_attributes(cls, aggregates = False):
@@ -168,7 +224,7 @@ class Object(BaseType, metaclass = ObjectMetaclass):
continue
if attribute.is_aggregate and not aggregates:
continue
-
+
yield attribute
def get_attributes(self, aggregates = False):
@@ -178,7 +234,7 @@ class Object(BaseType, metaclass = ObjectMetaclass):
return set(a.name for a in self.iter_attributes(aggregates = \
aggregates))
- def get_attribute_dict(self, field_names = None, aggregates = False,
+ 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)
@@ -192,14 +248,29 @@ class Object(BaseType, metaclass = ObjectMetaclass):
ret[a.name] = list()
for x in value:
if uuid and isinstance(x, Object):
- x = x._state.uuid._uuid
+ x = x._state.uuid._uuid
ret[a.name].append(x)
else:
if uuid and isinstance(value, Object):
- value = value._state.uuid._uuid
+ value = value._state.uuid._uuid
ret[a.name] = value
return ret
+ @classmethod
+ def iter_keys(cls):
+ for name in dir(cls):
+ key = getattr(cls, name)
+ if not isinstance(key, Key):
+ continue
+ yield key
+
+ @classmethod
+ def get_keys(cls):
+ return list(cls.iter_keys())
+
+ def get_key_dicts(self):
+ return [{attribute: self.get(attribute.name) for attribute in key} for key in self.iter_keys()]
+
def get_tuple(self):
return (self.__class__, self._get_attribute_dict())
@@ -229,3 +300,8 @@ class Object(BaseType, metaclass = ObjectMetaclass):
def has_attribute(cls, name):
return name in [a.name for a in cls.attributes()]
+ def get(self, attribute_name):
+ raise NotImplementedError
+
+ def set(self, attribute_name, value):
+ raise NotImplementedError
diff --git a/netmodel/model/query.py b/netmodel/model/query.py
index c182cb45..a1d331fb 100644
--- a/netmodel/model/query.py
+++ b/netmodel/model/query.py
@@ -46,7 +46,7 @@ FUNCTION2STR = {
STR2FUNCTION = dict((v, k) for k, v in FUNCTION2STR.items())
class Query:
- def __init__(self, action, object_name, filter = None, params = None,
+ 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
@@ -64,13 +64,13 @@ class Query:
if field_names:
if isinstance(field_names, FieldNames):
self.field_names = field_names
- else:
+ else:
self.field_names = FieldNames(field_names)
else:
self.field_names = FieldNames()
self.aggregate = aggregate
-
+
self.last = last
self.reply = reply
@@ -100,7 +100,7 @@ class Query:
field_names = FieldNames(star = True)
last = dic.get('last', False)
reply = dic.get('reply', False)
- return Query(action, object_name, filter, params, field_names,
+ return Query(action, object_name, filter, params, field_names,
aggregate, last)
def to_sql(self, multiline = False):
@@ -140,8 +140,7 @@ class Query:
return strmap[self.action] % locals()
- def __str__(self):
- return self.to_sql()
-
def __repr__(self):
return self.to_sql()
+
+ __str__ = __repr__
diff --git a/netmodel/model/sa_collections.py b/netmodel/model/sa_collections.py
new file mode 100644
index 00000000..5e651061
--- /dev/null
+++ b/netmodel/model/sa_collections.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This module is derived from code from SQLAlchemy
+#
+# orm/collections.py
+# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+
+import logging
+
+from netmodel.model.sa_compat import py2k
+from netmodel.model.uuid import UUID
+
+class InstrumentedListException(Exception): pass
+
+log = logging.getLogger(__name__)
+
+def _list_decorators():
+ """Tailored instrumentation wrappers for any list-like class."""
+
+ def _tidy(fn):
+ fn._sa_instrumented = True
+ fn.__doc__ = getattr(list, fn.__name__).__doc__
+
+ def append(fn):
+ def append(self, item):
+ try:
+ item = self._attribute.do_list_add(self._instance, item)
+ fn(self, item)
+ except InstrumentedListException as e:
+ pass
+ _tidy(append)
+ return append
+
+ def remove(fn):
+ def remove(self, value):
+ # testlib.pragma exempt:__eq__
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ fn(self, value)
+ except : pass
+ _tidy(remove)
+ return remove
+
+ def insert(fn):
+ def insert(self, index, value):
+ try:
+ value = self._attribute.do_list_add(self._instance, item)
+ fn(self, index, value)
+ except : pass
+ _tidy(insert)
+ return insert
+
+ def __getitem__(fn):
+ def __getitem__(self, index):
+ item = fn(self, index)
+ return self._attribute.handle_getitem(self._instance, item)
+ _tidy(__getitem__)
+ return __getitem__
+
+ def __setitem__(fn):
+ def __setitem__(self, index, value):
+ if not isinstance(index, slice):
+ existing = self[index]
+ if existing is not None:
+ try:
+ self._attribute.do_list_remove(self._instance, existing)
+ except: pass
+ try:
+ value = self._attribute.do_list_add(self._instance, value)
+ fn(self, index, value)
+ except: pass
+ else:
+ # slice assignment requires __delitem__, insert, __len__
+ step = index.step or 1
+ start = index.start or 0
+ if start < 0:
+ start += len(self)
+ if index.stop is not None:
+ stop = index.stop
+ else:
+ stop = len(self)
+ if stop < 0:
+ stop += len(self)
+
+ if step == 1:
+ for i in range(start, stop, step):
+ if len(self) > start:
+ del self[start]
+
+ for i, item in enumerate(value):
+ self.insert(i + start, item)
+ else:
+ rng = list(range(start, stop, step))
+ if len(value) != len(rng):
+ raise ValueError(
+ "attempt to assign sequence of size %s to "
+ "extended slice of size %s" % (len(value),
+ len(rng)))
+ for i, item in zip(rng, value):
+ self.__setitem__(i, item)
+ _tidy(__setitem__)
+ return __setitem__
+
+ def __delitem__(fn):
+ def __delitem__(self, index):
+ if not isinstance(index, slice):
+ item = self[index]
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ fn(self, index)
+ except : pass
+ else:
+ # slice deletion requires __getslice__ and a slice-groking
+ # __getitem__ for stepped deletion
+ # note: not breaking this into atomic dels
+ has_except = False
+ for item in self[index]:
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ except : has_except = True
+ if not has_except:
+ fn(self, index)
+ _tidy(__delitem__)
+ return __delitem__
+
+ if py2k:
+ def __setslice__(fn):
+ def __setslice__(self, start, end, values):
+ has_except = False
+ for value in self[start:end]:
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ except : has_except = True
+ #values = [self._attribute.do_list_add(self._instance, value) for value in values]
+ _values = list()
+ for value in values:
+ try:
+ _values.append(self._attribute.do_list_add(self._instance, value))
+ except: has_except = True
+ if not has_except:
+ fn(self, start, end, _values)
+ _tidy(__setslice__)
+ return __setslice__
+
+ def __delslice__(fn):
+ def __delslice__(self, start, end):
+ has_except = False
+ for value in self[start:end]:
+ try:
+ self._attribute.do_list_remove(self._instance, value)
+ except : has_except = True
+ if not has_except:
+ fn(self, start, end)
+ _tidy(__delslice__)
+ return __delslice__
+
+ def extend(fn):
+ def extend(self, iterable):
+ for value in iterable:
+ self.append(value)
+ _tidy(extend)
+ return extend
+
+ def __iadd__(fn):
+ def __iadd__(self, iterable):
+ # list.__iadd__ takes any iterable and seems to let TypeError
+ # raise as-is instead of returning NotImplemented
+ for value in iterable:
+ self.append(value)
+ return self
+ _tidy(__iadd__)
+ return __iadd__
+
+ def pop(fn):
+ def pop(self, index=-1):
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ item = fn(self, index)
+ return item
+ except : return None
+ _tidy(pop)
+ return pop
+
+ def __iter__(fn):
+ def __iter__(self):
+ for item in fn(self):
+ yield self._attribute.handle_getitem(self._instance, item)
+ _tidy(__iter__)
+ return __iter__
+
+ def __repr__(fn):
+ def __repr__(self):
+ return '<Collection {} {}>'.format(id(self), list.__repr__(self))
+ _tidy(__repr__)
+ return __repr__
+
+ __str__ = __repr__
+ #def __str__(fn):
+ # def __str__(self):
+ # return str(list(self))
+ # _tidy(__str__)
+ # return __str__
+
+ if not py2k:
+ def clear(fn):
+ def clear(self, index=-1):
+ has_except = False
+ for item in self:
+ try:
+ self._attribute.do_list_remove(self._instance, item)
+ except : has_except = True
+ if not has_except:
+ fn(self)
+ _tidy(clear)
+ return clear
+
+ # __imul__ : not wrapping this. all members of the collection are already
+ # present, so no need to fire appends... wrapping it with an explicit
+ # decorator is still possible, so events on *= can be had if they're
+ # desired. hard to imagine a use case for __imul__, though.
+
+ l = locals().copy()
+ l.pop('_tidy')
+ return l
+
+def _instrument_list(cls):
+ # inspired by sqlalchemy
+ for method, decorator in _list_decorators().items():
+ fn = getattr(cls, method, None)
+ if fn:
+ #if (fn and method not in methods and
+ # not hasattr(fn, '_sa_instrumented')):
+ setattr(cls, method, decorator(fn))
+
+class InstrumentedList(list):
+
+ @classmethod
+ def from_list(cls, value, instance, attribute):
+ lst = list()
+ if value:
+ for x in value:
+ if isinstance(x, UUID):
+ x = instance.from_uuid(x)
+ lst.append(x)
+ # Having a class method is important for inheritance
+ value = cls(lst)
+ value._attribute = attribute
+ value._instance = instance
+ return value
+
+ def __contains__(self, key):
+ from vicn.core.resource import Resource
+ if isinstance(key, Resource):
+ key = key.get_uuid()
+ return list.__contains__(self, key)
+
+ def __lshift__(self, item):
+ self.append(item)
+
+_instrument_list(InstrumentedList)
diff --git a/netmodel/model/sa_compat.py b/netmodel/model/sa_compat.py
new file mode 100644
index 00000000..34211455
--- /dev/null
+++ b/netmodel/model/sa_compat.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This module originates from SQLAlchemy
+#
+# util/compat.py
+# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+
+"""Handle Python version/platform incompatibilities."""
+
+import sys
+
+try:
+ import threading
+except ImportError:
+ import dummy_threading as threading
+
+py36 = sys.version_info >= (3, 6)
+py33 = sys.version_info >= (3, 3)
+py32 = sys.version_info >= (3, 2)
+py3k = sys.version_info >= (3, 0)
+py2k = sys.version_info < (3, 0)
+py265 = sys.version_info >= (2, 6, 5)
+jython = sys.platform.startswith('java')
+pypy = hasattr(sys, 'pypy_version_info')
+win32 = sys.platform.startswith('win')
+cpython = not pypy and not jython # TODO: something better for this ?
+
+import collections
+next = next
+
+if py3k:
+ import pickle
+else:
+ try:
+ import cPickle as pickle
+ except ImportError:
+ import pickle
+
+# work around http://bugs.python.org/issue2646
+if py265:
+ safe_kwarg = lambda arg: arg
+else:
+ safe_kwarg = str
+
+ArgSpec = collections.namedtuple("ArgSpec",
+ ["args", "varargs", "keywords", "defaults"])
+
+if py3k:
+ import builtins
+
+ from inspect import getfullargspec as inspect_getfullargspec
+ from urllib.parse import (quote_plus, unquote_plus,
+ parse_qsl, quote, unquote)
+ import configparser
+ from io import StringIO
+
+ from io import BytesIO as byte_buffer
+
+ def inspect_getargspec(func):
+ return ArgSpec(
+ *inspect_getfullargspec(func)[0:4]
+ )
+
+ string_types = str,
+ binary_types = bytes,
+ binary_type = bytes
+ text_type = str
+ int_types = int,
+ iterbytes = iter
+
+ def u(s):
+ return s
+
+ def ue(s):
+ return s
+
+ def b(s):
+ return s.encode("latin-1")
+
+ if py32:
+ callable = callable
+ else:
+ def callable(fn):
+ return hasattr(fn, '__call__')
+
+ def cmp(a, b):
+ return (a > b) - (a < b)
+
+ from functools import reduce
+
+ print_ = getattr(builtins, "print")
+
+ import_ = getattr(builtins, '__import__')
+
+ import itertools
+ itertools_filterfalse = itertools.filterfalse
+ itertools_filter = filter
+ itertools_imap = map
+ from itertools import zip_longest
+
+ import base64
+
+ def b64encode(x):
+ return base64.b64encode(x).decode('ascii')
+
+ def b64decode(x):
+ return base64.b64decode(x.encode('ascii'))
+
+else:
+ from inspect import getargspec as inspect_getfullargspec
+ inspect_getargspec = inspect_getfullargspec
+ from urllib import quote_plus, unquote_plus, quote, unquote
+ from urlparse import parse_qsl
+ import ConfigParser as configparser
+ from StringIO import StringIO
+ from cStringIO import StringIO as byte_buffer
+
+ string_types = basestring,
+ binary_types = bytes,
+ binary_type = str
+ text_type = unicode
+ int_types = int, long
+
+ def iterbytes(buf):
+ return (ord(byte) for byte in buf)
+
+ def u(s):
+ # this differs from what six does, which doesn't support non-ASCII
+ # strings - we only use u() with
+ # literal source strings, and all our source files with non-ascii
+ # in them (all are tests) are utf-8 encoded.
+ return unicode(s, "utf-8")
+
+ def ue(s):
+ return unicode(s, "unicode_escape")
+
+ def b(s):
+ return s
+
+ def import_(*args):
+ if len(args) == 4:
+ args = args[0:3] + ([str(arg) for arg in args[3]],)
+ return __import__(*args)
+
+ callable = callable
+ cmp = cmp
+ reduce = reduce
+
+ import base64
+ b64encode = base64.b64encode
+ b64decode = base64.b64decode
+
+ def print_(*args, **kwargs):
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+ for arg in enumerate(args):
+ if not isinstance(arg, basestring):
+ arg = str(arg)
+ fp.write(arg)
+
+ import itertools
+ itertools_filterfalse = itertools.ifilterfalse
+ itertools_filter = itertools.ifilter
+ itertools_imap = itertools.imap
+ from itertools import izip_longest as zip_longest
+
+
+import time
+if win32 or jython:
+ time_func = time.clock
+else:
+ time_func = time.time
+
+from collections import namedtuple
+from operator import attrgetter as dottedgetter
+
+
+if py3k:
+ def reraise(tp, value, tb=None, cause=None):
+ if cause is not None:
+ assert cause is not value, "Same cause emitted"
+ value.__cause__ = cause
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ # not as nice as that of Py3K, but at least preserves
+ # the code line where the issue occurred
+ exec("def reraise(tp, value, tb=None, cause=None):\n"
+ " if cause is not None:\n"
+ " assert cause is not value, 'Same cause emitted'\n"
+ " raise tp, value, tb\n")
+
+
+def raise_from_cause(exception, exc_info=None):
+ if exc_info is None:
+ exc_info = sys.exc_info()
+ exc_type, exc_value, exc_tb = exc_info
+ cause = exc_value if exc_value is not exception else None
+ reraise(type(exception), exception, tb=exc_tb, cause=cause)
+
+if py3k:
+ exec_ = getattr(builtins, 'exec')
+else:
+ def exec_(func_text, globals_, lcl=None):
+ if lcl is None:
+ exec('exec func_text in globals_')
+ else:
+ exec('exec func_text in globals_, lcl')
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass.
+
+ Drops the middle class upon creation.
+
+ Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/
+
+ """
+
+ class metaclass(meta):
+ __call__ = type.__call__
+ __init__ = type.__init__
+
+ def __new__(cls, name, this_bases, d):
+ if this_bases is None:
+ return type.__new__(cls, name, (), d)
+ return meta(name, bases, d)
+ return metaclass('temporary_class', None, {})
+
+
+from contextlib import contextmanager
+
+try:
+ from contextlib import nested
+except ImportError:
+ # removed in py3k, credit to mitsuhiko for
+ # workaround
+
+ @contextmanager
+ def nested(*managers):
+ exits = []
+ vars = []
+ exc = (None, None, None)
+ try:
+ for mgr in managers:
+ exit = mgr.__exit__
+ enter = mgr.__enter__
+ vars.append(enter())
+ exits.append(exit)
+ yield vars
+ except:
+ exc = sys.exc_info()
+ finally:
+ while exits:
+ exit = exits.pop()
+ try:
+ if exit(*exc):
+ exc = (None, None, None)
+ except:
+ exc = sys.exc_info()
+ if exc != (None, None, None):
+ reraise(exc[0], exc[1], exc[2])
diff --git a/netmodel/model/type.py b/netmodel/model/type.py
index 20dc2580..9a7b8740 100644
--- a/netmodel/model/type.py
+++ b/netmodel/model/type.py
@@ -16,27 +16,47 @@
# limitations under the License.
#
+from socket import inet_pton, inet_ntop, AF_INET6
+from struct import unpack, pack
+from abc import ABCMeta
+
from netmodel.util.meta import inheritors
class BaseType:
+ __choices__ = None
+
@staticmethod
def name():
return self.__class__.__name__.lower()
+ @classmethod
+ def restrict(cls, **kwargs):
+ class BaseType(cls):
+ __choices__ = kwargs.pop('choices', None)
+ return BaseType
+
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__()
+ __min_size__ = None
+ __max_size__ = None
+ __ascii__ = None
+ __forbidden__ = None
+
+ @classmethod
+ def restrict(cls, **kwargs):
+ base = super().restrict(**kwargs)
+ class String(base):
+ __max_size__ = kwargs.pop('max_size', None)
+ __min_size__ = kwargs.pop('min_size', None)
+ __ascii__ = kwargs.pop('ascii', None)
+ __forbidden__ = kwargs.pop('forbidden', None)
+ return String
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)
@@ -49,12 +69,161 @@ class Bool(BaseType):
class Dict(BaseType):
pass
+class PrefixTreeException(Exception): pass
+class NotEnoughAddresses(PrefixTreeException): pass
+class UnassignablePrefix(PrefixTreeException): pass
+
+class Prefix(BaseType, metaclass=ABCMeta):
+
+ def __init__(self, ip_address, prefix_len=None):
+ if not prefix_len:
+ if not isinstance(ip_address, str):
+ import pdb; pdb.set_trace()
+ if '/' in ip_address:
+ ip_address, prefix_len = ip_address.split('/')
+ prefix_len = int(prefix_len)
+ else:
+ prefix_len = self.MAX_PREFIX_SIZE
+ if isinstance(ip_address, str):
+ ip_address = self.aton(ip_address)
+ self.ip_address = ip_address
+ self.prefix_len = prefix_len
+
+ def __contains__(self, obj):
+ #it can be an IP as a integer
+ if isinstance(obj, int):
+ obj = type(self)(obj, self.MAX_PREFIX_SIZE)
+ #Or it's an IP string
+ if isinstance(obj, str):
+ #It's a prefix as 'IP/prefix'
+ if '/' in obj:
+ split_obj = obj.split('/')
+ obj = type(self)(split_obj[0], int(split_obj[1]))
+ else:
+ obj = type(self)(obj, self.MAX_PREFIX_SIZE)
+
+ return self._contains_prefix(obj)
+
+ @classmethod
+ def mask(cls):
+ mask_len = cls.MAX_PREFIX_SIZE//8 #Converts from bits to bytes
+ mask = 0
+ for step in range(0,mask_len):
+ mask = (mask << 8) | 0xff
+ return mask
+
+ def _contains_prefix(self, prefix):
+ assert isinstance(prefix, type(self))
+ return (prefix.prefix_len >= self.prefix_len and
+ prefix.ip_address >= self.first_prefix_address() and
+ prefix.ip_address <= self.last_prefix_address())
+
+ #Returns the first address of a prefix
+ def first_prefix_address(self):
+ return self.ip_address & (self.mask() << (self.MAX_PREFIX_SIZE-self.prefix_len))
+
+ def canonical_prefix(self):
+ return type(self)(self.first_prefix_address(), self.prefix_len)
+
+ def last_prefix_address(self):
+ return self.ip_address | (self.mask() >> self.prefix_len)
+
+ def limits(self):
+ return self.first_prefix_address(), self.last_prefix_address()
+
+ def __str__(self):
+ return "{}/{}".format(self.ntoa(self.first_prefix_address()), self.prefix_len)
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ return self.get_tuple() == other.get_tuple()
+
+ def get_tuple(self):
+ return (self.first_prefix_address(), self.prefix_len)
+
+ def __hash__(self):
+ return hash(self.get_tuple())
+
+ def __iter__(self):
+ return self.get_iterator()
+
+ #Iterates by steps of prefix_len, e.g., on all available /31 in a /24
+ def get_iterator(self, prefix_len=None):
+ if prefix_len is None:
+ prefix_len=self.MAX_PREFIX_SIZE
+ assert (prefix_len >= self.prefix_len and prefix_len<=self.MAX_PREFIX_SIZE)
+ step = 2**(self.MAX_PREFIX_SIZE - prefix_len)
+ for ip in range(self.first_prefix_address(), self.last_prefix_address()+1, step):
+ yield type(self)(ip, prefix_len)
+
+class Inet4Prefix(Prefix):
+
+ MAX_PREFIX_SIZE = 32
+
+ @classmethod
+ def aton(cls, address):
+ ret = 0
+ components = address.split('.')
+ for comp in components:
+ ret = (ret << 8) + int(comp)
+ return ret
+
+ @classmethod
+ def ntoa(cls, address):
+ components = []
+ for _ in range(0,4):
+ components.insert(0,'{}'.format(address % 256))
+ address = address >> 8
+ return '.'.join(components)
+
+class Inet6Prefix(Prefix):
+
+ MAX_PREFIX_SIZE = 128
+
+ @classmethod
+ def aton (cls, address):
+ prefix, suffix = unpack(">QQ", inet_pton(AF_INET6, address))
+ return (prefix << 64) | suffix
+
+ @classmethod
+ def ntoa (cls, address):
+ return inet_ntop(AF_INET6, pack(">QQ", address >> 64, address & ((1 << 64) -1)))
+
+ #skip_internet_address: skip a:b::0, as v6 often use default /64 prefixes
+ def get_iterator(self, prefix_len=None, skip_internet_address=None):
+ if skip_internet_address is None:
+ #We skip the internet address if we iterate over Addresses
+ if prefix_len is None:
+ skip_internet_address = True
+ #But not if we iterate over prefixes
+ else:
+ skip_internet_address = False
+ it = super().get_iterator(prefix_len)
+ if skip_internet_address:
+ next(it)
+ return it
+
+class InetAddress(Prefix):
+
+ def get_tuple(self):
+ return (self.ip_address, self.prefix_len)
+
+ def __str__(self):
+ return self.ntoa(self.ip_address)
+
+class Inet4Address(InetAddress, Inet4Prefix):
+ pass
+
+class Inet6Address(InetAddress, Inet6Prefix):
+ pass
+
class Self(BaseType):
"""Self-reference
"""
class Type:
- BASE_TYPES = (String, Integer, Double, Bool)
+ BASE_TYPES = (String, Integer, Double, Bool)
_registry = dict()
@staticmethod
diff --git a/netmodel/model/uuid.py b/netmodel/model/uuid.py
new file mode 100644
index 00000000..dae51d75
--- /dev/null
+++ b/netmodel/model/uuid.py
@@ -0,0 +1,51 @@
+#!/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 random
+import string
+
+# Separator for components of the UUID
+UUID_SEP = '-'
+
+# Length of the random component of the UUID
+UUID_LEN = 5
+
+class UUID:
+ def __init__(self, name, cls):
+ self._uuid = self._make_uuid(name, cls)
+
+ def _make_uuid(self, name, cls):
+ """Generate a unique resource identifier
+
+ The UUID consists in the type of the resource, to which is added a
+ random identifier of length UUID_LEN. Components of the UUID are
+ separated by UUID_SEP.
+ """
+ uuid = ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(UUID_LEN))
+ if name:
+ uuid = name # + UUID_SEP + uuid
+ return UUID_SEP.join([cls.__name__, uuid])
+
+ def __repr__(self):
+ return '<UUID {}>'.format(self._uuid)
+
+ def __lt__(self, other):
+ return self._uuid < other._uuid
+
+ __str__ = __repr__