vFW and vDNS support added to azure-plugin
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / modeling / relationship.py
1 # Licensed to the Apache Software Foundation (ASF) under one or more
2 # contributor license agreements.  See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License.  You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 """
17 ARIA modeling relationship module
18 """
19
20 # pylint: disable=invalid-name, redefined-outer-name
21
22 from sqlalchemy.orm import relationship, backref
23 from sqlalchemy.orm.collections import attribute_mapped_collection
24 from sqlalchemy.ext.associationproxy import association_proxy as original_association_proxy
25 from sqlalchemy import (
26     Column,
27     ForeignKey,
28     Integer,
29     Table
30 )
31
32 from ..utils import formatting
33
34 NO_BACK_POP = 'NO_BACK_POP'
35
36
37 def foreign_key(other_table, nullable=False):
38     """
39     Declare a foreign key property, which will also create a foreign key column in the table with
40     the name of the property. By convention the property name should end in "_fk".
41
42     You are required to explicitly create foreign keys in order to allow for one-to-one,
43     one-to-many, and many-to-one relationships (but not for many-to-many relationships). If you do
44     not do so, SQLAlchemy will fail to create the relationship property and raise an exception with
45     a clear error message.
46
47     You should normally not have to access this property directly, but instead use the associated
48     relationship properties.
49
50     *This utility method should only be used during class creation.*
51
52     :param other_table: other table name
53     :type other_table: basestring
54     :param nullable: ``True`` to allow null values (meaning that there is no relationship)
55     :type nullable: bool
56     """
57
58     return Column(Integer,
59                   ForeignKey('{table}.id'.format(table=other_table), ondelete='CASCADE'),
60                   nullable=nullable)
61
62
63 def one_to_one_self(model_class, fk):
64     """
65     Declare a one-to-one relationship property. The property value would be an instance of the same
66     model.
67
68     You will need an associated foreign key to our own table.
69
70     *This utility method should only be used during class creation.*
71
72     :param model_class: class in which this relationship will be declared
73     :type model_class: type
74     :param fk: foreign key name
75     :type fk: basestring
76     """
77
78     remote_side = '{model_class}.{remote_column}'.format(
79         model_class=model_class.__name__,
80         remote_column=model_class.id_column_name()
81     )
82
83     primaryjoin = '{remote_side} == {model_class}.{column}'.format(
84         remote_side=remote_side,
85         model_class=model_class.__name__,
86         column=fk
87     )
88     return _relationship(
89         model_class,
90         model_class.__tablename__,
91         relationship_kwargs={
92             'primaryjoin': primaryjoin,
93             'remote_side': remote_side,
94             'post_update': True
95         }
96     )
97
98
99 def one_to_one(model_class,
100                other_table,
101                fk=None,
102                other_fk=None,
103                back_populates=None):
104     """
105     Declare a one-to-one relationship property. The property value would be an instance of the other
106     table's model.
107
108     You have two options for the foreign key. Either this table can have an associated key to the
109     other table (use the ``fk`` argument) or the other table can have an associated foreign key to
110     this our table (use the ``other_fk`` argument).
111
112     *This utility method should only be used during class creation.*
113
114     :param model_class: class in which this relationship will be declared
115     :type model_class: type
116     :param other_table: other table name
117     :type other_table: basestring
118     :param fk: foreign key name at our table (no need specify if there's no ambiguity)
119     :type fk: basestring
120     :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity)
121     :type other_fk: basestring
122     :param back_populates: override name of matching many-to-many property at other table; set to
123      ``False`` to disable
124     :type back_populates: basestring or bool
125     """
126     backref_kwargs = None
127     if back_populates is not NO_BACK_POP:
128         if back_populates is None:
129             back_populates = model_class.__tablename__
130         backref_kwargs = {'name': back_populates, 'uselist': False}
131         back_populates = None
132
133     return _relationship(model_class,
134                          other_table,
135                          fk=fk,
136                          back_populates=back_populates,
137                          backref_kwargs=backref_kwargs,
138                          other_fk=other_fk)
139
140
141 def one_to_many(model_class,
142                 other_table=None,
143                 other_fk=None,
144                 dict_key=None,
145                 back_populates=None,
146                 rel_kwargs=None,
147                 self=False):
148     """
149     Declare a one-to-many relationship property. The property value would be a list or dict of
150     instances of the child table's model.
151
152     The child table will need an associated foreign key to our table.
153
154     The declaration will automatically create a matching many-to-one property at the child model,
155     named after our table name. Use the ``child_property`` argument to override this name.
156
157     *This utility method should only be used during class creation.*
158
159     :param model_class: class in which this relationship will be declared
160     :type model_class: type
161     :param other_table: other table name
162     :type other_table: basestring
163     :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity)
164     :type other_fk: basestring
165     :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will
166      be a list
167     :type dict_key: basestring
168     :param back_populates: override name of matching many-to-one property at other table; set to
169      ``False`` to disable
170     :type back_populates: basestring or bool
171     :param rel_kwargs: additional relationship kwargs to be used by SQLAlchemy
172     :type rel_kwargs: dict
173     :param self: used for relationships between a table and itself. if set, other_table will
174      become the same as the source table.
175     :type self: bool
176     """
177     relationship_kwargs = rel_kwargs or {}
178     if self:
179         assert other_fk
180         other_table_name = model_class.__tablename__
181         back_populates = False
182         relationship_kwargs['remote_side'] = '{model}.{column}'.format(model=model_class.__name__,
183                                                                        column=other_fk)
184
185     else:
186         assert other_table
187         other_table_name = other_table
188         if back_populates is None:
189             back_populates = model_class.__tablename__
190         relationship_kwargs.setdefault('cascade', 'all')
191
192     return _relationship(
193         model_class,
194         other_table_name,
195         back_populates=back_populates,
196         other_fk=other_fk,
197         dict_key=dict_key,
198         relationship_kwargs=relationship_kwargs)
199
200
201 def many_to_one(model_class,
202                 parent_table,
203                 fk=None,
204                 parent_fk=None,
205                 back_populates=None):
206     """
207     Declare a many-to-one relationship property. The property value would be an instance of the
208     parent table's model.
209
210     You will need an associated foreign key to the parent table.
211
212     The declaration will automatically create a matching one-to-many property at the child model,
213     named after the plural form of our table name. Use the ``parent_property`` argument to override
214     this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a
215     Python collection then use :func:`one_to_many` at that model.
216
217     *This utility method should only be used during class creation.*
218
219     :param model_class: class in which this relationship will be declared
220     :type model_class: type
221     :param parent_table: parent table name
222     :type parent_table: basestring
223     :param fk: foreign key name at our table (no need specify if there's no ambiguity)
224     :type fk: basestring
225     :param back_populates: override name of matching one-to-many property at parent table; set to
226      ``False`` to disable
227     :type back_populates: basestring or bool
228     """
229     if back_populates is None:
230         back_populates = formatting.pluralize(model_class.__tablename__)
231
232     return _relationship(model_class,
233                          parent_table,
234                          back_populates=back_populates,
235                          fk=fk,
236                          other_fk=parent_fk)
237
238
239 def many_to_many(model_class,
240                  other_table=None,
241                  prefix=None,
242                  dict_key=None,
243                  other_property=None,
244                  self=False):
245     """
246     Declare a many-to-many relationship property. The property value would be a list or dict of
247     instances of the other table's model.
248
249     You do not need associated foreign keys for this relationship. Instead, an extra table will be
250     created for you.
251
252     The declaration will automatically create a matching many-to-many property at the other model,
253     named after the plural form of our table name. Use the ``other_property`` argument to override
254     this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a
255     Python collection then use :func:`many_to_many` again at that model.
256
257     *This utility method should only be used during class creation.*
258
259     :param model_class: class in which this relationship will be declared
260     :type model_class: type
261     :param other_table: parent table name
262     :type other_table: basestring
263     :param prefix: optional prefix for extra table name as well as for ``other_property``
264     :type prefix: basestring
265     :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will
266      be a list
267     :type dict_key: basestring
268     :param other_property: override name of matching many-to-many property at other table; set to
269      ``False`` to disable
270     :type other_property: basestring or bool
271     :param self: used for relationships between a table and itself. if set, other_table will
272      become the same as the source table.
273     :type self: bool
274     """
275
276     this_table = model_class.__tablename__
277     this_column_name = '{0}_id'.format(this_table)
278     this_foreign_key = '{0}.id'.format(this_table)
279
280     if self:
281         other_table = this_table
282
283     other_column_name = '{0}_{1}'.format(other_table, 'self_ref_id' if self else 'id')
284     other_foreign_key = '{0}.{1}'.format(other_table, 'id')
285
286     secondary_table_name = '{0}_{1}'.format(this_table, other_table)
287
288     if prefix is not None:
289         secondary_table_name = '{0}_{1}'.format(prefix, secondary_table_name)
290         if other_property is None:
291             other_property = '{0}_{1}'.format(prefix, formatting.pluralize(this_table))
292
293     secondary_table = _get_secondary_table(
294         model_class.metadata,
295         secondary_table_name,
296         this_column_name,
297         other_column_name,
298         this_foreign_key,
299         other_foreign_key
300     )
301
302     kwargs = {'relationship_kwargs': {'secondary': secondary_table}}
303
304     if self:
305         kwargs['back_populates'] = NO_BACK_POP
306         kwargs['relationship_kwargs']['primaryjoin'] = \
307                     getattr(model_class, 'id') == getattr(secondary_table.c, this_column_name)
308         kwargs['relationship_kwargs']['secondaryjoin'] = \
309             getattr(model_class, 'id') == getattr(secondary_table.c, other_column_name)
310     else:
311         kwargs['backref_kwargs'] = \
312             {'name': other_property, 'uselist': True} if other_property else None
313         kwargs['dict_key'] = dict_key
314
315     return _relationship(model_class, other_table, **kwargs)
316
317
318 def association_proxy(*args, **kwargs):
319     if 'type' in kwargs:
320         type_ = kwargs.get('type')
321         del kwargs['type']
322     else:
323         type_ = ':obj:`basestring`'
324     proxy = original_association_proxy(*args, **kwargs)
325     proxy.__doc__ = """
326     Internal. For use in SQLAlchemy queries.
327
328     :type: {0}
329     """.format(type_)
330     return proxy
331
332
333 def _relationship(model_class,
334                   other_table_name,
335                   back_populates=None,
336                   backref_kwargs=None,
337                   relationship_kwargs=None,
338                   fk=None,
339                   other_fk=None,
340                   dict_key=None):
341     relationship_kwargs = relationship_kwargs or {}
342
343     if fk:
344         relationship_kwargs.setdefault(
345             'foreign_keys',
346             lambda: getattr(_get_class_for_table(model_class, model_class.__tablename__), fk)
347         )
348
349     elif other_fk:
350         relationship_kwargs.setdefault(
351             'foreign_keys',
352             lambda: getattr(_get_class_for_table(model_class, other_table_name), other_fk)
353         )
354
355     if dict_key:
356         relationship_kwargs.setdefault('collection_class',
357                                        attribute_mapped_collection(dict_key))
358
359     if backref_kwargs:
360         assert back_populates is None
361         return relationship(
362             lambda: _get_class_for_table(model_class, other_table_name),
363             backref=backref(**backref_kwargs),
364             **relationship_kwargs
365         )
366     else:
367         if back_populates is not NO_BACK_POP:
368             relationship_kwargs['back_populates'] = back_populates
369         return relationship(lambda: _get_class_for_table(model_class, other_table_name),
370                             **relationship_kwargs)
371
372
373 def _get_class_for_table(model_class, tablename):
374     if tablename in (model_class.__name__, model_class.__tablename__):
375         return model_class
376
377     for table_cls in model_class._decl_class_registry.itervalues():
378         if tablename == getattr(table_cls, '__tablename__', None):
379             return table_cls
380
381     raise ValueError('unknown table: {0}'.format(tablename))
382
383
384 def _get_secondary_table(metadata,
385                          name,
386                          first_column,
387                          second_column,
388                          first_foreign_key,
389                          second_foreign_key):
390     return Table(
391         name,
392         metadata,
393         Column(first_column, Integer, ForeignKey(first_foreign_key)),
394         Column(second_column, Integer, ForeignKey(second_foreign_key))
395     )