You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by mx...@apache.org on 2017/01/19 09:40:25 UTC
incubator-ariatosca git commit: wip
Repository: incubator-ariatosca
Updated Branches:
refs/heads/ARIA-44-Merge-parser-and-storage-models [created] 23f6c030a
wip
Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/23f6c030
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/23f6c030
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/23f6c030
Branch: refs/heads/ARIA-44-Merge-parser-and-storage-models
Commit: 23f6c030ab7583f318b5965d5c796ddd58d92fdc
Parents: 385b209
Author: mxmrlv <mx...@gmail.com>
Authored: Thu Jan 19 11:39:36 2017 +0200
Committer: mxmrlv <mx...@gmail.com>
Committed: Thu Jan 19 11:39:36 2017 +0200
----------------------------------------------------------------------
aria/modeling/__init__.py | 0
aria/modeling/elements.py | 128 ++++++++++++++++++++++
aria/modeling/instance_elements.py | 76 +++++++++++++
aria/modeling/model_elements.py | 182 ++++++++++++++++++++++++++++++++
aria/modeling/utils.py | 146 +++++++++++++++++++++++++
aria/storage/type.py | 152 +++++++++++++++++++++++++-
6 files changed, 683 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/modeling/__init__.py
----------------------------------------------------------------------
diff --git a/aria/modeling/__init__.py b/aria/modeling/__init__.py
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/modeling/elements.py
----------------------------------------------------------------------
diff --git a/aria/modeling/elements.py b/aria/modeling/elements.py
new file mode 100644
index 0000000..62bc7b8
--- /dev/null
+++ b/aria/modeling/elements.py
@@ -0,0 +1,128 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 ...utils.collections import StrictDict, OrderedDict
+from ...utils.console import puts
+from .utils import coerce_value
+
+
+class Function(object):
+ """
+ An intrinsic function.
+
+ Serves as a placeholder for a value that should eventually be derived
+ by calling the function.
+ """
+
+ @property
+ def as_raw(self):
+ raise NotImplementedError
+
+ def _evaluate(self, context, container):
+ raise NotImplementedError
+
+ def __deepcopy__(self, memo):
+ # Circumvent cloning in order to maintain our state
+ return self
+
+
+class Element(object):
+ """
+ Base class for :class:`ServiceInstance` elements.
+
+ All elements support validation, diagnostic dumping, and representation as
+ raw data (which can be translated into JSON or YAML) via :code:`as_raw`.
+ """
+
+ @property
+ def as_raw(self):
+ raise NotImplementedError
+
+ def validate(self, context):
+ pass
+
+ def coerce_values(self, context, container, report_issues):
+ pass
+
+ def dump(self, context):
+ pass
+
+
+class ModelElement(Element):
+ """
+ Base class for :class:`ServiceModel` elements.
+
+ All model elements can be instantiated into :class:`ServiceInstance` elements.
+ """
+
+ def instantiate(self, context, container):
+ raise NotImplementedError
+
+
+class Parameter(ModelElement):
+ """
+ Represents a typed value.
+
+ This class is used by both service model and service instance elements.
+ """
+
+ def __init__(self, type_name, value, description):
+ self.type_name = type_name
+ self.value = value
+ self.description = description
+
+ @property
+ def as_raw(self):
+ return OrderedDict((
+ ('type_name', self.type_name),
+ ('value', self.value),
+ ('description', self.description)))
+
+ def instantiate(self, context, container):
+ return Parameter(self.type_name, self.value, self.description)
+
+ def coerce_values(self, context, container, report_issues):
+ if self.value is not None:
+ self.value = coerce_value(context, container, self.value, report_issues)
+
+
+class Metadata(ModelElement):
+ """
+ Custom values associated with the deployment template and its plans.
+
+ This class is used by both service model and service instance elements.
+
+ Properties:
+
+ * :code:`values`: Dict of custom values
+ """
+
+ def __init__(self):
+ self.values = StrictDict(key_class=basestring)
+
+ @property
+ def as_raw(self):
+ return self.values
+
+ def instantiate(self, context, container):
+ metadata = Metadata()
+ metadata.values.update(self.values)
+ return metadata
+
+ def dump(self, context):
+ puts('Metadata:')
+ with context.style.indent:
+ for name, value in self.values.iteritems():
+ puts('%s: %s' % (name, context.style.meta(value)))
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/modeling/instance_elements.py
----------------------------------------------------------------------
diff --git a/aria/modeling/instance_elements.py b/aria/modeling/instance_elements.py
new file mode 100644
index 0000000..f0becd4
--- /dev/null
+++ b/aria/modeling/instance_elements.py
@@ -0,0 +1,76 @@
+
+from sqlalchemy import (
+ Column,
+ Text,
+)
+
+from . import elements
+from ..storage import structure, type
+from ..utils import collections
+
+
+class ServiceInstance(elements.Element, structure.ModelMixin):
+
+ description = Column(Text)
+ metadata = Column()
+ # TODO: is the solution is really to create a list of foreign key
+ # (which would replace the dict) and filter according to the name etc...?
+ nodes = Column(type.StrictDict(str, "Node"))
+ groups = Column(type.StrictDict(str, "Group"))
+ policies = Column(type.StrictDict(str, "Policy"))
+ substitution = Column()
+ inputs = Column(type.StrictDict(str, "Parameter"))
+ outputs = Column(type.StrictDict(str, "Parameter"))
+ operations = Column(type.StrictDict(str, "Operation"))
+
+ def satisfy_requirements(self, context):
+ satisfied = True
+ for node in self.nodes.itervalues():
+ if not node.satisfy_requirements(context):
+ satisfied = False
+ return satisfied
+
+ def validate_capabilities(self, context):
+ satisfied = True
+ for node in self.nodes.itervalues():
+ if not node.validate_capabilities(context):
+ satisfied = False
+ return satisfied
+
+ def find_nodes(self, node_template_name):
+ nodes = []
+ for node in self.nodes.itervalues():
+ if node.template_name == node_template_name:
+ nodes.append(node)
+ return collections.FrozenList(nodes)
+
+ def get_node_ids(self, node_template_name):
+ return collections.FrozenList((node.id for node in self.find_nodes(node_template_name)))
+
+ def find_groups(self, group_template_name):
+ groups = []
+ for group in self.groups.itervalues():
+ if group.template_name == group_template_name:
+ groups.append(group)
+ return collections.FrozenList(groups)
+
+ def get_group_ids(self, group_template_name):
+ return collections.FrozenList((group.id for group in self.find_groups(group_template_name)))
+
+ def is_node_a_target(self, context, target_node):
+ for node in self.nodes.itervalues():
+ if self._is_node_a_target(context, node, target_node):
+ return True
+ return False
+
+ def _is_node_a_target(self, context, source_node, target_node):
+ if source_node.relationships:
+ for relationship in source_node.relationships:
+ if relationship.target_node_id == target_node.id:
+ return True
+ else:
+ node = context.modeling.instance.nodes.get(relationship.target_node_id)
+ if node is not None:
+ if self._is_node_a_target(context, node, target_node):
+ return True
+ return False
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/modeling/model_elements.py
----------------------------------------------------------------------
diff --git a/aria/modeling/model_elements.py b/aria/modeling/model_elements.py
new file mode 100644
index 0000000..ab68fbe
--- /dev/null
+++ b/aria/modeling/model_elements.py
@@ -0,0 +1,182 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 copy import deepcopy
+
+from sqlalchemy import (
+ Column,
+ Text,
+ DateTime
+)
+
+from . import elements, instance_elements, utils
+from ..storage import structure, type
+from ..utils import collections
+from ..utils import formatting, console
+
+
+class ServiceTemplate(elements.ModelElement, structure.ModelMixin):
+
+ __tablename__ = 'blueprints'
+
+ description = Column(Text)
+ metadata = Column(Text)
+ node_templates = Column(type.StrictDict(str, "NodeTemplate"))
+ group_templates = Column(type.StrictDict(str, "GroupTemplate"))
+ policy_templates = Column(type.StrictDict(str, "PolicyTemplate"))
+ substitution_template = Column()
+ inputs = Column(type.StrictDict(str, "Parameter"))
+ outputs = Column(type.StrictDict(str, "Parameter"))
+ operation_templates = Column(type.StrictDict(str, "OperationTemplate"))
+
+ created_at = Column(DateTime, nullable=False, index=True)
+ main_file_name = Column(Text, nullable=False)
+ plan = Column(type.Dict, nullable=False)
+ updated_at = Column(DateTime)
+
+ @property
+ def as_raw(self):
+ return collections.OrderedDict((
+ ('description', self.description),
+ ('metadata', formatting.as_raw(self.metadata)),
+ ('node_templates', formatting.as_raw_list(self.node_templates)),
+ ('group_templates', formatting.as_raw_list(self.group_templates)),
+ ('policy_templates', formatting.as_raw_list(self.policy_templates)),
+ ('substitution_template', formatting.as_raw(self.substitution_template)),
+ ('inputs', formatting.as_raw_dict(self.inputs)),
+ ('outputs', formatting.as_raw_dict(self.outputs)),
+ ('operation_templates', formatting.as_raw_list(self.operation_templates))))
+
+ def instantiate(self, context, container):
+ service_instance = instance_elements.ServiceInstance()
+ context.modeling.instance = service_instance
+
+ service_instance.description = deepcopy_with_locators(self.description)
+
+ if self.metadata is not None:
+ service_instance.metadata = self.metadata.instantiate(context, container)
+
+ for node_template in self.node_templates.itervalues():
+ for _ in range(node_template.default_instances):
+ node = node_template.instantiate(context, container)
+ service_instance.nodes[node.id] = node
+
+ utils.instantiate_dict(context, self, service_instance.groups, self.group_templates)
+ utils.instantiate_dict(context, self, service_instance.policies, self.policy_templates)
+ utils.instantiate_dict(context, self, service_instance.operations, self.operation_templates)
+
+ if self.substitution_template is not None:
+ service_instance.substitution = self.substitution_template.instantiate(context,
+ container)
+
+ utils.instantiate_dict(context, self, service_instance.inputs, self.inputs)
+ utils.instantiate_dict(context, self, service_instance.outputs, self.outputs)
+
+ for name, the_input in context.modeling.inputs.iteritems():
+ if name not in service_instance.inputs:
+ context.validation.report('input "%s" is not supported' % name)
+ else:
+ service_instance.inputs[name].value = the_input
+
+ return service_instance
+
+ def validate(self, context):
+ if self.metadata is not None:
+ self.metadata.validate(context)
+ utils.validate_dict_values(context, self.node_templates)
+ utils.validate_dict_values(context, self.group_templates)
+ utils.validate_dict_values(context, self.policy_templates)
+ if self.substitution_template is not None:
+ self.substitution_template.validate(context)
+ utils.validate_dict_values(context, self.inputs)
+ utils.validate_dict_values(context, self.outputs)
+ utils.validate_dict_values(context, self.operation_templates)
+
+ def coerce_values(self, context, container, report_issues):
+ if self.metadata is not None:
+ self.metadata.coerce_values(context, container, report_issues)
+ utils.coerce_dict_values(context, container, self.node_templates, report_issues)
+ utils.coerce_dict_values(context, container, self.group_templates, report_issues)
+ utils.coerce_dict_values(context, container, self.policy_templates, report_issues)
+ if self.substitution_template is not None:
+ self.substitution_template.coerce_values(context, container, report_issues)
+ utils.coerce_dict_values(context, container, self.inputs, report_issues)
+ utils.coerce_dict_values(context, container, self.outputs, report_issues)
+ utils.coerce_dict_values(context, container, self.operation_templates, report_issues)
+
+ def dump(self, context):
+ if self.description is not None:
+ console.puts(context.style.meta(self.description))
+ if self.metadata is not None:
+ self.metadata.dump(context)
+ for node_template in self.node_templates.itervalues():
+ node_template.dump(context)
+ for group_template in self.group_templates.itervalues():
+ group_template.dump(context)
+ for policy_template in self.policy_templates.itervalues():
+ policy_template.dump(context)
+ if self.substitution_template is not None:
+ self.substitution_template.dump(context)
+ dump_parameters(context, self.inputs, 'Inputs')
+ dump_parameters(context, self.outputs, 'Outputs')
+ utils.dump_dict_values(context, self.operation_templates, 'Operation templates')
+
+
+def dump_parameters(context, parameters, name='Properties'):
+ if not parameters:
+ return
+ console.puts('%s:' % name)
+ with context.style.indent:
+ for parameter_name, parameter in parameters.items():
+ if parameter.type_name is not None:
+ console.puts('%s = %s (%s)' % (context.style.property(parameter_name),
+ context.style.literal(parameter.value),
+ context.style.type(parameter.type_name)))
+ else:
+ console.puts('%s = %s' % (context.style.property(parameter_name),
+ context.style.literal(parameter.value)))
+ if parameter.description:
+ console.puts(context.style.meta(parameter.description))
+
+
+# TODO (left for tal): Move following two methods to some place parser specific
+def deepcopy_with_locators(value):
+ """
+ Like :code:`deepcopy`, but also copies over locators.
+ """
+
+ res = deepcopy(value)
+ copy_locators(res, value)
+ return res
+
+def copy_locators(target, source):
+ """
+ Copies over :code:`_locator` for all elements, recursively.
+
+ Assumes that target and source have exactly the same list/dict structure.
+ """
+
+ locator = getattr(source, '_locator', None)
+ if locator is not None:
+ try:
+ setattr(target, '_locator', locator)
+ except AttributeError:
+ pass
+
+ if isinstance(target, list) and isinstance(source, list):
+ for i, _ in enumerate(target):
+ copy_locators(target[i], source[i])
+ elif isinstance(target, dict) and isinstance(source, dict):
+ for k, v in target.items():
+ copy_locators(v, source[k])
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/modeling/utils.py
----------------------------------------------------------------------
diff --git a/aria/modeling/utils.py b/aria/modeling/utils.py
new file mode 100644
index 0000000..906106e
--- /dev/null
+++ b/aria/modeling/utils.py
@@ -0,0 +1,146 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 random import randrange
+
+from shortuuid import ShortUUID
+
+from ...utils.collections import OrderedDict
+from ...utils.console import puts
+from ..exceptions import InvalidValueError
+from ..presentation import Value
+from .exceptions import CannotEvaluateFunctionException
+
+# UUID = ShortUUID() # default alphabet is base57, which is alphanumeric without visually ambiguous
+# characters; ID length is 22
+UUID = ShortUUID(alphabet='abcdefghijklmnopqrstuvwxyz0123456789') # alphanumeric; ID length is 25
+
+
+def generate_id_string(length=None):
+ """
+ A random string with a strong guarantee of universal uniqueness (uses UUID).
+
+ The default length is 25 characters.
+ """
+
+ the_id = UUID.uuid()
+ if length is not None:
+ the_id = the_id[:length]
+ return the_id
+
+
+def generate_hex_string():
+ """
+ A random string of 5 hex digits with no guarantee of universal uniqueness.
+ """
+
+ return '%05x' % randrange(16 ** 5)
+
+
+def coerce_value(context, container, value, report_issues=False):
+ if isinstance(value, Value):
+ value = value.value
+
+ if isinstance(value, list):
+ return [coerce_value(context, container, v, report_issues) for v in value]
+ elif isinstance(value, dict):
+ return OrderedDict((k, coerce_value(context, container, v, report_issues))
+ for k, v in value.iteritems())
+ elif hasattr(value, '_evaluate'):
+ try:
+ value = value._evaluate(context, container)
+ value = coerce_value(context, container, value, report_issues)
+ except CannotEvaluateFunctionException:
+ pass
+ except InvalidValueError as e:
+ if report_issues:
+ context.validation.report(e.issue)
+ return value
+
+
+def validate_dict_values(context, the_dict):
+ if not the_dict:
+ return
+ validate_list_values(context, the_dict.itervalues())
+
+
+def validate_list_values(context, the_list):
+ if not the_list:
+ return
+ for value in the_list:
+ value.validate(context)
+
+
+def coerce_dict_values(context, container, the_dict, report_issues=False):
+ if not the_dict:
+ return
+ coerce_list_values(context, container, the_dict.itervalues(), report_issues)
+
+
+def coerce_list_values(context, container, the_list, report_issues=False):
+ if not the_list:
+ return
+ for value in the_list:
+ value.coerce_values(context, container, report_issues)
+
+
+def instantiate_dict(context, container, the_dict, from_dict):
+ if not from_dict:
+ return
+ for name, value in from_dict.iteritems():
+ value = value.instantiate(context, container)
+ if value is not None:
+ the_dict[name] = value
+
+
+def dump_list_values(context, the_list, name):
+ if not the_list:
+ return
+ puts('%s:' % name)
+ with context.style.indent:
+ for value in the_list:
+ value.dump(context)
+
+
+def dump_dict_values(context, the_dict, name):
+ if not the_dict:
+ return
+ dump_list_values(context, the_dict.itervalues(), name)
+
+
+def dump_parameters(context, parameters, name='Properties'):
+ if not parameters:
+ return
+ puts('%s:' % name)
+ with context.style.indent:
+ for parameter_name, parameter in parameters.iteritems():
+ if parameter.type_name is not None:
+ puts('%s = %s (%s)' % (context.style.property(parameter_name),
+ context.style.literal(parameter.value),
+ context.style.type(parameter.type_name)))
+ else:
+ puts('%s = %s' % (context.style.property(parameter_name),
+ context.style.literal(parameter.value)))
+ if parameter.description:
+ puts(context.style.meta(parameter.description))
+
+
+def dump_interfaces(context, interfaces, name='Interfaces'):
+ if not interfaces:
+ return
+ puts('%s:' % name)
+ with context.style.indent:
+ for interface in interfaces.itervalues():
+ interface.dump(context)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/23f6c030/aria/storage/type.py
----------------------------------------------------------------------
diff --git a/aria/storage/type.py b/aria/storage/type.py
index dd2a5ce..02b0ee3 100644
--- a/aria/storage/type.py
+++ b/aria/storage/type.py
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
+from collections import namedtuple
from sqlalchemy import (
TypeDecorator,
@@ -61,10 +62,68 @@ class List(_MutableType):
return list
+class _StrictDictMixin(object):
+ ANY_TYPE = 'any_type'
+
+ _key_cls = ANY_TYPE
+ _value_cls = ANY_TYPE
+
+ @classmethod
+ def coerce(cls, key, value):
+ "Convert plain dictionaries to MutableDict."
+ try:
+ if not isinstance(value, cls):
+ if isinstance(value, dict):
+ for k, v in value.items():
+ cls._assert_strict_key(k)
+ cls._assert_strict_value(v)
+ return cls(value)
+ return mutable.MutableDict.coerce(key, value)
+ else:
+ return value
+ except ValueError as e:
+ raise exceptions.StorageError('SQL Storage error: {0}'.format(str(e)))
+
+ def __setitem__(self, key, value):
+ self._assert_strict_key(key)
+ self._assert_strict_value(value)
+ super(_StrictDictMixin, self).__setitem__(key, value)
+
+ def setdefault(self, key, value):
+ self._assert_strict_key(key)
+ self._assert_strict_value(value)
+ super(_StrictDictMixin, self).setdefault(key, value)
+
+ def update(self, *a, **kw):
+ if isinstance(a, dict):
+ for k, v in a.items():
+ self._assert_strict_key(k)
+ self._assert_strict_value(v)
+ for k, v in kw.items():
+ self._assert_strict_key(k)
+ self._assert_strict_value(v)
+ super(_StrictDictMixin, self).update(*a, **kw)
+
+ @classmethod
+ def _assert_strict_key(cls, key):
+ if not isinstance(key, cls._key_cls):
+ raise exceptions.StorageError("Key type was set strictly to {0}, but was {1}".format(
+ cls._key_cls, type(key)
+ ))
+
+ @classmethod
+ def _assert_strict_value(cls, value):
+ if not isinstance(value, cls._value_cls):
+ raise exceptions.StorageError("Value type was set strictly to {0}, but was {1}".format(
+ cls._value_cls, type(value)
+ ))
+
+
class _MutableDict(mutable.MutableDict):
"""
Enables tracking for dict values.
"""
+
@classmethod
def coerce(cls, key, value):
"Convert plain dictionaries to MutableDict."
@@ -74,16 +133,107 @@ class _MutableDict(mutable.MutableDict):
raise exceptions.StorageError('SQL Storage error: {0}'.format(str(e)))
+class _StrictListMixin(object):
+ ANY_TYPE = 'any_type'
+
+ _item_cls = ANY_TYPE
+
+ @classmethod
+ def coerce(cls, key, value):
+ "Convert plain dictionaries to MutableDict."
+ try:
+ if not isinstance(value, cls):
+ if isinstance(value, list):
+ for item in value:
+ cls._assert_item(item)
+ return cls(value)
+ return mutable.MutableList.coerce(key, value)
+ else:
+ return value
+ except ValueError as e:
+ raise exceptions.StorageError('SQL Storage error: {0}'.format(str(e)))
+
+ def __setitem__(self, index, value):
+ """Detect list set events and emit change events."""
+ self._assert_item(value)
+ super(_StrictListMixin, self).__setitem__(index, value)
+
+ def append(self, x):
+ self._assert_item(x)
+ super(_StrictListMixin, self).append(x)
+
+ def extend(self, x):
+ self._assert_item(x)
+ super(_StrictListMixin, self).extend(x)
+
+ def insert(self, i, x):
+ self._assert_item(x)
+ super(_StrictListMixin, self).insert(i, x)
+
+ @classmethod
+ def _assert_item(cls, item):
+ if not isinstance(item, cls._item_cls):
+ raise exceptions.StorageError("Key type was set strictly to {0}, but was {1}".format(
+ cls._item_cls, type(item)
+ ))
+
+
class _MutableList(mutable.MutableList):
@classmethod
def coerce(cls, key, value):
"Convert plain dictionaries to MutableDict."
+
try:
- return mutable.MutableList.coerce(key, value)
+ if not isinstance(value, cls):
+ if isinstance(value, list):
+ return cls(value)
+
+ return mutable.Mutable.coerce(key, value)
+ else:
+ return value
+
except ValueError as e:
raise exceptions.StorageError('SQL Storage error: {0}'.format(str(e)))
+_strict_dict_id = namedtuple('strict_dict_id', 'key_cls, value_cls')
+_strict_map = {}
+
+
+def StrictDict(key_cls, value_cls):
+ strict_dict_map_key = _strict_dict_id(key_cls=key_cls, value_cls=value_cls)
+ if strict_dict_map_key not in _strict_map:
+ strict_dict_cls = type(
+ 'StrictDict_{0}_{1}'.format(key_cls.__name__, value_cls.__name__),
+ (Dict, ),
+ {}
+ )
+ type(
+ 'StrictMutableDict_{0}_{1}'.format(key_cls.__name__, value_cls.__name__),
+ (_StrictDictMixin, _MutableDict),
+ {'_key_cls': key_cls, '_value_cls': value_cls}
+ ).associate_with(strict_dict_cls)
+ _strict_map[strict_dict_map_key] = strict_dict_cls
+
+ return _strict_map[strict_dict_map_key]
+
+
+def StrictList(item_cls):
+ if item_cls not in _strict_map:
+ strict_list_cls = type(
+ 'StrictList_{0}'.format(item_cls.__name__),
+ (List, ),
+ {}
+ )
+ type(
+ 'StrictMutableList_{0}'.format(item_cls.__name__),
+ (_StrictListMixin, _MutableList),
+ {'_item_cls': item_cls}
+ ).associate_with(strict_list_cls)
+ _strict_map[item_cls] = strict_list_cls
+
+ return _strict_map[item_cls]
+
def _mutable_association_listener(mapper, cls):
for prop in mapper.column_attrs: