1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2025-12-28 11:46:54 +01:00
lodel2_mirror/lodel/leapi/leobject.py

694 lines
27 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#-*- coding: utf-8 -*-
import importlib
import warnings
import copy
from lodel.plugin import Plugin
from lodel import logger
from lodel.settings import Settings
from lodel.settings.utils import SettingsError
from .query import LeInsertQuery, LeUpdateQuery, LeDeleteQuery, LeGetQuery
from .exceptions import *
from lodel.plugin.hooks import LodelHook
from lodel.leapi.datahandlers.base_classes import DatasConstructor
##@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 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
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 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
@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 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 = cls._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 = cls._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 Replace the _datasource attribute value by a datasource instance
#
#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
#@param ds_name str : The name of the datasource to instanciate
#@param ro bool : if true initialise the _ro_datasource attribute else
#initialise _rw_datasource attribute
#@throw SettingsError if an error occurs
@classmethod
def _init_datasource(cls, ds_name, ro):
expt_msg = "In LeAPI class '%s' " % cls.__name__
if ds_name not in Settings.datasources._fields:
#Checking that datasource exists
expt_msg += "Unknown or unconfigured datasource %s for class %s"
expt_msg %= (ds_name, cls.__name__)
raise SettingsError(expt_msg)
try:
#fetching plugin name
ds_plugin_name, ds_identifier = cls._get_ds_plugin_name(ds_name, ro)
except NameError:
expt_msg += "Datasource %s is missconfigured, missing identifier."
expt_msg %= ds_name
raise SettingsError(expt_msg)
except RuntimeError:
expt_msg += "Error in datasource %s configuration. Trying to use \
a read only as a read&write datasource"
expt_msg %= ds_name
raise SettingsError(expt_msg)
except ValueError as e:
expt_msg += str(e)
raise SettingsError(expt_msg)
try:
ds_conf = cls._get_ds_connection_conf(ds_identifier, ds_plugin_name)
except NameError as e:
expt_msg += str(e)
raise SettingsError(expt_msg)
#Checks that the datasource plugin exists
ds_plugin_module = Plugin.get(ds_plugin_name).loader_module()
try:
datasource_class = getattr(ds_plugin_module, "Datasource")
except AttributeError as e:
expt_msg += "The datasource plugin %s seems to be invalid. Error \
raised when trying to import Datasource"
expt_msg %= ds_identifier
raise SettingsError(expt_msg)
return datasource_class(**ds_conf)
##@brief Try to fetch a datasource configuration
#@param ds_identifier str : datasource name
#@param ds_plugin_name : datasource plugin name
#@return a dict containing datasource initialisation options
#@throw NameError if a datasource plugin or instance cannot be found
@staticmethod
def _get_ds_connection_conf(ds_identifier,ds_plugin_name):
if ds_plugin_name not in Settings.datasource._fields:
msg = "Unknown or unconfigured datasource plugin %s"
msg %= ds_plugin
raise NameError(msg)
ds_conf = getattr(Settings.datasource, ds_plugin_name)
if ds_identifier not in ds_conf._fields:
msg = "Unknown or unconfigured datasource instance %s"
msg %= ds_identifier
raise NameError(msg)
ds_conf = getattr(ds_conf, ds_identifier)
return {k: getattr(ds_conf,k) for k in ds_conf._fields }
##@brief fetch datasource plugin name
#@param ds_name str : datasource name
#@param ro bool : if true consider the datasource as read only
#@return a tuple(DATASOURCE_PLUGIN_NAME, DATASOURCE_CONNECTION_NAME)
#@throw NameError if datasource identifier not found
#@throw RuntimeError if datasource is read_only but ro flag was false
@staticmethod
def _get_ds_plugin_name(ds_name, ro):
datasource_orig_name = ds_name
# fetching connection identifier given datasource name
ds_identifier = getattr(Settings.datasources, ds_name)
read_only = getattr(ds_identifier, 'read_only')
try:
ds_identifier = getattr(ds_identifier, 'identifier')
except NameError as e:
raise e
if read_only and not ro:
raise RuntimeError()
res = ds_identifier.split('.')
if len(res) != 2:
raise ValueError("expected value for identifier is like \
DS_PLUGIN_NAME.DS_INSTANCE_NAME. But got %s" % ds_identifier)
return res
##@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 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] = fields.construct_data( self,
fname,
self.__datas,
self.__datas[fname]
)
except Exception as e:
err_list[fname] = e
# 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 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 uids a list: lists of (fieldname, fieldvalue), with fieldname in cls._uids
#@returns the
@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 target_class LeObject : class of object the query is about
#@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
#@param Inst
#@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