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