Separate plugin specification form plugin; move dry support to CLI; various renames and refactorings
Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/dd5bfa93 Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/dd5bfa93 Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/dd5bfa93 Branch: refs/heads/Unified_coerce Commit: dd5bfa930dede8a81ce382e65cad8fe924d4e99d Parents: aa01cd4 Author: Tal Liron <[email protected]> Authored: Fri Mar 10 16:57:17 2017 -0600 Committer: Tal Liron <[email protected]> Committed: Fri Mar 10 16:57:17 2017 -0600 ---------------------------------------------------------------------- aria/cli/args_parser.py | 5 + aria/cli/commands.py | 4 + aria/cli/dry.py | 75 + aria/modeling/__init__.py | 18 +- aria/modeling/bases.py | 405 ----- aria/modeling/misc.py | 234 --- aria/modeling/mixins.py | 405 +++++ aria/modeling/models.py | 91 +- aria/modeling/orchestration.py | 235 +-- aria/modeling/service.py | 1529 ------------------ aria/modeling/service_changes.py | 219 +++ aria/modeling/service_common.py | 270 ++++ aria/modeling/service_instance.py | 1529 ++++++++++++++++++ aria/modeling/service_template.py | 260 +-- aria/orchestrator/workflows/api/task.py | 44 +- .../workflows/builtin/execute_operation.py | 3 +- aria/orchestrator/workflows/builtin/utils.py | 13 +- .../orchestrator/workflows/builtin/workflows.py | 60 +- aria/orchestrator/workflows/core/task.py | 13 +- aria/orchestrator/workflows/dry.py | 53 - .../simple_v1_0/modeling/__init__.py | 47 +- tests/end2end/test_orchestrator.py | 3 + tests/mock/models.py | 16 + tests/modeling/__init__.py | 34 + tests/modeling/test_mixins.py | 219 +++ tests/modeling/test_model_storage.py | 102 ++ tests/modeling/test_models.py | 835 ++++++++++ tests/orchestrator/context/test_operation.py | 15 +- tests/orchestrator/context/test_serialize.py | 7 +- tests/orchestrator/context/test_toolbelt.py | 2 +- tests/orchestrator/workflows/api/test_task.py | 33 +- .../workflows/builtin/test_execute_operation.py | 2 +- tests/orchestrator/workflows/core/test_task.py | 15 +- .../tosca-simple-1.0/node-cellar/workflows.py | 3 +- tests/storage/__init__.py | 22 +- tests/storage/test_instrumentation.py | 8 +- tests/storage/test_model_storage.py | 102 -- tests/storage/test_models.py | 833 ---------- tests/storage/test_structures.py | 220 --- 39 files changed, 4069 insertions(+), 3914 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/cli/args_parser.py ---------------------------------------------------------------------- diff --git a/aria/cli/args_parser.py b/aria/cli/args_parser.py index e092ee6..1d18145 100644 --- a/aria/cli/args_parser.py +++ b/aria/cli/args_parser.py @@ -137,6 +137,11 @@ def add_workflow_parser(workflow): '-w', '--workflow', default='install', help='The workflow name') + workflow.add_flag_argument( + 'dry', + default=True, + help_true='dry run', + help_false='wet run') @sub_parser_decorator( http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/cli/commands.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands.py b/aria/cli/commands.py index 52d4f14..a9079c5 100644 --- a/aria/cli/commands.py +++ b/aria/cli/commands.py @@ -50,6 +50,7 @@ from ..utils.collections import OrderedDict from ..orchestrator import WORKFLOW_DECORATOR_RESERVED_ARGUMENTS from ..orchestrator.runner import Runner from ..orchestrator.workflows.builtin import BUILTIN_WORKFLOWS +from .dry import convert_to_dry from .exceptions import ( AriaCliFormatInputsError, @@ -212,6 +213,7 @@ class WorkflowCommand(BaseCommand): context = self._parse(args_namespace.uri) workflow_fn, inputs = self._get_workflow(context, args_namespace.workflow) + self._dry = args_namespace.dry self._run(context, args_namespace.workflow, workflow_fn, inputs) def _parse(self, uri): @@ -265,6 +267,8 @@ class WorkflowCommand(BaseCommand): def _run(self, context, workflow_name, workflow_fn, inputs): # Storage def _initialize_storage(model_storage): + if self._dry: + convert_to_dry(context.modeling.instance) context.modeling.store(model_storage) # Create runner http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/cli/dry.py ---------------------------------------------------------------------- diff --git a/aria/cli/dry.py b/aria/cli/dry.py new file mode 100644 index 0000000..98b7217 --- /dev/null +++ b/aria/cli/dry.py @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock + +from ..modeling import models +from ..orchestrator.decorators import operation +from ..utils.collections import OrderedDict +from ..utils.console import puts, Colored +from ..utils.formatting import safe_repr + + +_TERMINAL_LOCK = RLock() + + +def convert_to_dry(service): + for workflow in service.workflows: + convert_operation_to_dry(workflow) + + for node in service.nodes: + for interface in node.interfaces.itervalues(): + for oper in interface.operations.itervalues(): + convert_operation_to_dry(oper) + for relationship in node.outbound_relationships: + for interface in relationship.interfaces.itervalues(): + for oper in interface.operations.itervalues(): + convert_operation_to_dry(oper) + + +def convert_operation_to_dry(oper): + plugin = oper.plugin_specification.name \ + if oper.plugin_specification is not None else None + if oper.inputs is None: + oper.inputs = OrderedDict() + oper.inputs['_implementation'] = models.Parameter(name='_implementation', + type_name='string', + value=oper.implementation) + oper.inputs['_plugin'] = models.Parameter(name='_plugin', + type_name='string', + value=plugin) + oper.implementation = '{0}.{1}'.format(__name__, 'dry_operation') + oper.plugin_specification = None + + +@operation +def dry_operation(ctx, _plugin, _implementation, **kwargs): + with _TERMINAL_LOCK: + print ctx.name + if hasattr(ctx, 'relationship'): + puts('> Relationship: {0} -> {1}'.format( + Colored.red(ctx.relationship.source_node.name), + Colored.red(ctx.relationship.target_node.name))) + else: + puts('> Node: {0}'.format(Colored.red(ctx.node.name))) + puts(' Operation: {0}'.format(Colored.green(ctx.name))) + _dump_implementation(_plugin, _implementation) + + +def _dump_implementation(plugin, implementation): + if plugin: + puts(' Plugin: {0}'.format(Colored.magenta(plugin, bold=True))) + if implementation: + puts(' Implementation: {0}'.format(Colored.magenta(safe_repr(implementation)))) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/modeling/__init__.py ---------------------------------------------------------------------- diff --git a/aria/modeling/__init__.py b/aria/modeling/__init__.py index 67b5703..4dfc39d 100644 --- a/aria/modeling/__init__.py +++ b/aria/modeling/__init__.py @@ -16,26 +16,32 @@ from collections import namedtuple from . import ( - bases, + mixins, types, models, service_template as _service_template_bases, - service as _service_bases, - orchestration as _orchestration_bases, + service_instance as _service_instance_bases, + service_changes as _service_changes_bases, + service_common as _service_common_bases, + orchestration as _orchestration_bases ) _ModelBasesCls = namedtuple('ModelBase', 'service_template,' - 'service,' + 'service_instance,' + 'service_changes,' + 'service_common,' 'orchestration') model_bases = _ModelBasesCls(service_template=_service_template_bases, - service=_service_bases, + service_instance=_service_instance_bases, + service_changes=_service_changes_bases, + service_common=_service_common_bases, orchestration=_orchestration_bases) __all__ = ( - 'bases', + 'mixins', 'types', 'models', 'model_bases', http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/modeling/bases.py ---------------------------------------------------------------------- diff --git a/aria/modeling/bases.py b/aria/modeling/bases.py deleted file mode 100644 index efcb968..0000000 --- a/aria/modeling/bases.py +++ /dev/null @@ -1,405 +0,0 @@ -# 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. - -""" -ARIA's storage.structures module -Path: aria.storage.structures - -models module holds ARIA's models. - -classes: - * ModelMixin - abstract model implementation. - * ModelIDMixin - abstract model implementation with IDs. -""" - -from sqlalchemy.orm import relationship, backref -from sqlalchemy.orm.collections import attribute_mapped_collection -from sqlalchemy.ext import associationproxy -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, - Table, -) - -from . import utils -from ..utils import formatting - - -class ModelMixin(object): - - @utils.classproperty - def __modelname__(cls): # pylint: disable=no-self-argument - return getattr(cls, '__mapiname__', cls.__tablename__) - - @classmethod - def id_column_name(cls): - raise NotImplementedError - - @classmethod - def name_column_name(cls): - raise NotImplementedError - - @classmethod - def foreign_key(cls, parent_table, nullable=False): - """ - Return a ForeignKey object. - - :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), - ondelete='CASCADE'), - nullable=nullable) - - @classmethod - def relationship_to_self(cls, - column_name, - relationship_kwargs=None): - relationship_kwargs = relationship_kwargs or {} - - remote_side = '{cls}.{remote_column}'.format( - cls=cls.__name__, - remote_column=cls.id_column_name() - ) - - 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, - 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, - key=None, - foreign_key=None, - backreference=None, - backref_kwargs=None, - relationship_kwargs=None): - 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, - key=None, - foreign_key=None, - dict_key=None, - backreference=None, - backref_kwargs=None, - relationship_kwargs=None): - backref_kwargs = backref_kwargs or {} - backref_kwargs.setdefault('uselist', False) - - 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, - key=None, - foreign_key=None, - backreference=None, - backref_kwargs=None, - relationship_kwargs=None): - """ - Return a one-to-many SQL relationship object - Meant to be used from inside the *child* object - - :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) - """ - - if backreference is None: - backreference = formatting.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 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, - table_prefix=None, - key=None, - dict_key=None, - backreference=None, - backref_kwargs=None, - relationship_kwargs=None): - """ - Return a many-to-many SQL relationship object - - Notes: - - 1. The backreference name is the current table's table name - 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: 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 - """ - - 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) - - helper_table = '{0}_{1}'.format(this_table, other_table) - - if backreference is None: - backreference = formatting.pluralize(this_table) - if table_prefix: - helper_table = '{0}_{1}'.format(table_prefix, helper_table) - backreference = '{0}_{1}'.format(table_prefix, backreference) - - backref_kwargs = backref_kwargs or {} - backref_kwargs.setdefault('uselist', True) - - relationship_kwargs = relationship_kwargs or {} - relationship_kwargs.setdefault('secondary', cls._get_secondary_table( - cls.metadata, - helper_table, - this_column_name, - other_column_name, - this_foreign_key, - other_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): - """ - Return a dict representation of the model - - :param suppress_error: If set to True, sets `None` to attributes that - 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: - try: - field_value = getattr(self, field) - except AttributeError: - if suppress_error: - field_value = None - else: - raise - if isinstance(field_value, list): - field_value = list(field_value) - elif isinstance(field_value, dict): - field_value = dict(field_value) - elif isinstance(field_value, ModelMixin): - field_value = field_value.to_dict() - res[field] = field_value - - 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): - yield col - - @classmethod - def fields(cls): - """ - Return the list of field names for this table - - 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__', [])) - - @classmethod - def _get_cls_by_tablename(cls, tablename): - """ - Return class reference mapped to table. - - :param tablename: String with name of table. - :return: Class reference or None. - """ - - if tablename in (cls.__name__, cls.__tablename__): - return cls - - for table_cls in cls._decl_class_registry.values(): - if tablename == getattr(table_cls, '__tablename__', None): - return table_cls - - def __repr__(self): - return '<{cls} id=`{id}`>'.format( - cls=self.__class__.__name__, - id=getattr(self, self.name_column_name())) - - -class ModelIDMixin(object): - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(Text, index=True) - - @classmethod - def id_column_name(cls): - return 'id' - - @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/dd5bfa93/aria/modeling/misc.py ---------------------------------------------------------------------- diff --git a/aria/modeling/misc.py b/aria/modeling/misc.py deleted file mode 100644 index 105876a..0000000 --- a/aria/modeling/misc.py +++ /dev/null @@ -1,234 +0,0 @@ -# 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. - -# pylint: disable=no-self-argument, no-member, abstract-method - -import cPickle as pickle -import logging - -from sqlalchemy import ( - Column, - Text, - Binary -) -from sqlalchemy.ext.declarative import declared_attr - -from ..storage import exceptions -from ..utils import collections, formatting, console -from .bases import InstanceModelMixin, TemplateModelMixin -from . import utils - - -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) - type_name = Column(Text) - - # Check: value type - _value = Column(Binary, name='value') - 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, TypeError): - logging.getLogger('aria').warn('Could not pickle parameter of type "{0}": {1}' - .format(self.type_name, value)) - 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 = utils.coerce_value(context, container, self.value, - report_issues) - - def dump(self, context): - if self.type_name is not None: - console.puts('{0}: {1} ({2})'.format( - context.style.property(self.name), - context.style.literal(self.value), - context.style.type(self.type_name))) - else: - console.puts('{0}: {1}'.format( - context.style.property(self.name), - context.style.literal(self.value))) - if self.description: - console.puts(context.style.meta(self.description)) - - -class TypeBase(InstanceModelMixin): - """ - Represents a type and its children. - """ - - __tablename__ = 'type' - - variant = Column(Text, nullable=False) - description = Column(Text) - _role = Column(Text, name='role') - - @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 - - @property - def role(self): - def get_role(the_type): - if the_type is None: - return None - elif the_type._role is None: - return get_role(the_type.parent) - return the_type._role - - return get_role(self) - - @role.setter - def role(self, value): - self._role = value - - 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 - - @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' - - 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) - - def dump(self, context): - console.puts('{0}: {1}'.format( - context.style.property(self.name), - context.style.literal(self.value))) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/modeling/mixins.py ---------------------------------------------------------------------- diff --git a/aria/modeling/mixins.py b/aria/modeling/mixins.py new file mode 100644 index 0000000..04497b5 --- /dev/null +++ b/aria/modeling/mixins.py @@ -0,0 +1,405 @@ +# 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. + +""" +ARIA's storage.structures module +Path: aria.storage.structures + +models module holds ARIA's models. + +classes: + * ModelMixin - abstract model implementation. + * ModelIDMixin - abstract model implementation with IDs. +""" + +from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.ext import associationproxy +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, + Table, +) + +from . import utils +from ..utils import formatting + + +class ModelMixin(object): + + @utils.classproperty + def __modelname__(cls): # pylint: disable=no-self-argument + return getattr(cls, '__mapiname__', cls.__tablename__) + + @classmethod + def id_column_name(cls): + raise NotImplementedError + + @classmethod + def name_column_name(cls): + raise NotImplementedError + + def to_dict(self, fields=None, suppress_error=False): + """ + Return a dict representation of the model + + :param suppress_error: If set to True, sets `None` to attributes that + 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: + try: + field_value = getattr(self, field) + except AttributeError: + if suppress_error: + field_value = None + else: + raise + if isinstance(field_value, list): + field_value = list(field_value) + elif isinstance(field_value, dict): + field_value = dict(field_value) + elif isinstance(field_value, ModelMixin): + field_value = field_value.to_dict() + res[field] = field_value + + return res + + @classmethod + def fields(cls): + """ + Return the list of field names for this table + + Mostly for backwards compatibility in the code (that uses `fields`) + """ + + fields = set(cls._iter_association_proxies()) + fields.update(cls.__table__.columns.keys()) + return fields - set(getattr(cls, '__private_fields__', [])) + + @classmethod + def _create_foreign_key(cls, parent_table, nullable=False): + """ + Return a ForeignKey object. + + :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), + ondelete='CASCADE'), + nullable=nullable) + + @classmethod + def _create_relationship_to_self(cls, + column_name, + relationship_kwargs=None): + relationship_kwargs = relationship_kwargs or {} + + remote_side = '{cls}.{remote_column}'.format( + cls=cls.__name__, + remote_column=cls.id_column_name() + ) + + 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, + remote_side=remote_side, + post_update=True, + **relationship_kwargs + ) + + @classmethod + def _create_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 _create_one_to_one_relationship(cls, + other_table, + key=None, + foreign_key=None, + backreference=None, + backref_kwargs=None, + relationship_kwargs=None): + 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 _create_one_to_many_relationship(cls, + child_table, + key=None, + foreign_key=None, + dict_key=None, + backreference=None, + backref_kwargs=None, + relationship_kwargs=None): + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', False) + + return cls._create_relationship(child_table, backref_kwargs, relationship_kwargs, + backreference, key=key, foreign_key=foreign_key, + dict_key=dict_key) + + @classmethod + def _create_many_to_one_relationship(cls, + parent_table, + key=None, + foreign_key=None, + backreference=None, + backref_kwargs=None, + relationship_kwargs=None): + """ + Return a one-to-many SQL relationship object + Meant to be used from inside the *child* object + + :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) + """ + + if backreference is None: + backreference = formatting.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 cls._create_relationship(parent_table, backref_kwargs, relationship_kwargs, + backreference, key=key, foreign_key=foreign_key) + + @classmethod + def _create_many_to_many_relationship(cls, + other_table, + table_prefix=None, + key=None, + dict_key=None, + backreference=None, + backref_kwargs=None, + relationship_kwargs=None): + """ + Return a many-to-many SQL relationship object + + Notes: + + 1. The backreference name is the current table's table name + 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: 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 + """ + + 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) + + helper_table = '{0}_{1}'.format(this_table, other_table) + + if backreference is None: + backreference = formatting.pluralize(this_table) + if table_prefix: + helper_table = '{0}_{1}'.format(table_prefix, helper_table) + backreference = '{0}_{1}'.format(table_prefix, backreference) + + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('uselist', True) + + relationship_kwargs = relationship_kwargs or {} + relationship_kwargs.setdefault('secondary', cls._get_secondary_table( + cls.metadata, + helper_table, + this_column_name, + other_column_name, + this_foreign_key, + other_foreign_key + )) + + return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs, + backreference, key=key, dict_key=dict_key) + + @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 _iter_association_proxies(cls): + for col, value in vars(cls).items(): + if isinstance(value, associationproxy.AssociationProxy): + yield col + + @classmethod + def _get_cls_by_tablename(cls, tablename): + """ + Return class reference mapped to table. + + :param tablename: String with name of table. + :return: Class reference or None. + """ + + if tablename in (cls.__name__, cls.__tablename__): + return cls + + for table_cls in cls._decl_class_registry.values(): + if tablename == getattr(table_cls, '__tablename__', None): + return table_cls + + def __repr__(self): + return '<{cls} id=`{id}`>'.format( + cls=self.__class__.__name__, + id=getattr(self, self.name_column_name())) + + +class ModelIDMixin(object): + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Text, index=True) + + @classmethod + def id_column_name(cls): + return 'id' + + @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/dd5bfa93/aria/modeling/models.py ---------------------------------------------------------------------- diff --git a/aria/modeling/models.py b/aria/modeling/models.py index fc2c669..90238e6 100644 --- a/aria/modeling/models.py +++ b/aria/modeling/models.py @@ -19,14 +19,15 @@ from sqlalchemy.ext.declarative import declarative_base from . import ( service_template, - service, + service_instance, + service_changes, + service_common, orchestration, - misc, - bases, + mixins, ) -aria_declarative_base = declarative_base(cls=bases.ModelIDMixin) +aria_declarative_base = declarative_base(cls=mixins.ModelIDMixin) # region service template models @@ -84,91 +85,99 @@ class ArtifactTemplate(aria_declarative_base, service_template.ArtifactTemplateB # region service instance models -class Service(aria_declarative_base, service.ServiceBase): +class Service(aria_declarative_base, service_instance.ServiceBase): pass -class Node(aria_declarative_base, service.NodeBase): +class Node(aria_declarative_base, service_instance.NodeBase): pass -class Group(aria_declarative_base, service.GroupBase): +class Group(aria_declarative_base, service_instance.GroupBase): pass -class Policy(aria_declarative_base, service.PolicyBase): +class Policy(aria_declarative_base, service_instance.PolicyBase): pass -class Substitution(aria_declarative_base, service.SubstitutionBase): +class Substitution(aria_declarative_base, service_instance.SubstitutionBase): pass -class SubstitutionMapping(aria_declarative_base, service.SubstitutionMappingBase): +class SubstitutionMapping(aria_declarative_base, service_instance.SubstitutionMappingBase): pass -class Relationship(aria_declarative_base, service.RelationshipBase): +class Relationship(aria_declarative_base, service_instance.RelationshipBase): pass -class Capability(aria_declarative_base, service.CapabilityBase): +class Capability(aria_declarative_base, service_instance.CapabilityBase): pass -class Interface(aria_declarative_base, service.InterfaceBase): +class Interface(aria_declarative_base, service_instance.InterfaceBase): pass -class Operation(aria_declarative_base, service.OperationBase): +class Operation(aria_declarative_base, service_instance.OperationBase): pass -class Artifact(aria_declarative_base, service.ArtifactBase): +class Artifact(aria_declarative_base, service_instance.ArtifactBase): pass # endregion -# region orchestration models +# region service changes models -class Execution(aria_declarative_base, orchestration.Execution): +class ServiceUpdate(aria_declarative_base, service_changes.ServiceUpdateBase): pass -class ServiceUpdate(aria_declarative_base, orchestration.ServiceUpdateBase): +class ServiceUpdateStep(aria_declarative_base, service_changes.ServiceUpdateStepBase): pass -class ServiceUpdateStep(aria_declarative_base, orchestration.ServiceUpdateStepBase): +class ServiceModification(aria_declarative_base, service_changes.ServiceModificationBase): pass +# endregion + + +# region common service models -class ServiceModification(aria_declarative_base, orchestration.ServiceModificationBase): +class Parameter(aria_declarative_base, service_common.ParameterBase): pass -class Plugin(aria_declarative_base, orchestration.PluginBase): +class Type(aria_declarative_base, service_common.TypeBase): pass -class Task(aria_declarative_base, orchestration.TaskBase): +class Metadata(aria_declarative_base, service_common.MetadataBase): + pass + + +class PluginSpecification(aria_declarative_base, service_common.PluginSpecificationBase): pass # endregion -# region misc models +# region orchestration models -class Parameter(aria_declarative_base, misc.ParameterBase): +class Execution(aria_declarative_base, orchestration.ExecutionBase): pass -class Type(aria_declarative_base, misc.TypeBase): +class Plugin(aria_declarative_base, orchestration.PluginBase): pass -class Metadata(aria_declarative_base, misc.MetadataBase): +class Task(aria_declarative_base, orchestration.TaskBase): pass # endregion @@ -202,18 +211,21 @@ models_to_register = [ Operation, Artifact, - # Orchestration models - Execution, + # Service changes models ServiceUpdate, ServiceUpdateStep, ServiceModification, - Plugin, - Task, - # Misc models + # Common service models Parameter, Type, - Metadata + Metadata, + PluginSpecification, + + # Orchestration models + Execution, + Plugin, + Task ] __all__ = ( @@ -247,16 +259,19 @@ __all__ = ( 'Operation', 'Artifact', - # Orchestration models - 'Execution', + # Service changes models 'ServiceUpdate', 'ServiceUpdateStep', 'ServiceModification', - 'Plugin', - 'Task', - # Misc models + # Common service models 'Parameter', 'Type', - 'Metadata' + 'Metadata', + 'PluginSpecification', + + # Orchestration models + 'Execution', + 'Plugin', + 'Task' ) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/dd5bfa93/aria/modeling/orchestration.py ---------------------------------------------------------------------- diff --git a/aria/modeling/orchestration.py b/aria/modeling/orchestration.py index c842c07..d8bdd3c 100644 --- a/aria/modeling/orchestration.py +++ b/aria/modeling/orchestration.py @@ -14,23 +14,14 @@ # limitations under the License. """ -ARIA's storage.models module -Path: aria.storage.models - -models module holds ARIA's models. - classes: * Execution - execution implementation model. - * ServiceUpdate - service update implementation model. - * ServiceUpdateStep - service update step implementation model. - * ServiceModification - service modification implementation model. * Plugin - plugin implementation model. * Task - a task """ # pylint: disable=no-self-argument, no-member, abstract-method -from collections import namedtuple from datetime import datetime from sqlalchemy import ( @@ -49,19 +40,16 @@ from sqlalchemy.ext.declarative import declared_attr from ..orchestrator.exceptions import (TaskAbortException, TaskRetryException) from .types import (List, Dict) -from .bases import ModelMixin +from .mixins import ModelMixin __all__ = ( - 'Execution', - 'ServiceUpdateBase', - 'ServiceUpdateStepBase', - 'ServiceModificationBase', + 'ExecutionBase', 'PluginBase', 'TaskBase' ) -class Execution(ModelMixin): +class ExecutionBase(ModelMixin): """ Execution model representation. """ @@ -117,7 +105,7 @@ class Execution(ModelMixin): @declared_attr def service(cls): - return cls.many_to_one_relationship('service') + return cls._create_many_to_one_relationship('service') @declared_attr def service_name(cls): @@ -133,7 +121,7 @@ class Execution(ModelMixin): @declared_attr def service_fk(cls): - return cls.foreign_key('service') + return cls._create_foreign_key('service') # endregion @@ -145,182 +133,6 @@ class Execution(ModelMixin): ) -class ServiceUpdateBase(ModelMixin): - """ - Deployment update model representation. - """ - - steps = None - - __tablename__ = 'service_update' - - _private_fields = ['execution_fk', - 'service_fk'] - - created_at = Column(DateTime, nullable=False, index=True) - service_plan = Column(Dict, nullable=False) - service_update_nodes = Column(Dict) - service_update_service = Column(Dict) - service_update_node_templates = Column(List) - modified_entity_ids = Column(Dict) - state = Column(Text) - - @declared_attr - def execution(cls): - return cls.many_to_one_relationship('execution') - - @declared_attr - def execution_name(cls): - return association_proxy('execution', cls.name_column_name()) - - @declared_attr - def service(cls): - return cls.many_to_one_relationship('service', - backreference='updates') - - @declared_attr - def service_name(cls): - return association_proxy('service', cls.name_column_name()) - - # region foreign keys - - __private_fields__ = ['service_fk', - 'execution_fk'] - - @declared_attr - def execution_fk(cls): - return cls.foreign_key('execution', nullable=True) - - @declared_attr - def service_fk(cls): - return cls.foreign_key('service') - - # endregion - - def to_dict(self, suppress_error=False, **kwargs): - dep_update_dict = super(ServiceUpdateBase, self).to_dict(suppress_error) #pylint: disable=no-member - # Taking care of the fact the DeploymentSteps are _BaseModels - dep_update_dict['steps'] = [step.to_dict() for step in self.steps] - return dep_update_dict - - -class ServiceUpdateStepBase(ModelMixin): - """ - Deployment update step model representation. - """ - - __tablename__ = 'service_update_step' - - _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY') - ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify') - - _entity_types = namedtuple( - 'ENTITY_TYPES', - 'NODE, RELATIONSHIP, PROPERTY, OPERATION, WORKFLOW, OUTPUT, DESCRIPTION, GROUP, PLUGIN') - ENTITY_TYPES = _entity_types( - NODE='node', - RELATIONSHIP='relationship', - PROPERTY='property', - OPERATION='operation', - WORKFLOW='workflow', - OUTPUT='output', - DESCRIPTION='description', - GROUP='group', - PLUGIN='plugin' - ) - - action = Column(Enum(*ACTION_TYPES, name='action_type'), nullable=False) - entity_id = Column(Text, nullable=False) - entity_type = Column(Enum(*ENTITY_TYPES, name='entity_type'), nullable=False) - - @declared_attr - def service_update(cls): - return cls.many_to_one_relationship('service_update', - backreference='steps') - - @declared_attr - def service_update_name(cls): - return association_proxy('service_update', cls.name_column_name()) - - # region foreign keys - - __private_fields__ = ['service_update_fk'] - - @declared_attr - def service_update_fk(cls): - return cls.foreign_key('service_update') - - # endregion - - def __hash__(self): - return hash((getattr(self, self.id_column_name()), self.entity_id)) - - def __lt__(self, other): - """ - the order is 'remove' < 'modify' < 'add' - :param other: - :return: - """ - if not isinstance(other, self.__class__): - return not self >= other - - if self.action != other.action: - if self.action == 'remove': - return_value = True - elif self.action == 'add': - return_value = False - else: - return_value = other.action == 'add' - return return_value - - if self.action == 'add': - return self.entity_type == 'node' and other.entity_type == 'relationship' - if self.action == 'remove': - return self.entity_type == 'relationship' and other.entity_type == 'node' - return False - - -class ServiceModificationBase(ModelMixin): - """ - Deployment modification model representation. - """ - - __tablename__ = 'service_modification' - - STARTED = 'started' - FINISHED = 'finished' - ROLLEDBACK = 'rolledback' - - STATES = [STARTED, FINISHED, ROLLEDBACK] - END_STATES = [FINISHED, ROLLEDBACK] - - context = Column(Dict) - created_at = Column(DateTime, nullable=False, index=True) - ended_at = Column(DateTime, index=True) - modified_node_templates = Column(Dict) - nodes = Column(Dict) - status = Column(Enum(*STATES, name='service_modification_status')) - - @declared_attr - def service(cls): - return cls.many_to_one_relationship('service', - backreference='modifications') - - @declared_attr - 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. @@ -340,16 +152,6 @@ class PluginBase(ModelMixin): uploaded_at = Column(DateTime, nullable=False, index=True) wheels = Column(List, nullable=False) - # region foreign keys - - __private_fields__ = ['service_template_fk'] - - @declared_attr - def service_template_fk(cls): - return cls.foreign_key('service_template', nullable=True) - - # endregion - class TaskBase(ModelMixin): """ @@ -389,7 +191,7 @@ class TaskBase(ModelMixin): @declared_attr def node(cls): - return cls.many_to_one_relationship('node') + return cls._create_many_to_one_relationship('node') @declared_attr def relationship_name(cls): @@ -397,15 +199,15 @@ class TaskBase(ModelMixin): @declared_attr def relationship(cls): - return cls.many_to_one_relationship('relationship') + return cls._create_many_to_one_relationship('relationship') @declared_attr def plugin(cls): - return cls.many_to_one_relationship('plugin') + return cls._create_many_to_one_relationship('plugin') @declared_attr def execution(cls): - return cls.many_to_one_relationship('execution') + return cls._create_many_to_one_relationship('execution') @declared_attr def execution_name(cls): @@ -413,8 +215,8 @@ class TaskBase(ModelMixin): @declared_attr def inputs(cls): - return cls.many_to_many_relationship('parameter', table_prefix='inputs', - dict_key='name') + return cls._create_many_to_many_relationship('parameter', table_prefix='inputs', + dict_key='name') status = Column(Enum(*STATES, name='status'), default=PENDING) @@ -428,9 +230,6 @@ class TaskBase(ModelMixin): # Operation specific fields implementation = Column(String) - # This is unrelated to the plugin of the task. This field is related to the plugin name - # received from the blueprint. - plugin_name = Column(String) _runs_on = Column(Enum(*RUNS_ON, name='runs_on'), name='runs_on') @property @@ -468,28 +267,28 @@ class TaskBase(ModelMixin): @declared_attr def node_fk(cls): - return cls.foreign_key('node', nullable=True) + return cls._create_foreign_key('node', nullable=True) @declared_attr def relationship_fk(cls): - return cls.foreign_key('relationship', nullable=True) + return cls._create_foreign_key('relationship', nullable=True) @declared_attr def plugin_fk(cls): - return cls.foreign_key('plugin', nullable=True) + return cls._create_foreign_key('plugin', nullable=True) @declared_attr def execution_fk(cls): - return cls.foreign_key('execution', nullable=True) + return cls._create_foreign_key('execution', nullable=True) # endregion @classmethod - def as_node_task(cls, instance, runs_on, **kwargs): + def for_node(cls, instance, runs_on, **kwargs): return cls(node=instance, _runs_on=runs_on, **kwargs) @classmethod - def as_relationship_task(cls, instance, runs_on, **kwargs): + def for_relationship(cls, instance, runs_on, **kwargs): return cls(relationship=instance, _runs_on=runs_on, **kwargs) @staticmethod
