123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- #-*- coding: utf-8 -*-
-
- import importlib
- import warnings
- import copy
-
- from lodel.context import LodelContext
-
- LodelContext.expose_modules(globals(), {
- 'lodel.logger': 'logger',
- 'lodel.settings': 'Settings',
- 'lodel.settings.utils': 'SettingsError',
- 'lodel.leapi.query': ['LeInsertQuery', 'LeUpdateQuery', 'LeDeleteQuery',
- 'LeGetQuery'],
- 'lodel.leapi.exceptions': ['LeApiError', 'LeApiErrors',
- 'LeApiDataCheckError', 'LeApiDataCheckErrors', 'LeApiQueryError',
- 'LeApiQueryErrors'],
- 'lodel.plugin.exceptions': ['PluginError', 'PluginTypeError',
- 'LodelScriptError', 'DatasourcePluginError'],
- 'lodel.exceptions': ['LodelFatalError'],
- 'lodel.plugin.hooks': ['LodelHook'],
- 'lodel.plugin': ['Plugin', 'DatasourcePlugin'],
- 'lodel.leapi.datahandlers.base_classes': ['DatasConstructor', 'Reference']})
-
- ##@brief Stores the name of the field present in each LeObject that indicates
- #the name of LeObject subclass represented by this object
- CLASS_ID_FIELDNAME = "classname"
-
- ##@brief Wrapper class for LeObject getter & setter
- #
- # This class intend to provide easy & friendly access to LeObject fields values
- # without name collision problems
- # @note Wrapped methods are : LeObject.data() & LeObject.set_data()
- class LeObjectValues(object):
-
- ##@brief Construct a new LeObjectValues
- # @param fieldnames_callback method
- # @param set_callback method : The LeObject.set_datas() method of corresponding LeObject class
- # @param get_callback method : The LeObject.get_datas() method of corresponding LeObject class
- def __init__(self, fieldnames_callback, set_callback, get_callback):
- self._setter = set_callback
- self._getter = get_callback
-
- ##@brief Provide read access to datas values
- # @note Read access should be provided for all fields
- # @param fname str : Field name
- def __getattribute__(self, fname):
- getter = super().__getattribute__('_getter')
- return getter(fname)
-
- ##@brief Provide write access to datas values
- # @note Write acces shouldn't be provided for internal or immutable fields
- # @param fname str : Field name
- # @param fval * : the field value
- def __setattribute__(self, fname, fval):
- setter = super().__getattribute__('_setter')
- return setter(fname, fval)
-
-
- class LeObject(object):
-
- ##@brief boolean that tells if an object is abtract or not
- _abstract = None
- ##@brief A dict that stores DataHandler instances indexed by field name
- _fields = None
- ##@brief A tuple of fieldname (or a uniq fieldname) representing uid
- _uid = None
- ##@brief Read only datasource ( see @ref lodel2_datasources )
- _ro_datasource = None
- ##@brief Read & write datasource ( see @ref lodel2_datasources )
- _rw_datasource = None
- ##@brief Store the list of child classes
- _child_classes = None
- ##@brief Name of the datasource plugin
- _datasource_name = None
-
- def __new__(cls, **kwargs):
-
- self = object.__new__(cls)
- ##@brief A dict that stores fieldvalues indexed by fieldname
- self.__datas = { fname:None for fname in self._fields }
- ##@brief Store a list of initianilized fields when instanciation not complete else store True
- self.__initialized = list()
- ##@brief Datas accessor. Instance of @ref LeObjectValues
- self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
- for fieldname, fieldval in kwargs.items():
- self.__datas[fieldname] = fieldval
- self.__initialized.append(fieldname)
- self.__is_initialized = False
- self.__set_initialized()
- return self
-
- ##@brief Construct an object representing an Editorial component
- # @note Can be considered as EmClass instance
- def __init__(self, **kwargs):
- if self._abstract:
- raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ )
-
- # Checks that uid is given
- for uid_name in self._uid:
- if uid_name not in kwargs:
- raise LeApiError("Cannot instanciate a LeObject without it's identifier")
- self.__datas[uid_name] = kwargs[uid_name]
- del(kwargs[uid_name])
- self.__initialized.append(uid_name)
-
- # Processing given fields
- allowed_fieldnames = self.fieldnames(include_ro = False)
- err_list = dict()
- for fieldname, fieldval in kwargs.items():
- if fieldname not in allowed_fieldnames:
- if fieldname in self._fields:
- err_list[fieldname] = LeApiError(
- "Value given but the field is internal")
- else:
- err_list[fieldname] = LeApiError(
- "Unknown fieldname : '%s'" % fieldname)
- else:
- self.__datas[fieldname] = fieldval
- self.__initialized.append(fieldname)
- if len(err_list) > 0:
- raise LeApiErrors(msg = "Unable to __init__ %s" % self.__class__,
- exceptions = err_list)
- self.__set_initialized()
-
- #-----------------------------------#
- # Fields datas handling methods #
- #-----------------------------------#
-
- ##@brief Property method True if LeObject is initialized else False
- @property
- def initialized(self):
- return self.__is_initialized
-
- ##@return The uid field name
- @classmethod
- def uid_fieldname(cls):
- return cls._uid
-
- ##@brief Return a list of fieldnames
- # @param include_ro bool : if True include read only field names
- # @return a list of str
- @classmethod
- def fieldnames(cls, include_ro = False):
- if not include_ro:
- return [ fname for fname in cls._fields if not cls._fields[fname].is_internal() ]
- else:
- return list(cls._fields.keys())
-
- @classmethod
- def name2objname(cls, name):
- return name.title()
-
- ##@brief Return the datahandler asssociated with a LeObject field
- # @param fieldname str : The fieldname
- # @return A data handler instance
- #@todo update class of exception raised
- @classmethod
- def data_handler(cls, fieldname):
- if not fieldname in cls._fields:
- raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
- return cls._fields[fieldname]
-
- ##@brief Getter for references datahandlers
- #@param with_backref bool : if true return only references with back_references
- #@return <code>{'fieldname': datahandler, ...}</code>
- @classmethod
- def reference_handlers(cls, with_backref = True):
- return { fname: fdh
- for fname, fdh in cls.fields(True).items()
- if fdh.is_reference() and \
- (not with_backref or fdh.back_reference is not None)}
-
- ##@brief Return a LeObject child class from a name
- # @warning This method has to be called from dynamically generated LeObjects
- # @param leobject_name str : LeObject name
- # @return A LeObject child class
- # @throw NameError if invalid name given
- @classmethod
- def name2class(cls, leobject_name):
- if cls.__module__ == 'lodel.leapi.leobject':
- raise NotImplementedError("Abstract method")
- mod = importlib.import_module(cls.__module__)
- try:
- return getattr(mod, leobject_name)
- except (AttributeError, TypeError) :
- raise LeApiError("No LeObject named '%s'" % leobject_name)
-
- @classmethod
- def is_abstract(cls):
- return cls._abstract
-
- ##@brief Field data handler getter
- #@param fieldname str : The field name
- #@return A datahandler instance
- #@throw NameError if the field doesn't exist
- @classmethod
- def field(cls, fieldname):
- try:
- return cls._fields[fieldname]
- except KeyError:
- raise NameError("No field named '%s' in %s" % ( fieldname,
- cls.__name__))
- ##@return A dict with fieldname as key and datahandler as instance
- @classmethod
- def fields(cls, include_ro = False):
- if include_ro:
- return copy.copy(cls._fields)
- else:
- return {fname:cls._fields[fname] for fname in cls._fields if not cls._fields[fname].is_internal()}
-
- ##@brief Return the list of parents classes
- #
- #@note the first item of the list is the current class, the second is it's
- #parent etc...
- #@param cls
- #@warning multiple inheritance broken by this method
- #@return a list of LeObject child classes
- #@todo multiple parent capabilities implementation
- @classmethod
- def hierarch(cls):
- res = [cls]
- cur = cls
- while True:
- cur = cur.__bases__[0] # Multiple inheritance broken HERE
- if cur in (LeObject, object):
- break
- else:
- res.append(cur)
- return res
-
- ##@brief Return a tuple a child classes
- #@return a tuple of child classes
- @classmethod
- def child_classes(cls):
- return copy.copy(cls._child_classes)
-
-
- ##@brief Return the parent class that is the "source" of uid
- #
- #The method goal is to return the parent class that defines UID.
- #@return a LeObject child class or false if no UID defined
- @classmethod
- def uid_source(cls):
- if cls._uid is None or len(cls._uid) == 0:
- return False
- hierarch = cls.hierarch()
- prev = hierarch[0]
- uid_handlers = set( cls._fields[name] for name in cls._uid )
- for pcls in cls.hierarch()[1:]:
- puid_handlers = set(cls._fields[name] for name in pcls._uid)
- if set(pcls._uid) != set(prev._uid) \
- or puid_handlers != uid_handlers:
- break
- prev = pcls
- return prev
-
- ##@brief Initialise both datasources (ro and rw)
- #
- #This method is used once at dyncode load to replace the datasource string
- #by a datasource instance to avoid doing this operation for each query
- #@see LeObject::_init_datasource()
- @classmethod
- def _init_datasources(cls):
- if isinstance(cls._datasource_name, str):
- rw_ds = ro_ds = cls._datasource_name
- else:
- ro_ds, rw_ds = cls._datasource_name
- #Read only datasource initialisation
- cls._ro_datasource = DatasourcePlugin.init_datasource(ro_ds, True)
- if cls._ro_datasource is None:
- log_msg = "No read only datasource set for LeObject %s"
- log_msg %= cls.__name__
- logger.debug(log_msg)
- else:
- log_msg = "Read only datasource '%s' initialized for LeObject %s"
- log_msg %= (ro_ds, cls.__name__)
- logger.debug(log_msg)
- #Read write datasource initialisation
- cls._rw_datasource = DatasourcePlugin.init_datasource(rw_ds, False)
- if cls._ro_datasource is None:
- log_msg = "No read/write datasource set for LeObject %s"
- log_msg %= cls.__name__
- logger.debug(log_msg)
- else:
- log_msg = "Read/write datasource '%s' initialized for LeObject %s"
- log_msg %= (ro_ds, cls.__name__)
- logger.debug(log_msg)
-
- ##@brief Return the uid of the current LeObject instance
- #@return the uid value
- #@warning Broke multiple uid capabilities
- def uid(self):
- return self.data(self._uid[0])
-
- ##@brief Read only access to all datas
- # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
- # @param field_name str : field name
- # @return the Value
- # @throw RuntimeError if the field is not initialized yet
- # @throw NameError if name is not an existing field name
- def data(self, field_name):
- if field_name not in self._fields.keys():
- raise NameError("No such field in %s : %s" % (self.__class__.__name__, field_name))
- if not self.initialized and field_name not in self.__initialized:
- raise RuntimeError("The field %s is not initialized yet (and have no value)" % field_name)
- return self.__datas[field_name]
-
- ##@brief Read only access to all datas
- #@return a dict representing datas of current instance
- def datas(self, internal = False):
- return {fname:self.data(fname) for fname in self.fieldnames(internal)}
-
-
- ##@brief Datas setter
- # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
- # @param fname str : field name
- # @param fval * : field value
- # @return the value that is really set
- # @throw NameError if fname is not valid
- # @throw AttributeError if the field is not writtable
- def set_data(self, fname, fval):
- if fname not in self.fieldnames(include_ro = False):
- if fname not in self._fields.keys():
- raise NameError("No such field in %s : %s" % (self.__class__.__name__, fname))
- else:
- raise AttributeError("The field %s is read only" % fname)
- self.__datas[fname] = fval
- if not self.initialized and fname not in self.__initialized:
- # Add field to initialized fields list
- self.__initialized.append(fname)
- self.__set_initialized()
- if self.initialized:
- # Running full value check
- ret = self.__check_modified_values()
- if ret is None:
- return self.__datas[fname]
- else:
- raise LeApiErrors("Data check error", ret)
- else:
- # Doing value check on modified field
- # We skip full validation here because the LeObject is not fully initialized yet
- val, err = self._fields[fname].check_data_value(fval)
- if isinstance(err, Exception):
- #Revert change to be in valid state
- del(self.__datas[fname])
- del(self.__initialized[-1])
- raise LeApiErrors("Data check error", {fname:err})
- else:
- self.__datas[fname] = val
-
- ##@brief Update the __initialized attribute according to LeObject internal state
- #
- # Check the list of initialized fields and set __initialized to True if all fields initialized
- def __set_initialized(self):
- if isinstance(self.__initialized, list):
- expected_fields = self.fieldnames(include_ro = False) + self._uid
- if set(expected_fields) == set(self.__initialized):
- self.__is_initialized = True
-
- ##@brief Designed to be called when datas are modified
- #
- # Make different checks on the LeObject given it's state (fully initialized or not)
- # @return None if checks succeded else return an exception list
- def __check_modified_values(self):
- err_list = dict()
- if self.__initialized is True:
- # Data value check
- for fname in self.fieldnames(include_ro = False):
- val, err = self._fields[fname].check_data_value(self.__datas[fname])
- if err is not None:
- err_list[fname] = err
- else:
- self.__datas[fname] = val
- # Data construction
- if len(err_list) == 0:
- for fname in self.fieldnames(include_ro = True):
- try:
- field = self._fields[fname]
- self.__datas[fname] = field.construct_data( self,
- fname,
- self.__datas,
- self.__datas[fname]
- )
- except Exception as exp:
- err_list[fname] = exp
- # Datas consistency check
- if len(err_list) == 0:
- for fname in self.fieldnames(include_ro = True):
- field = self._fields[fname]
- ret = field.check_data_consistency(self, fname, self.__datas)
- if isinstance(ret, Exception):
- err_list[fname] = ret
- else:
- # Data value check for initialized datas
- for fname in self.__initialized:
- val, err = self._fields[fname].check_data_value(self.__datas[fname])
- if err is not None:
- err_list[fname] = err
- else:
- self.__datas[fname] = val
- return err_list if len(err_list) > 0 else None
-
- #--------------------#
- # Other methods #
- #--------------------#
-
- ##@brief Temporary method to set private fields attribute at dynamic code generation
- #
- # This method is used in the generated dynamic code to set the _fields attribute
- # at the end of the dyncode parse
- # @warning This method is deleted once the dynamic code loaded
- # @param field_list list : list of EmField instance
- # @param cls
- @classmethod
- def _set__fields(cls, field_list):
- cls._fields = field_list
-
- ## @brief Check that datas are valid for this type
- # @param datas dict : key == field name value are field values
- # @param complete bool : if True expect that datas provide values for all non internal fields
- # @param allow_internal bool : if True don't raise an error if a field is internal
- # @param cls
- # @return Checked datas
- # @throw LeApiDataCheckError if errors reported during check
- @classmethod
- def check_datas_value(cls, datas, complete = False, allow_internal = True):
- err_l = dict() #Error storing
- correct = set() #valid fields name
- mandatory = set() #mandatory fields name
- for fname, datahandler in cls._fields.items():
- if allow_internal or not datahandler.is_internal():
- correct.add(fname)
- if complete and not hasattr(datahandler, 'default'):
- mandatory.add(fname)
- provided = set(datas.keys())
- # searching for unknow fields
- for u_f in provided - correct:
- #Here we can check if the field is invalid or rejected because
- # it is internel
- err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'" % u_f)
- # searching for missing mandatory fieldsa
- for missing in mandatory - provided:
- err_l[missing] = AttributeError("The data for field '%s' is missing" % missing)
- #Checks datas
- checked_datas = dict()
- for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
- dh = cls._fields[name]
- res = dh.check_data_value(value)
- checked_datas[name], err = res
- if err:
- err_l[name] = err
-
- if len(err_l) > 0:
- raise LeApiDataCheckErrors("Error while checking datas", err_l)
- return checked_datas
-
- ##@brief Check and prepare datas
- #
- # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
- #
- # @param datas dict : {fieldname : fieldvalue, ...}
- # @param complete bool : If True you MUST give all the datas
- # @param allow_internal : Wether or not interal fields are expected in datas
- # @param cls
- # @return Datas ready for use
- # @todo: complete is very unsafe, find a way to get rid of it
- @classmethod
- def prepare_datas(cls, datas, complete=False, allow_internal=True):
- if not complete:
- warnings.warn("\nActual implementation can make broken datas \
- construction and consitency when datas are not complete\n")
- ret_datas = cls.check_datas_value(datas, complete, allow_internal)
- if isinstance(ret_datas, Exception):
- raise ret_datas
- if complete:
- ret_datas = cls._construct_datas(ret_datas)
- cls._check_datas_consistency(ret_datas)
- return ret_datas
-
- ## @brief Construct datas values
- #
- # @param cls
- # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
- # @return A new dict of datas
- # @todo IMPLEMENTATION
- @classmethod
- def _construct_datas(cls, datas):
- constructor = DatasConstructor(cls, datas, cls._fields)
- ret = {
- fname:constructor[fname]
- for fname, ftype in cls._fields.items()
- if not ftype.is_internal() or ftype.internal != 'autosql'
- }
- return ret
-
- ## @brief Check datas consistency
- #
- # @warning assert that datas is complete
- # @param cls
- # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
- # @throw LeApiDataCheckError if fails
- @classmethod
- def _check_datas_consistency(cls, datas):
- err_l = []
- err_l = dict()
- for fname, dh in cls._fields.items():
- ret = dh.check_data_consistency(cls, fname, datas)
- if isinstance(ret, Exception):
- err_l[fname] = ret
-
- if len(err_l) > 0:
- raise LeApiDataCheckError("Datas consistency checks fails", err_l)
-
- ## @brief Check datas consistency
- #
- # @warning assert that datas is complete
- # @param cls
- # @param datas dict : Datas that have been returned by LeCrud.prepare_datas() method
- # @param type_query str : Type of query to be performed , default value : insert
- @classmethod
- def make_consistency(cls, datas, type_query = 'insert'):
- for fname, dh in cls._fields.items():
- ret = dh.make_consistency(fname, datas, type_query)
-
- ## @brief Add a new instance of LeObject
- # @return a new uid en case of success, False otherwise
- @classmethod
- def insert(cls, datas):
- query = LeInsertQuery(cls)
- return query.execute(datas)
-
- ## @brief Update an instance of LeObject
- #
- #@param datas : list of new datas
- def update(self, datas = None):
- datas = self.datas(internal=False) if datas is None else datas
- uids = self._uid
- query_filter = list()
- for uid in uids:
- query_filter.append((uid, '=', self.data(uid)))
- try:
- query = LeUpdateQuery(self.__class__, query_filter)
- except Exception as err:
- raise err
-
- try:
- result = query.execute(datas)
- except Exception as err:
- raise err
-
- return result
-
- ## @brief Delete an instance of LeObject
- #
- #@return 1 if the objet has been deleted
- def delete(self):
- uids = self._uid
- query_filter = list()
- for uid in uids:
- query_filter.append((uid, '=', self.data(uid)))
-
- query = LeDeleteQuery(self.__class__, query_filter)
-
- result = query.execute()
-
- return result
-
- ## @brief Delete instances of LeObject
- #@param query_filters list
- #@returns the number of deleted items
- @classmethod
- def delete_bundle(cls, query_filters):
- deleted = 0
- try:
- query = LeDeleteQuery(cls, query_filters)
- except Exception as err:
- raise err
-
- try:
- result = query.execute()
- except Exception as err:
- raise err
- if not result is None:
- deleted += result
- return deleted
-
- ## @brief Get instances of LeObject
- #
- #@param query_filters dict : (filters, relational filters), with filters is a list of tuples : (FIELD, OPERATOR, VALUE) )
- #@param field_list list|None : list of string representing fields see
- #@ref leobject_filters
- #@param order list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
- #@param group list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
- #@param limit int : The maximum number of returned results
- #@param offset int : offset
- #@return a list of items (lists of (fieldname, fieldvalue))
- @classmethod
- def get(cls, query_filters, field_list=None, order=None, group=None, limit=None, offset=0):
- if field_list is not None:
- for uid in [ uidname
- for uidname in cls.uid_fieldname()
- if uidname not in field_list ]:
- field_list.append(uid)
- if CLASS_ID_FIELDNAME not in field_list:
- field_list.append(CLASS_ID_FIELDNAME)
- try:
- query = LeGetQuery(
- cls, query_filters = query_filters, field_list = field_list,
- order = order, group = group, limit = limit, offset = offset)
- except ValueError as err:
- raise err
-
- try:
- result = query.execute()
- except Exception as err:
- raise err
-
- objects = list()
- for res in result:
- res_cls = cls.name2class(res[CLASS_ID_FIELDNAME])
- inst = res_cls.__new__(res_cls,**res)
- objects.append(inst)
-
- return objects
-
- ##@brief Retrieve an object given an UID
- #@todo broken multiple UID
- @classmethod
- def get_from_uid(cls, uid):
- if cls.uid_fieldname() is None:
- raise LodelFatalError(
- "No uid defined for class %s" % cls.__name__)
- uidname = cls.uid_fieldname()[0] #Brokes composed UID
- res = cls.get([(uidname,'=', uid)])
-
- #dedoublonnage vu que query ou la datasource est bugué
- if len(res) > 1:
- res_cp = res
- res = []
- while len(res_cp) > 0:
- cur_res = res_cp.pop()
- if cur_res.uid() in [ r.uid() for r in res_cp]:
- logger.error("DOUBLON detected in query results !!!")
- else:
- res.append(cur_res)
- if len(res) > 1:
- raise LodelFatalError("Get from uid returned more than one \
- object ! For class %s with uid value = %s" % (cls, uid))
- elif len(res) == 0:
- return None
- return res[0]
-
- ##@brief Checks if an object exists
- @classmethod
- def is_exist(cls, uid):
- if cls.uid_fieldname() is None:
- raise LodelFatalError(
- "No uid defined for class %s" % cls.__name__)
- from .query import is_exist
- return is_exist(cls, uid)
|