b4ea9dc27d92c72691a857ae1d9db3fdbdf6ea67
[tiramisu.git] / tiramisu / storage / sqlalchemy / option.py
1 # -*- coding: utf-8 -*-
2 ""
3 # Copyright (C) 2014 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # ____________________________________________________________
20 from tiramisu.i18n import _
21 from tiramisu.setting import groups, undefined
22 from tiramisu.error import ConfigError
23 from .util import SqlAlchemyBase
24 import util
25
26 from sqlalchemy import not_, or_, and_, inspect
27 from sqlalchemy.ext.declarative import declared_attr
28 from sqlalchemy.ext.associationproxy import association_proxy
29 from sqlalchemy import Column, Integer, String, Boolean, PickleType, \
30     ForeignKey, Table
31 from sqlalchemy.orm import relationship, backref
32 from sqlalchemy.orm.collections import attribute_mapped_collection
33
34 from itertools import chain
35
36
37 def load_requires(collection_type, proxy):
38     def getter(obj):
39         if obj is None:
40             return None
41         ret = []
42         requires = getattr(obj, proxy.value_attr)
43         for require in requires:
44             option = util.session.query(_Base).filter_by(id=require.option).first()
45             ret.append(tuple([option, require.expected, require.action, require.inverse, require.transitive, require.same_action]))
46         return tuple(ret)
47
48     def setter(obj, value):
49         setattr(obj, proxy.value_attr, value)
50     return getter, setter
51
52
53 class _Require(SqlAlchemyBase):
54     __tablename__ = "require"
55     id = Column(Integer, primary_key=True)
56     requires_id = Column(Integer, ForeignKey("baseoption.id"), nullable=False)
57     requires = relationship('_RequireOption')
58
59     def __init__(self, requires):
60         for require in requires:
61             self.requires.append(_RequireOption(require))
62
63
64 class _RequireOption(SqlAlchemyBase):
65     __tablename__ = 'requireoption'
66     id = Column(Integer, primary_key=True)
67     require_id = Column(Integer, ForeignKey("require.id"), nullable=False)
68     option = Column(Integer, nullable=False)
69     _expected = relationship("_RequireExpected", collection_class=list,
70                              cascade="all, delete-orphan")
71     expected = association_proxy("_expected", "expected")
72     #expected = Column(String)
73     action = Column(String, nullable=False)
74     inverse = Column(Boolean, default=False)
75     transitive = Column(Boolean, default=True)
76     same_action = Column(Boolean, default=True)
77
78     def __init__(self, values):
79         option, expected, action, inverse, transitive, same_action = values
80         self.option = option.id
81         self.expected = expected
82         self.action = action
83         self.inverse = inverse
84         self.transitive = transitive
85         self.same_action = same_action
86
87
88 class _RequireExpected(SqlAlchemyBase):
89     __tablename__ = 'expected'
90     id = Column(Integer, primary_key=True)
91     require = Column(Integer, ForeignKey('requireoption.id'), nullable=False)
92     expected = Column(PickleType)
93
94     def __init__(self, expected):
95         #FIXME ne pas creer plusieurs fois la meme _expected_
96         #FIXME pareil avec calc_properties
97         self.expected = expected
98
99
100 class _CalcProperties(SqlAlchemyBase):
101     __tablename__ = 'calcproperty'
102     id = Column(Integer, primary_key=True)
103     require = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
104     name = Column(PickleType)
105
106     def __init__(self, name):
107         #FIXME ne pas creer plusieurs fois la meme _expected_
108         #FIXME pareil avec calc_properties
109         self.name = name
110
111
112 #____________________________________________________________
113 #
114 # properties
115 class _PropertyOption(SqlAlchemyBase):
116     __tablename__ = 'propertyoption'
117     id = Column(Integer, primary_key=True)
118     option = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
119     name = Column(String)
120
121     def __init__(self, name):
122         self.name = name
123
124
125 #____________________________________________________________
126 #
127 # information
128 class _Information(SqlAlchemyBase):
129     __tablename__ = 'information'
130     id = Column(Integer, primary_key=True)
131     option = Column(Integer, ForeignKey('baseoption.id'), nullable=False)
132     key = Column(String)
133     value = Column(PickleType)
134
135     def __init__(self, key, value):
136         self.key = key
137         self.value = value
138
139
140 #____________________________________________________________
141 #
142 # callback
143 def load_callback_parm(collection_type, proxy):
144     def getter(obj):
145         if obj is None:
146             return None
147         ret = []
148         requires = getattr(obj, proxy.value_attr)
149         for require in requires:
150             if require.value is not None:
151                 ret.append(require.value)
152             else:
153                 option = util.session.query(_Base).filter_by(id=require.option).first()
154                 ret.append((option, require.force_permissive))
155         return tuple(ret)
156
157     def setter(obj, value):
158         setattr(obj, proxy.value_attr, value)
159     return getter, setter
160
161
162 class _CallbackParamOption(SqlAlchemyBase):
163     __tablename__ = 'callback_param_option'
164     id = Column(Integer, primary_key=True)
165     callback_param = Column(Integer, ForeignKey('callback_param.id'))
166     option = Column(Integer)
167     force_permissive = Column(Boolean)
168     value = Column(PickleType)
169
170     def __init__(self, option=undefined, force_permissive=undefined,  value=undefined):
171         if value is not undefined:
172             self.value = value
173         elif option is not undefined:
174             self.option = option.id
175             self.force_permissive = force_permissive
176
177
178 class _CallbackParam(SqlAlchemyBase):
179     __tablename__ = 'callback_param'
180     id = Column(Integer, primary_key=True)
181     callback = Column(Integer, ForeignKey('baseoption.id'))
182     key = Column(String)
183     params = relationship('_CallbackParamOption')
184
185     def __init__(self, key, params):
186         self.key = key
187         for param in params:
188             if isinstance(param, tuple):
189                 if param == (None,):
190                     self.params.append(_CallbackParamOption())
191                 else:
192                     self.params.append(_CallbackParamOption(option=param[0],
193                                                             force_permissive=param[1]))
194             else:
195                 self.params.append(_CallbackParamOption(value=param))
196
197
198 #____________________________________________________________
199 #
200 # consistency
201 consistency_table = Table('consistencyopt', SqlAlchemyBase.metadata,
202                           Column('id', Integer, primary_key=True),
203                           Column('left_id', Integer, ForeignKey('consistency.id')),
204                           Column('right_id', Integer, ForeignKey('baseoption.id'))
205                           )
206
207
208 class _Consistency(SqlAlchemyBase):
209     __tablename__ = 'consistency'
210     id = Column(Integer, primary_key=True)
211     func = Column(PickleType)
212     params = Column(PickleType)
213
214     def __init__(self, func, all_cons_opts, params):
215         self.func = func
216         for option in all_cons_opts:
217             option._consistencies.append(self)
218             print type(option._consistencies)
219         self.params = params
220
221
222 class _Parent(SqlAlchemyBase):
223     __tablename__ = 'parent'
224     id = Column(Integer, primary_key=True)
225     child_id = Column(Integer)
226     child_name = Column(String)
227     parent_id = Column(Integer)
228
229     def __init__(self, parent, child):
230         self.parent_id = parent.id
231         self.child_id = child.id
232         self.child_name = child._name
233
234
235 #____________________________________________________________
236 #
237 # Base
238 class _Base(SqlAlchemyBase):
239     __tablename__ = 'baseoption'
240     id = Column(Integer, primary_key=True)
241     _name = Column(String)
242     #FIXME not autoload
243     _infos = relationship("_Information",
244                           collection_class=attribute_mapped_collection('key'),
245                           cascade="all, delete-orphan")
246     _informations = association_proxy("_infos", "value")
247     _default = Column(PickleType)
248     _default_multi = Column(PickleType)
249     _subdyn = Column(Integer)
250     _dyn = Column(String)
251     _opt = Column(Integer)
252     _choice_values = Column(PickleType)
253     _cho_params = relationship('_CallbackParam',
254                                collection_class=
255                                attribute_mapped_collection('key'))
256     _choice_params = association_proxy("_cho_params", "params",
257                                        getset_factory=load_callback_parm)
258     _reqs = relationship("_Require", collection_class=list)
259     _requires = association_proxy("_reqs", "requires", getset_factory=load_requires)
260     _multi = Column(Integer)
261     ######
262     _callback = Column(PickleType)
263     _call_params = relationship('_CallbackParam',
264                                 collection_class=
265                                 attribute_mapped_collection('key'))
266     _callback_params = association_proxy("_call_params", "params",
267                                          getset_factory=load_callback_parm)
268     _validator = Column(PickleType)
269     _val_params = relationship('_CallbackParam',
270                                collection_class=
271                                attribute_mapped_collection('key'))
272     _validator_params = association_proxy("_val_params", "params",
273                                           getset_factory=load_callback_parm)
274     ######
275     #FIXME not autoload
276     _props = relationship("_PropertyOption", collection_class=set)
277     _properties = association_proxy("_props", "name")
278     _calc_props = relationship("_CalcProperties", collection_class=set)
279     _calc_properties = association_proxy("_calc_props", "name")
280     _warnings_only = Column(Boolean)
281     _readonly = Column(Boolean, default=False)
282     _consistencies = relationship('_Consistency', secondary=consistency_table,
283                                   backref=backref('options',
284                                                   enable_typechecks=False))
285     _type = Column(String(50))
286     _stated = Column(Boolean)
287     __mapper_args__ = {
288         'polymorphic_identity': 'option',
289         'polymorphic_on': _type
290     }
291     _extra = Column(PickleType)
292     #FIXME devrait etre une table
293     _group_type = Column(String)
294     _is_build_cache = Column(Boolean, default=False)
295
296     #def __init__(self):
297     def __init__(self, name, multi, warnings_only, doc, extra, calc_properties,
298                  requires, properties, opt=undefined):
299         util.session.add(self)
300         self._name = name
301         if multi is not undefined:
302             self._multi = multi
303         if warnings_only is not undefined:
304             self._warnings_only = warnings_only
305         if doc is not undefined:
306             self._informations = {'doc': doc}
307         if opt is not undefined:
308             self._opt = opt.id
309         if extra is not undefined:
310             self._extra = extra
311         if calc_properties is not undefined:
312             self._calc_properties = calc_properties
313         if requires is not undefined:
314             self._requires = requires
315         if properties is not undefined:
316             self._properties = properties
317
318     def commit(self):
319         util.session.commit()
320
321     def _add_consistency(self, func, all_cons_opts, params):
322         _Consistency(func, all_cons_opts, params)
323
324     def _set_default_values(self, default, default_multi):
325         if self.impl_is_multi() and default is None:
326                 default = []
327         self.impl_validate(default)
328         self._default = default
329         if self.impl_is_multi() and default_multi is not None:
330             self._validate(default_multi)
331             self._default_multi = default_multi
332
333     def _get_consistencies(self):
334         return [(consistency.func, consistency.options, consistency.params)
335                 for consistency in self._consistencies]
336
337     def _get_id(self):
338         return self.id
339
340     def impl_get_callback(self):
341         ret = self._callback
342         if ret is None:
343             return (None, {})
344         return ret, self._callback_params
345
346     def impl_get_validator(self):
347         ret = self._validator
348         if ret is None:
349             return (None, {})
350         return ret, self._validator_params
351
352     def _impl_getsubdyn(self):
353         return util.session.query(_Base).filter_by(id=self._subdyn).first()
354
355     def _impl_getopt(self):
356         return util.session.query(_Base).filter_by(id=self._opt).first()
357
358     def impl_getname(self):
359         return self._name
360
361     def impl_getrequires(self):
362         return self._requires
363
364     def impl_getdefault(self):
365         ret = self._default
366         if self.impl_is_multi():
367             if ret is None:
368                 return []
369             return list(ret)
370         return ret
371
372     def impl_getdefault_multi(self):
373         if self.impl_is_multi():
374             return self._default_multi
375
376     def _get_extra(self, key):
377         return self._extra[key]
378
379     def _impl_setopt(self, opt):
380         self._opt = opt.id
381
382     def _impl_setsubdyn(self, subdyn):
383         self._subdyn = subdyn.id
384         self.commit()
385
386     def _set_readonly(self):
387         self._readonly = True
388
389     def _set_callback(self, callback, callback_params):
390         self._callback = callback
391         if callback_params is not None:
392             self._callback_params = callback_params
393
394     def _set_validator(self, validator, validator_params):
395         self._validator = validator
396         if validator_params is not None:
397             self._validator_params = validator_params
398
399     def impl_is_readonly(self):
400         try:
401             return self._readonly
402         except AttributeError:
403             return False
404
405     def impl_is_multi(self):
406         return self._multi == 0 or self._multi == 2
407
408     def impl_is_submulti(self):
409         return self._multi == 2
410
411     def _is_warnings_only(self):
412         return self._warnings_only
413
414     def impl_get_calc_properties(self):
415         try:
416             return self._calc_properties
417         except AttributeError:
418             return frozenset()
419
420     # information
421     def impl_set_information(self, key, value):
422         self._informations[key] = value
423
424     def impl_get_information(self, key, default=undefined):
425         """retrieves one information's item
426
427         :param key: the item string (ex: "help")
428         """
429         if default is not undefined:
430             return self._informations.get(key, default)
431         try:
432             return self._informations[key]
433         except KeyError:  # pragma: optional cover
434             raise ValueError(_("information's item not found: {0}").format(
435                 key))
436
437     def _impl_getattributes(self):
438         slots = set()
439         mapper = inspect(self)
440         for column in mapper.attrs:
441                 slots.add(column.key)
442         return slots
443
444
445 class Cache(SqlAlchemyBase):
446     __tablename__ = 'cache'
447     id = Column(Integer, primary_key=True)
448     path = Column(String, nullable=False, index=True)
449     descr = Column(Integer, nullable=False, index=True)
450     parent = Column(Integer, nullable=False, index=True)
451     option = Column(Integer, nullable=False, index=True)
452     opt_type = Column(String, nullable=False, index=True)
453     is_subdyn = Column(Boolean, nullable=False, index=True)
454     subdyn_path = Column(String)
455
456     def __init__(self, descr, parent, option, path, subdyn_path):
457         #context
458         self.descr = descr.id
459         self.parent = parent.id
460         self.option = option.id
461         self.path = path
462         self.opt_type = option.__class__.__name__
463         if subdyn_path:
464             self.is_subdyn = True
465             self.subdyn_path = subdyn_path
466         else:
467             self.is_subdyn = False
468             self.subdyn_path = None
469
470
471 class StorageOptionDescription(object):
472     def impl_already_build_caches(self):
473         return self._is_build_cache
474
475     def impl_get_opt_by_path(self, path):
476         ret = util.session.query(Cache).filter_by(descr=self.id, path=path).first()
477         if ret is None:
478             raise AttributeError(_('no option for path {0}').format(path))
479         return util.session.query(_Base).filter_by(id=ret.option).first()
480
481     def impl_get_path_by_opt(self, opt):
482         ret = util.session.query(Cache).filter_by(descr=self.id,
483                                                   option=opt.id).first()
484         if ret is None:
485             raise AttributeError(_('no option {0} found').format(opt))
486         return ret.path
487
488     def impl_get_group_type(self):
489         return getattr(groups, self._group_type)
490
491     def impl_build_cache_option(self, descr=None, _currpath=None,
492                                 subdyn_path=None):
493         if descr is None:
494             save = True
495             descr = self
496             _currpath = []
497         else:
498             save = False
499         for option in self._impl_getchildren(dyn=False):
500             attr = option.impl_getname()
501             if isinstance(option, StorageOptionDescription):
502                 sub = subdyn_path
503                 if option.impl_is_dynoptiondescription():
504                     sub = '.'.join(_currpath)
505                 util.session.add(Cache(descr, self, option,
506                                        str('.'.join(_currpath + [attr])),
507                                        sub))
508                 _currpath.append(attr)
509                 option.impl_build_cache_option(descr,
510                                                _currpath,
511                                                sub)
512                 _currpath.pop()
513             else:
514                 if subdyn_path:
515                     subdyn_path = '.'.join(_currpath)
516                 util.session.add(Cache(descr, self, option,
517                                        str('.'.join(_currpath + [attr])),
518                                        subdyn_path))
519         if save:
520             self._is_build_cache = True
521             util.session.commit()
522
523     def impl_get_options_paths(self, bytype, byname, _subpath, only_first,
524                                context):
525         def _build_ret_opt(opt, option, suffix, name):
526             subdyn_path = opt.subdyn_path
527             dynpaths = opt.path[len(subdyn_path):].split('.')
528
529             path = subdyn_path
530             dot = False
531             for dynpath in dynpaths:
532                 if dot:
533                     path += '.'
534                 path += dynpath + suffix
535                 dot = True
536             _opt = option._impl_to_dyn(name + suffix, path)
537             return (path, _opt)
538
539         sqlquery = util.session.query(Cache).filter_by(descr=self.id)
540         if bytype is None:
541             sqlquery = sqlquery.filter(and_(not_(
542                 Cache.opt_type == 'OptionDescription'),
543                 not_(Cache.opt_type == 'DynOptionDescription')))
544         else:
545             sqlquery = sqlquery.filter_by(opt_type=bytype.__name__)
546
547         query = ''
548         or_query = ''
549         if _subpath is not None:
550             query += _subpath + '.%'
551         #if byname is not None:
552         #    or_query = query + byname
553         #    query += '%.' + byname
554         if query != '':
555             filter_query = Cache.path.like(query)
556             if or_query != '':
557                 filter_query = or_(Cache.path == or_query, filter_query)
558             sqlquery = sqlquery.filter(filter_query)
559         #if only_first:
560         #    opt = sqlquery.first()
561         #    if opt is None:
562         #        return tuple()
563         #    option = util.session.query(_Base).filter_by(id=opt.option).first()
564         #    return ((opt.path, option),)
565         #else:
566         ret = []
567         for opt in sqlquery.all():
568             option = util.session.query(_Base).filter_by(id=opt.option).first()
569             if opt.is_subdyn:
570                 name = option.impl_getname()
571                 if byname is not None:
572                     if byname.startswith(name):
573                         found = False
574                         dynoption = option._impl_getsubdyn()
575                         for suffix in dynoption._impl_get_suffixes(
576                                 context):
577                             if byname == name + suffix:
578                                 found = True
579                                 break
580                         if not found:
581                             continue
582                         ret_opt = _build_ret_opt(opt, option, suffix, name)
583                     else:
584                         ret_opt = _build_ret_opt(opt, option, suffix, name)
585                 else:
586                     if not only_first:
587                         ret_opt = []
588                     dynoption = option._impl_getsubdyn()
589                     for suffix in dynoption._impl_get_suffixes(context):
590                         val = _build_ret_opt(opt, option, suffix, name)
591                         if only_first:
592                             ret_opt = val
593                         else:
594                             ret_opt.append(val)
595             else:
596                 if byname is not None and byname != option.impl_getname():
597                     continue
598                 ret_opt = (opt.path, option)
599             if only_first:
600                 return ret_opt
601             if isinstance(ret_opt, list):
602                 if ret_opt != []:
603                     ret.extend(ret_opt)
604             else:
605                 ret.append(ret_opt)
606         return ret
607
608     def _add_children(self, child_names, children):
609         for child in children:
610             util.session.add(_Parent(self, child))
611
612     def _impl_st_getchildren(self, context, only_dyn=False):
613         if only_dyn is False or context is undefined:
614             for child in util.session.query(_Parent).filter_by(
615                     parent_id=self.id).all():
616                 yield(util.session.query(_Base).filter_by(id=child.child_id
617                                                           ).first())
618         else:
619             descr = context.cfgimpl_get_description().id
620             for child in util.session.query(Cache).filter_by(descr=descr,
621                                                              parent=self.id
622                                                              ).all():
623                 yield(util.session.query(_Base).filter_by(id=child.option).first())
624
625     def _getattr(self, name, suffix=undefined, context=undefined, dyn=True):
626         error = False
627         if suffix is not undefined:
628             try:
629                 if undefined in [suffix, context]:  # pragma: optional cover
630                     raise ConfigError(_("suffix and context needed if "
631                                         "it's a dyn option"))
632                 if name.endswith(suffix):
633                     oname = name[:-len(suffix)]
634                     #child = self._children[1][self._children[0].index(oname)]
635                     child = util.session.query(_Parent).filter_by(
636                         parent_id=self.id, child_name=oname).first()
637                     if child is None:
638                         error = True
639                     else:
640                         opt = util.session.query(_Base).filter_by(
641                             id=child.child_id).first()
642                         return self._impl_get_dynchild(opt, suffix)
643                 else:
644                     error = True
645             except ValueError:  # pragma: optional cover
646                 error = True
647         else:
648             child = util.session.query(_Parent).filter_by(parent_id=self.id,
649                                                           child_name=name
650                                                           ).first()
651             if child is None:
652                 child = self._impl_search_dynchild(name, context=context)
653                 if child != []:
654                     return child
655                 error = True
656             if error is False:
657                 return util.session.query(_Base).filter_by(id=child.child_id
658                                                            ).first()
659         if error:
660             raise AttributeError(_('unknown Option {0} in OptionDescription {1}'
661                                  '').format(name, self.impl_getname()))
662
663     def _get_force_store_value(self):
664         #only option in current tree
665         current_ids = tuple(chain(*util.session.query(Cache.option).filter_by(
666             descr=self.id).all()))
667         for prop in util.session.query(_PropertyOption).filter(
668                 _PropertyOption.option.in_(current_ids),
669                 _PropertyOption.name == 'force_store_value').all():
670             opt = util.session.query(_Base).filter_by(id=prop.option).first()
671             path = self.impl_get_path_by_opt(opt)
672             yield (opt, path)
673
674
675 class StorageBase(_Base):
676     @declared_attr
677     def __mapper_args__(self):
678         return {'polymorphic_identity': self.__name__.lower()}