From b66f3904c8ddf0c3c68b05c14b16298ec5c7fec4 Mon Sep 17 00:00:00 2001 From: Alexander Tivelkov Date: Thu, 4 Sep 2014 23:02:59 +0400 Subject: [PATCH] Declarative definitions of Artifact Types Add a notation which allows to declaratively define schema of Artifacts' type-specific metadata fields, with appropriate constraints and validators. The classes utilizing this declarative notation will be used to define artifact types and will be imported using the plugin system. The generated classes provide input validation in both initializers and property setters. At the same time the model contain meta-definitions defining attributes of the properties, such as immutability, internal access etc, which may be used by other layers of code. Co-Authored-By: Alexander Tivelkov Co-Authored-By: Inessa Vasilevskaya Co-Authored-By: Mike Fedosin Implements-blueprint: artifact-repository Change-Id: Ie0deb84d5fbe0397c047862b7cfbaecd31603e70 --- glance/common/artifacts/__init__.py | 0 glance/common/artifacts/declarative.py | 747 +++++++++++ glance/common/artifacts/definitions.py | 571 +++++++++ glance/common/artifacts/serialization.py | 265 ++++ glance/common/exception.py | 27 + ...test_artifact_type_definition_framework.py | 1109 +++++++++++++++++ 6 files changed, 2719 insertions(+) create mode 100644 glance/common/artifacts/__init__.py create mode 100644 glance/common/artifacts/declarative.py create mode 100644 glance/common/artifacts/definitions.py create mode 100644 glance/common/artifacts/serialization.py create mode 100644 glance/tests/unit/test_artifact_type_definition_framework.py diff --git a/glance/common/artifacts/__init__.py b/glance/common/artifacts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/glance/common/artifacts/declarative.py b/glance/common/artifacts/declarative.py new file mode 100644 index 0000000000..01b62d04e2 --- /dev/null +++ b/glance/common/artifacts/declarative.py @@ -0,0 +1,747 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import re +import types + +import semantic_version +import six + +from glance.common import exception as exc +from glance import i18n + + +_ = i18n._ + + +class AttributeDefinition(object): + """A base class for the attribute definitions which may be added to + declaratively defined artifact types + """ + + ALLOWED_TYPES = (object,) + + def __init__(self, + display_name=None, + description=None, + readonly=False, + mutable=True, + required=False, + default=None): + """Initializes attribute definition + + :param display_name: Display name of the attribute + :param description: Description of the attribute + :param readonly: Flag indicating if the value of attribute may not be + changed once an artifact is created + :param mutable: Flag indicating if the value of attribute may not be + changed once an artifact is published + :param required: Flag indicating if the value of attribute is required + :param default: default value of the attribute + """ + self.name = None + self.display_name = display_name + self.description = description + self.readonly = readonly + self.required = required + self.mutable = mutable + self.default = default + self._add_validator('type', + lambda v: isinstance(v, self.ALLOWED_TYPES), + _("Not a valid value type")) + self._validate_default() + + def _set_name(self, value): + self.name = value + if self.display_name is None: + self.display_name = value + + def _add_validator(self, name, func, message): + if not hasattr(self, '_validators'): + self._validators = [] + self._validators_index = {} + pair = (func, message) + self._validators.append(pair) + self._validators_index[name] = pair + + def _get_validator(self, name): + return self._validators_index.get(name) + + def _remove_validator(self, name): + pair = self._validators_index.pop(name, None) + if pair is not None: + self._validators.remove(pair) + + def _check_definition(self): + self._validate_default() + + def _validate_default(self): + if self.default: + try: + self.validate(self.default, 'default') + except exc.InvalidArtifactPropertyValue: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Default value is invalid")) + + def get_value(self, obj): + return getattr(obj, self.name) + + def set_value(self, obj, value): + return setattr(obj, self.name, value) + + def validate(self, value, name=None): + if value is None: + if self.required: + raise exc.InvalidArtifactPropertyValue( + name=name or self.name, + val=value, + msg=_('Value is required')) + else: + return + + first_error = next((msg for v_func, msg in self._validators + if not v_func(value)), None) + if first_error: + raise exc.InvalidArtifactPropertyValue(name=name or self.name, + val=value, + msg=first_error) + + +class ListAttributeDefinition(AttributeDefinition): + """A base class for Attribute definitions having List-semantics + + Is inherited by Array, ArtifactReferenceList and BinaryObjectList + """ + ALLOWED_TYPES = (types.ListType,) + ALLOWED_ITEM_TYPES = (AttributeDefinition, ) + + def _check_item_type(self, item): + if not isinstance(item, self.ALLOWED_ITEM_TYPES): + raise exc.InvalidArtifactTypePropertyDefinition( + _('Invalid item type specification')) + if item.default is not None: + raise exc.InvalidArtifactTypePropertyDefinition( + _('List definitions may hot have defaults')) + + def __init__(self, item_type, min_size=0, max_size=None, unique=False, + **kwargs): + + super(ListAttributeDefinition, self).__init__(**kwargs) + if isinstance(item_type, types.ListType): + for it in item_type: + self._check_item_type(it) + + # we need to copy the item_type collection + self.item_type = item_type[:] + + if min_size != 0: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Cannot specify 'min_size' explicitly") + ) + + if max_size is not None: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Cannot specify 'max_size' explicitly") + ) + + # setting max_size and min_size to the length of item_type, + # as tuple-semantic assumes that the number of elements is set + # by the type spec + min_size = max_size = len(item_type) + else: + self._check_item_type(item_type) + self.item_type = item_type + + if min_size: + self.min_size(min_size) + + if max_size: + self.max_size(max_size) + + if unique: + self.unique() + + def min_size(self, value): + self._min_size = value + if value is not None: + self._add_validator('min_size', + lambda v: len(v) >= self._min_size, + _('List size is less than minimum')) + else: + self._remove_validator('min_size') + + def max_size(self, value): + self._max_size = value + if value is not None: + self._add_validator('max_size', + lambda v: len(v) <= self._max_size, + _('List size is greater than maximum')) + else: + self._remove_validator('max_size') + + def unique(self, value=True): + self._unique = value + if value: + def _unique(items): + seen = set() + for item in items: + if item in seen: + return False + seen.add(item) + return True + self._add_validator('unique', + _unique, _('Items have to be unique')) + else: + self._remove_validator('unique') + + def _set_name(self, value): + super(ListAttributeDefinition, self)._set_name(value) + if isinstance(self.item_type, types.ListType): + for i, item in enumerate(self.item_type): + item._set_name("%s[%i]" % (value, i)) + else: + self.item_type._set_name("%s[*]" % value) + + def validate(self, value, name=None): + super(ListAttributeDefinition, self).validate(value, name) + if value is not None: + for i, item in enumerate(value): + self._validate_item_at(item, i) + + def get_item_definition_at_index(self, index): + if isinstance(self.item_type, types.ListType): + if index < len(self.item_type): + return self.item_type[index] + else: + return None + return self.item_type + + def _validate_item_at(self, item, index): + item_type = self.get_item_definition_at_index(index) + # set name if none has been given to the list element at given index + if (isinstance(self.item_type, types.ListType) and item_type and + not item_type.name): + item_type.name = "%s[%i]" % (self.name, index) + if item_type: + item_type.validate(item) + + +class DictAttributeDefinition(AttributeDefinition): + """A base class for Attribute definitions having Map-semantics + + Is inherited by Dict + """ + ALLOWED_TYPES = (types.DictionaryType,) + ALLOWED_PROPERTY_TYPES = (AttributeDefinition,) + + def _check_prop(self, key, item): + if (not isinstance(item, self.ALLOWED_PROPERTY_TYPES) or + (key is not None and not isinstance(key, types.StringTypes))): + raise exc.InvalidArtifactTypePropertyDefinition( + _('Invalid dict property type specification')) + + @staticmethod + def _validate_key(key): + if not isinstance(key, types.StringTypes): + raise exc.InvalidArtifactPropertyValue( + _('Invalid dict property type')) + + def __init__(self, properties, min_properties=0, max_properties=0, + **kwargs): + super(DictAttributeDefinition, self).__init__(**kwargs) + if isinstance(properties, types.DictionaryType): + for key, value in six.iteritems(properties): + self._check_prop(key, value) + # copy the properties dict + self.properties = properties.copy() + + self._add_validator('keys', + lambda v: set(v.keys()) <= set( + self.properties.keys()), + _('Dictionary contains unexpected key(s)')) + else: + self._check_prop(None, properties) + self.properties = properties + + if min_properties: + self.min_properties(min_properties) + + if max_properties: + self.max_properties(max_properties) + + def min_properties(self, value): + self._min_properties = value + if value is not None: + self._add_validator('min_properties', + lambda v: len(v) >= self._min_properties, + _('Dictionary size is less than ' + 'minimum')) + else: + self._remove_validator('min_properties') + + def max_properties(self, value): + self._max_properties = value + if value is not None: + self._add_validator('max_properties', + lambda v: len(v) <= self._max_properties, + _('Dictionary size is ' + 'greater than maximum')) + else: + self._remove_validator('max_properties') + + def _set_name(self, value): + super(DictAttributeDefinition, self)._set_name(value) + if isinstance(self.properties, types.DictionaryType): + for k, v in six.iteritems(self.properties): + v._set_name(value) + else: + self.properties._set_name(value) + + def validate(self, value, name=None): + super(DictAttributeDefinition, self).validate(value, name) + if value is not None: + for k, v in six.iteritems(value): + self._validate_item_with_key(v, k) + + def _validate_item_with_key(self, value, key): + self._validate_key(key) + if isinstance(self.properties, types.DictionaryType): + prop_def = self.properties.get(key) + if prop_def is not None: + name = "%s[%s]" % (prop_def.name, key) + prop_def.validate(value, name=name) + else: + name = "%s[%s]" % (self.properties.name, key) + self.properties.validate(value, name=name) + + def get_prop_definition_at_key(self, key): + if isinstance(self.properties, types.DictionaryType): + return self.properties.get(key) + else: + return self.properties + + +class PropertyDefinition(AttributeDefinition): + """A base class for Attributes defining generic or type-specific metadata + properties + """ + DB_TYPE = None + + def __init__(self, + internal=False, + allowed_values=None, + validators=None, + **kwargs): + """Defines a metadata property + + :param internal: a flag indicating that the property is internal, i.e. + not returned to client + :param allowed_values: specifies a list of values allowed for the + property + :param validators: specifies a list of custom validators for the + property + """ + super(PropertyDefinition, self).__init__(**kwargs) + self.internal = internal + self._allowed_values = None + + if validators is not None: + try: + for i, (f, m) in enumerate(validators): + self._add_validator("custom_%i" % i, f, m) + except ValueError: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Custom validators list should contain tuples " + "'(function, message)'")) + + if allowed_values is not None: + # copy the allowed_values, as this is going to create a + # closure, and we need to make sure that external modification of + # this list does not affect the created validator + self.allowed_values(allowed_values) + self._check_definition() + + def _validate_allowed_values(self): + if self._allowed_values: + try: + for allowed_value in self._allowed_values: + self.validate(allowed_value, 'allowed_value') + except exc.InvalidArtifactPropertyValue: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Allowed values %s are invalid under given validators") % + self._allowed_values) + + def allowed_values(self, values): + self._allowed_values = values[:] + if values is not None: + self._add_validator('allowed', lambda v: v in self._allowed_values, + _("Is not allowed value")) + else: + self._remove_validator('allowed') + self._check_definition() + + def _check_definition(self): + self._validate_allowed_values() + super(PropertyDefinition, self)._check_definition() + + +class RelationDefinition(AttributeDefinition): + """A base class for Attributes defining cross-artifact relations""" + def __init__(self, internal=False, **kwargs): + self.internal = internal + kwargs.setdefault('mutable', False) + # if mutable=True has been passed -> raise an exception + if kwargs['mutable'] is True: + raise exc.InvalidArtifactTypePropertyDefinition( + _("Dependency relations cannot be mutable")) + super(RelationDefinition, self).__init__(**kwargs) + + +class BlobDefinition(AttributeDefinition): + """A base class for Attributes defining binary objects""" + pass + + +class ArtifactTypeMetaclass(type): + """A metaclass to build Artifact Types. Not intended to be used directly + + Use `get_declarative_base` to get the base class instead + """ + def __init__(cls, class_name, bases, attributes): + if '_declarative_artifact_type' not in cls.__dict__: + _build_declarative_meta(cls) + super(ArtifactTypeMetaclass, cls).__init__(class_name, bases, + attributes) + + +class ArtifactPropertyDescriptor(object): + """A descriptor object for working with artifact attributes""" + + def __init__(self, prop, collection_wrapper_class=None): + self.prop = prop + self.collection_wrapper_class = collection_wrapper_class + + def __get__(self, instance, owner): + if instance is None: + # accessed via owner class + return self.prop + else: + v = getattr(instance, '_' + self.prop.name, None) + if v is None and self.prop.default is not None: + v = copy.copy(self.prop.default) + self.__set__(instance, v, ignore_mutability=True) + return self.__get__(instance, owner) + else: + if v is not None and self.collection_wrapper_class: + if self.prop.readonly: + readonly = True + elif (not self.prop.mutable and + hasattr(instance, '__is_mutable__') and + not hasattr(instance, + '__suspend_mutability_checks__')): + + readonly = not instance.__is_mutable__() + else: + readonly = False + if readonly: + v = v.__make_immutable__() + return v + + def __set__(self, instance, value, ignore_mutability=False): + if instance: + if self.prop.readonly: + if hasattr(instance, '_' + self.prop.name): + raise exc.InvalidArtifactPropertyValue( + _('Attempt to set readonly property')) + if not self.prop.mutable: + if (hasattr(instance, '__is_mutable__') and + not hasattr(instance, + '__suspend_mutability_checks__')): + mutable = instance.__is_mutable__() or ignore_mutability + if not mutable: + raise exc.InvalidArtifactPropertyValue( + _('Attempt to set value of immutable property')) + if value is not None and self.collection_wrapper_class: + value = self.collection_wrapper_class(value) + value.property = self.prop + self.prop.validate(value) + setattr(instance, '_' + self.prop.name, value) + + +class ArtifactAttributes(object): + """A container class storing description of Artifact Type attributes""" + def __init__(self): + self.properties = {} + self.dependencies = {} + self.blobs = {} + self.all = {} + + @property + def default_dependency(self): + """Returns the default dependency relation for an artifact type""" + if len(self.dependencies) == 1: + return self.dependencies.values()[0] + + @property + def default_blob(self): + """Returns the default blob object for an artifact type""" + if len(self.blobs) == 1: + return self.blobs.values()[0] + + @property + def default_properties_dict(self): + """Returns a default properties dict for an artifact type""" + dict_props = [v for v in self.properties.values() if + isinstance(v, DictAttributeDefinition)] + if len(dict_props) == 1: + return dict_props[0] + + @property + def tags(self): + """Returns tags property for an artifact type""" + return self.properties.get('tags') + + def add(self, attribute): + self.all[attribute.name] = attribute + if isinstance(attribute, PropertyDefinition): + self.properties[attribute.name] = attribute + elif isinstance(attribute, BlobDefinition): + self.blobs[attribute.name] = attribute + elif isinstance(attribute, RelationDefinition): + self.dependencies[attribute.name] = attribute + + +class ArtifactTypeMetadata(object): + """A container to store the meta-information about an artifact type""" + + def __init__(self, type_name, type_display_name, type_version, + type_description, endpoint): + """Initializes the Artifact Type metadata + + :param type_name: name of the artifact type + :param type_display_name: display name of the artifact type + :param type_version: version of the artifact type + :param type_description: description of the artifact type + :param endpoint: REST API URI suffix to call the artifacts of this type + """ + + self.attributes = ArtifactAttributes() + + # These are going to be defined by third-party plugin + # developers, so we need to do some validations on these values and + # raise InvalidArtifactTypeDefinition if they are violated + self.type_name = type_name + self.type_display_name = type_display_name or type_name + self.type_version = type_version or '1.0' + self.type_description = type_description + self.endpoint = endpoint or type_name.lower() + + self._validate_string(self.type_name, 'Type name', min_length=1, + max_length=255) + self._validate_string(self.type_display_name, 'Type display name', + max_length=255) + self._validate_string(self.type_description, 'Type description') + self._validate_string(self.endpoint, 'endpoint', min_length=1) + try: + semantic_version.Version(self.type_version, partial=True) + except ValueError: + raise exc.InvalidArtifactTypeDefinition( + message=_("Type version has to be a valid semver string")) + + @staticmethod + def _validate_string(value, name, min_length=0, max_length=None, + pattern=None): + if value is None: + if min_length > 0: + raise exc.InvalidArtifactTypeDefinition( + message=_("%(attribute)s is required"), attribute=name) + else: + return + if not isinstance(value, six.string_types): + raise exc.InvalidArtifactTypeDefinition( + message=_("%(attribute)s have to be string"), attribute=name) + if max_length and len(value) > max_length: + raise exc.InvalidArtifactTypeDefinition( + message=_("%(attribute)s may not be longer than %(length)i"), + attribute=name, length=max_length) + if min_length and len(value) < min_length: + raise exc.InvalidArtifactTypeDefinition( + message=_("%(attribute)s may not be shorter than %(length)i"), + attribute=name, length=min_length) + if pattern and not re.match(pattern, value): + raise exc.InvalidArtifactTypeDefinition( + message=_("%(attribute)s should match pattern %(pattern)s"), + attribute=name, pattern=pattern.pattern) + + +def _build_declarative_meta(cls): + attrs = dict(cls.__dict__) + type_name = None + type_display_name = None + type_version = None + type_description = None + endpoint = None + + for base in cls.__mro__: + for name, value in six.iteritems(vars(base)): + if name == '__type_name__': + if not type_name: + type_name = cls.__type_name__ + elif name == '__type_version__': + if not type_version: + type_version = cls.__type_version__ + elif name == '__type_description__': + if not type_description: + type_description = cls.__type_description__ + elif name == '__endpoint__': + if not endpoint: + endpoint = cls.__endpoint__ + elif name == '__type_display_name__': + if not type_display_name: + type_display_name = cls.__type_display_name__ + elif base is not cls and name not in attrs: + if isinstance(value, AttributeDefinition): + attrs[name] = value + elif isinstance(value, ArtifactPropertyDescriptor): + attrs[name] = value.prop + + meta = ArtifactTypeMetadata(type_name=type_name or cls.__name__, + type_display_name=type_display_name, + type_version=type_version, + type_description=type_description, + endpoint=endpoint) + setattr(cls, 'metadata', meta) + for k, v in attrs.items(): + if k == 'metadata': + raise exc.InvalidArtifactTypePropertyDefinition( + _("Cannot declare artifact property with reserved name " + "'metadata'")) + if isinstance(v, AttributeDefinition): + v._set_name(k) + wrapper_class = None + if isinstance(v, ListAttributeDefinition): + wrapper_class = type("ValidatedList", (list,), {}) + _add_validation_to_list(wrapper_class) + if isinstance(v, DictAttributeDefinition): + wrapper_class = type("ValidatedDict", (dict,), {}) + _add_validation_to_dict(wrapper_class) + prop_descr = ArtifactPropertyDescriptor(v, wrapper_class) + setattr(cls, k, prop_descr) + meta.attributes.add(v) + + +def _validating_method(method, klass): + def wrapper(self, *args, **kwargs): + instance_copy = klass(self) + method(instance_copy, *args, **kwargs) + self.property.validate(instance_copy) + method(self, *args, **kwargs) + + return wrapper + + +def _immutable_method(method): + def substitution(*args, **kwargs): + raise exc.InvalidArtifactPropertyValue( + _("Unable to modify collection in " + "immutable or readonly property")) + + return substitution + + +def _add_immutable_wrappers(class_to_add, wrapped_methods): + for method_name in wrapped_methods: + method = getattr(class_to_add, method_name, None) + if method: + setattr(class_to_add, method_name, _immutable_method(method)) + + +def _add_validation_wrappers(class_to_validate, base_class, validated_methods): + for method_name in validated_methods: + method = getattr(class_to_validate, method_name, None) + if method: + setattr(class_to_validate, method_name, + _validating_method(method, base_class)) + readonly_class = type("Readonly" + class_to_validate.__name__, + (class_to_validate,), {}) + _add_immutable_wrappers(readonly_class, validated_methods) + + def __make_immutable__(self): + return readonly_class(self) + + class_to_validate.__make_immutable__ = __make_immutable__ + + +def _add_validation_to_list(list_based_class): + validated_methods = ['append', 'extend', 'insert', 'pop', 'remove', + 'reverse', 'sort', '__setitem__', '__delitem__', + '__delslice__'] + _add_validation_wrappers(list_based_class, list, validated_methods) + + +def _add_validation_to_dict(dict_based_class): + validated_methods = ['pop', 'popitem', 'setdefault', 'update', + '__delitem__', '__setitem__', 'clear'] + _add_validation_wrappers(dict_based_class, dict, validated_methods) + + +def _kwarg_init_constructor(self, **kwargs): + self.__suspend_mutability_checks__ = True + try: + for k in kwargs: + if not hasattr(type(self), k): + raise exc.ArtifactInvalidProperty(prop=k) + setattr(self, k, kwargs[k]) + self._validate_required(self.metadata.attributes.properties) + finally: + del self.__suspend_mutability_checks__ + + +def _validate_required(self, attribute_dict): + for k, v in six.iteritems(attribute_dict): + if v.required and (not hasattr(self, k) or getattr(self, k) is None): + raise exc.InvalidArtifactPropertyValue(name=k, val=None, + msg=_('Value is required')) + + +def _update(self, values): + for k in values: + if hasattr(type(self), k): + setattr(self, k, values[k]) + else: + raise exc.ArtifactInvalidProperty(prop=k) + + +def _pre_publish_validator(self, *args, **kwargs): + self._validate_required(self.metadata.attributes.blobs) + self._validate_required(self.metadata.attributes.dependencies) + + +_kwarg_init_constructor.__name__ = '__init__' +_pre_publish_validator.__name__ = '__pre_publish__' +_update.__name__ = 'update' + + +def get_declarative_base(name='base', base_class=object): + """Returns a base class which should be inherited to construct Artifact + Type object using the declarative syntax of attribute definition + """ + bases = not isinstance(base_class, tuple) and (base_class,) or base_class + class_dict = {'__init__': _kwarg_init_constructor, + '_validate_required': _validate_required, + '__pre_publish__': _pre_publish_validator, + '_declarative_artifact_type': True, + 'update': _update} + return ArtifactTypeMetaclass(name, bases, class_dict) diff --git a/glance/common/artifacts/definitions.py b/glance/common/artifacts/definitions.py new file mode 100644 index 0000000000..b3791c44fa --- /dev/null +++ b/glance/common/artifacts/definitions.py @@ -0,0 +1,571 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# 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 datetime +import numbers +import re +import types + +import semantic_version +import six + +from glance.common.artifacts import declarative +import glance.common.exception as exc +from glance import i18n + + +_ = i18n._ + + +class Text(declarative.PropertyDefinition): + """A text metadata property of arbitrary length + + Maps to TEXT columns in database, does not support sorting or filtering + """ + ALLOWED_TYPES = (six.string_types,) + DB_TYPE = 'text' + + +# noinspection PyAttributeOutsideInit +class String(Text): + """A string metadata property of limited length + + Maps to VARCHAR columns in database, supports filtering and sorting. + May have constrains on length and regexp patterns. + + The maximum length is limited to 255 characters + """ + + DB_TYPE = 'string' + + def __init__(self, max_length=255, min_length=0, pattern=None, **kwargs): + """Defines a String metadata property. + + :param max_length: maximum value length + :param min_length: minimum value length + :param pattern: regexp pattern to match + """ + super(String, self).__init__(**kwargs) + + self.max_length(max_length) + self.min_length(min_length) + if pattern: + self.pattern(pattern) + # if default and/or allowed_values are specified (in base classes) + # then we need to validate them against the newly added validators + self._check_definition() + + def max_length(self, value): + """Sets the maximum value length""" + self._max_length = value + if value is not None: + if value > 255: + raise exc.InvalidArtifactTypePropertyDefinition( + _('Max string length may not exceed 255 characters')) + self._add_validator('max_length', + lambda v: len(v) <= self._max_length, + _('Length is greater than maximum')) + else: + self._remove_validator('max_length') + self._check_definition() + + def min_length(self, value): + """Sets the minimum value length""" + self._min_length = value + if value is not None: + if value < 0: + raise exc.InvalidArtifactTypePropertyDefinition( + _('Min string length may not be negative')) + + self._add_validator('min_length', + lambda v: len(v) >= self._min_length, + _('Length is less than minimum')) + else: + self._remove_validator('min_length') + self._check_definition() + + def pattern(self, value): + """Sets the regexp pattern to match""" + self._pattern = value + if value is not None: + self._add_validator('pattern', + lambda v: re.match(self._pattern, + v) is not None, + _('Does not match pattern')) + else: + self._remove_validator('pattern') + self._check_definition() + + +class SemVerString(String): + """A String metadata property matching semver pattern""" + + def __init__(self, **kwargs): + def validate(value): + try: + semantic_version.Version(value, partial=True) + except ValueError: + return False + return True + + super(SemVerString, + self).__init__(validators=[(validate, + "Invalid semver string")], + **kwargs) + + +# noinspection PyAttributeOutsideInit +class Integer(declarative.PropertyDefinition): + """An Integer metadata property + + Maps to INT columns in Database, supports filtering and sorting. + May have constraints on value + """ + + ALLOWED_TYPES = (six.integer_types,) + DB_TYPE = 'int' + + def __init__(self, min_value=None, max_value=None, **kwargs): + """Defines an Integer metadata property + + :param min_value: minimum allowed value + :param max_value: maximum allowed value + """ + super(Integer, self).__init__(**kwargs) + if min_value is not None: + self.min_value(min_value) + + if max_value is not None: + self.max_value(max_value) + + # if default and/or allowed_values are specified (in base classes) + # then we need to validate them against the newly added validators + self._check_definition() + + def min_value(self, value): + """Sets the minimum allowed value""" + self._min_value = value + if value is not None: + self._add_validator('min_value', + lambda v: v >= self._min_value, + _('Value is less than minimum')) + else: + self._remove_validator('min_value') + self._check_definition() + + def max_value(self, value): + """Sets the maximum allowed value""" + self._max_value = value + if value is not None: + self._add_validator('max_value', + lambda v: v <= self._max_value, + _('Value is greater than maximum')) + else: + self._remove_validator('max_value') + self._check_definition() + + +# noinspection PyAttributeOutsideInit +class DateTime(declarative.PropertyDefinition): + """A DateTime metadata property + + Maps to a DATETIME columns in database. + Is not supported as Type Specific property, may be used only as Generic one + + May have constraints on value + """ + ALLOWED_TYPES = (datetime.datetime,) + DB_TYPE = 'datetime' + + def __init__(self, min_value=None, max_value=None, **kwargs): + """Defines a DateTime metadata property + + :param min_value: minimum allowed value + :param max_value: maximum allowed value + """ + super(DateTime, self).__init__(**kwargs) + if min_value is not None: + self.min_value(min_value) + + if max_value is not None: + self.max_value(max_value) + + # if default and/or allowed_values are specified (in base classes) + # then we need to validate them against the newly added validators + self._check_definition() + + def min_value(self, value): + """Sets the minimum allowed value""" + self._min_value = value + if value is not None: + self._add_validator('min_value', + lambda v: v >= self._min_value, + _('Value is less than minimum')) + else: + self._remove_validator('min_value') + self._check_definition() + + def max_value(self, value): + """Sets the maximum allowed value""" + self._max_value = value + if value is not None: + self._add_validator('max_value', + lambda v: v <= self._max_value, + _('Value is greater than maximum')) + else: + self._remove_validator('max_value') + self._check_definition() + + +# noinspection PyAttributeOutsideInit +class Numeric(declarative.PropertyDefinition): + """A Numeric metadata property + + Maps to floating point number columns in Database, supports filtering and + sorting. May have constraints on value + """ + ALLOWED_TYPES = numbers.Number + DB_TYPE = 'numeric' + + def __init__(self, min_value=None, max_value=None, **kwargs): + """Defines a Numeric metadata property + + :param min_value: minimum allowed value + :param max_value: maximum allowed value + """ + super(Numeric, self).__init__(**kwargs) + if min_value is not None: + self.min_value(min_value) + + if max_value is not None: + self.max_value(max_value) + + # if default and/or allowed_values are specified (in base classes) + # then we need to validate them against the newly added validators + self._check_definition() + + def min_value(self, value): + """Sets the minimum allowed value""" + self._min_value = value + if value is not None: + self._add_validator('min_value', + lambda v: v >= self._min_value, + _('Value is less than minimum')) + else: + self._remove_validator('min_value') + self._check_definition() + + def max_value(self, value): + """Sets the maximum allowed value""" + self._max_value = value + if value is not None: + self._add_validator('max_value', + lambda v: v <= self._max_value, + _('Value is greater than maximum')) + else: + self._remove_validator('max_value') + self._check_definition() + + +class Boolean(declarative.PropertyDefinition): + """A Boolean metadata property + + Maps to Boolean columns in database. Supports filtering and sorting. + """ + ALLOWED_TYPES = (types.BooleanType,) + DB_TYPE = 'bool' + + +class Array(declarative.ListAttributeDefinition, + declarative.PropertyDefinition, list): + """An array metadata property + + May contain elements of any other PropertyDefinition types except Dict and + Array. Each elements maps to appropriate type of columns in database. + Preserves order. Allows filtering based on "Array contains Value" semantics + + May specify constrains on types of elements, their amount and uniqueness. + """ + ALLOWED_ITEM_TYPES = (declarative.PropertyDefinition,) + + def __init__(self, item_type=String(), min_size=0, max_size=None, + unique=False, extra_items=True, **kwargs): + """Defines an Array metadata property + + :param item_type: defines the types of elements in Array. If set to an + instance of PropertyDefinition then all the elements have to be of that + type. If set to list of such instances, then the elements on the + corresponding positions have to be of the appropriate type. + :param min_size: minimum size of the Array + :param max_size: maximum size of the Array + :param unique: if set to true, all the elements in the Array have to be + unique + """ + if isinstance(item_type, Array): + msg = _("Array property can't have item_type=Array") + raise exc.InvalidArtifactTypePropertyDefinition(msg) + declarative.ListAttributeDefinition.__init__(self, + item_type=item_type, + min_size=min_size, + max_size=max_size, + unique=unique) + declarative.PropertyDefinition.__init__(self, **kwargs) + + +class Dict(declarative.DictAttributeDefinition, + declarative.PropertyDefinition, dict): + """A dictionary metadata property + + May contain elements of any other PropertyDefinition types except Dict. + Each elements maps to appropriate type of columns in database. Allows + filtering and sorting by values of each key except the ones mapping the + Text fields. + + May specify constrains on types of elements and their amount. + """ + ALLOWED_PROPERTY_TYPES = (declarative.PropertyDefinition,) + + def __init__(self, properties=String(), min_properties=0, + max_properties=None, **kwargs): + """Defines a dictionary metadata property + + :param properties: defines the types of dictionary values. If set to an + instance of PropertyDefinition then all the value have to be of that + type. If set to a dictionary with string keys and values of + PropertyDefinition type, then the elements mapped by the corresponding + have have to be of the appropriate type. + :param min_properties: minimum allowed amount of properties in the dict + :param max_properties: maximum allowed amount of properties in the dict + """ + declarative.DictAttributeDefinition. \ + __init__(self, + properties=properties, + min_properties=min_properties, + max_properties=max_properties) + declarative.PropertyDefinition.__init__(self, **kwargs) + + +class ArtifactType(declarative.get_declarative_base()): # noqa + """A base class for all the Artifact Type definitions + + Defines the Generic metadata properties as attributes. + """ + id = String(required=True, readonly=True) + type_name = String(required=True, readonly=True) + type_version = SemVerString(required=True, readonly=True) + name = String(required=True, mutable=False) + version = SemVerString(required=True, mutable=False) + description = Text() + tags = Array(unique=True, default=[]) + visibility = String(required=True, + allowed_values=["private", "public", "shared", + "community"], + default="private") + state = String(required=True, readonly=True, allowed_values=["creating", + "active", + "deactivated", + "deleted"]) + owner = String(required=True, readonly=True) + created_at = DateTime(required=True, readonly=True) + updated_at = DateTime(required=True, readonly=True) + published_at = DateTime(readonly=True) + deleted_at = DateTime(readonly=True) + + def __init__(self, **kwargs): + if "type_name" in kwargs: + raise exc.InvalidArtifactPropertyValue( + _("Unable to specify artifact type explicitly")) + if "type_version" in kwargs: + raise exc.InvalidArtifactPropertyValue( + _("Unable to specify artifact type version explicitly")) + super(ArtifactType, + self).__init__(type_name=self.metadata.type_name, + type_version=self.metadata.type_version, **kwargs) + + def __eq__(self, other): + if not isinstance(other, ArtifactType): + return False + return self.id == other.id + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.id) + + def __is_mutable__(self): + return self.state == "creating" + + +class ArtifactReference(declarative.RelationDefinition): + """An artifact reference definition + + Allows to define constraints by the name and version of target artifact + """ + ALLOWED_TYPES = ArtifactType + + def __init__(self, type_name=None, type_version=None, **kwargs): + """Defines an artifact reference + + :param type_name: type name of the target artifact + :param type_version: type version of the target artifact + """ + super(ArtifactReference, self).__init__(**kwargs) + if type_name is not None: + if isinstance(type_name, types.ListType): + type_names = list(type_name) + if type_version is not None: + raise exc.InvalidArtifactTypePropertyDefinition( + _('Unable to specify version ' + 'if multiple types are possible')) + else: + type_names = [type_name] + + def validate_reference(artifact): + if artifact.type_name not in type_names: + return False + if (type_version is not None and + artifact.type_version != type_version): + return False + return True + + self._add_validator('referenced_type', + validate_reference, + _("Invalid referenced type")) + elif type_version is not None: + raise exc.InvalidArtifactTypePropertyDefinition( + _('Unable to specify version ' + 'if type is not specified')) + self._check_definition() + + +class ArtifactReferenceList(declarative.ListAttributeDefinition, + declarative.RelationDefinition, list): + """A list of Artifact References + + Allows to define a collection of references to other artifacts, each + optionally constrained by type name and type version + """ + ALLOWED_ITEM_TYPES = (ArtifactReference,) + + def __init__(self, references=ArtifactReference(), min_size=0, + max_size=None, **kwargs): + if isinstance(references, types.ListType): + raise exc.InvalidArtifactTypePropertyDefinition( + _("Invalid reference list specification")) + declarative.RelationDefinition.__init__(self, **kwargs) + declarative.ListAttributeDefinition.__init__(self, + item_type=references, + min_size=min_size, + max_size=max_size, + unique=True, + default=[] + if min_size == 0 else + None) + + +class Blob(object): + """A Binary object being part of the Artifact""" + def __init__(self, size=0, locations=None, checksum=None, item_key=None): + """Initializes a new Binary Object for an Artifact + + :param size: the size of Binary Data + :param locations: a list of data locations in backing stores + :param checksum: a checksum for the data + """ + if locations is None: + locations = [] + self.size = size + self.checksum = checksum + self.locations = locations + self.item_key = item_key + + def to_dict(self): + return { + "size": self.size, + "checksum": self.checksum, + } + + +class BinaryObject(declarative.BlobDefinition, Blob): + """A definition of BinaryObject binding + + Adds a BinaryObject to an Artifact Type, optionally constrained by file + size and amount of locations + """ + ALLOWED_TYPES = (Blob,) + + def __init__(self, + max_file_size=None, + min_file_size=None, + min_locations=None, + max_locations=None, + **kwargs): + """Defines a binary object as part of Artifact Type + :param max_file_size: maximum size of the associate Blob + :param min_file_size: minimum size of the associated Blob + :param min_locations: minimum number of locations in the associated + Blob + :param max_locations: maximum number of locations in the associated + Blob + """ + super(BinaryObject, self).__init__(default=None, readonly=False, + mutable=False, **kwargs) + self._max_file_size = max_file_size + self._min_file_size = min_file_size + self._min_locations = min_locations + self._max_locations = max_locations + + self._add_validator('size_not_empty', + lambda v: v.size is not None, + _('Blob size is not set')) + if max_file_size: + self._add_validator('max_size', + lambda v: v.size <= self._max_file_size, + _("File too large")) + if min_file_size: + self._add_validator('min_size', + lambda v: v.size >= self._min_file_size, + _("File too small")) + if min_locations: + self._add_validator('min_locations', + lambda v: len( + v.locations) >= self._min_locations, + _("Too few locations")) + if max_locations: + self._add_validator( + 'max_locations', + lambda v: len(v.locations) <= self._max_locations, + _("Too many locations")) + + +class BinaryObjectList(declarative.ListAttributeDefinition, + declarative.BlobDefinition, list): + """A definition of binding to the list of BinaryObject + + Adds a list of BinaryObject's to an artifact type, optionally constrained + by the number of objects in the list and their uniqueness + + """ + ALLOWED_ITEM_TYPES = (BinaryObject,) + + def __init__(self, objects=BinaryObject(), min_count=0, max_count=None, + **kwargs): + declarative.BlobDefinition.__init__(self, **kwargs) + declarative.ListAttributeDefinition.__init__(self, + item_type=objects, + min_size=min_count, + max_size=max_count, + unique=True) + self.default = [] if min_count == 0 else None diff --git a/glance/common/artifacts/serialization.py b/glance/common/artifacts/serialization.py new file mode 100644 index 0000000000..9712a9c8bc --- /dev/null +++ b/glance/common/artifacts/serialization.py @@ -0,0 +1,265 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# 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 six + +from glance.common.artifacts import declarative +from glance.common.artifacts import definitions +from glance.common import exception +from glance import i18n + + +_ = i18n._ + +COMMON_ARTIFACT_PROPERTIES = ['id', + 'type_name', + 'type_version', + 'name', + 'version', + 'description', + 'visibility', + 'state', + 'tags', + 'owner', + 'created_at', + 'updated_at', + 'published_at', + 'deleted_at'] + + +def _serialize_list_prop(prop, values): + """ + A helper func called to correctly serialize an Array property. + + Returns a dict {'type': some_supported_db_type, 'value': serialized_data} + """ + # FIXME(Due to a potential bug in declarative framework, for Arrays, that + # are values to some dict items (Dict(properties={"foo": Array()})), + # prop.get_value(artifact) returns not the real list of items, but the + # whole dict). So we can't rely on prop.get_value(artifact) and will pass + # correctly retrieved values to this function + serialized_value = [] + for i, val in enumerate(values or []): + db_type = prop.get_item_definition_at_index(i).DB_TYPE + if db_type is None: + continue + serialized_value.append({ + 'type': db_type, + 'value': val + }) + return serialized_value + + +def _serialize_dict_prop(artifact, prop, key, value, save_prop_func): + key_to_save = prop.name + '.' + key + dict_key_prop = prop.get_prop_definition_at_key(key) + db_type = dict_key_prop.DB_TYPE + if (db_type is None and + not isinstance(dict_key_prop, + declarative.ListAttributeDefinition)): + # nothing to do here, don't know how to deal with this type + return + elif isinstance(dict_key_prop, + declarative.ListAttributeDefinition): + serialized = _serialize_list_prop( + dict_key_prop, + # FIXME(see comment for _serialize_list_prop func) + values=(dict_key_prop.get_value(artifact) or {}).get(key, [])) + save_prop_func(key_to_save, 'array', serialized) + else: + save_prop_func(key_to_save, db_type, value) + + +def _serialize_dependencies(artifact): + """Returns a dict of serialized dependencies for given artifact""" + dependencies = {} + for relation in artifact.metadata.attributes.dependencies.values(): + serialized_dependency = [] + if isinstance(relation, declarative.ListAttributeDefinition): + for dep in relation.get_value(artifact): + serialized_dependency.append(dep.id) + else: + relation_data = relation.get_value(artifact) + if relation_data: + serialized_dependency.append(relation.get_value(artifact).id) + dependencies[relation.name] = serialized_dependency + return dependencies + + +def _serialize_blobs(artifact): + """Return a dict of serialized blobs for given artifact""" + blobs = {} + for blob in artifact.metadata.attributes.blobs.values(): + serialized_blob = [] + if isinstance(blob, declarative.ListAttributeDefinition): + for b in blob.get_value(artifact) or []: + serialized_blob.append({ + 'size': b.size, + 'locations': b.locations, + 'checksum': b.checksum, + 'item_key': b.item_key + }) + else: + b = blob.get_value(artifact) + # if no value for blob has been set -> continue + if not b: + continue + serialized_blob.append({ + 'size': b.size, + 'locations': b.locations, + 'checksum': b.checksum, + 'item_key': b.item_key + }) + blobs[blob.name] = serialized_blob + return blobs + + +def serialize_for_db(artifact): + result = {} + custom_properties = {} + + def _save_prop(prop_key, prop_type, value): + custom_properties[prop_key] = { + 'type': prop_type, + 'value': value + } + + for prop in artifact.metadata.attributes.properties.values(): + if prop.name in COMMON_ARTIFACT_PROPERTIES: + result[prop.name] = prop.get_value(artifact) + continue + if isinstance(prop, declarative.ListAttributeDefinition): + serialized_value = _serialize_list_prop(prop, + prop.get_value(artifact)) + _save_prop(prop.name, 'array', serialized_value) + elif isinstance(prop, declarative.DictAttributeDefinition): + fields_to_set = prop.get_value(artifact) or {} + # if some keys are not present (like in prop == {}), then have to + # set their values to None. + # XXX FIXME prop.properties may be a dict ({'foo': '', 'bar': ''}) + # or String\Integer\whatsoever, limiting the possible dict values. + # In the latter case have no idea how to remove old values during + # serialization process. + if isinstance(prop.properties, dict): + for key in [k for k in prop.properties + if k not in fields_to_set.keys()]: + _serialize_dict_prop(artifact, prop, key, None, _save_prop) + # serialize values of properties present + for key, value in six.iteritems(fields_to_set): + _serialize_dict_prop(artifact, prop, key, value, _save_prop) + elif prop.DB_TYPE is not None: + _save_prop(prop.name, prop.DB_TYPE, prop.get_value(artifact)) + + result['properties'] = custom_properties + result['dependencies'] = _serialize_dependencies(artifact) + result['blobs'] = _serialize_blobs(artifact) + return result + + +def _deserialize_blobs(artifact_type, blobs_from_db, artifact_properties): + """Retrieves blobs from database""" + for blob_name, blob_value in six.iteritems(blobs_from_db): + if not blob_value: + continue + if isinstance(artifact_type.metadata.attributes.blobs.get(blob_name), + declarative.ListAttributeDefinition): + val = [] + for v in blob_value: + b = definitions.Blob(size=v['size'], + locations=v['locations'], + checksum=v['checksum'], + item_key=v['item_key']) + val.append(b) + elif len(blob_value) == 1: + val = definitions.Blob(size=blob_value[0]['size'], + locations=blob_value[0]['locations'], + checksum=blob_value[0]['checksum'], + item_key=blob_value[0]['item_key']) + else: + raise exception.InvalidArtifactPropertyValue( + message=_('Blob %(name)s may not have multiple values'), + name=blob_name) + artifact_properties[blob_name] = val + + +def _deserialize_dependencies(artifact_type, deps_from_db, + artifact_properties, type_dictionary): + """Retrieves dependencies from database""" + for dep_name, dep_value in six.iteritems(deps_from_db): + if not dep_value: + continue + if isinstance( + artifact_type.metadata.attributes.dependencies.get(dep_name), + declarative.ListAttributeDefinition): + val = [] + for v in dep_value: + val.append(deserialize_from_db(v, type_dictionary)) + elif len(dep_value) == 1: + val = deserialize_from_db(dep_value[0], type_dictionary) + else: + raise exception.InvalidArtifactPropertyValue( + message=_('Relation %(name)s may not have multiple values'), + name=dep_name) + artifact_properties[dep_name] = val + + +def deserialize_from_db(db_dict, type_dictionary): + artifact_properties = {} + type_name = None + type_version = None + + for prop_name in COMMON_ARTIFACT_PROPERTIES: + prop_value = db_dict.pop(prop_name, None) + if prop_name == 'type_name': + type_name = prop_value + elif prop_name == 'type_version': + type_version = prop_value + else: + artifact_properties[prop_name] = prop_value + + if type_name and type_version and (type_version in + type_dictionary.get(type_name, [])): + artifact_type = type_dictionary[type_name][type_version] + else: + raise exception.UnknownArtifactType(name=type_name, + version=type_version) + + type_specific_properties = db_dict.pop('properties', {}) + for prop_name, prop_value in six.iteritems(type_specific_properties): + prop_type = prop_value.get('type') + prop_value = prop_value.get('value') + if prop_value is None: + continue + if '.' in prop_name: # dict-based property + name, key = prop_name.split('.', 1) + artifact_properties.setdefault(name, {}) + if prop_type == 'array': + artifact_properties[name][key] = [item.get('value') for item in + prop_value] + else: + artifact_properties[name][key] = prop_value + elif prop_type == 'array': # list-based property + artifact_properties[prop_name] = [item.get('value') for item in + prop_value] + else: + artifact_properties[prop_name] = prop_value + + blobs = db_dict.pop('blobs', {}) + _deserialize_blobs(artifact_type, blobs, artifact_properties) + + dependencies = db_dict.pop('dependencies', {}) + _deserialize_dependencies(artifact_type, dependencies, + artifact_properties, type_dictionary) + + return artifact_type(**artifact_properties) diff --git a/glance/common/exception.py b/glance/common/exception.py index 6f282bccec..4b57b41c2f 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -491,9 +491,36 @@ class ArtifactPropertyValueNotFound(NotFound): message = _("Property's %(prop)s value has not been found") +class ArtifactInvalidProperty(Invalid): + message = _("Artifact has no property %(prop)s") + + class ArtifactInvalidPropertyParameter(Invalid): message = _("Cannot use this parameter with the operator %(op)s") class ArtifactInvalidStateTransition(Invalid): message = _("Artifact state cannot be changed from %(curr)s to %(to)s") + + +class InvalidArtifactTypePropertyDefinition(Invalid): + message = _("Invalid property definition") + + +class InvalidArtifactTypeDefinition(Invalid): + message = _("Invalid type definition") + + +class InvalidArtifactPropertyValue(Invalid): + message = _("Property '%(name)s' may not have value '%(val)s': %(msg)s") + + def __init__(self, message=None, *args, **kwargs): + super(InvalidArtifactPropertyValue, self).__init__(message, *args, + **kwargs) + self.name = kwargs.get('name') + self.value = kwargs.get('val') + + +class UnknownArtifactType(NotFound): + message = _("Artifact type with name '%(name)s' and version '%(version)s' " + "is not known") diff --git a/glance/tests/unit/test_artifact_type_definition_framework.py b/glance/tests/unit/test_artifact_type_definition_framework.py new file mode 100644 index 0000000000..a0302b8d4c --- /dev/null +++ b/glance/tests/unit/test_artifact_type_definition_framework.py @@ -0,0 +1,1109 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# 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 datetime + +from glance.common.artifacts import declarative +import glance.common.artifacts.definitions as defs +from glance.common.artifacts import serialization +import glance.common.exception as exc +import glance.tests.utils as test_utils + + +BASE = declarative.get_declarative_base() + + +class TestDeclarativeProperties(test_utils.BaseTestCase): + def test_artifact_type_properties(self): + class SomeTypeWithNoExplicitName(BASE): + some_attr = declarative.AttributeDefinition() + + class InheritedType(SomeTypeWithNoExplicitName): + __type_version__ = '1.0' + __type_name__ = 'ExplicitName' + __type_description__ = 'Type description' + __type_display_name__ = 'EXPLICIT_NAME' + __endpoint__ = 'some_endpoint' + + some_attr = declarative.AttributeDefinition(display_name='NAME') + + base_type = SomeTypeWithNoExplicitName + base_instance = SomeTypeWithNoExplicitName() + self.assertIsNotNone(base_type.metadata) + self.assertIsNotNone(base_instance.metadata) + self.assertEqual(base_type.metadata, base_instance.metadata) + self.assertEqual("SomeTypeWithNoExplicitName", + base_type.metadata.type_name) + self.assertEqual("SomeTypeWithNoExplicitName", + base_type.metadata.type_display_name) + self.assertEqual("1.0", base_type.metadata.type_version) + self.assertIsNone(base_type.metadata.type_description) + self.assertEqual('sometypewithnoexplicitname', + base_type.metadata.endpoint) + + self.assertIsNone(base_instance.some_attr) + self.assertIsNotNone(base_type.some_attr) + self.assertEqual(base_type.some_attr, + base_instance.metadata.attributes.all['some_attr']) + self.assertEqual('some_attr', base_type.some_attr.name) + self.assertEqual('some_attr', base_type.some_attr.display_name) + self.assertIsNone(base_type.some_attr.description) + + derived_type = InheritedType + derived_instance = InheritedType() + + self.assertIsNotNone(derived_type.metadata) + self.assertIsNotNone(derived_instance.metadata) + self.assertEqual(derived_type.metadata, derived_instance.metadata) + self.assertEqual('ExplicitName', derived_type.metadata.type_name) + self.assertEqual('EXPLICIT_NAME', + derived_type.metadata.type_display_name) + self.assertEqual('1.0', derived_type.metadata.type_version) + self.assertEqual('Type description', + derived_type.metadata.type_description) + self.assertEqual('some_endpoint', derived_type.metadata.endpoint) + self.assertIsNone(derived_instance.some_attr) + self.assertIsNotNone(derived_type.some_attr) + self.assertEqual(derived_type.some_attr, + derived_instance.metadata.attributes.all['some_attr']) + self.assertEqual('some_attr', derived_type.some_attr.name) + self.assertEqual('NAME', derived_type.some_attr.display_name) + + def test_wrong_type_definition(self): + def declare_wrong_type_version(): + class WrongType(BASE): + __type_version__ = 'abc' # not a semver + + return WrongType + + def declare_wrong_type_name(): + class WrongType(BASE): + __type_name__ = 'a' * 256 # too long + + return WrongType + + self.assertRaises(exc.InvalidArtifactTypeDefinition, + declare_wrong_type_version) + self.assertRaises(exc.InvalidArtifactTypeDefinition, + declare_wrong_type_name) + + def test_base_declarative_attributes(self): + class TestType(BASE): + defaulted = declarative.PropertyDefinition(default=42) + read_only = declarative.PropertyDefinition(readonly=True) + required_attr = declarative.PropertyDefinition(required=True) + + e = self.assertRaises(exc.InvalidArtifactPropertyValue, TestType) + self.assertEqual('required_attr', e.name) + self.assertIsNone(e.value) + tt = TestType(required_attr="universe") + self.assertEqual('universe', tt.required_attr) + self.assertEqual(42, tt.defaulted) + self.assertIsNone(tt.read_only) + + tt = TestType(required_attr="universe", defaulted=0, read_only="Hello") + self.assertEqual(0, tt.defaulted) + self.assertEqual("Hello", tt.read_only) + + tt.defaulted = 5 + self.assertEqual(5, tt.defaulted) + tt.required_attr = 'Foo' + self.assertEqual('Foo', tt.required_attr) + + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'read_only', 'some_val') + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'required_attr', None) + + # no type checks in base AttributeDefinition + o = object() + tt.required_attr = o + self.assertEqual(o, tt.required_attr) + + def test_generic_property(self): + class TestType(BASE): + simple_prop = declarative.PropertyDefinition() + immutable_internal = declarative.PropertyDefinition(mutable=False, + internal=True) + prop_with_allowed = declarative.PropertyDefinition( + allowed_values=["Foo", True, 42]) + + class DerivedType(TestType): + prop_with_allowed = declarative.PropertyDefinition( + allowed_values=["Foo", True, 42], required=True, default=42) + + tt = TestType() + self.assertEqual(True, + tt.metadata.attributes.all['simple_prop'].mutable) + self.assertEqual(False, + tt.metadata.attributes.all['simple_prop'].internal) + self.assertEqual(False, + tt.metadata.attributes.all[ + 'immutable_internal'].mutable) + self.assertEqual(True, + tt.metadata.attributes.all[ + 'immutable_internal'].internal) + self.assertIsNone(tt.prop_with_allowed) + tt = TestType(prop_with_allowed=42) + self.assertEqual(42, tt.prop_with_allowed) + tt = TestType(prop_with_allowed=True) + self.assertEqual(True, tt.prop_with_allowed) + tt = TestType(prop_with_allowed='Foo') + self.assertEqual('Foo', tt.prop_with_allowed) + + tt.prop_with_allowed = 42 + self.assertEqual(42, tt.prop_with_allowed) + tt.prop_with_allowed = 'Foo' + self.assertEqual('Foo', tt.prop_with_allowed) + tt.prop_with_allowed = True + self.assertEqual(True, tt.prop_with_allowed) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, 'prop_with_allowed', 'bar') + # ensure that wrong assignment didn't change the value + self.assertEqual(True, tt.prop_with_allowed) + self.assertRaises(exc.InvalidArtifactPropertyValue, TestType, + prop_with_allowed=False) + + dt = DerivedType() + self.assertEqual(42, dt.prop_with_allowed) + + def test_default_violates_allowed(self): + def declare_wrong_type(): + class WrongType(BASE): + prop = declarative.PropertyDefinition( + allowed_values=['foo', 'bar'], + default='baz') + + return WrongType + + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_type) + + def test_string_property(self): + class TestType(BASE): + simple = defs.String() + with_length = defs.String(max_length=10, min_length=5) + with_pattern = defs.String(pattern='^\\d+$', default='42') + + tt = TestType() + tt.simple = 'foo' + self.assertEqual('foo', tt.simple) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, 'simple', 42) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, 'simple', 'x' * 256) + self.assertRaises(exc.InvalidArtifactPropertyValue, TestType, + simple='x' * 256) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, 'with_length', 'x' * 11) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, 'with_length', 'x' * 4) + tt.simple = 'x' * 5 + self.assertEqual('x' * 5, tt.simple) + tt.simple = 'x' * 10 + self.assertEqual('x' * 10, tt.simple) + + self.assertEqual("42", tt.with_pattern) + tt.with_pattern = '0' + self.assertEqual('0', tt.with_pattern) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'with_pattern', 'abc') + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'with_pattern', '.123.') + + def test_default_and_allowed_violates_string_constrains(self): + def declare_wrong_default(): + class WrongType(BASE): + prop = defs.String(min_length=4, default='foo') + + return WrongType + + def declare_wrong_allowed(): + class WrongType(BASE): + prop = defs.String(min_length=4, allowed_values=['foo', 'bar']) + + return WrongType + + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_default) + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_allowed) + + def test_integer_property(self): + class TestType(BASE): + simple = defs.Integer() + constrained = defs.Integer(min_value=10, max_value=50) + + tt = TestType() + self.assertIsNone(tt.simple) + self.assertIsNone(tt.constrained) + + tt.simple = 0 + tt.constrained = 10 + self.assertEqual(0, tt.simple) + self.assertEqual(10, tt.constrained) + + tt.constrained = 50 + self.assertEqual(50, tt.constrained) + + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'constrained', 1) + self.assertEqual(50, tt.constrained) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'constrained', 51) + self.assertEqual(50, tt.constrained) + + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'simple', '11') + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'simple', 10.5) + + def test_default_and_allowed_violates_int_constrains(self): + def declare_wrong_default(): + class WrongType(BASE): + prop = defs.Integer(min_value=4, default=1) + + return WrongType + + def declare_wrong_allowed(): + class WrongType(BASE): + prop = defs.Integer(min_value=4, max_value=10, + allowed_values=[1, 15]) + + return WrongType + + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_default) + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_allowed) + + def test_numeric_values(self): + class TestType(BASE): + simple = defs.Numeric() + constrained = defs.Numeric(min_value=3.14, max_value=4.1) + + tt = TestType(simple=0.1, constrained=4) + self.assertEqual(0.1, tt.simple) + self.assertEqual(4.0, tt.constrained) + + tt.simple = 1 + self.assertEqual(1, tt.simple) + tt.constrained = 3.14 + self.assertEqual(3.14, tt.constrained) + tt.constrained = 4.1 + self.assertEqual(4.1, tt.constrained) + + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'simple', 'qwerty') + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'constrained', 3) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + 'constrained', 5) + + def test_default_and_allowed_violates_numeric_constrains(self): + def declare_wrong_default(): + class WrongType(BASE): + prop = defs.Numeric(min_value=4.0, default=1.1) + + return WrongType + + def declare_wrong_allowed(): + class WrongType(BASE): + prop = defs.Numeric(min_value=4.0, max_value=10.0, + allowed_values=[1.0, 15.5]) + + return WrongType + + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_default) + self.assertRaises(exc.InvalidArtifactTypePropertyDefinition, + declare_wrong_allowed) + + def test_same_item_type_array(self): + class TestType(BASE): + simple = defs.Array() + unique = defs.Array(unique=True) + simple_with_allowed_values = defs.Array( + defs.String(allowed_values=["Foo", "Bar"])) + defaulted = defs.Array(defs.Boolean(), default=[True, False]) + constrained = defs.Array(item_type=defs.Numeric(min_value=0), + min_size=3, max_size=5, unique=True) + + tt = TestType(simple=[]) + self.assertEqual([], tt.simple) + tt.simple.append("Foo") + self.assertEqual(["Foo"], tt.simple) + tt.simple.append("Foo") + self.assertEqual(["Foo", "Foo"], tt.simple) + self.assertEqual(2, len(tt.simple)) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.simple.append, + 42) + tt.simple.pop(1) + self.assertEqual(["Foo"], tt.simple) + del tt.simple[0] + self.assertEqual(0, len(tt.simple)) + + tt.simple_with_allowed_values = ["Foo"] + tt.simple_with_allowed_values.insert(0, "Bar") + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.simple_with_allowed_values.append, "Baz") + + self.assertEqual([True, False], tt.defaulted) + tt.defaulted.pop() + self.assertEqual([True], tt.defaulted) + tt2 = TestType() + self.assertEqual([True, False], tt2.defaulted) + + self.assertIsNone(tt.constrained) + tt.constrained = [10, 5, 4] + self.assertEqual([10, 5, 4], tt.constrained) + tt.constrained[1] = 15 + self.assertEqual([10, 15, 4], tt.constrained) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained.__setitem__, 1, -5) + self.assertEqual([10, 15, 4], tt.constrained) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained.remove, 15) + self.assertEqual([10, 15, 4], tt.constrained) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained.__delitem__, 1) + self.assertEqual([10, 15, 4], tt.constrained) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained.append, 15) + self.assertEqual([10, 15, 4], tt.constrained) + + tt.unique = [] + tt.unique.append("foo") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.unique.append, + "foo") + + def test_tuple_style_array(self): + class TestType(BASE): + address = defs.Array( + item_type=[defs.String(20), defs.Integer(min_value=1), + defs.Boolean()]) + + tt = TestType(address=["Hope Street", 1234, True]) + self.assertEqual("Hope Street", tt.address[0]) + self.assertEqual(1234, tt.address[1]) + self.assertEqual(True, tt.address[2]) + + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.sort) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop, 0) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop, 1) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.pop) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.address.append, + "Foo") + + def test_same_item_type_dict(self): + class TestType(BASE): + simple_props = defs.Dict() + constrained_props = defs.Dict( + properties=defs.Integer(min_value=1, allowed_values=[1, 2]), + min_properties=2, + max_properties=3) + + tt = TestType() + self.assertIsNone(tt.simple_props) + self.assertIsNone(tt.constrained_props) + tt.simple_props = {} + self.assertEqual({}, tt.simple_props) + tt.simple_props["foo"] = "bar" + self.assertEqual({"foo": "bar"}, tt.simple_props) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.simple_props.__setitem__, 42, "foo") + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.simple_props.setdefault, "bar", 42) + + tt.constrained_props = {"foo": 1, "bar": 2} + self.assertEqual({"foo": 1, "bar": 2}, tt.constrained_props) + tt.constrained_props["baz"] = 1 + self.assertEqual({"foo": 1, "bar": 2, "baz": 1}, tt.constrained_props) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained_props.__setitem__, "foo", 3) + self.assertEqual(1, tt.constrained_props["foo"]) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained_props.__setitem__, "qux", 2) + tt.constrained_props.pop("foo") + self.assertEqual({"bar": 2, "baz": 1}, tt.constrained_props) + tt.constrained_props['qux'] = 2 + self.assertEqual({"qux": 2, "bar": 2, "baz": 1}, tt.constrained_props) + tt.constrained_props.popitem() + dict_copy = tt.constrained_props.copy() + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.constrained_props.popitem) + self.assertEqual(dict_copy, tt.constrained_props) + + def test_composite_dict(self): + class TestType(BASE): + props = defs.Dict(properties={"foo": defs.String(), + "bar": defs.Boolean()}) + fixed = defs.Dict(properties={"name": defs.String(min_length=2), + "age": defs.Integer(min_value=0, + max_value=99)}) + + tt = TestType() + tt.props = {"foo": "FOO"} + tt.props["bar"] = False + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.props.__setitem__, "bar", 123) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.props.__setitem__, "extra", "value") + tt.fixed = {"name": "Alex"} + tt.fixed["age"] = 42 + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.fixed.__setitem__, "age", 120) + + def test_immutables(self): + class TestType(BASE): + activated = defs.Boolean(required=True, default=False) + name = defs.String(mutable=False) + + def __is_mutable__(self): + return not self.activated + + tt = TestType() + self.assertEqual(False, tt.activated) + self.assertIsNone(tt.name) + tt.name = "Foo" + self.assertEqual("Foo", tt.name) + tt.activated = True + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, + tt, "name", "Bar") + self.assertEqual("Foo", tt.name) + tt.activated = False + tt.name = "Bar" + self.assertEqual("Bar", tt.name) + + def test_readonly_array_dict(self): + class TestType(BASE): + arr = defs.Array(readonly=True) + dict = defs.Dict(readonly=True) + + tt = TestType(arr=["Foo", "Bar"], dict={"qux": "baz"}) + self.assertEqual(["Foo", "Bar"], tt.arr) + self.assertEqual({"qux": "baz"}, tt.dict) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append, + "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.insert, + 0, "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.__setitem__, + 0, "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.remove, + "Foo") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.pop) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.dict.pop, + "qux") + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.dict.__setitem__, "qux", "foo") + + def test_mutable_array_dict(self): + class TestType(BASE): + arr = defs.Array(mutable=False) + dict = defs.Dict(mutable=False) + activated = defs.Boolean() + + def __is_mutable__(self): + return not self.activated + + tt = TestType() + tt.arr = [] + tt.dict = {} + tt.arr.append("Foo") + tt.arr.insert(0, "Bar") + tt.dict["baz"] = "qux" + tt.activated = True + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append, + "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.insert, + 0, "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.__setitem__, + 0, "Baz") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.remove, + "Foo") + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.pop) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.dict.pop, + "qux") + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.dict.__setitem__, "qux", "foo") + + def test_readonly_as_write_once(self): + class TestType(BASE): + prop = defs.String(readonly=True) + arr = defs.Array(readonly=True) + + tt = TestType() + self.assertIsNone(tt.prop) + tt.prop = "Foo" + self.assertEqual("Foo", tt.prop) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt, + "prop", "bar") + tt2 = TestType() + self.assertIsNone(tt2.prop) + tt2.prop = None + self.assertIsNone(tt2.prop) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt2, + "prop", None) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, tt2, + "prop", "foo") + self.assertIsNone(tt.arr) + tt.arr = ["foo", "bar"] + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append, + 'baz') + self.assertIsNone(tt2.arr) + tt2.arr = None + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.arr.append, + 'baz') + + +class TestArtifactType(test_utils.BaseTestCase): + def test_create_artifact(self): + a = defs.ArtifactType(**get_artifact_fixture()) + self.assertIsNotNone(a) + self.assertEqual("123", a.id) + self.assertEqual("ArtifactType", a.type_name) + self.assertEqual("1.0", a.type_version) + self.assertEqual("11.2", a.version) + self.assertEqual("Foo", a.name) + self.assertEqual("private", a.visibility) + self.assertEqual("creating", a.state) + self.assertEqual("my_tenant", a.owner) + self.assertEqual(a.created_at, a.updated_at) + self.assertIsNone(a.description) + self.assertIsNone(a.published_at) + self.assertIsNone(a.deleted_at) + + self.assertIsNone(a.description) + + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, "id", + "foo") + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "state", "active") + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "owner", "some other") + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "created_at", datetime.datetime.now()) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "deleted_at", datetime.datetime.now()) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "updated_at", datetime.datetime.now()) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "published_at", datetime.datetime.now()) + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, a, + "visibility", "wrong") + + def test_dependency_prop(self): + class DerivedType(defs.ArtifactType): + depends_on_any = defs.ArtifactReference() + depends_on_self = defs.ArtifactReference(type_name='DerivedType') + depends_on_self_version = defs.ArtifactReference( + type_name='DerivedType', + type_version='1.0') + + class DerivedTypeV11(DerivedType): + __type_name__ = 'DerivedType' + __type_version__ = '1.1' + depends_on_self_version = defs.ArtifactReference( + type_name='DerivedType', + type_version='1.1') + + d1 = DerivedType(**get_artifact_fixture()) + d2 = DerivedTypeV11(**get_artifact_fixture()) + a = defs.ArtifactType(**get_artifact_fixture()) + d1.depends_on_any = a + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, d1, + 'depends_on_self', a) + d1.depends_on_self = d2 + d2.depends_on_self = d1 + d1.depends_on_self_version = d1 + d2.depends_on_self_version = d2 + self.assertRaises(exc.InvalidArtifactPropertyValue, setattr, d1, + 'depends_on_self_version', d2) + + def test_dependency_list(self): + class FooType(defs.ArtifactType): + pass + + class BarType(defs.ArtifactType): + pass + + class TestType(defs.ArtifactType): + depends_on = defs.ArtifactReferenceList() + depends_on_self_or_foo = defs.ArtifactReferenceList( + references=defs.ArtifactReference(['FooType', 'TestType'])) + + a = defs.ArtifactType(**get_artifact_fixture(id="1")) + a_copy = defs.ArtifactType(**get_artifact_fixture(id="1")) + b = defs.ArtifactType(**get_artifact_fixture(id="2")) + + tt = TestType(**get_artifact_fixture(id="3")) + foo = FooType(**get_artifact_fixture(id='4')) + bar = BarType(**get_artifact_fixture(id='4')) + + tt.depends_on.append(a) + tt.depends_on.append(b) + self.assertEqual([a, b], tt.depends_on) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.depends_on.append, a) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.depends_on.append, a_copy) + + tt.depends_on_self_or_foo.append(tt) + tt.depends_on_self_or_foo.append(foo) + self.assertRaises(exc.InvalidArtifactPropertyValue, + tt.depends_on_self_or_foo.append, bar) + self.assertEqual([tt, foo], tt.depends_on_self_or_foo) + + def test_blob(self): + class TestType(defs.ArtifactType): + image_file = defs.BinaryObject(max_file_size=201054, + min_locations=1, + max_locations=5) + screen_shots = defs.BinaryObjectList( + objects=defs.BinaryObject(min_file_size=100), min_count=1) + + tt = TestType(**get_artifact_fixture()) + blob = defs.Blob() + blob.size = 1024 + blob.locations.append("file://some.file.path") + tt.image_file = blob + + self.assertEqual(1024, tt.image_file.size) + self.assertEqual(["file://some.file.path"], tt.image_file.locations) + + def test_pre_publish_blob_validation(self): + class TestType(defs.ArtifactType): + required_blob = defs.BinaryObject(required=True) + optional_blob = defs.BinaryObject() + + tt = TestType(**get_artifact_fixture()) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.__pre_publish__) + tt.required_blob = defs.Blob(size=0) + tt.__pre_publish__() + + def test_pre_publish_dependency_validation(self): + class TestType(defs.ArtifactType): + required_dependency = defs.ArtifactReference(required=True) + optional_dependency = defs.ArtifactReference() + + tt = TestType(**get_artifact_fixture()) + self.assertRaises(exc.InvalidArtifactPropertyValue, tt.__pre_publish__) + tt.required_dependency = defs.ArtifactType(**get_artifact_fixture()) + tt.__pre_publish__() + + def test_default_value_of_immutable_field_in_active_state(self): + class TestType(defs.ArtifactType): + foo = defs.String(default='Bar', mutable=False) + tt = TestType(**get_artifact_fixture(state='active')) + self.assertEqual('Bar', tt.foo) + + +class SerTestType(defs.ArtifactType): + some_string = defs.String() + some_text = defs.Text() + some_version = defs.SemVerString() + some_int = defs.Integer() + some_numeric = defs.Numeric() + some_bool = defs.Boolean() + some_array = defs.Array() + another_array = defs.Array( + item_type=[defs.Integer(), defs.Numeric(), defs.Boolean()]) + some_dict = defs.Dict() + another_dict = defs.Dict( + properties={'foo': defs.Integer(), 'bar': defs.Boolean()}) + some_ref = defs.ArtifactReference() + some_ref_list = defs.ArtifactReferenceList() + some_blob = defs.BinaryObject() + some_blob_list = defs.BinaryObjectList() + + +class TestSerialization(test_utils.BaseTestCase): + def test_serialization_to_db(self): + ref1 = defs.ArtifactType(**get_artifact_fixture(id="1")) + ref2 = defs.ArtifactType(**get_artifact_fixture(id="2")) + ref3 = defs.ArtifactType(**get_artifact_fixture(id="3")) + + blob1 = defs.Blob(size=100, locations=['http://example.com/blob1'], + item_key='some_key', checksum='abc') + blob2 = defs.Blob(size=200, locations=['http://example.com/blob2'], + item_key='another_key', checksum='fff') + blob3 = defs.Blob(size=300, locations=['http://example.com/blob3'], + item_key='third_key', checksum='123') + + fixture = get_artifact_fixture() + tt = SerTestType(**fixture) + tt.some_string = 'bar' + tt.some_text = 'bazz' + tt.some_version = '11.22.33-beta' + tt.some_int = 50 + tt.some_numeric = 10.341 + tt.some_bool = True + tt.some_array = ['q', 'w', 'e', 'r', 't', 'y'] + tt.another_array = [1, 1.2, False] + tt.some_dict = {'foobar': "FOOBAR", 'baz': "QUX"} + tt.another_dict = {'foo': 1, 'bar': True} + tt.some_ref = ref1 + tt.some_ref_list = [ref2, ref3] + tt.some_blob = blob1 + tt.some_blob_list = [blob2, blob3] + + results = serialization.serialize_for_db(tt) + expected = fixture + expected['type_name'] = 'SerTestType' + expected['type_version'] = '1.0' + expected['properties'] = { + 'some_string': { + 'type': 'string', + 'value': 'bar' + }, + 'some_text': { + 'type': 'text', + 'value': 'bazz' + }, + 'some_version': { + 'type': 'string', + 'value': '11.22.33-beta' + }, + 'some_int': { + 'type': 'int', + 'value': 50 + }, + 'some_numeric': { + 'type': 'numeric', + 'value': 10.341 + }, + 'some_bool': { + 'type': 'bool', + 'value': True + }, + 'some_array': { + 'type': 'array', + 'value': [ + { + 'type': 'string', + 'value': 'q' + }, + { + 'type': 'string', + 'value': 'w' + }, + { + 'type': 'string', + 'value': 'e' + }, + { + 'type': 'string', + 'value': 'r' + }, + { + 'type': 'string', + 'value': 't' + }, + { + 'type': 'string', + 'value': 'y' + } + ] + }, + 'another_array': { + 'type': 'array', + 'value': [ + { + 'type': 'int', + 'value': 1 + }, + { + 'type': 'numeric', + 'value': 1.2 + }, + { + 'type': 'bool', + 'value': False + } + ] + }, + 'some_dict.foobar': { + 'type': 'string', + 'value': 'FOOBAR' + }, + 'some_dict.baz': { + 'type': 'string', + 'value': 'QUX' + }, + 'another_dict.foo': { + 'type': 'int', + 'value': 1 + }, + 'another_dict.bar': { + 'type': 'bool', + 'value': True + } + } + expected['dependencies'] = { + 'some_ref': ['1'], + 'some_ref_list': ['2', '3'] + } + expected['blobs'] = { + 'some_blob': [ + { + 'size': 100, + 'checksum': 'abc', + 'item_key': 'some_key', + 'locations': ['http://example.com/blob1'] + }], + 'some_blob_list': [ + { + 'size': 200, + 'checksum': 'fff', + 'item_key': 'another_key', + 'locations': ['http://example.com/blob2'] + }, + { + 'size': 300, + 'checksum': '123', + 'item_key': 'third_key', + 'locations': ['http://example.com/blob3'] + } + ] + } + + self.assertEqual(expected, results) + + def test_deserialize_from_db(self): + ts = datetime.datetime.now() + db_dict = { + "type_name": 'SerTestType', + "type_version": '1.0', + "id": "123", + "version": "11.2", + "description": None, + "name": "Foo", + "visibility": "private", + "state": "creating", + "owner": "my_tenant", + "created_at": ts, + "updated_at": ts, + "deleted_at": None, + "published_at": None, + "tags": ["test", "fixture"], + "properties": { + 'some_string': { + 'type': 'string', + 'value': 'bar' + }, + 'some_text': { + 'type': 'text', + 'value': 'bazz' + }, + 'some_version': { + 'type': 'string', + 'value': '11.22.33-beta' + }, + 'some_int': { + 'type': 'int', + 'value': 50 + }, + 'some_numeric': { + 'type': 'numeric', + 'value': 10.341 + }, + 'some_bool': { + 'type': 'bool', + 'value': True + }, + 'some_array': { + 'type': 'array', + 'value': [ + { + 'type': 'string', + 'value': 'q' + }, + { + 'type': 'string', + 'value': 'w' + }, + { + 'type': 'string', + 'value': 'e' + }, + { + 'type': 'string', + 'value': 'r' + }, + { + 'type': 'string', + 'value': 't' + }, + { + 'type': 'string', + 'value': 'y' + } + ] + }, + 'another_array': { + 'type': 'array', + 'value': [ + { + 'type': 'int', + 'value': 1 + }, + { + 'type': 'numeric', + 'value': 1.2 + }, + { + 'type': 'bool', + 'value': False + } + ] + }, + 'some_dict.foobar': { + 'type': 'string', + 'value': 'FOOBAR' + }, + 'some_dict.baz': { + 'type': 'string', + 'value': 'QUX' + }, + 'another_dict.foo': { + 'type': 'int', + 'value': 1 + }, + 'another_dict.bar': { + 'type': 'bool', + 'value': True + } + }, + 'blobs': { + 'some_blob': [ + { + 'size': 100, + 'checksum': 'abc', + 'item_key': 'some_key', + 'locations': ['http://example.com/blob1'] + }], + 'some_blob_list': [ + { + 'size': 200, + 'checksum': 'fff', + 'item_key': 'another_key', + 'locations': ['http://example.com/blob2'] + }, + { + 'size': 300, + 'checksum': '123', + 'item_key': 'third_key', + 'locations': ['http://example.com/blob3'] + } + ] + }, + 'dependencies': { + 'some_ref': [ + { + "type_name": 'ArtifactType', + "type_version": '1.0', + "id": "1", + "version": "11.2", + "description": None, + "name": "Foo", + "visibility": "private", + "state": "creating", + "owner": "my_tenant", + "created_at": ts, + "updated_at": ts, + "deleted_at": None, + "published_at": None, + "tags": ["test", "fixture"], + "properties": {}, + "blobs": {}, + "dependencies": {} + } + ], + 'some_ref_list': [ + { + "type_name": 'ArtifactType', + "type_version": '1.0', + "id": "2", + "version": "11.2", + "description": None, + "name": "Foo", + "visibility": "private", + "state": "creating", + "owner": "my_tenant", + "created_at": ts, + "updated_at": ts, + "deleted_at": None, + "published_at": None, + "tags": ["test", "fixture"], + "properties": {}, + "blobs": {}, + "dependencies": {} + }, + { + "type_name": 'ArtifactType', + "type_version": '1.0', + "id": "3", + "version": "11.2", + "description": None, + "name": "Foo", + "visibility": "private", + "state": "creating", + "owner": "my_tenant", + "created_at": ts, + "updated_at": ts, + "deleted_at": None, + "published_at": None, + "tags": ["test", "fixture"], + "properties": {}, + "blobs": {}, + "dependencies": {} + } + ] + } + } + + art = serialization.deserialize_from_db(db_dict, + { + 'SerTestType': { + '1.0': SerTestType}, + 'ArtifactType': { + '1.0': + defs.ArtifactType} + }) + self.assertEqual('123', art.id) + self.assertEqual('11.2', art.version) + self.assertIsNone(art.description) + self.assertEqual('Foo', art.name) + self.assertEqual('private', art.visibility) + self.assertEqual('private', art.visibility) + + +def get_artifact_fixture(**kwargs): + ts = datetime.datetime.now() + fixture = { + "id": "123", + "version": "11.2", + "description": None, + "name": "Foo", + "visibility": "private", + "state": "creating", + "owner": "my_tenant", + "created_at": ts, + "updated_at": ts, + "deleted_at": None, + "published_at": None, + "tags": ["test", "fixture"] + } + fixture.update(kwargs) + return fixture