Add types to modeling, plus many fixes to models
Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/daa2d538 Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/daa2d538 Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/daa2d538 Branch: refs/heads/ARIA-105-integrate-modeling Commit: daa2d538b590e38ff97dcec2e721a011c37b3e15 Parents: 58aac8d Author: Tal Liron <[email protected]> Authored: Thu Mar 2 20:15:12 2017 -0600 Committer: Tal Liron <[email protected]> Committed: Thu Mar 2 20:15:12 2017 -0600 ---------------------------------------------------------------------- aria/VERSION.py | 4 +- aria/__init__.py | 40 +- aria/cli/commands.py | 3 +- aria/exceptions.py | 4 +- aria/modeling/bases.py | 309 +++++---- aria/modeling/misc.py | 208 ++++++ aria/modeling/models.py | 163 +++-- aria/modeling/orchestration.py | 166 ++--- aria/modeling/service.py | 467 ++++++------- aria/modeling/service_template.py | 664 ++++++++----------- aria/modeling/types.py | 6 + aria/orchestrator/__init__.py | 2 +- aria/orchestrator/workflows/api/task.py | 4 +- aria/orchestrator/workflows/builtin/utils.py | 1 - .../workflows/core/events_handler.py | 2 +- aria/orchestrator/workflows/dry.py | 2 +- aria/orchestrator/workflows/events_logging.py | 2 +- aria/parser/consumption/style.py | 2 +- aria/parser/modeling/__init__.py | 8 +- aria/parser/modeling/context.py | 31 +- aria/storage/__init__.py | 10 +- aria/storage/core.py | 10 +- docs/requirements.txt | 4 +- .../simple_v1_0/modeling/__init__.py | 418 ++++++------ 24 files changed, 1392 insertions(+), 1138 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/VERSION.py ---------------------------------------------------------------------- diff --git a/aria/VERSION.py b/aria/VERSION.py index 7e11072..9ce332c 100644 --- a/aria/VERSION.py +++ b/aria/VERSION.py @@ -14,8 +14,8 @@ # limitations under the License. """ -Aria Version module: - * version: Aria Package version +ARIA Version module: + * version: ARIA Package version """ version = '0.1.0' # pylint: disable=C0103 http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/__init__.py ---------------------------------------------------------------------- diff --git a/aria/__init__.py b/aria/__init__.py index 7e5da07..4675fc8 100644 --- a/aria/__init__.py +++ b/aria/__init__.py @@ -14,7 +14,7 @@ # limitations under the License. """ -Aria top level package +ARIA top level package """ import pkgutil @@ -62,45 +62,9 @@ def application_model_storage(api, api_kwargs=None, initiator=None, initiator_kw """ Initiate model storage """ - models_to_register = [ - modeling.models.ServiceTemplate, - modeling.models.NodeTemplate, - modeling.models.GroupTemplate, - modeling.models.PolicyTemplate, - modeling.models.SubstitutionTemplate, - modeling.models.SubstitutionTemplateMapping, - modeling.models.RequirementTemplate, - modeling.models.RelationshipTemplate, - modeling.models.CapabilityTemplate, - modeling.models.InterfaceTemplate, - modeling.models.OperationTemplate, - modeling.models.ArtifactTemplate, - - modeling.models.Parameter, - modeling.models.Metadata, - - modeling.models.Service, - modeling.models.Node, - modeling.models.Group, - modeling.models.Policy, - modeling.models.SubstitutionMapping, - modeling.models.Substitution, - modeling.models.Relationship, - modeling.models.Capability, - modeling.models.Interface, - modeling.models.Operation, - modeling.models.Artifact, - - modeling.models.Execution, - modeling.models.ServiceUpdate, - modeling.models.ServiceUpdateStep, - modeling.models.ServiceModification, - modeling.models.Plugin, - modeling.models.Task - ] return storage.ModelStorage(api_cls=api, api_kwargs=api_kwargs, - items=models_to_register, + items=modeling.models.models_to_register, initiator=initiator, initiator_kwargs=initiator_kwargs or {}) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/cli/commands.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands.py b/aria/cli/commands.py index 7ad85af..25a4e0d 100644 --- a/aria/cli/commands.py +++ b/aria/cli/commands.py @@ -260,8 +260,7 @@ class WorkflowCommand(BaseCommand): def _run(self, context, workflow_name, workflow_fn, inputs): # Storage def _initialize_storage(model_storage): - model_storage.service_template.put(context.modeling.template) - model_storage.service.put(context.modeling.instance) + context.modeling.store(model_storage) # Create runner runner = Runner(workflow_name, workflow_fn, inputs, _initialize_storage, http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/exceptions.py ---------------------------------------------------------------------- diff --git a/aria/exceptions.py b/aria/exceptions.py index 28f8be9..0a60f01 100644 --- a/aria/exceptions.py +++ b/aria/exceptions.py @@ -14,8 +14,8 @@ # limitations under the License. """ -Aria exceptions module -Every sub-package in Aria has a module with its exceptions. +ARIA exceptions module +Every sub-package in ARIA has a module with its exceptions. aria.exceptions module conveniently collects all these exceptions for easier imports. """ http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/bases.py ---------------------------------------------------------------------- diff --git a/aria/modeling/bases.py b/aria/modeling/bases.py index 0e583cf..c1b2f7a 100644 --- a/aria/modeling/bases.py +++ b/aria/modeling/bases.py @@ -53,15 +53,15 @@ class ModelMixin(object): raise NotImplementedError @classmethod - def foreign_key(cls, parent_table_name, nullable=False): + def foreign_key(cls, parent_table, nullable=False): """ Return a ForeignKey object. - :param parent_table_name: Parent table name + :param parent_table: Parent table name :param nullable: Should the column be allowed to remain empty """ return Column(Integer, - ForeignKey('{table}.id'.format(table=parent_table_name), + ForeignKey('{table}.id'.format(table=parent_table), ondelete='CASCADE'), nullable=nullable) @@ -71,68 +71,75 @@ class ModelMixin(object): relationship_kwargs=None): relationship_kwargs = relationship_kwargs or {} - remote_side_str = '{cls}.{remote_column}'.format( + remote_side = '{cls}.{remote_column}'.format( cls=cls.__name__, remote_column=cls.id_column_name() ) - primaryjoin_str = '{remote_side_str} == {cls}.{column}'.format( - remote_side_str=remote_side_str, + primaryjoin = '{remote_side} == {cls}.{column}'.format( + remote_side=remote_side, cls=cls.__name__, column=column_name ) return relationship( cls._get_cls_by_tablename(cls.__tablename__).__name__, - primaryjoin=primaryjoin_str, - remote_side=remote_side_str, + primaryjoin=primaryjoin, + remote_side=remote_side, post_update=True, **relationship_kwargs ) @classmethod + def one_to_many_relationship_to_self(cls, + key, + dict_key=None, + relationship_kwargs=None): + relationship_kwargs = relationship_kwargs or {} + + relationship_kwargs.setdefault('remote_side', '{cls}.{remote_column}'.format( + cls=cls.__name__, + remote_column=key + )) + + return cls._create_relationship(cls.__tablename__, None, relationship_kwargs, + backreference='', dict_key=dict_key) + + @classmethod def one_to_one_relationship(cls, - other_table_name, + other_table, + key=None, + foreign_key=None, backreference=None, + backref_kwargs=None, relationship_kwargs=None): - relationship_kwargs = relationship_kwargs or {} - - return relationship( - lambda: cls._get_cls_by_tablename(other_table_name), - backref=backref(backreference or cls.__tablename__, uselist=False), - **relationship_kwargs - ) + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', False) + return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs, + backreference, key=key, foreign_key=foreign_key) + @classmethod def one_to_many_relationship(cls, - child_table_name, - foreign_key_name=None, + child_table, + key=None, + foreign_key=None, + dict_key=None, backreference=None, - key_column_name=None, + backref_kwargs=None, relationship_kwargs=None): - relationship_kwargs = relationship_kwargs or {} - - foreign_keys = lambda: getattr(cls._get_cls_by_tablename(child_table_name), - foreign_key_name) \ - if foreign_key_name \ - else None - - collection_class = attribute_mapped_collection(key_column_name) \ - if key_column_name \ - else list + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', False) - return relationship( - lambda: cls._get_cls_by_tablename(child_table_name), - backref=backref(backreference or cls.__tablename__, uselist=False), - foreign_keys=foreign_keys, - collection_class=collection_class, - **relationship_kwargs - ) + return cls._create_relationship(child_table, backref_kwargs, relationship_kwargs, + backreference, key=key, foreign_key=foreign_key, + dict_key=dict_key) @classmethod def many_to_one_relationship(cls, - parent_table_name, - foreign_key_column=None, + parent_table, + key=None, + foreign_key=None, backreference=None, backref_kwargs=None, relationship_kwargs=None): @@ -140,33 +147,32 @@ class ModelMixin(object): Return a one-to-many SQL relationship object Meant to be used from inside the *child* object - :param parent_class: Class of the parent table - :param cls: Class of the child table - :param foreign_key_column: The column of the foreign key (from the child table) + :param parent_table: Name of the parent table + :param foreign_key: The column of the foreign key (from the child table) :param backreference: The name to give to the reference to the child (on the parent table) """ - relationship_kwargs = relationship_kwargs or {} - if foreign_key_column: - relationship_kwargs.setdefault('foreign_keys', getattr(cls, foreign_key_column)) + if backreference is None: + backreference = utils.pluralize(cls.__tablename__) backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', True) backref_kwargs.setdefault('lazy', 'dynamic') # The following line make sure that when the *parent* is deleted, all its connected children # are deleted as well backref_kwargs.setdefault('cascade', 'all') - return relationship( - lambda: cls._get_cls_by_tablename(parent_table_name), - backref=backref(backreference or utils.pluralize(cls.__tablename__), **backref_kwargs), - **relationship_kwargs - ) + return cls._create_relationship(parent_table, backref_kwargs, relationship_kwargs, + backreference, key=key, foreign_key=foreign_key) @classmethod def many_to_many_relationship(cls, - other_table_name, + other_table, table_prefix=None, - key_column_name=None, + key=None, + dict_key=None, + backreference=None, + backref_kwargs=None, relationship_kwargs=None): """ Return a many-to-many SQL relationship object @@ -177,80 +183,41 @@ class ModelMixin(object): 2. This method creates a new helper table in the DB :param cls: The class of the table we're connecting from - :param other_table_name: The class of the table we're connecting to - :param table_prefix: Custom prefix for the helper table name and the - backreference name - :param key_column_name: If provided, will use a dict class with this column as the key + :param other_table: The class of the table we're connecting to + :param table_prefix: Custom prefix for the helper table name and the backreference name + :param dict_key: If provided, will use a dict class with this column as the key """ - relationship_kwargs = relationship_kwargs or {} - current_table_name = cls.__tablename__ - current_column_name = '{0}_id'.format(current_table_name) - current_foreign_key = '{0}.id'.format(current_table_name) + this_table = cls.__tablename__ + this_column_name = '{0}_id'.format(this_table) + this_foreign_key = '{0}.id'.format(this_table) + + other_column_name = '{0}_id'.format(other_table) + other_foreign_key = '{0}.id'.format(other_table) - other_column_name = '{0}_id'.format(other_table_name) - other_foreign_key = '{0}.id'.format(other_table_name) + helper_table = '{0}_{1}'.format(this_table, other_table) - helper_table_name = '{0}_{1}'.format(current_table_name, other_table_name) + if backreference is None: + backreference = this_table + if table_prefix: + helper_table = '{0}_{1}'.format(table_prefix, helper_table) + backreference = '{0}_{1}'.format(table_prefix, backreference) - backref_name = current_table_name - if table_prefix: - helper_table_name = '{0}_{1}'.format(table_prefix, helper_table_name) - backref_name = '{0}_{1}'.format(table_prefix, backref_name) + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', True) - secondary_table = cls.get_secondary_table( + relationship_kwargs = relationship_kwargs or {} + relationship_kwargs.setdefault('secondary', cls._get_secondary_table( cls.metadata, - helper_table_name, - current_column_name, + helper_table, + this_column_name, other_column_name, - current_foreign_key, + this_foreign_key, other_foreign_key - ) - - collection_class = attribute_mapped_collection(key_column_name) \ - if key_column_name \ - else list + )) - return relationship( - lambda: cls._get_cls_by_tablename(other_table_name), - secondary=secondary_table, - backref=backref(backref_name), - collection_class=collection_class, - **relationship_kwargs - ) - - @staticmethod - def get_secondary_table(metadata, - helper_table_name, - first_column_name, - second_column_name, - first_foreign_key, - second_foreign_key): - """ - Create a helper table for a many-to-many relationship - - :param helper_table_name: The name of the table - :param first_column_name: The name of the first column in the table - :param second_column_name: The name of the second column in the table - :param first_foreign_key: The string representing the first foreign key, - for example `blueprint.storage_id`, or `tenants.id` - :param second_foreign_key: The string representing the second foreign key - :return: A Table object - """ - return Table( - helper_table_name, - metadata, - Column( - first_column_name, - Integer, - ForeignKey(first_foreign_key) - ), - Column( - second_column_name, - Integer, - ForeignKey(second_foreign_key) - ) - ) + return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs, + backreference, key=key, dict_key=dict_key) def to_dict(self, fields=None, suppress_error=False): """ @@ -260,6 +227,7 @@ class ModelMixin(object): it's unable to retrieve (e.g., if a relationship wasn't established yet, and so it's impossible to access a property through it) """ + res = dict() fields = fields or self.fields() for field in fields: @@ -281,6 +249,76 @@ class ModelMixin(object): return res @classmethod + def _create_relationship(cls, table, backref_kwargs, relationship_kwargs, backreference, + key=None, foreign_key=None, dict_key=None): + relationship_kwargs = relationship_kwargs or {} + + if key: + relationship_kwargs.setdefault('foreign_keys', + lambda: getattr( + cls._get_cls_by_tablename(cls.__tablename__), + key)) + + elif foreign_key: + relationship_kwargs.setdefault('foreign_keys', + lambda: getattr( + cls._get_cls_by_tablename(table), + foreign_key)) + + if dict_key: + relationship_kwargs.setdefault('collection_class', + attribute_mapped_collection(dict_key)) + + if backreference == '': + return relationship( + lambda: cls._get_cls_by_tablename(table), + **relationship_kwargs + ) + else: + if backreference is None: + backreference = cls.__tablename__ + backref_kwargs = backref_kwargs or {} + return relationship( + lambda: cls._get_cls_by_tablename(table), + backref=backref(backreference, **backref_kwargs), + **relationship_kwargs + ) + + @staticmethod + def _get_secondary_table(metadata, + helper_table, + first_column, + second_column, + first_foreign_key, + second_foreign_key): + """ + Create a helper table for a many-to-many relationship + + :param helper_table: The name of the table + :param first_column_name: The name of the first column in the table + :param second_column_name: The name of the second column in the table + :param first_foreign_key: The string representing the first foreign key, + for example `blueprint.storage_id`, or `tenants.id` + :param second_foreign_key: The string representing the second foreign key + :return: A Table object + """ + + return Table( + helper_table, + metadata, + Column( + first_column, + Integer, + ForeignKey(first_foreign_key) + ), + Column( + second_column, + Integer, + ForeignKey(second_foreign_key) + ) + ) + + @classmethod def _association_proxies(cls): for col, value in vars(cls).items(): if isinstance(value, associationproxy.AssociationProxy): @@ -293,6 +331,7 @@ class ModelMixin(object): Mostly for backwards compatibility in the code (that uses `fields`) """ + fields = set(cls._association_proxies()) fields.update(cls.__table__.columns.keys()) return fields - set(getattr(cls, '__private_fields__', [])) @@ -302,9 +341,10 @@ class ModelMixin(object): """ Return class reference mapped to table. - :param tablename: String with name of table. - :return: Class reference or None. - """ + :param tablename: String with name of table. + :return: Class reference or None. + """ + if tablename in (cls.__name__, cls.__tablename__): return cls @@ -329,3 +369,36 @@ class ModelIDMixin(object): @classmethod def name_column_name(cls): return 'name' + + +class InstanceModelMixin(ModelMixin): + """ + Mixin for :class:`ServiceInstance` models. + + All models support validation, diagnostic dumping, and representation as + raw data (which can be translated into JSON or YAML) via :code:`as_raw`. + """ + + @property + def as_raw(self): + raise NotImplementedError + + def validate(self, context): + pass + + def coerce_values(self, context, container, report_issues): + pass + + def dump(self, context): + pass + + +class TemplateModelMixin(InstanceModelMixin): + """ + Mixin for :class:`ServiceTemplate` models. + + All model models can be instantiated into :class:`ServiceInstance` models. + """ + + def instantiate(self, context, container): + raise NotImplementedError http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/misc.py ---------------------------------------------------------------------- diff --git a/aria/modeling/misc.py b/aria/modeling/misc.py new file mode 100644 index 0000000..0b50f9a --- /dev/null +++ b/aria/modeling/misc.py @@ -0,0 +1,208 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cPickle as pickle + +from sqlalchemy import ( + Column, + Text, + Binary +) +from sqlalchemy.ext.declarative import declared_attr + +from ..storage import exceptions +from ..parser.modeling import utils as parser_utils +from ..utils import collections, formatting, console +from .bases import InstanceModelMixin, TemplateModelMixin + + +class ParameterBase(TemplateModelMixin): + """ + Represents a typed value. + + This class is used by both service template and service instance elements. + + :ivar name: Name + :ivar type_name: Type name + :ivar value: Value + :ivar description: Description + """ + + __tablename__ = 'parameter' + + name = Column(Text, nullable=False) + type_name = Column(Text, nullable=False) + + # Check: value type + _value = Column(Binary, nullable=True) + description = Column(Text) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type_name), + ('value', self.value), + ('description', self.description))) + + @property + def value(self): + if self._value is None: + return None + try: + return pickle.loads(self._value) + except BaseException: + raise exceptions.StorageError('Bad format for parameter of type "{0}": {1}'.format( + self.type_name, self._value)) + + @value.setter + def value(self, value): + if value is None: + self._value = None + else: + try: + self._value = pickle.dumps(value) + except pickle.PicklingError: + # TODO debug log + self._value = pickle.dumps(str(value)) + + def instantiate(self, context, container): + from . import models + return models.Parameter(name=self.name, + type_name=self.type_name, + _value=self._value, + description=self.description) + + def coerce_values(self, context, container, report_issues): + if self.value is not None: + self.value = parser_utils.coerce_value(context, container, self.value, + report_issues) + + +class TypeBase(InstanceModelMixin): + """ + Represents a type and its children. + """ + + __tablename__ = 'type' + + variant = Column(Text) + description = Column(Text) + role = Column(Text) + + @declared_attr + def parent(cls): + return cls.relationship_to_self('parent_type_fk') + + @declared_attr + def children(cls): + return cls.one_to_many_relationship_to_self('parent_type_fk') + + # region foreign keys + + __private_fields__ = ['parent_type_fk'] + + # Type one-to-many to Type + @declared_attr + def parent_type_fk(cls): + return cls.foreign_key('type', nullable=True) + + # endregion + + def is_descendant(self, base_name, name): + base = self.get_descendant(base_name) + if base is not None: + if base.get_descendant(name) is not None: + return True + return False + + def get_descendant(self, name): + if self.name == name: + return self + for child in self.children: + found = child.get_descendant(name) + if found is not None: + return found + return None + + def iter_descendants(self): + for child in self.children: + yield child + for descendant in child.iter_descendants(): + yield descendant + + def get_role(self, name): + def _get_role(the_type): + if the_type is None: + return None + elif the_type.role is None: + return _get_role(self.parent) + return the_type.role + + return _get_role(self.get_descendant(name)) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('role', self.role))) + + @property + def as_raw_all(self): + types = [] + self._append_raw_children(types) + return types + + def dump(self, context): + if self.name: + console.puts(context.style.type(self.name)) + with context.style.indent: + for child in self.children: + child.dump(context) + + def _append_raw_children(self, types): + for child in self.children: + raw_child = formatting.as_raw(child) + raw_child['parent'] = self.name + types.append(raw_child) + child._append_raw_children(types) + + +class MetadataBase(TemplateModelMixin): + """ + Custom values associated with the service. + + This class is used by both service template and service instance elements. + + :ivar name: Name + :ivar value: Value + """ + + __tablename__ = 'metadata' + + name = Column(Text, nullable=False) + value = Column(Text) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('value', self.value))) + + def instantiate(self, context, container): + from . import models + return models.Metadata(name=self.name, + value=self.value) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/models.py ---------------------------------------------------------------------- diff --git a/aria/modeling/models.py b/aria/modeling/models.py index b424dea..fc2c669 100644 --- a/aria/modeling/models.py +++ b/aria/modeling/models.py @@ -13,62 +13,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=abstract-method + from sqlalchemy.ext.declarative import declarative_base from . import ( service_template, service, orchestration, + misc, bases, ) -__all__ = ( - 'aria_declarative_base', - - # Service template models - 'ServiceTemplate', - 'NodeTemplate', - 'GroupTemplate', - 'PolicyTemplate', - 'SubstitutionTemplate', - 'SubstitutionTemplateMapping', - 'RequirementTemplate', - 'RelationshipTemplate', - 'CapabilityTemplate', - 'InterfaceTemplate', - 'OperationTemplate', - 'ArtifactTemplate', - - # Service template and instance models - 'Parameter', - 'Metadata', - - # Service instance models - 'Service', - 'Node', - 'Group', - 'Policy', - 'Substitution', - 'SubstitutionMapping', - 'Relationship', - 'Capability', - 'Interface', - 'Operation', - 'Artifact', - - # Orchestrator models - 'Execution', - 'ServiceUpdate', - 'ServiceUpdateStep', - 'ServiceModification', - 'Plugin', - 'Task' -) aria_declarative_base = declarative_base(cls=bases.ModelIDMixin) -# pylint: disable=abstract-method - # region service template models @@ -123,18 +82,6 @@ class ArtifactTemplate(aria_declarative_base, service_template.ArtifactTemplateB # endregion -# region service template and instance models - -class Parameter(aria_declarative_base, service_template.ParameterBase): - pass - - -class Metadata(aria_declarative_base, service_template.MetadataBase): - pass - -# endregion - - # region service instance models class Service(aria_declarative_base, service.ServiceBase): @@ -209,3 +156,107 @@ class Task(aria_declarative_base, orchestration.TaskBase): pass # endregion + + +# region misc models + +class Parameter(aria_declarative_base, misc.ParameterBase): + pass + + +class Type(aria_declarative_base, misc.TypeBase): + pass + + +class Metadata(aria_declarative_base, misc.MetadataBase): + pass + +# endregion + + +models_to_register = [ + # Service template models + ServiceTemplate, + NodeTemplate, + GroupTemplate, + PolicyTemplate, + SubstitutionTemplate, + SubstitutionTemplateMapping, + RequirementTemplate, + RelationshipTemplate, + CapabilityTemplate, + InterfaceTemplate, + OperationTemplate, + ArtifactTemplate, + + # Service instance models + Service, + Node, + Group, + Policy, + SubstitutionMapping, + Substitution, + Relationship, + Capability, + Interface, + Operation, + Artifact, + + # Orchestration models + Execution, + ServiceUpdate, + ServiceUpdateStep, + ServiceModification, + Plugin, + Task, + + # Misc models + Parameter, + Type, + Metadata +] + +__all__ = ( + 'aria_declarative_base', + 'models_to_register', + + # Service template models + 'ServiceTemplate', + 'NodeTemplate', + 'GroupTemplate', + 'PolicyTemplate', + 'SubstitutionTemplate', + 'SubstitutionTemplateMapping', + 'RequirementTemplate', + 'RelationshipTemplate', + 'CapabilityTemplate', + 'InterfaceTemplate', + 'OperationTemplate', + 'ArtifactTemplate', + + # Service instance models + 'Service', + 'Node', + 'Group', + 'Policy', + 'Substitution', + 'SubstitutionMapping', + 'Relationship', + 'Capability', + 'Interface', + 'Operation', + 'Artifact', + + # Orchestration models + 'Execution', + 'ServiceUpdate', + 'ServiceUpdateStep', + 'ServiceModification', + 'Plugin', + 'Task', + + # Misc models + 'Parameter', + 'Type', + 'Metadata' +) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/orchestration.py ---------------------------------------------------------------------- diff --git a/aria/modeling/orchestration.py b/aria/modeling/orchestration.py index 6b5609f..9cbb3cf 100644 --- a/aria/modeling/orchestration.py +++ b/aria/modeling/orchestration.py @@ -27,6 +27,9 @@ classes: * Plugin - plugin implementation model. * Task - a task """ + +# pylint: disable=no-self-argument, no-member, abstract-method + from collections import namedtuple from datetime import datetime @@ -57,16 +60,13 @@ __all__ = ( 'TaskBase' ) -# pylint: disable=no-self-argument, no-member, abstract-method - class Execution(ModelMixin): """ Execution model representation. """ - __tablename__ = 'execution' # redundancy for PyLint: SqlAlchemy injects this - __private_fields__ = ['service_fk'] + __tablename__ = 'execution' TERMINATED = 'terminated' FAILED = 'failed' @@ -129,6 +129,8 @@ class Execution(ModelMixin): # region foreign keys + __private_fields__ = ['service_fk'] + @declared_attr def service_fk(cls): return cls.foreign_key('service') @@ -147,13 +149,10 @@ class ServiceUpdateBase(ModelMixin): """ Deployment update model representation. """ - # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. + steps = None - __tablename__ = 'service_update' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['service_fk', - 'execution_fk'] + __tablename__ = 'service_update' _private_fields = ['execution_fk', 'deployment_fk'] @@ -183,6 +182,9 @@ class ServiceUpdateBase(ModelMixin): # region foreign keys + __private_fields__ = ['service_fk', + 'execution_fk'] + @declared_attr def execution_fk(cls): return cls.foreign_key('execution', nullable=True) @@ -204,9 +206,8 @@ class ServiceUpdateStepBase(ModelMixin): """ Deployment update step model representation. """ - __tablename__ = 'service_update_step' # redundancy for PyLint: SqlAlchemy injects this - __private_fields__ = ['service_update_fk'] + __tablename__ = 'service_update_step' _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY') ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify') @@ -243,6 +244,8 @@ class ServiceUpdateStepBase(ModelMixin): # region foreign keys + __private_fields__ = ['service_update_fk'] + @declared_attr def service_update_fk(cls): return cls.foreign_key('service_update') @@ -281,9 +284,8 @@ class ServiceModificationBase(ModelMixin): """ Deployment modification model representation. """ - __tablename__ = 'service_modification' # redundancy for PyLint: SqlAlchemy injects this - __private_fields__ = ['service_fk'] + __tablename__ = 'service_modification' STARTED = 'started' FINISHED = 'finished' @@ -300,10 +302,6 @@ class ServiceModificationBase(ModelMixin): status = Column(Enum(*STATES, name='deployment_modification_status')) @declared_attr - def service_fk(cls): - return cls.foreign_key('service') - - @declared_attr def service(cls): return cls.many_to_one_relationship('service', backreference='modifications') @@ -312,14 +310,23 @@ class ServiceModificationBase(ModelMixin): def service_name(cls): return association_proxy('service', cls.name_column_name()) + # region foreign keys + + __private_fields__ = ['service_fk'] + + @declared_attr + def service_fk(cls): + return cls.foreign_key('service') + + # endregion + class PluginBase(ModelMixin): """ Plugin model representation. """ - __tablename__ = 'plugin' # redundancy for PyLint: SqlAlchemy injects this - __private_fields__ = ['service_template_fk'] + __tablename__ = 'plugin' archive_name = Column(Text, nullable=False, index=True) distribution = Column(Text) @@ -335,6 +342,8 @@ class PluginBase(ModelMixin): # region foreign keys + __private_fields__ = ['service_template_fk'] + @declared_attr def service_template_fk(cls): return cls.foreign_key('service_template', nullable=True) @@ -346,12 +355,33 @@ class TaskBase(ModelMixin): """ A Model which represents an task """ - __tablename__ = 'task' # redundancy for PyLint: SqlAlchemy injects this - __private_fields__ = ['node_fk', - 'relationship_fk', - 'plugin_fk', - 'execution_fk'] + __tablename__ = 'task' + + PENDING = 'pending' + RETRYING = 'retrying' + SENT = 'sent' + STARTED = 'started' + SUCCESS = 'success' + FAILED = 'failed' + STATES = ( + PENDING, + RETRYING, + SENT, + STARTED, + SUCCESS, + FAILED, + ) + + WAIT_STATES = [PENDING, RETRYING] + END_STATES = [SUCCESS, FAILED] + + RUNS_ON_SOURCE = 'source' + RUNS_ON_TARGET = 'target' + RUNS_ON_NODE = 'node' + RUNS_ON = (RUNS_ON_NODE, RUNS_ON_SOURCE, RUNS_ON_TARGET) + + INFINITE_RETRIES = -1 @declared_attr def node_name(cls): @@ -384,60 +414,7 @@ class TaskBase(ModelMixin): @declared_attr def inputs(cls): return cls.many_to_many_relationship('parameter', table_prefix='inputs', - key_column_name='name') - - # region foreign keys - - @declared_attr - def node_fk(cls): - return cls.foreign_key('node', nullable=True) - - @declared_attr - def relationship_fk(cls): - return cls.foreign_key('relationship', nullable=True) - - @declared_attr - def plugin_fk(cls): - return cls.foreign_key('plugin', nullable=True) - - @declared_attr - def execution_fk(cls): - return cls.foreign_key('execution', nullable=True) - - # endregion - - PENDING = 'pending' - RETRYING = 'retrying' - SENT = 'sent' - STARTED = 'started' - SUCCESS = 'success' - FAILED = 'failed' - STATES = ( - PENDING, - RETRYING, - SENT, - STARTED, - SUCCESS, - FAILED, - ) - - WAIT_STATES = [PENDING, RETRYING] - END_STATES = [SUCCESS, FAILED] - - RUNS_ON_SOURCE = 'source' - RUNS_ON_TARGET = 'target' - RUNS_ON_NODE = 'node' - RUNS_ON = (RUNS_ON_NODE, RUNS_ON_SOURCE, RUNS_ON_TARGET) - - @orm.validates('max_attempts') - def validate_max_attempts(self, _, value): # pylint: disable=no-self-use - """Validates that max attempts is either -1 or a positive number""" - if value < 1 and value != TaskBase.INFINITE_RETRIES: - raise ValueError('Max attempts can be either -1 (infinite) or any positive number. ' - 'Got {value}'.format(value=value)) - return value - - INFINITE_RETRIES = -1 + dict_key='name') status = Column(Enum(*STATES, name='status'), default=PENDING) @@ -474,6 +451,39 @@ class TaskBase(ModelMixin): """ return self.node or self.relationship + @orm.validates('max_attempts') + def validate_max_attempts(self, _, value): # pylint: disable=no-self-use + """Validates that max attempts is either -1 or a positive number""" + if value < 1 and value != TaskBase.INFINITE_RETRIES: + raise ValueError('Max attempts can be either -1 (infinite) or any positive number. ' + 'Got {value}'.format(value=value)) + return value + + # region foreign keys + + __private_fields__ = ['node_fk', + 'relationship_fk', + 'plugin_fk', + 'execution_fk'] + + @declared_attr + def node_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def relationship_fk(cls): + return cls.foreign_key('relationship', nullable=True) + + @declared_attr + def plugin_fk(cls): + return cls.foreign_key('plugin', nullable=True) + + @declared_attr + def execution_fk(cls): + return cls.foreign_key('execution', nullable=True) + + # endregion + @classmethod def as_node_task(cls, instance, runs_on, **kwargs): return cls(node=instance, _runs_on=runs_on, **kwargs) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/daa2d538/aria/modeling/service.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service.py b/aria/modeling/service.py index 78cd920..1309472 100644 --- a/aria/modeling/service.py +++ b/aria/modeling/service.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=no-self-argument, no-member, abstract-method + from sqlalchemy import ( Column, Text, @@ -22,7 +24,7 @@ from sqlalchemy import DateTime from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declared_attr -from .bases import ModelMixin +from .bases import InstanceModelMixin from ..parser import validation from ..utils import collections, formatting, console @@ -31,34 +33,10 @@ from . import ( types as modeling_types ) -# pylint: disable=no-self-argument, no-member, abstract-method - - -class _InstanceModelMixin(ModelMixin): - """ - Mixin for :class:`ServiceInstance` models. - All models support validation, diagnostic dumping, and representation as - raw data (which can be translated into JSON or YAML) via :code:`as_raw`. +class ServiceBase(InstanceModelMixin): # pylint: disable=too-many-public-methods """ - - @property - def as_raw(self): - raise NotImplementedError - - def validate(self, context): - pass - - def coerce_values(self, context, container, report_issues): - pass - - def dump(self, context): - pass - - -class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-methods - """ - A service instance is an instance of a :class:`ServiceTemplate`. + A service instance is usually an instance of a :class:`ServiceTemplate`. You will usually not create it programmatically, but instead instantiate it from the template. @@ -73,17 +51,14 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method :ivar operations: Dict of :class:`Operation` """ - __tablename__ = 'service' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['substituion_fk', - 'service_template_fk'] + __tablename__ = 'service' description = Column(Text) @declared_attr def meta_data(cls): # Warning! We cannot use the attr name "metadata" because it's used by SqlAlchemy! - return cls.many_to_many_relationship('metadata', key_column_name='name') + return cls.many_to_many_relationship('metadata', dict_key='name') @declared_attr def nodes(cls): @@ -104,22 +79,22 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method @declared_attr def inputs(cls): return cls.many_to_many_relationship('parameter', table_prefix='inputs', - key_column_name='name') + dict_key='name') @declared_attr def outputs(cls): return cls.many_to_many_relationship('parameter', table_prefix='outputs', - key_column_name='name') + dict_key='name') @declared_attr def operations(cls): - return cls.one_to_many_relationship('operation', key_column_name='name') + return cls.one_to_many_relationship('operation', dict_key='name') @declared_attr def service_template(cls): return cls.many_to_one_relationship('service_template') - # region orchestrator required columns + # region orchestration created_at = Column(DateTime, nullable=False, index=True) updated_at = Column(DateTime) @@ -135,6 +110,9 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method # region foreign keys + __private_fields__ = ['substituion_fk', + 'service_template_fk'] + # Service one-to-one to Substitution @declared_attr def substitution_fk(cls): @@ -143,7 +121,7 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method # Service many-to-one to ServiceTemplate @declared_attr def service_template_fk(cls): - return cls.foreign_key('service_template') + return cls.foreign_key('service_template', nullable=True) # endregion @@ -276,14 +254,13 @@ class ServiceBase(_InstanceModelMixin): # pylint: disable=too-many-public-method self._dump_graph_node(context, target_node) -class NodeBase(_InstanceModelMixin): +class NodeBase(InstanceModelMixin): """ - An instance of a :class:`NodeTemplate`. + Usually an instance of a :class:`NodeTemplate`. Nodes may have zero or more :class:`Relationship` instances to other nodes. - :ivar name: Unique ID (prefixed with the template name) - :ivar type_name: Must be represented in the :class:`ModelingContext` + :ivar name: Unique ID (often prefixed with the template name) :ivar properties: Dict of :class:`Parameter` :ivar interfaces: Dict of :class:`Interface` :ivar artifacts: Dict of :class:`Artifact` @@ -291,41 +268,39 @@ class NodeBase(_InstanceModelMixin): :ivar relationships: List of :class:`Relationship` """ - __tablename__ = 'node' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'node' - __private_fields__ = ['host_fk', - 'service_fk', - 'node_template_fk'] - - type_name = Column(Text) + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') @declared_attr def interfaces(cls): - return cls.one_to_many_relationship('interface', key_column_name='name') + return cls.one_to_many_relationship('interface', dict_key='name') @declared_attr def artifacts(cls): - return cls.one_to_many_relationship('artifact', key_column_name='name') + return cls.one_to_many_relationship('artifact', dict_key='name') @declared_attr def capabilities(cls): - return cls.one_to_many_relationship('capability', key_column_name='name') + return cls.one_to_many_relationship('capability', dict_key='name') @declared_attr def outbound_relationships(cls): return cls.one_to_many_relationship('relationship', - foreign_key_name='source_node_fk', + foreign_key='source_node_fk', backreference='source_node') @declared_attr def inbound_relationships(cls): return cls.one_to_many_relationship('relationship', - foreign_key_name='target_node_fk', + foreign_key='target_node_fk', backreference='target_node') @declared_attr @@ -336,7 +311,7 @@ class NodeBase(_InstanceModelMixin): def node_template(cls): return cls.many_to_one_relationship('node_template') - # region orchestrator required columns + # region orchestration runtime_properties = Column(modeling_types.Dict) scaling_groups = Column(modeling_types.List) @@ -369,6 +344,16 @@ class NodeBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['type_fk', + 'host_fk', + 'service_fk', + 'node_template_fk'] + + # Node many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Node one-to-one to Node @declared_attr def host_fk(cls): @@ -382,7 +367,7 @@ class NodeBase(_InstanceModelMixin): # Node many-to-one to NodeTemplate @declared_attr def node_template_fk(cls): - return cls.foreign_key('node_template') + return cls.foreign_key('node_template', nullable=True) # endregion @@ -426,13 +411,16 @@ class NodeBase(_InstanceModelMixin): target_node = target_nodes[0] if target_node is not None: - relationship = models.Relationship( - name=requirement_template.name, - requirement_template=requirement_template, - target_node=target_node, - capability=target_capability - ) + if requirement_template.relationship_template is not None: + relationship = \ + requirement_template.relationship_template.instantiate(context, self) + else: + relationship = models.Relationship(capability=target_capability) + relationship.name = requirement_template.name + relationship.requirement_template = requirement_template + relationship.target_node = target_node self.outbound_relationships.append(relationship) + return True else: context.validation.report('requirement "{0}" of node "{1}" targets node ' 'template "{2}" but its instantiated nodes do not ' @@ -503,8 +491,8 @@ class NodeBase(_InstanceModelMixin): def dump(self, context): console.puts('Node: {0}'.format(context.style.node(self.name))) with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) console.puts('Template: {0}'.format(context.style.node(self.node_template.name))) - console.puts('Type: {0}'.format(context.style.type(self.type_name))) utils.dump_parameters(context, self.properties) utils.dump_interfaces(context, self.interfaces) utils.dump_dict_values(context, self.artifacts, 'Artifacts') @@ -512,38 +500,33 @@ class NodeBase(_InstanceModelMixin): utils.dump_list_values(context, self.outbound_relationships, 'Relationships') -class GroupBase(_InstanceModelMixin): +class GroupBase(InstanceModelMixin): """ - An instance of a :class:`GroupTemplate`. + Usually an instance of a :class:`GroupTemplate`. - :ivar name: Unique ID (prefixed with the template name) - :ivar type_name: Must be represented in the :class:`ModelingContext` - :ivar template_name: Must be represented in the :class:`ServiceTemplate` + :ivar name: Unique ID (often equal to the template name) :ivar properties: Dict of :class:`Parameter` :ivar interfaces: Dict of :class:`Interface` - :ivar member_node_ids: Must be represented in the :class:`ServiceInstance` - :ivar member_group_ids: Must be represented in the :class:`ServiceInstance` """ - __tablename__ = 'group' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['service_fk', - 'group_template_fk'] + __tablename__ = 'group' - type_name = Column(Text) - template_name = Column(Text) + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') @declared_attr def interfaces(cls): - return cls.one_to_many_relationship('interface', key_column_name='name') + return cls.one_to_many_relationship('interface', dict_key='name') - member_node_ids = Column(modeling_types.StrictList(basestring)) - member_group_ids = Column(modeling_types.StrictList(basestring)) + @declared_attr + def nodes(cls): + return cls.many_to_many_relationship('node') @declared_attr def group_template(cls): @@ -551,6 +534,15 @@ class GroupBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['type_fk', + 'service_fk', + 'group_template_fk'] + + # Group many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Service one-to-many to Group @declared_attr def service_fk(cls): @@ -559,7 +551,7 @@ class GroupBase(_InstanceModelMixin): # Group many-to-one to GroupTemplate @declared_attr def group_template_fk(cls): - return cls.foreign_key('group_template') + return cls.foreign_key('group_template', nullable=True) # endregion @@ -567,21 +559,10 @@ class GroupBase(_InstanceModelMixin): def as_raw(self): return collections.OrderedDict(( ('name', self.name), - ('type_name', self.type_name), - ('template_name', self.template_name), ('properties', formatting.as_raw_dict(self.properties)), - ('interfaces', formatting.as_raw_list(self.interfaces)), - ('member_node_ids', self.member_node_ids), - ('member_group_ids', self.member_group_ids))) + ('interfaces', formatting.as_raw_list(self.interfaces)))) def validate(self, context): - if context.modeling.group_types.get_descendant(self.type_name) is None: - context.validation.report('group "{0}" has an unknown type: {1}'.format( - self.name, # pylint: disable=no-member - # TODO fix self.name reference - formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) - utils.validate_dict_values(context, self.properties) utils.validate_dict_values(context, self.interfaces) @@ -592,42 +573,42 @@ class GroupBase(_InstanceModelMixin): def dump(self, context): console.puts('Group: {0}'.format(context.style.node(self.name))) with context.style.indent: - console.puts('Type: {0}'.format(context.style.type(self.type_name))) - console.puts('Template: {0}'.format(context.style.type(self.template_name))) + console.puts('Type: {0}'.format(context.style.type(self.type.name))) utils.dump_parameters(context, self.properties) utils.dump_interfaces(context, self.interfaces) - if self.member_node_ids: + if self.nodes: console.puts('Member nodes:') with context.style.indent: - for node_id in self.member_node_ids: - console.puts(context.style.node(node_id)) + for node in self.nodes: + console.puts(context.style.node(node.name)) -class PolicyBase(_InstanceModelMixin): +class PolicyBase(InstanceModelMixin): """ - An instance of a :class:`PolicyTemplate`. + Usually an instance of a :class:`PolicyTemplate`. :ivar name: Name - :ivar type_name: Must be represented in the :class:`ModelingContext` :ivar properties: Dict of :class:`Parameter` - :ivar target_node_ids: Must be represented in the :class:`ServiceInstance` - :ivar target_group_ids: Must be represented in the :class:`ServiceInstance` """ - __tablename__ = 'policy' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'policy' - __private_fields__ = ['service_fk', - 'policy_template_fk'] - - type_name = Column(Text) + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') - target_node_ids = Column(modeling_types.StrictList(basestring)) - target_group_ids = Column(modeling_types.StrictList(basestring)) + @declared_attr + def nodes(cls): + return cls.many_to_many_relationship('node') + + @declared_attr + def groups(cls): + return cls.many_to_many_relationship('group') @declared_attr def policy_template(cls): @@ -635,6 +616,15 @@ class PolicyBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['type_fk', + 'service_fk', + 'policy_template_fk'] + + # Policy many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Service one-to-many to Policy @declared_attr def service_fk(cls): @@ -643,7 +633,7 @@ class PolicyBase(_InstanceModelMixin): # Policy many-to-one to PolicyTemplate @declared_attr def policy_template_fk(cls): - return cls.foreign_key('policy_template') + return cls.foreign_key('policy_template', nullable=True) # endregion @@ -652,16 +642,9 @@ class PolicyBase(_InstanceModelMixin): return collections.OrderedDict(( ('name', self.name), ('type_name', self.type_name), - ('properties', formatting.as_raw_dict(self.properties)), - ('target_node_ids', self.target_node_ids), - ('target_group_ids', self.target_group_ids))) + ('properties', formatting.as_raw_dict(self.properties)))) def validate(self, context): - if context.modeling.policy_types.get_descendant(self.type_name) is None: - context.validation.report('policy "{0}" has an unknown type: {1}'.format( - self.name, formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) - utils.validate_dict_values(context, self.properties) def coerce_values(self, context, container, report_issues): @@ -670,37 +653,36 @@ class PolicyBase(_InstanceModelMixin): def dump(self, context): console.puts('Policy: {0}'.format(context.style.node(self.name))) with context.style.indent: - console.puts('Type: {0}'.format(context.style.type(self.type_name))) + console.puts('Type: {0}'.format(context.style.type(self.type.name))) utils.dump_parameters(context, self.properties) - if self.target_node_ids: + if self.nodes: console.puts('Target nodes:') with context.style.indent: - for node_id in self.target_node_ids: - console.puts(context.style.node(node_id)) - if self.target_group_ids: + for node in self.nodes: + console.puts(context.style.node(node.name)) + if self.groups: console.puts('Target groups:') with context.style.indent: - for group_id in self.target_group_ids: - console.puts(context.style.node(group_id)) + for group in self.groups: + console.puts(context.style.node(group.name)) -class SubstitutionBase(_InstanceModelMixin): +class SubstitutionBase(InstanceModelMixin): """ - An instance of a :class:`SubstitutionTemplate`. + Usually an instance of a :class:`SubstitutionTemplate`. - :ivar node_type_name: Must be represented in the :class:`ModelingContext` :ivar mappings: Dict of :class:` SubstitutionMapping` """ - __tablename__ = 'substitution' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['substitution_template_fk'] + __tablename__ = 'substitution' - node_type_name = Column(Text) + @declared_attr + def node_type(cls): + return cls.many_to_one_relationship('type') @declared_attr def mappings(cls): - return cls.one_to_many_relationship('substitution_mapping', key_column_name='name') + return cls.one_to_many_relationship('substitution_mapping', dict_key='name') @declared_attr def substitution_template(cls): @@ -708,12 +690,21 @@ class SubstitutionBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['node_type_fk', + 'substitution_template_fk'] + + # Substitution many-to-one to Type + @declared_attr + def node_type_fk(cls): + return cls.foreign_key('type') + # Substitution many-to-one to SubstitutionTemplate @declared_attr def substitution_template_fk(cls): - return cls.foreign_key('substitution_template') + return cls.foreign_key('substitution_template', nullable=True) # endregion + @property def as_raw(self): return collections.OrderedDict(( @@ -721,13 +712,6 @@ class SubstitutionBase(_InstanceModelMixin): ('mappings', formatting.as_raw_dict(self.mappings)))) def validate(self, context): - if context.modeling.node_types.get_descendant(self.node_type_name) is None: - context.validation.report('substitution "{0}" has an unknown type: {1}'.format( - self.name, # pylint: disable=no-member - # TODO fix self.name reference - formatting.safe_repr(self.node_type_name)), - level=validation.Issue.BETWEEN_TYPES) - utils.validate_dict_values(context, self.mappings) def coerce_values(self, context, container, report_issues): @@ -736,23 +720,18 @@ class SubstitutionBase(_InstanceModelMixin): def dump(self, context): console.puts('Substitution:') with context.style.indent: - console.puts('Node type: {0}'.format(context.style.type(self.node_type_name))) + console.puts('Node type: {0}'.format(context.style.type(self.node_type.name))) utils.dump_dict_values(context, self.mappings, 'Mappings') -class SubstitutionMappingBase(_InstanceModelMixin): +class SubstitutionMappingBase(InstanceModelMixin): """ An instance of a :class:`SubstitutionMappingTemplate`. :ivar name: Exposed capability or requirement name """ - __tablename__ = 'substitution_mapping' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['substitution_fk', - 'node_fk', - 'capability_fk', - 'requirement_template_fk'] + __tablename__ = 'substitution_mapping' @declared_attr def node(cls): @@ -768,6 +747,11 @@ class SubstitutionMappingBase(_InstanceModelMixin): # region foreign keys + __private_fields__ = ['substitution_fk', + 'node_fk', + 'capability_fk', + 'requirement_template_fk'] + # Substitution one-to-many to SubstitutionMapping @declared_attr def substitution_fk(cls): @@ -812,36 +796,31 @@ class SubstitutionMappingBase(_InstanceModelMixin): else self.requirement_template.name))) -class RelationshipBase(_InstanceModelMixin): +class RelationshipBase(InstanceModelMixin): """ Connects :class:`Node` to another node. - An instance of a :class:`RelationshipTemplate`. + Might be an instance of a :class:`RelationshipTemplate`. :ivar name: Name (usually the name of the requirement at the source node template) - :ivar type_name: Must be represented in the :class:`ModelingContext` :ivar properties: Dict of :class:`Parameter` :ivar interfaces: Dict of :class:`Interface` """ - __tablename__ = 'relationship' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'relationship' - __private_fields__ = ['source_node_fk', - 'target_node_fk', - 'capability_fk', - 'requirement_template_fk', - 'relationship_template_fk'] - - type_name = Column(Text) + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') @declared_attr def interfaces(cls): - return cls.one_to_many_relationship('interface', key_column_name='name') + return cls.one_to_many_relationship('interface', dict_key='name') @declared_attr def capability(cls): @@ -855,7 +834,7 @@ class RelationshipBase(_InstanceModelMixin): def relationship_template(cls): return cls.many_to_one_relationship('relationship_template') - # region orchestrator required columns + # region orchestration source_position = Column(Integer) # ??? target_position = Column(Integer) # ??? @@ -864,6 +843,18 @@ class RelationshipBase(_InstanceModelMixin): # region foreign keys + __private_fields__ = ['type_fk', + 'source_node_fk', + 'target_node_fk', + 'capability_fk', + 'requirement_template_fk', + 'relationship_template_fk'] + + # Relationship many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type', nullable=True) + # Node one-to-many to Relationship @declared_attr def source_node_fk(cls): @@ -882,7 +873,7 @@ class RelationshipBase(_InstanceModelMixin): # Relationship many-to-one to RequirementTemplate @declared_attr def requirement_template_fk(cls): - return cls.foreign_key('requirement_template') + return cls.foreign_key('requirement_template', nullable=True) # Relationship many-to-one to RelationshipTemplate @declared_attr @@ -902,12 +893,6 @@ class RelationshipBase(_InstanceModelMixin): ('interfaces', formatting.as_raw_list(self.interfaces)))) def validate(self, context): - if self.type_name: - if context.modeling.relationship_types.get_descendant(self.type_name) is None: - context.validation.report('relationship "{0}" has an unknown type: {1}'.format( - self.name, - formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) utils.validate_dict_values(context, self.properties) utils.validate_dict_values(context, self.interfaces) @@ -922,34 +907,35 @@ class RelationshipBase(_InstanceModelMixin): console.puts('->') with context.style.indent: console.puts('Node: {0}'.format(context.style.node(self.target_node.name))) - if self.type_name is not None: - console.puts('Relationship type: {0}'.format(context.style.type(self.type_name))) - #if self.template_name is not None: - # console.puts('Relationship template: {0}'.format( - # context.style.node(self.template_name))) + if self.capability: + console.puts('Capability: {0}'.format(context.style.node(self.capability.name))) + if self.type is not None: + console.puts('Relationship type: {0}'.format(context.style.type(self.type.name))) + if (self.relationship_template is not None) and self.relationship_template.name: + console.puts('Relationship template: {0}'.format( + context.style.node(self.relationship_template.name))) utils.dump_parameters(context, self.properties) utils.dump_interfaces(context, self.interfaces, 'Interfaces') -class CapabilityBase(_InstanceModelMixin): +class CapabilityBase(InstanceModelMixin): """ A capability of a :class:`Node`. - An instance of a :class:`CapabilityTemplate`. + Usually an instance of a :class:`CapabilityTemplate`. :ivar name: Name - :ivar type_name: Must be represented in the :class:`ModelingContext` :ivar min_occurrences: Minimum number of requirement matches required :ivar max_occurrences: Maximum number of requirement matches allowed :ivar properties: Dict of :class:`Parameter` """ - __tablename__ = 'capability' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'capability' - __private_fields__ = ['node_fk', - 'capability_template_fk'] + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') - type_name = Column(Text) min_occurrences = Column(Integer, default=None) # optional max_occurrences = Column(Integer, default=None) # optional occurrences = Column(Integer, default=0) @@ -957,7 +943,7 @@ class CapabilityBase(_InstanceModelMixin): @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') @declared_attr def capability_template(cls): @@ -965,6 +951,15 @@ class CapabilityBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['capability_fk', + 'node_fk', + 'capability_template_fk'] + + # Capability many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Node one-to-many to Capability @declared_attr def node_fk(cls): @@ -973,7 +968,7 @@ class CapabilityBase(_InstanceModelMixin): # Capability many-to-one to CapabilityTemplate @declared_attr def capability_template_fk(cls): - return cls.foreign_key('capability_template') + return cls.foreign_key('capability_template', nullable=True) # endregion @@ -998,12 +993,6 @@ class CapabilityBase(_InstanceModelMixin): ('properties', formatting.as_raw_dict(self.properties)))) def validate(self, context): - if context.modeling.capability_types.get_descendant(self.type_name) is None: - context.validation.report('capability "{0}" has an unknown type: {1}'.format( - self.name, - formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) - utils.validate_dict_values(context, self.properties) def coerce_values(self, context, container, report_issues): @@ -1012,7 +1001,7 @@ class CapabilityBase(_InstanceModelMixin): def dump(self, context): console.puts(context.style.node(self.name)) with context.style.indent: - console.puts('Type: {0}'.format(context.style.type(self.type_name))) + console.puts('Type: {0}'.format(context.style.type(self.type.name))) console.puts('Occurrences: {0:d} ({1:d}{2})'.format( self.occurrences, self.min_occurrences or 0, @@ -1022,35 +1011,34 @@ class CapabilityBase(_InstanceModelMixin): utils.dump_parameters(context, self.properties) -class InterfaceBase(_InstanceModelMixin): +class InterfaceBase(InstanceModelMixin): """ A typed set of :class:`Operation`. + + Usually an instance of :class:`InterfaceTemplate`. :ivar name: Name :ivar description: Description - :ivar type_name: Must be represented in the :class:`ModelingContext` :ivar inputs: Dict of :class:`Parameter` :ivar operations: Dict of :class:`Operation` """ - __tablename__ = 'interface' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'interface' - __private_fields__ = ['node_fk', - 'group_fk', - 'relationship_fk', - 'interface_template_fk'] + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') description = Column(Text) - type_name = Column(Text) @declared_attr def inputs(cls): return cls.many_to_many_relationship('parameter', table_prefix='inputs', - key_column_name='name') + dict_key='name') @declared_attr def operations(cls): - return cls.one_to_many_relationship('operation', key_column_name='name') + return cls.one_to_many_relationship('operation', dict_key='name') @declared_attr def interface_template(cls): @@ -1058,6 +1046,17 @@ class InterfaceBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['type_fk', + 'node_fk', + 'group_fk', + 'relationship_fk', + 'interface_template_fk'] + + # Interface many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Node one-to-many to Interface @declared_attr def node_fk(cls): @@ -1076,7 +1075,7 @@ class InterfaceBase(_InstanceModelMixin): # Interface many-to-one to InterfaceTemplate @declared_attr def interface_template_fk(cls): - return cls.foreign_key('interface_template') + return cls.foreign_key('interface_template', nullable=True) # endregion @@ -1090,13 +1089,6 @@ class InterfaceBase(_InstanceModelMixin): ('operations', formatting.as_raw_list(self.operations)))) def validate(self, context): - if self.type_name: - if context.modeling.interface_types.get_descendant(self.type_name) is None: - context.validation.report('interface "{0}" has an unknown type: {1}'.format( - self.name, - formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) - utils.validate_dict_values(context, self.inputs) utils.validate_dict_values(context, self.operations) @@ -1109,14 +1101,16 @@ class InterfaceBase(_InstanceModelMixin): if self.description: console.puts(context.style.meta(self.description)) with context.style.indent: - console.puts('Interface type: {0}'.format(context.style.type(self.type_name))) + console.puts('Interface type: {0}'.format(context.style.type(self.type.name))) utils.dump_parameters(context, self.inputs, 'Inputs') utils.dump_dict_values(context, self.operations, 'Operations') -class OperationBase(_InstanceModelMixin): +class OperationBase(InstanceModelMixin): """ An operation in a :class:`Interface`. + + Might be an instance of :class:`OperationTemplate`. :ivar name: Name :ivar description: Description @@ -1128,12 +1122,7 @@ class OperationBase(_InstanceModelMixin): :ivar inputs: Dict of :class:`Parameter` """ - __tablename__ = 'operation' # redundancy for PyLint: SqlAlchemy injects this - - __private_fields__ = ['service_fk', - 'interface_fk', - 'plugin_fk', - 'operation_template_fk'] + __tablename__ = 'operation' description = Column(Text) implementation = Column(Text) @@ -1145,7 +1134,7 @@ class OperationBase(_InstanceModelMixin): @declared_attr def inputs(cls): return cls.many_to_many_relationship('parameter', table_prefix='inputs', - key_column_name='name') + dict_key='name') @declared_attr def plugin(cls): @@ -1157,6 +1146,11 @@ class OperationBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['service_fk', + 'interface_fk', + 'plugin_fk', + 'operation_template_fk'] + # Service one-to-many to Operation @declared_attr def service_fk(cls): @@ -1175,7 +1169,7 @@ class OperationBase(_InstanceModelMixin): # Operation many-to-one to OperationTemplate @declared_attr def operation_template_fk(cls): - return cls.foreign_key('operation_template') + return cls.foreign_key('operation_template', nullable=True) # endregion @@ -1192,6 +1186,7 @@ class OperationBase(_InstanceModelMixin): ('inputs', formatting.as_raw_dict(self.inputs)))) def validate(self, context): + # TODO must be associated with interface or service utils.validate_dict_values(context, self.inputs) def coerce_values(self, context, container, report_issues): @@ -1219,13 +1214,14 @@ class OperationBase(_InstanceModelMixin): utils.dump_parameters(context, self.inputs, 'Inputs') -class ArtifactBase(_InstanceModelMixin): +class ArtifactBase(InstanceModelMixin): """ A file associated with a :class:`Node`. + + Usually an instance of :class:`ArtifactTemplate`. :ivar name: Name :ivar description: Description - :ivar type_name: Must be represented in the :class:`ModelingContext` :ivar source_path: Source path (CSAR or repository) :ivar target_path: Path at destination machine :ivar repository_url: Repository URL @@ -1233,10 +1229,11 @@ class ArtifactBase(_InstanceModelMixin): :ivar properties: Dict of :class:`Parameter` """ - __tablename__ = 'artifact' # redundancy for PyLint: SqlAlchemy injects this + __tablename__ = 'artifact' - __private_fields__ = ['node_fk', - 'artifact_template_fk'] + @declared_attr + def type(cls): + return cls.many_to_one_relationship('type') description = Column(Text) type_name = Column(Text) @@ -1248,7 +1245,7 @@ class ArtifactBase(_InstanceModelMixin): @declared_attr def properties(cls): return cls.many_to_many_relationship('parameter', table_prefix='properties', - key_column_name='name') + dict_key='name') @declared_attr def artifact_template(cls): @@ -1256,6 +1253,15 @@ class ArtifactBase(_InstanceModelMixin): # region foreign_keys + __private_fields__ = ['type_fk', + 'node_fk', + 'artifact_template_fk'] + + # Artifact many-to-one to Type + @declared_attr + def type_fk(cls): + return cls.foreign_key('type') + # Node one-to-many to Artifact @declared_attr def node_fk(cls): @@ -1264,7 +1270,7 @@ class ArtifactBase(_InstanceModelMixin): # Artifact many-to-one to ArtifactTemplate @declared_attr def artifact_template_fk(cls): - return cls.foreign_key('artifact_template') + return cls.foreign_key('artifact_template', nullable=True) # endregion @@ -1281,11 +1287,6 @@ class ArtifactBase(_InstanceModelMixin): ('properties', formatting.as_raw_dict(self.properties)))) def validate(self, context): - if context.modeling.artifact_types.get_descendant(self.type_name) is None: - context.validation.report('artifact "{0}" has an unknown type: {1}'.format( - self.name, - formatting.safe_repr(self.type_name)), - level=validation.Issue.BETWEEN_TYPES) utils.validate_dict_values(context, self.properties) def coerce_values(self, context, container, report_issues): @@ -1296,7 +1297,7 @@ class ArtifactBase(_InstanceModelMixin): if self.description: console.puts(context.style.meta(self.description)) with context.style.indent: - console.puts('Artifact type: {0}'.format(context.style.type(self.type_name))) + console.puts('Artifact type: {0}'.format(context.style.type(self.type.name))) console.puts('Source path: {0}'.format(context.style.literal(self.source_path))) if self.target_path is not None: console.puts('Target path: {0}'.format(context.style.literal(self.target_path)))
