Merge "vFW and vDNS support added to azure-plugin"
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / modeling / relationship.py
diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py
new file mode 100644 (file)
index 0000000..0d906de
--- /dev/null
@@ -0,0 +1,395 @@
+# 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 modeling relationship module
+"""
+
+# pylint: disable=invalid-name, redefined-outer-name
+
+from sqlalchemy.orm import relationship, backref
+from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.ext.associationproxy import association_proxy as original_association_proxy
+from sqlalchemy import (
+    Column,
+    ForeignKey,
+    Integer,
+    Table
+)
+
+from ..utils import formatting
+
+NO_BACK_POP = 'NO_BACK_POP'
+
+
+def foreign_key(other_table, nullable=False):
+    """
+    Declare 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 (but not for many-to-many relationships). If you do
+    not do so, SQLAlchemy will fail to create the relationship property 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.
+
+    *This utility method should only be used during class creation.*
+
+    :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
+    """
+
+    return Column(Integer,
+                  ForeignKey('{table}.id'.format(table=other_table), ondelete='CASCADE'),
+                  nullable=nullable)
+
+
+def one_to_one_self(model_class, fk):
+    """
+    Declare 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.
+
+    *This utility method should only be used during class creation.*
+
+    :param model_class: class in which this relationship will be declared
+    :type model_class: type
+    :param fk: foreign key name
+    :type fk: basestring
+    """
+
+    remote_side = '{model_class}.{remote_column}'.format(
+        model_class=model_class.__name__,
+        remote_column=model_class.id_column_name()
+    )
+
+    primaryjoin = '{remote_side} == {model_class}.{column}'.format(
+        remote_side=remote_side,
+        model_class=model_class.__name__,
+        column=fk
+    )
+    return _relationship(
+        model_class,
+        model_class.__tablename__,
+        relationship_kwargs={
+            'primaryjoin': primaryjoin,
+            'remote_side': remote_side,
+            'post_update': True
+        }
+    )
+
+
+def one_to_one(model_class,
+               other_table,
+               fk=None,
+               other_fk=None,
+               back_populates=None):
+    """
+    Declare 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).
+
+    *This utility method should only be used during class creation.*
+
+    :param model_class: class in which this relationship will be declared
+    :type model_class: type
+    :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 back_populates: override name of matching many-to-many property at other table; set to
+     ``False`` to disable
+    :type back_populates: basestring or bool
+    """
+    backref_kwargs = None
+    if back_populates is not NO_BACK_POP:
+        if back_populates is None:
+            back_populates = model_class.__tablename__
+        backref_kwargs = {'name': back_populates, 'uselist': False}
+        back_populates = None
+
+    return _relationship(model_class,
+                         other_table,
+                         fk=fk,
+                         back_populates=back_populates,
+                         backref_kwargs=backref_kwargs,
+                         other_fk=other_fk)
+
+
+def one_to_many(model_class,
+                other_table=None,
+                other_fk=None,
+                dict_key=None,
+                back_populates=None,
+                rel_kwargs=None,
+                self=False):
+    """
+    Declare 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 declaration 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.
+
+    *This utility method should only be used during class creation.*
+
+    :param model_class: class in which this relationship will be declared
+    :type model_class: type
+    :param other_table: other table name
+    :type other_table: basestring
+    :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity)
+    :type other_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 back_populates: override name of matching many-to-one property at other table; set to
+     ``False`` to disable
+    :type back_populates: basestring or bool
+    :param rel_kwargs: additional relationship kwargs to be used by SQLAlchemy
+    :type rel_kwargs: dict
+    :param self: used for relationships between a table and itself. if set, other_table will
+     become the same as the source table.
+    :type self: bool
+    """
+    relationship_kwargs = rel_kwargs or {}
+    if self:
+        assert other_fk
+        other_table_name = model_class.__tablename__
+        back_populates = False
+        relationship_kwargs['remote_side'] = '{model}.{column}'.format(model=model_class.__name__,
+                                                                       column=other_fk)
+
+    else:
+        assert other_table
+        other_table_name = other_table
+        if back_populates is None:
+            back_populates = model_class.__tablename__
+        relationship_kwargs.setdefault('cascade', 'all')
+
+    return _relationship(
+        model_class,
+        other_table_name,
+        back_populates=back_populates,
+        other_fk=other_fk,
+        dict_key=dict_key,
+        relationship_kwargs=relationship_kwargs)
+
+
+def many_to_one(model_class,
+                parent_table,
+                fk=None,
+                parent_fk=None,
+                back_populates=None):
+    """
+    Declare 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 declaration 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 SQLAlchemy query object; if you need a
+    Python collection then use :func:`one_to_many` at that model.
+
+    *This utility method should only be used during class creation.*
+
+    :param model_class: class in which this relationship will be declared
+    :type model_class: type
+    :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 back_populates: override name of matching one-to-many property at parent table; set to
+     ``False`` to disable
+    :type back_populates: basestring or bool
+    """
+    if back_populates is None:
+        back_populates = formatting.pluralize(model_class.__tablename__)
+
+    return _relationship(model_class,
+                         parent_table,
+                         back_populates=back_populates,
+                         fk=fk,
+                         other_fk=parent_fk)
+
+
+def many_to_many(model_class,
+                 other_table=None,
+                 prefix=None,
+                 dict_key=None,
+                 other_property=None,
+                 self=False):
+    """
+    Declare 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 declaration 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 SQLAlchemy query object; if you need a
+    Python collection then use :func:`many_to_many` again at that model.
+
+    *This utility method should only be used during class creation.*
+
+    :param model_class: class in which this relationship will be declared
+    :type model_class: type
+    :param other_table: parent table name
+    :type other_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 or bool
+    :param self: used for relationships between a table and itself. if set, other_table will
+     become the same as the source table.
+    :type self: bool
+    """
+
+    this_table = model_class.__tablename__
+    this_column_name = '{0}_id'.format(this_table)
+    this_foreign_key = '{0}.id'.format(this_table)
+
+    if self:
+        other_table = this_table
+
+    other_column_name = '{0}_{1}'.format(other_table, 'self_ref_id' if self else 'id')
+    other_foreign_key = '{0}.{1}'.format(other_table, 'id')
+
+    secondary_table_name = '{0}_{1}'.format(this_table, other_table)
+
+    if prefix is not None:
+        secondary_table_name = '{0}_{1}'.format(prefix, secondary_table_name)
+        if other_property is None:
+            other_property = '{0}_{1}'.format(prefix, formatting.pluralize(this_table))
+
+    secondary_table = _get_secondary_table(
+        model_class.metadata,
+        secondary_table_name,
+        this_column_name,
+        other_column_name,
+        this_foreign_key,
+        other_foreign_key
+    )
+
+    kwargs = {'relationship_kwargs': {'secondary': secondary_table}}
+
+    if self:
+        kwargs['back_populates'] = NO_BACK_POP
+        kwargs['relationship_kwargs']['primaryjoin'] = \
+                    getattr(model_class, 'id') == getattr(secondary_table.c, this_column_name)
+        kwargs['relationship_kwargs']['secondaryjoin'] = \
+            getattr(model_class, 'id') == getattr(secondary_table.c, other_column_name)
+    else:
+        kwargs['backref_kwargs'] = \
+            {'name': other_property, 'uselist': True} if other_property else None
+        kwargs['dict_key'] = dict_key
+
+    return _relationship(model_class, other_table, **kwargs)
+
+
+def association_proxy(*args, **kwargs):
+    if 'type' in kwargs:
+        type_ = kwargs.get('type')
+        del kwargs['type']
+    else:
+        type_ = ':obj:`basestring`'
+    proxy = original_association_proxy(*args, **kwargs)
+    proxy.__doc__ = """
+    Internal. For use in SQLAlchemy queries.
+
+    :type: {0}
+    """.format(type_)
+    return proxy
+
+
+def _relationship(model_class,
+                  other_table_name,
+                  back_populates=None,
+                  backref_kwargs=None,
+                  relationship_kwargs=None,
+                  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(model_class, model_class.__tablename__), fk)
+        )
+
+    elif other_fk:
+        relationship_kwargs.setdefault(
+            'foreign_keys',
+            lambda: getattr(_get_class_for_table(model_class, other_table_name), other_fk)
+        )
+
+    if dict_key:
+        relationship_kwargs.setdefault('collection_class',
+                                       attribute_mapped_collection(dict_key))
+
+    if backref_kwargs:
+        assert back_populates is None
+        return relationship(
+            lambda: _get_class_for_table(model_class, other_table_name),
+            backref=backref(**backref_kwargs),
+            **relationship_kwargs
+        )
+    else:
+        if back_populates is not NO_BACK_POP:
+            relationship_kwargs['back_populates'] = back_populates
+        return relationship(lambda: _get_class_for_table(model_class, other_table_name),
+                            **relationship_kwargs)
+
+
+def _get_class_for_table(model_class, tablename):
+    if tablename in (model_class.__name__, model_class.__tablename__):
+        return model_class
+
+    for table_cls in model_class._decl_class_registry.itervalues():
+        if tablename == getattr(table_cls, '__tablename__', None):
+            return table_cls
+
+    raise ValueError('unknown table: {0}'.format(tablename))
+
+
+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))
+    )