summaryrefslogtreecommitdiffstats
path: root/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py
blob: 0d906de838842b44d55b0da3f58336246c4cf259 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
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))
    )