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: