Repository: incubator-ariatosca Updated Branches: refs/heads/ARIA-105-integrate-modeling 16a945383 -> f6da64acb
New relationship decorators Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/f6da64ac Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/f6da64ac Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/f6da64ac Branch: refs/heads/ARIA-105-integrate-modeling Commit: f6da64acbf8baa02be486b0d15a62606662811d0 Parents: 16a9453 Author: Tal Liron <[email protected]> Authored: Mon Mar 13 20:31:52 2017 -0500 Committer: Tal Liron <[email protected]> Committed: Mon Mar 13 20:31:52 2017 -0500 ---------------------------------------------------------------------- aria/modeling/mixins.py | 7 +- aria/modeling/relationship.py | 421 +++++++++++++++++++++++++++++++++ aria/modeling/service_common.py | 2 +- aria/modeling/service_template.py | 141 +++++------ 4 files changed, 487 insertions(+), 84 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/f6da64ac/aria/modeling/mixins.py ---------------------------------------------------------------------- diff --git a/aria/modeling/mixins.py b/aria/modeling/mixins.py index b9e5079..9133a85 100644 --- a/aria/modeling/mixins.py +++ b/aria/modeling/mixins.py @@ -145,13 +145,12 @@ class ModelMixin(object): )) return cls._create_relationship(cls.__tablename__, None, relationship_kwargs, - backreference='', dict_key=dict_key) + backreference=False, 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): @@ -159,7 +158,7 @@ class ModelMixin(object): backref_kwargs.setdefault('uselist', False) return cls._create_relationship(other_table, backref_kwargs, relationship_kwargs, - backreference, key=key, foreign_key=foreign_key) + backreference, key=key) @classmethod def _create_one_to_many_relationship(cls, @@ -282,7 +281,7 @@ class ModelMixin(object): relationship_kwargs.setdefault('collection_class', attribute_mapped_collection(dict_key)) - if backreference == '': + if backreference is False: return relationship( lambda: cls._get_cls_by_tablename(table), **relationship_kwargs http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/f6da64ac/aria/modeling/relationship.py ---------------------------------------------------------------------- diff --git a/aria/modeling/relationship.py b/aria/modeling/relationship.py new file mode 100644 index 0000000..2fdbe0c --- /dev/null +++ b/aria/modeling/relationship.py @@ -0,0 +1,421 @@ +# 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=invalid-name, redefined-outer-name, unused-argument + +from functools import wraps + +from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Table +) + +from ..utils import formatting + + +def fk(other_table, + nullable=False): + """ + Decorator for a foreign key property, which will also create a foreign key column in the table + with the name of the property. By convention the property name should end in "_fk". + + You are required to explicitly create foreign keys in order to allow for one-to-one, + one-to-many, and many-to-one relationships. If you do not do so, SqlAlchemy will fail to create + the relationship and raise an exception with a clear error message. + + You should normally not have to access this property directly, but instead use the + associated relationship properties. + + :param other_table: Other table name + :type other_table: basestring + :param nullable: True to allow null values (meaning that there is no relationship) + :type nullable: bool + """ + + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + if not hasattr(cls, '__private_fields__'): + cls.__private_fields__ = [] + cls.__private_fields__.append(fn.__name__) + return Column(Integer, + ForeignKey('{table}.id'.format(table=other_table), ondelete='CASCADE'), + nullable=nullable) + return attr + return decorator + + +def one_to_one_self(fk, + relationship_kwargs=None): + """ + Decorator for a one-to-one relationship property. The property value would be an instance of + the same model. + + You will need an associated foreign key to our own table. + + :param fk: Foreign key name + :type fk: basestring + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + relationship_kwargs = scope['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=fk + ) + + return relationship( + _get_class_for_table(cls, cls.__tablename__).__name__, + primaryjoin=primaryjoin, + remote_side=remote_side, + post_update=True, + **relationship_kwargs + ) + return attr + return decorator + + +def one_to_many_self(fk, + dict_key=None, + relationship_kwargs=None): + """ + Decorator for a one-to-many relationship property. The property value would be a list or dict + of instances of the same model. + + You will need an associated foreign key to our own table. + + :param fk: Foreign key name + :type fk: basestring + :param dict_key: If set the value will be a dict with this key as the dict key; otherwise will + be a list + :type dict_key: basestring + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + relationship_kwargs = scope['relationship_kwargs'] or {} + + relationship_kwargs.setdefault('remote_side', '{cls}.{remote_column}'.format( + cls=cls.__name__, + remote_column=fk + )) + + return _create_relationship(cls, cls.__tablename__, None, relationship_kwargs, + other_property=False, dict_key=dict_key) + return attr + return decorator + + +def one_to_one(other_table, + fk=None, + other_fk=None, + other_property=None, + relationship_kwargs=None, + backref_kwargs=None): + """ + Decorator for a one-to-one relationship property. The property value would be an instance of + the other table's model. + + You have two options for the foreign key. Either this table can have an associated key to the + other table (use the `fk` argument) or the other table can have an associated foreign key to + this our table (use the `other_fk` argument). + + :param other_table: Other table name + :type other_table: basestring + :param fk: Foreign key name at our table (no need specify if there's no ambiguity) + :type fk: basestring + :param other_fk: Foreign key name at the other table (no need specify if there's no ambiguity) + :type other_fk: basestring + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + :param backref_kwargs: Extra kwargs for SqlAlchemy `backref` + :type backref_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + backref_kwargs = scope['backref_kwargs'] or {} + backref_kwargs.setdefault('uselist', False) + + return _create_relationship(cls, other_table, backref_kwargs, relationship_kwargs, + other_property, fk=fk, other_fk=other_fk) + return attr + return decorator + + +def one_to_many(child_table, + child_fk=None, + dict_key=None, + child_property=None, + relationship_kwargs=None, + backref_kwargs=None): + """ + Decorator for a one-to-many relationship property. The property value would be a list or dict + of instances of the child table's model. + + The child table will need an associated foreign key to our table. + + The decorator will automatically create a matching many-to-one property at the child model, + named after our table name. Use the `child_property` argument to override this name. + + :param child_table: Child table name + :type child_table: basestring + :param child_fk: Foreign key name at the child table (no need specify if there's no ambiguity) + :type child_fk: basestring + :param dict_key: If set the value will be a dict with this key as the dict key; otherwise will + be a list + :type dict_key: basestring + :param child_property: Override name of matching many-to-one property at child table; set to + false to disable + :type child_property: basestring|bool + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + :param backref_kwargs: Extra kwargs for SqlAlchemy `backref` + :type backref_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + backref_kwargs = scope['backref_kwargs'] or {} + backref_kwargs.setdefault('uselist', False) + + return _create_relationship(cls, child_table, backref_kwargs, relationship_kwargs, + child_property, other_fk=child_fk, dict_key=dict_key) + return attr + return decorator + + +def many_to_one(parent_table, + fk=None, + parent_fk=None, + parent_property=None, + relationship_kwargs=None, + backref_kwargs=None): + """ + Decorator for a many-to-one relationship property. The property value would be an instance of + the parent table's model. + + You will need an associated foreign key to the parent table. + + The decorator will automatically create a matching one-to-many property at the child model, + named after the plural form of our table name. Use the `parent_property` argument to override + this name. Note: the automatic property will always be a list; if you need it to be a dict, use + the :func:`one_to_many` decorator on that model instead. + + :param parent_table: Parent table name + :type parent_table: basestring + :param fk: Foreign key name at our table (no need specify if there's no ambiguity) + :type fk: basestring + :param parent_property: Override name of matching one-to-many property at parent table; set to + false to disable + :type parent_property: basestring|bool + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + :param backref_kwargs: Extra kwargs for SqlAlchemy `backref` + :type backref_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + parent_property = scope['parent_property'] + if parent_property is None: + parent_property = formatting.pluralize(cls.__tablename__) + + backref_kwargs = scope['backref_kwargs'] or {} + backref_kwargs.setdefault('uselist', True) + backref_kwargs.setdefault('lazy', 'dynamic') + backref_kwargs.setdefault('cascade', 'all') # delete children when parent is deleted + + return _create_relationship(cls, parent_table, backref_kwargs, relationship_kwargs, + parent_property, fk=fk, other_fk=parent_fk) + return attr + return decorator + + +def many_to_many(other_table, + prefix=None, + dict_key=None, + other_property=None, + relationship_kwargs=None, + backref_kwargs=None): + """ + Decorator for a many-to-many relationship property. The property value would be a list or dict + of instances of the other table's model. + + You do not need associated foreign keys for this relationship. Instead, an extra table will be + created for you. + + The decorator will automatically create a matching many-to-many property at the other model, + named after the plural form of our table name. Use the `other_property` argument to override + this name. Note: the automatic property will always be a list; if you need it to be a dict, use + the `many_to_many` decorator again on that model. + + :param parent_table: Parent table name + :type parent_table: basestring + :param prefix: Optional prefix for extra table name as well as for `other_property` + :type prefix: basestring + :param dict_key: If set the value will be a dict with this key as the dict key; otherwise will + be a list + :type dict_key: basestring + :param other_property: Override name of matching many-to-many property at other table; set to + false to disable + :type other_property: basestring|bool + :param relationship_kwargs: Extra kwargs for SqlAlchemy `relationship` + :type relationship_kwargs: {} + :param backref_kwargs: Extra kwargs for SqlAlchemy `backref` + :type backref_kwargs: {} + """ + + scope = locals() + def decorator(fn): + @declared_attr + @wraps(fn) + def attr(cls): + 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) + + secondary_table = '{0}_{1}'.format(this_table, other_table) + + other_property = scope['other_property'] + if other_property is None: + other_property = formatting.pluralize(this_table) + if prefix is not None: + secondary_table = '{0}_{1}'.format(prefix, secondary_table) + other_property = '{0}_{1}'.format(prefix, other_property) + + backref_kwargs = scope['backref_kwargs'] or {} + backref_kwargs.setdefault('uselist', True) + + relationship_kwargs = scope['relationship_kwargs'] or {} + relationship_kwargs.setdefault('secondary', _get_secondary_table( + cls.metadata, + secondary_table, + this_column_name, + other_column_name, + this_foreign_key, + other_foreign_key + )) + + return _create_relationship(cls, other_table, backref_kwargs, relationship_kwargs, + other_property, dict_key=dict_key) + return attr + return decorator + + +def _create_relationship(cls, other_table, backref_kwargs, relationship_kwargs, other_property, + fk=None, other_fk=None, dict_key=None): + relationship_kwargs = relationship_kwargs or {} + + if fk: + relationship_kwargs.setdefault('foreign_keys', + lambda: getattr( + _get_class_for_table(cls, cls.__tablename__), + fk)) + + elif other_fk: + relationship_kwargs.setdefault('foreign_keys', + lambda: getattr( + _get_class_for_table(cls, other_table), + other_fk)) + + if dict_key: + relationship_kwargs.setdefault('collection_class', + attribute_mapped_collection(dict_key)) + + if other_property is False: + # No backref + return relationship( + lambda: _get_class_for_table(cls, other_table), + **relationship_kwargs + ) + else: + if other_property is None: + other_property = cls.__tablename__ + backref_kwargs = backref_kwargs or {} + return relationship( + lambda: _get_class_for_table(cls, other_table), + backref=backref(other_property, **backref_kwargs), + **relationship_kwargs + ) + + +def _get_secondary_table(metadata, + name, + first_column, + second_column, + first_foreign_key, + second_foreign_key): + return Table( + name, + metadata, + Column( + first_column, + Integer, + ForeignKey(first_foreign_key) + ), + Column( + second_column, + Integer, + ForeignKey(second_foreign_key) + ) + ) + + +def _get_class_for_table(cls, tablename): + 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 + + raise ValueError('unknown table: {0}'.format(tablename)) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/f6da64ac/aria/modeling/service_common.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service_common.py b/aria/modeling/service_common.py index e6b3c8d..7422211 100644 --- a/aria/modeling/service_common.py +++ b/aria/modeling/service_common.py @@ -191,7 +191,7 @@ class TypeBase(InstanceModelMixin): self._append_raw_children(types) return types - def dump(self,): + def dump(self): context = ConsumptionContext.get_thread_local() if self.name: console.puts(context.style.type(self.name)) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/f6da64ac/aria/modeling/service_template.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service_template.py b/aria/modeling/service_template.py index 09da8e1..d2d6c4b 100644 --- a/aria/modeling/service_template.py +++ b/aria/modeling/service_template.py @@ -38,6 +38,8 @@ from . import ( types as modeling_types ) +from . import relationship + class ServiceTemplateBase(TemplateModelMixin): # pylint: disable=too-many-public-methods """ @@ -100,76 +102,70 @@ class ServiceTemplateBase(TemplateModelMixin): # pylint: disable=too-many-public description = Column(Text) main_file_name = Column(Text) - @declared_attr + @relationship.many_to_many('metadata', dict_key='name') def meta_data(cls): # Warning! We cannot use the attr name "metadata" because it's used by SqlAlchemy! - return cls._create_many_to_many_relationship('metadata', dict_key='name') + pass - @declared_attr + @relationship.one_to_many('node_template') def node_templates(cls): - return cls._create_one_to_many_relationship('node_template') + pass - @declared_attr + @relationship.one_to_many('group_template') def group_templates(cls): - return cls._create_one_to_many_relationship('group_template') + pass - @declared_attr + @relationship.one_to_many('policy_template') def policy_templates(cls): - return cls._create_one_to_many_relationship('policy_template') + pass - @declared_attr + @relationship.one_to_one('substitution_template') def substitution_template(cls): - return cls._create_one_to_one_relationship('substitution_template') + pass - @declared_attr + @relationship.many_to_many('parameter', prefix='inputs', dict_key='name') def inputs(cls): - return cls._create_many_to_many_relationship('parameter', table_prefix='inputs', - dict_key='name') + pass - @declared_attr + @relationship.many_to_many('parameter', prefix='outputs', dict_key='name') def outputs(cls): - return cls._create_many_to_many_relationship('parameter', table_prefix='outputs', - dict_key='name') + pass - @declared_attr + @relationship.one_to_many('operation_template', dict_key='name') def workflow_templates(cls): - return cls._create_one_to_many_relationship('operation_template', dict_key='name') + pass - @declared_attr + @relationship.one_to_many('plugin_specification') def plugin_specifications(cls): - return cls._create_one_to_many_relationship('plugin_specification') + pass - @declared_attr + @relationship.one_to_one('type', fk='node_type_fk', other_property=False) def node_types(cls): - return cls._create_one_to_one_relationship('type', key='node_type_fk', backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='group_type_fk', other_property=False) def group_types(cls): - return cls._create_one_to_one_relationship('type', key='group_type_fk', backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='policy_type_fk', other_property=False) def policy_types(cls): - return cls._create_one_to_one_relationship('type', key='policy_type_fk', backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='relationship_type_fk', other_property=False) def relationship_types(cls): - return cls._create_one_to_one_relationship('type', key='relationship_type_fk', - backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='capability_type_fk', other_property=False) def capability_types(cls): - return cls._create_one_to_one_relationship('type', key='capability_type_fk', - backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='interface_type_fk', other_property=False) def interface_types(cls): - return cls._create_one_to_one_relationship('type', key='interface_type_fk', - backreference='') + pass - @declared_attr + @relationship.one_to_one('type', fk='artifact_type_fk', other_property=False) def artifact_types(cls): - return cls._create_one_to_one_relationship('type', key='artifact_type_fk', - backreference='') + pass # region orchestration @@ -180,54 +176,37 @@ class ServiceTemplateBase(TemplateModelMixin): # pylint: disable=too-many-public # region foreign keys - __private_fields__ = ['substitution_template_fk', - 'node_type_fk', - 'group_type_fk', - 'policy_type_fk', - 'relationship_type_fk', - 'capability_type_fk', - 'interface_type_fk', - 'artifact_type_fk'] - - # ServiceTemplate one-to-one to SubstitutionTemplate - @declared_attr + @relationship.fk('substitution_template', nullable=True) def substitution_template_fk(cls): - return cls._create_foreign_key('substitution_template', nullable=True) + """For ServiceTemplate one-to-one to SubstitutionTemplate""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def node_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def group_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def policy_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def relationship_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def capability_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def interface_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" - # ServiceTemplate one-to-one to Type - @declared_attr + @relationship.fk('type', nullable=True) def artifact_type_fk(cls): - return cls._create_foreign_key('type', nullable=True) + """For ServiceTemplate one-to-one to Type""" # endregion @@ -343,6 +322,10 @@ class ServiceTemplateBase(TemplateModelMixin): # pylint: disable=too-many-public utils.coerce_dict_values(container, self.workflow_templates, report_issues) def dump(self): + #print dir(self) + #from inspect import getdoc + #print getdoc(self.__class__.substitution_template_fk) + #exit() context = ConsumptionContext.get_thread_local() if self.description is not None: console.puts(context.style.meta(self.description)) @@ -980,17 +963,17 @@ class RequirementTemplateBase(TemplateModelMixin): @declared_attr def target_node_type(cls): return cls._create_many_to_one_relationship('type', key='target_node_type_fk', - backreference='') + backreference=False) @declared_attr def target_node_template(cls): return cls._create_one_to_one_relationship('node_template', key='target_node_template_fk', - backreference='') + backreference=False) @declared_attr def target_capability_type(cls): return cls._create_one_to_one_relationship('type', key='target_capability_type_fk', - backreference='') + backreference=False) target_capability_name = Column(Text) target_node_template_constraints = Column(modeling_types.StrictList(FunctionType)) @@ -1196,13 +1179,13 @@ class RelationshipTemplateBase(TemplateModelMixin): def instantiate(self, container): from . import models - relationship = models.Relationship(type=self.type, - relationship_template=self) + relationship_model = models.Relationship(type=self.type, + relationship_template=self) utils.instantiate_dict(container, - relationship.properties, self.properties) + relationship_model.properties, self.properties) utils.instantiate_dict(container, - relationship.interfaces, self.interface_templates) - return relationship + relationship_model.interfaces, self.interface_templates) + return relationship_model def validate(self): # TODO: either type or name must be set
