1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2026-01-13 02:02:15 +01:00

Merge branch 'master' of git@git.labocleo.org:lodel2

This commit is contained in:
Quentin Bonaventure 2017-03-28 12:27:35 +02:00
commit cb95014a9b
27 changed files with 993 additions and 822 deletions

View file

@ -1,4 +1,9 @@
#-*- coding: utf-8 -*-
## @package lodel.leapi.datahandlers.datas
# This module contains specific datahandlers extending the basic ones from the lodel.leapi.datahandlers.datas_base module.
import warnings
import inspect
import re
@ -12,22 +17,29 @@ LodelContext.expose_modules(globals(), {
'LodelFatalError', 'DataNoneValid', 'FieldValidationError']})
##@brief Data field designed to handle formated strings
## @brief Data field designed to handle formated strings
class FormatString(Varchar):
help = 'Automatic string field, designed to use the str % operator to \
build its content'
help = 'Automatic string field, designed to use the str % operator to build its content'
base_type = 'char'
##@brief Build its content with a field list and a format string
# @param format_string str
# @param field_list list : List of field to use
# @param **kwargs
## @brief Constructor
# @param _field_list list : List of fields to use
# @param _format_string str : formatted string
# @param **kwargs : additional options
def __init__(self, format_string, field_list, **kwargs):
self._field_list = field_list
self._format_string = format_string
super().__init__(internal='automatic', **kwargs)
## @brief constructs the formatted string data
# The string can be truncated depending on the maximum length defined for this field.
#
# @param emcomponent EmComponent
# @param fname str
# @param datas dict
# @param cur_value str
# @return str
def _construct_data(self, emcomponent, fname, datas, cur_value):
ret = self._format_string % tuple(
datas[fname] for fname in self._field_list)
@ -35,27 +47,28 @@ build its content'
warnings.warn("Format field overflow. Truncating value")
ret = ret[:self.max_length]
return ret
##@brief Varchar validated by a regex
## @brief Varchar validated by a regex
class Regex(Varchar):
help = 'String field validated with a regex. Takes two options : \
max_length and regex'
base_type = 'char'
##@brief A string field validated by a regex
# @param regex str : a regex string (passed as argument to re.compile())
## @brief A string field validated by a regex
# @param regex str : a regex string (passed as argument to re.compile()), default value is an empty string
# @param max_length int : the max length for this field (default : 10)
# @param **kwargs
# @param **kwargs : additional options
def __init__(self, regex='', max_length=10, **kwargs):
self.regex = regex
self.compiled_re = re.compile(regex) # trigger an error if invalid regex
super(self.__class__, self).__init__(max_length=max_length, **kwargs)
##@brief Check and cast value in appropriate type
#@param value *
#@throw FieldValidationError if value is unappropriate or can not be cast
#@return value
## @brief Check and cast value in appropriate type
# @param value *
# @throw FieldValidationError if value is unappropriate or can not be cast
# @return str
def _check_data_value(self, value):
value = super()._check_data_value(value)
if not self.compiled_re.match(value) or len(value) > self.max_length:
@ -63,6 +76,10 @@ max_length and regex'
raise FieldValidationError(msg)
return value
## @brief checks if another datahandler can override this one
#
# @param data_handler Datahandler
# @return bool
def can_override(self, data_handler):
if not super().can_override(data_handler):
return False
@ -71,36 +88,58 @@ max_length and regex'
return False
return True
##@brief Handles uniq ID
class UniqID(Integer):
help = 'Fieldtype designed to handle editorial model UID'
base_type = 'int'
##@brief A uid field
# @param **kwargs
## @brief A uid field
#
# @param **kwargs dict
def __init__(self, **kwargs):
kwargs['internal'] = 'automatic'
super(self.__class__, self).__init__(primary_key = True, **kwargs)
## @brief Constructs the field's data
# @param emcomponent EmComponent : Component corresponding to the field
# @param fname
# @param datas
# @param cur_value str : current value to use (is retrieved from the datasource if not given)
# @return str
# @remarks fname and datas are not used and should become non mandatory, cur_value should have a None default value
def construct_data(self, emcomponent, fname, datas, cur_value):
if cur_value is None:
#Ask datasource to provide a new uniqID
return emcomponent._ro_datasource.new_numeric_id(emcomponent)
return cur_value
## @brief Class representing a LeObject subclass
class LeobjectSubclassIdentifier(Varchar):
help = 'Datahandler designed to handle LeObject subclass identifier in DB'
base_type = 'varchar'
## @brief Constructor
# @param kwargs dict : additional options
# @throw RuntimeError
# @todo define the "internal" option that can be given in the kwargs, and document its meaning
def __init__(self, **kwargs):
if 'internal' in kwargs and not kwargs['internal']:
raise RuntimeError(self.__class__.__name__+" datahandler can only \
be internal")
kwargs['internal'] = True
super().__init__(**kwargs)
## @brief Returns the class' name
# @param emcomponent EmComponent : Component correponding to the field
# @param fname
# @param datas
# @param cur_value
# @return str
# @remarks fname, datas and cur_value should be given default values as they are not mandatory here.
def construct_data(self, emcomponent, fname, datas, cur_value):
cls = emcomponent
if not inspect.isclass(emcomponent):
@ -108,13 +147,13 @@ be internal")
return cls.__name__
##@brief Data field designed to handle concatenated fields
## @brief Data field designed to handle concatenated fields
class Concat(FormatString):
help = 'Automatic strings concatenation'
base_type = 'char'
##@brief Build its content with a field list and a separator
# @param field_list list : List of field to use
## @brief Build its content with a field list and a separator
# @param field_list list : List of fields to concatenate
# @param separator str
# @param **kwargs
def __init__(self, field_list, separator=' ', **kwargs):
@ -124,22 +163,34 @@ class Concat(FormatString):
**kwargs)
## @brief Datahandler managing a password
class Password(Varchar):
help = 'Handle passwords'
base_type = 'password'
pass
## @brief Datahandler turning a string into a list
class VarcharList(Varchar):
help = 'DataHandler designed to make a list out of a string.'
base_type = 'varchar'
## @brief Constructor
# @param delimiter str : default value is a whitespace character
# @param **kwargs : additional options
# @throw LodelException : this exception is raised when the delimiter is not a string
def __init__(self, delimiter=' ', **kwargs):
if not isinstance(delimiter, str):
raise LodelException("The delimiter has to be a string, %s given" % type(delimiter))
self.delimiter = str(delimiter)
super().__init__(**kwargs)
## @brief Constructs the field's data
# @param emcomponent EmComponent
# @param fname
# @param datas
# @param cur_value : current value to use
# @return list
# @remarks emcomponent, fname and datas should be given a default value as they seem to be non mandatory
def construct_data(self, emcomponent, fname, datas, cur_value):
result = cur_value.split(self.delimiter)
return result

View file

@ -1,3 +1,5 @@
## @brief Exception classes for datahandlers
class LodelDataHandlerException(Exception):
pass

View file

@ -9,12 +9,13 @@ LodelContext.expose_modules(globals(), {
'LodelFatalError', 'DataNoneValid',
'FieldValidationError']})
## @brief Child class of SingleRef. The object referenced must exist
class Link(SingleRef):
pass
## @brief Child class of MultipleRef where references are represented in the form of a python list
# All the objects referenced must exist
class List(MultipleRef):
## @brief instanciates a list reference
@ -97,13 +98,16 @@ class Map(MultipleRef):
## @brief This Reference class is designed to handler hierarchy with some constraint
class Hierarch(MultipleRef):
directly_editable = False
## @brief Instanciate a data handler handling hierarchical relation with constraints
# @param back_reference tuple : Here it is mandatory to have a back ref (like a parent field)
# @param max_depth int | None : limit of depth
# @param max_childs int | Nine : maximum number of childs by nodes
# @param kwargs :
# - allowed_classes list | None : list of allowed em classes if None no restriction
# - internal bool : if False, the field is not internal
def __init__(self, back_reference, max_depth=None, max_childs=None, **kwargs):
super().__init__(back_reference=back_reference,
max_depth=max_depth,

View file

@ -4,6 +4,7 @@ from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
'lodel.exceptions': ['LodelExceptions', 'LodelException']})
##@brief Handles LeApi error
class LeApiError(LodelException):
pass
@ -13,20 +14,20 @@ class LeApiErrors(LodelExceptions, LeApiError):
pass
##@brief When an error concerns a datas
##@brief When an error concerns a data
class LeApiDataCheckError(LeApiError):
pass
##@brief Handles LeApi data errors
class LeApiDataCheckErrors(LodelExceptions, LeApiError):
pass
##@brief Handles leapi query errors
class LeApiQueryError(LeApiError):
pass
##@brief Handles mulitple query errors
##@brief Handles multiple query errors
class LeApiQueryErrors(LodelExceptions, LeApiQueryError):
pass

View file

@ -1,22 +1,25 @@
#-*- coding: utf-8 -*-
import os, os.path
import os
import os.path
import functools
from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
'lodel.editorial_model.components': ['EmComponent', 'EmClass', 'EmField',
'EmGroup'],
'EmGroup'],
'lodel.leapi.leobject': ['LeObject'],
'lodel.leapi.datahandlers.base_classes': ['DataHandler'],
'lodel.logger': 'logger'})
##@brief Generate python module code from a given model
# @brief Generates python module code from a given model
# @param model lodel.editorial_model.model.EditorialModel
def dyncode_from_em(model):
# Generation of LeObject child classes code
cls_code, modules, bootstrap_instr = generate_classes(model)
cls_code, bootstrap_instr = generate_classes(model)
# Header
imports = """from lodel.context import LodelContext
@ -25,10 +28,8 @@ LodelContext.expose_modules(globals(), {
'lodel.leapi.datahandlers.base_classes': ['DataField'],
'lodel.plugin.hooks': ['LodelHook']})
"""
for module in modules:
imports += "import %s\n" % module
class_list = [ LeObject.name2objname(cls.uid) for cls in get_classes(model) ]
# generates the list of all classes in the editorial model
class_list = [LeObject.name2objname(cls.uid) for cls in get_classes(model)]
# formating all components of output
res_code = """#-*- coding: utf-8 -*-
@ -41,17 +42,21 @@ dynclasses = {class_list}
dynclasses_dict = {class_dict}
{common_code}
""".format(
imports = imports,
classes = cls_code,
bootstrap_instr = bootstrap_instr,
class_list = '[' + (', '.join([cls for cls in class_list]))+']',
class_dict = '{' + (', '.join([ "'%s': %s" % (cls, cls)
for cls in class_list]))+'}',
common_code = common_code(),
imports=imports,
classes=cls_code,
bootstrap_instr=bootstrap_instr,
class_list='[' + (', '.join([cls for cls in class_list])) + ']',
class_dict='{' + (', '.join(["'%s': %s" % (cls, cls)
for cls in class_list])) + '}',
common_code=common_code(),
)
return res_code
##@brief Return the content of lodel.leapi.lefactory_common
# @brief Returns the content of lodel.leapi.lefactory_common
#
# @return a string
def common_code():
res = ""
fname = os.path.dirname(__file__)
@ -61,23 +66,32 @@ def common_code():
if not line.startswith('#-'):
res += line
return res
##@brief return A list of EmClass sorted by dependencies
# @brief return A list of EmClass sorted by dependencies
#
# The first elts in the list depends on nothing, etc.
# The first elts in the list depend on nothing, etc.
# @param a list of Emclass instances to be sorted
# @return a list of EmClass instances
def emclass_sorted_by_deps(emclass_list):
def emclass_deps_cmp(cls_a, cls_b):
return len(cls_a.parents_recc) - len(cls_b.parents_recc)
ret = sorted(emclass_list, key = functools.cmp_to_key(emclass_deps_cmp))
ret = sorted(emclass_list, key=functools.cmp_to_key(emclass_deps_cmp))
return ret
##@brief Returns a list of EmClass that will be represented as LeObject child classes
def get_classes(model):
return [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ]
# @brief Returns a list of EmClass instances that will be represented as LeObject child classes
# @param model : an EditorialModel instance
# @return a list of EmClass instances
def get_classes(model):
return [cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract]
# @brief Given an EmField returns the data_handler constructor suitable for dynamic code
# @param a EmField instance
# @return a string
##@brief Given an EmField returns the data_handler constructor suitable for dynamic code
def data_handler_constructor(emfield):
#dh_module_name = DataHandler.module_name(emfield.data_handler_name)+'.DataHandler'
get_handler_class_instr = 'DataField.from_name(%s)' % repr(emfield.data_handler_name)
@ -85,60 +99,65 @@ def data_handler_constructor(emfield):
for name, val in emfield.data_handler_options.items():
if name == 'back_reference' and isinstance(val, tuple):
options.append('{optname}: ({leo_name}, {fieldname})'.format(
optname = repr(name),
leo_name = LeObject.name2objname(val[0]),
fieldname = repr(val[1]),))
optname=repr(name),
leo_name=LeObject.name2objname(val[0]),
fieldname=repr(val[1]),))
else:
options.append(repr(name)+': '+forge_optval(val))
options.append(repr(name) + ': ' + forge_optval(val))
return '{handler_instr}(**{{ {options} }})'.format(
handler_instr = get_handler_class_instr,
options = ', '.join(options))
##@brief Return a python repr of option values
handler_instr=get_handler_class_instr,
options=', '.join(options))
# @brief Return a python repr of option values
# @param A value of any type which represents option
# @return a string
def forge_optval(optval):
if isinstance(optval, dict):
return '{' + (', '.join( [ '%s: %s' % (repr(name), forge_optval(val)) for name, val in optval.items()])) + '}'
return '{' + (', '.join(['%s: %s' % (repr(name), forge_optval(val)) for name, val in optval.items()])) + '}'
if isinstance(optval, (set, list, tuple)):
return '[' + (', '.join([forge_optval(val) for val in optval])) + ']'
if isinstance(optval, EmField):
return "{leobject}.data_handler({fieldname})".format(
leobject = LeObject.name2objname(optval._emclass.uid),
fieldname = repr(optval.uid)
)
elif isinstance(optval, EmClass):
leobject=LeObject.name2objname(optval._emclass.uid),
fieldname=repr(optval.uid)
)
if isinstance(optval, EmClass):
return LeObject.name2objname(optval.uid)
else:
return repr(optval)
##@brief Generate dyncode from an EmClass
# @param model EditorialModel :
# @todo delete imports. It is never use, consequently changed return parameters.
return repr(optval)
# @brief Generate dyncode from an EmClass
# @param model EditorialModel :
# @return a tuple with emclass python code, a set containing modules name to import, and a list of python instruction to bootstrap dynamic code, in this order
def generate_classes(model):
res = ""
imports = list()
bootstrap = ""
# Generating field list for LeObjects generated from EmClass
for em_class in get_classes(model):
logger.info("Generating a dynamic class for %s" % em_class.uid)
uid = list() # List of fieldnames that are part of the EmClass primary key
parents = list() # List of parents EmClass
# Determine pk
uid = list() # List for fieldnames that are part of the EmClass primary key
parents = list() # List for em_class's parents
# Determines primary key
for field in em_class.fields():
if field.data_handler_instance.is_primary_key():
uid.append(field.uid)
# Determine parent for inheritance
# Determines parentsfor inheritance
if len(em_class.parents) > 0:
for parent in em_class.parents:
parents.append(LeObject.name2objname(parent.uid))
parents.append(LeObject.name2objname(parent.uid))
else:
parents.append('LeObject')
datasource_name = em_class.datasource
# Dynamic code generation for LeObject childs classes
# Dynamic code generation for LeObject child classes
em_cls_code = """
class {clsname}({parents}):
_abstract = {abstract}
@ -150,12 +169,12 @@ class {clsname}({parents}):
_child_classes = None
""".format(
clsname = LeObject.name2objname(em_class.uid),
parents = ', '.join(parents),
abstract = 'True' if em_class.abstract else 'False',
uid_list = repr(uid),
datasource_name = repr(datasource_name),
)
clsname=LeObject.name2objname(em_class.uid),
parents=', '.join(parents),
abstract='True' if em_class.abstract else 'False',
uid_list=repr(uid),
datasource_name=repr(datasource_name),
)
res += em_cls_code
# Dyncode fields bootstrap instructions
child_classes = model.get_class_childs(em_class.uid)
@ -163,14 +182,14 @@ class {clsname}({parents}):
child_classes = 'tuple()'
else:
child_classes = '(%s,)' % (', '.join(
[ LeObject.name2objname(emcls.uid) for emcls in child_classes]))
[LeObject.name2objname(emcls.uid) for emcls in child_classes]))
bootstrap += """{classname}._set__fields({fields})
{classname}._child_classes = {child_classes}
""".format(
classname = LeObject.name2objname(em_class.uid),
fields = '{' + (', '.join(['\n\t%s: %s' % (repr(emfield.uid),data_handler_constructor(emfield)) for emfield in em_class.fields()])) + '}',
child_classes = child_classes,
)
classname=LeObject.name2objname(em_class.uid),
fields='{' + (', '.join(['\n\t%s: %s' % (repr(emfield.uid),
data_handler_constructor(emfield)) for emfield in em_class.fields()])) + '}',
child_classes=child_classes,
)
bootstrap += "\n"
return res, set(imports), bootstrap
return res, bootstrap

View file

@ -5,7 +5,7 @@
#- All lines that begins with #- will be deleted from dynamically generated
#- code...
##@brief Return a dynamically generated class given it's name
##@brief Returns a dynamically generated class given its name
#@param name str : The dynamic class name
#@return False or a child class of LeObject
def name2class(name):
@ -14,7 +14,7 @@ def name2class(name):
return dynclasses_dict[name]
##@brief Return a dynamically generated class given it's name
##@brief Returns a dynamically generated class given its name
#@note Case insensitive version of name2class
#@param name str
#@return False or a child class of LeObject
@ -26,11 +26,10 @@ def lowername2class(name):
return new_dict[name]
##@brief Trigger dynclasses datasources initialisation
##@brief Triggers dynclasses datasources initialisation
@LodelHook("lodel2_plugins_loaded")
def lodel2_dyncode_datasources_init(self, caller, payload):
for cls in dynclasses:
cls._init_datasources()
LodelContext.expose_modules(globals(), {'lodel.plugin.hooks': ['LodelHook']})
LodelHook.call_hook("lodel2_dyncode_loaded", __name__, dynclasses)

View file

@ -1,5 +1,9 @@
#-*- coding: utf-8 -*-
## @package lodel.leapi.leobject
# This module is centered around the basic LeObject class, which is the main class for all the objects managed by lodel.
import importlib
import warnings
import copy
@ -22,20 +26,16 @@ LodelContext.expose_modules(globals(), {
'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
## @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
## @brief Wrapper class for LeObject getter & setter
#
# This class intend to provide easy & friendly access to LeObject fields values
# without name collision problems
# 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
@ -43,47 +43,49 @@ class LeObjectValues(object):
self._setter = set_callback
self._getter = get_callback
# @brief Provide read access to datas values
## @brief Provides read access to datas values
# @note Read access should be provided for all fields
# @param fname str : Field name
# @return method
def __getattribute__(self, fname):
getter = super().__getattribute__('_getter')
return getter(fname)
# @brief Provide write access to datas values
## @brief Provides 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
# @return method
def __setattribute__(self, fname, fval):
setter = super().__getattribute__('_setter')
return setter(fname, fval)
## @brief Represents a handled object in Lodel.
class LeObject(object):
# @brief boolean that tells if an object is abtract or not
## @brief boolean that tells if an object is abtract or not
_abstract = None
# @brief A dict that stores DataHandler instances indexed by field name
## @brief A dict that stores DataHandler instances indexed by field name
_fields = None
# @brief A tuple of fieldname (or a uniq fieldname) representing uid
## @brief A tuple of fieldname (or a uniq fieldname) representing uid
_uid = None
# @brief Read only datasource ( see @ref lodel2_datasources )
## @brief Read only datasource ( see @ref lodel2_datasources )
_ro_datasource = None
# @brief Read & write datasource ( see @ref lodel2_datasources )
## @brief Read & write datasource ( see @ref lodel2_datasources )
_rw_datasource = None
# @brief Store the list of child classes
## @brief Store the list of child classes
_child_classes = None
# @brief Name of the datasource plugin
## @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
## @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
## @brief Store a list of initianilized fields when instanciation not complete else store True
self.__initialized = list()
# @brief Datas accessor. Instance of @ref LeObjectValues
## @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
@ -92,8 +94,10 @@ class LeObject(object):
self.__set_initialized()
return self
# @brief Construct an object representing an Editorial component
# @note Can be considered as EmClass instance
# @param **kwargs
# @throw NotImplementedError when the class being instanciated is noted as abstract and then should not be instanciated.
# @throw LeApiError in case of missing or invalid data.
def __init__(self, **kwargs):
if self._abstract:
raise NotImplementedError(
@ -130,19 +134,21 @@ class LeObject(object):
# Fields datas handling methods #
#-----------------------------------#
# @brief Property method True if LeObject is initialized else False
## @brief Property method True if LeObject is initialized else False
# @return bool
@property
def initialized(self):
return self.__is_initialized
# @return The uid field name
## @brief Returns the uid field name
# @return str
@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
## @brief Returns a list of fieldnames
# @param include_ro bool : if True includes the read only field names
# @return list of string
@classmethod
def fieldnames(cls, include_ro=False):
if not include_ro:
@ -150,13 +156,17 @@ class LeObject(object):
else:
return list(cls._fields.keys())
## @brief Returns a name, capitalizing the first character of each word
# @param name str
# @return str
@classmethod
def name2objname(cls, name):
return name.title()
# @brief Return the datahandler asssociated with a LeObject field
# @param fieldname str : The fieldname
## @brief Returns the datahandler asssociated with a LeObject field
# @param fieldname str : The field's name
# @return A data handler instance
# @throw NameError when the given field name doesn't exist
#@todo update class of exception raised
@classmethod
def data_handler(cls, fieldname):
@ -164,9 +174,9 @@ class LeObject(object):
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>
## @brief Returns a dictionary containing the reference datahandlers
# @param with_backref bool : if true return only references with back_references
# @return dict : <code>{'fieldname': datahandler, ...}</code>
@classmethod
def reference_handlers(cls, with_backref=True):
return {fname: fdh
@ -174,11 +184,12 @@ class LeObject(object):
if fdh.is_reference() and
(not with_backref or fdh.back_reference is not None)}
# @brief Return a LeObject child class from a name
## @brief Returns 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
# @throw NotImplementedError if the method is abstract (if we use the LeObject class)
# @throw LeApiError if an unexisting name is given
@classmethod
def name2class(cls, leobject_name):
if cls.__module__ == 'lodel.leapi.leobject':
@ -189,14 +200,16 @@ class LeObject(object):
except (AttributeError, TypeError):
raise LeApiError("No LeObject named '%s'" % leobject_name)
## @brief Checks if the class is abstract or not
# @return bool
@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
## @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:
@ -204,8 +217,9 @@ class LeObject(object):
except KeyError:
raise NameError("No field named '%s' in %s" % (fieldname,
cls.__name__))
# @return A dict with fieldname as key and datahandler as instance
## @brief Returns the fields' datahandlers as a dictionary
# @param include_ro bool : if True, includes the read-only fields (default value : False)
# @return dict
@classmethod
def fields(cls, include_ro=False):
if include_ro:
@ -214,14 +228,12 @@ class LeObject(object):
return {fname: cls._fields[fname] for fname in cls._fields\
if not cls._fields[fname].is_internal()}
# @brief Return the list of parents classes
## @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
# @note the first item of the list is the current class, the second is its parent etc...
# @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]
@ -234,16 +246,15 @@ class LeObject(object):
res.append(cur)
return res
# @brief Return a tuple a child classes
#@return a tuple of child classes
## @brief Returns a tuple of child classes
# @return tuple
@classmethod
def child_classes(cls):
return copy.copy(cls._child_classes)
# @brief Return the parent class that is the "source" of uid
## @brief Returns the parent class that defines the unique id
#
# The method goal is to return the parent class that defines UID.
#@return a LeObject child class or false if no UID defined
# @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:
@ -259,11 +270,11 @@ class LeObject(object):
prev = pcls
return prev
# @brief Initialise both datasources (ro and rw)
## @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()
# @see LeObject::_init_datasource()
@classmethod
def _init_datasources(cls):
if isinstance(cls._datasource_name, str):
@ -291,16 +302,16 @@ class LeObject(object):
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
## @brief Returns the uid of the current LeObject instance
# @return str
# @warning Broke multiple uid capabilities
def uid(self):
return self.data(self._uid[0])
# @brief Read only access to all datas
## @brief Returns the value of a field
# @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
# @param field_name str : field name
# @return the Value
# @param field_name str : field's 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):
@ -311,18 +322,19 @@ class LeObject(object):
"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
## @brief Returns a dictionary containing all the fields' values
# @return dict
def datas(self, internal=False):
return {fname: self.data(fname) for fname in self.fieldnames(internal)}
# @brief Datas setter
## @brief Datas setter
# @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
# @param fname str : field name
# @param fname str : field's 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
# @throw LeApiErrors if the data check generates an error
def set_data(self, fname, fval):
if fname not in self.fieldnames(include_ro=False):
if fname not in self._fields.keys():
@ -353,18 +365,18 @@ class LeObject(object):
else:
self.__datas[fname] = val
# @brief Update the __initialized attribute according to LeObject internal state
## @brief Updates the __initialized attribute according to LeObject internal state
#
# Check the list of initialized fields and set __initialized to True if all fields initialized
# Checks the list of initialized fields and sets __initialized at 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
## @brief Designed to be called when datas are modified
#
# Make different checks on the LeObject given it's state (fully initialized or not)
# Makes 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()
@ -409,24 +421,24 @@ class LeObject(object):
# Other methods #
#--------------------#
# @brief Temporary method to set private fields attribute at dynamic code generation
## @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
# @param field_list list : list of EmField instance
@classmethod
def _set__fields(cls, field_list):
cls._fields = field_list
# @brief Check that datas are valid for this type
## @brief Checks if the data is 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 complete bool : if True expects that values are provided for all non internal fields
# @param allow_internal bool : if True does not raise an error if a field is internal
# @param cls
# @return Checked datas
# @throw LeApiDataCheckError if errors reported during check
# @throw LeApiDataCheckErrors if errors are reported during check
@classmethod
def check_datas_value(cls, datas, complete=False, allow_internal=True):
err_l = dict() # Error storing
@ -459,14 +471,13 @@ class LeObject(object):
raise LeApiDataCheckErrors("Error while checking datas", err_l)
return checked_datas
# @brief Check and prepare datas
## @brief Checks and prepares all the data
#
# @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
# @param complete bool : If True you MUST give all the datas (default value : False)
# @param allow_internal : Wether or not interal fields are expected in datas (default value : True)
# @return Datas ready for use
# @todo: complete is very unsafe, find a way to get rid of it
@classmethod
@ -482,9 +493,8 @@ construction and consitency when datas are not complete\n")
cls._check_datas_consistency(ret_datas)
return ret_datas
# @brief Construct datas values
## @brief Constructs 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
@ -498,12 +508,12 @@ construction and consitency when datas are not complete\n")
}
return ret
# @brief Check datas consistency
## @brief Checks 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
# @throw LeApiDataCheckError in case of failure
@classmethod
def _check_datas_consistency(cls, datas):
err_l = []
@ -516,27 +526,28 @@ construction and consitency when datas are not complete\n")
if len(err_l) > 0:
raise LeApiDataCheckError("Datas consistency checks fails", err_l)
# @brief Check datas consistency
## @brief Checks data consistency
# 
# @warning assert that datas is complete
# @param cls
# @param datas dict : Datas that have been returned by LeCrud.prepare_datas() method
# @param datas dict : Data that have been returned by 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
## @brief Adds a new instance of LeObject
# @param datas dict : LeObject's data
# @return a new uid in case of success, False otherwise
@classmethod
def insert(cls, datas):
query = LeInsertQuery(cls)
return query.execute(datas)
# @brief Update an instance of LeObject
## @brief Update an instance of LeObject
#
#@param datas : list of new datas
# @param datas : list of new datas
# @return LeObject
def update(self, datas=None):
datas = self.datas(internal=False) if datas is None else datas
uids = self._uid
@ -555,9 +566,9 @@ construction and consitency when datas are not complete\n")
return result
# @brief Delete an instance of LeObject
## @brief Delete an instance of LeObject
#
#@return 1 if the objet has been deleted
# @return 1 if the objet has been deleted
def delete(self):
uids = self._uid
query_filter = list()
@ -570,9 +581,9 @@ construction and consitency when datas are not complete\n")
return result
# @brief Delete instances of LeObject
#@param query_filters list
#@returns the number of deleted items
## @brief Deletes instances of LeObject
# @param query_filters list
# @return the number of deleted items
@classmethod
def delete_bundle(cls, query_filters):
deleted = 0
@ -589,16 +600,16 @@ construction and consitency when datas are not complete\n")
deleted += result
return deleted
# @brief Get instances of LeObject
## @brief Gets 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))
# @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 (default value : 0)
# @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:
@ -628,7 +639,10 @@ construction and consitency when datas are not complete\n")
return objects
# @brief Retrieve an object given an UID
## @brief Retrieves an object given an UID
# @param uid str : Unique ID of the searched LeObject
# @return LeObject
# @throw LodelFatalError if the class does not have such a UID defined or if duplicates are found
#@todo broken multiple UID
@classmethod
def get_from_uid(cls, uid):
@ -645,7 +659,7 @@ construction and consitency when datas are not complete\n")
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 !!!")
logger.error("Duplicates detected in query results !!!")
else:
res.append(cur_res)
if len(res) > 1:

View file

@ -8,20 +8,22 @@ import warnings
from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
'lodel.leapi.exceptions': ['LeApiError', 'LeApiErrors',
'LeApiDataCheckError', 'LeApiDataCheckErrors', 'LeApiQueryError',
'LeApiQueryErrors'],
'LeApiDataCheckError', 'LeApiDataCheckErrors', 'LeApiQueryError',
'LeApiQueryErrors'],
'lodel.plugin.hooks': ['LodelHook'],
'lodel.logger': ['logger']})
##@todo check datas when running query
# @todo check data when running query
class LeQuery(object):
##@brief Hookname prefix
# @brief Hookname prefix
_hook_prefix = None
##@brief arguments for the LeObject.check_data_value()
# @brief arguments for the LeObject.check_data_value()
_data_check_args = {'complete': False, 'allow_internal': False}
##@brief Abstract constructor
# @brief Abstract constructor
# @param target_class LeObject : class of object the query is about
def __init__(self, target_class):
from .leobject import LeObject
@ -29,77 +31,80 @@ class LeQuery(object):
raise NotImplementedError("Abstract class")
if not inspect.isclass(target_class) or \
not issubclass(target_class, LeObject):
raise TypeError("target class has to be a child class of LeObject but %s given"% target_class)
raise TypeError(
"target class has to be a child class of LeObject but %s given" % target_class)
self._target_class = target_class
self._ro_datasource = target_class._ro_datasource
self._rw_datasource = target_class._rw_datasource
##@brief Execute a query and return the result
#@param **datas
# @brief Executes a query and returns the result
#@param **data
#@return the query result
#@see LeQuery._query()
#@todo check that the check_datas_value is not duplicated/useless
def execute(self, datas):
if not datas is None:
def execute(self, data):
if data is not None:
self._target_class.check_datas_value(
datas,
**self._data_check_args)
self._target_class.prepare_datas(datas) #not yet implemented
data,
**self._data_check_args)
self._target_class.prepare_datas(data) # not yet implemented
if self._hook_prefix is None:
raise NotImplementedError("Abstract method")
LodelHook.call_hook(self._hook_prefix+'pre',
self._target_class,
datas)
ret = self._query(datas=datas)
ret = LodelHook.call_hook(self._hook_prefix+'post',
self._target_class,
ret)
LodelHook.call_hook(self._hook_prefix + 'pre',
self._target_class,
data)
ret = self._query(data=data)
ret = LodelHook.call_hook(self._hook_prefix + 'post',
self._target_class,
ret)
return ret
##@brief Childs classes implements this method to execute the query
#@param **datas
# @brief Child classes implement this method to execute the query
#@param **data
#@return query result
def _query(self, **datas):
def _query(self, **data):
raise NotImplementedError("Asbtract method")
##@return a dict with query infos
# @return a dict with query infos
def dump_infos(self):
return {'target_class': self._target_class}
def __repr__(self):
ret = "<{classname} target={target_class}>"
return ret.format(
classname=self.__class__.__name__,
target_class = self._target_class)
classname=self.__class__.__name__,
target_class=self._target_class)
# @brief Abstract class handling query with filters
##@brief Abstract class handling query with filters
class LeFilteredQuery(LeQuery):
##@brief The available operators used in query definitions
# @brief The available operators used in query definitions
_query_operators = [
' = ',
' <= ',
' >= ',
' != ',
' < ',
' > ',
' in ',
' not in ',
' like ',
' not like ']
' = ',
' <= ',
' >= ',
' != ',
' < ',
' > ',
' in ',
' not in ',
' like ',
' not like ']
##@brief Regular expression to process filters
# @brief Regular expression to process filters
_query_re = None
##@brief Abtract constructor for queries with filter
# @brief Abtract constructor for queries with filter
#@param target_class LeObject : class of object the query is about
#@param query_filters list : with a tuple (only one filter) or a list of
# tuple or a dict: {OP,list(filters)} with OP = 'OR' or 'AND for tuple
# (FIELD,OPERATOR,VALUE)
def __init__(self, target_class, query_filters=None):
super().__init__(target_class)
##@brief The query filter tuple(std_filter, relational_filters)
# @brief The query filter tuple(std_filter, relational_filters)
self._query_filter = None
##@brief Stores potential subqueries (used when a query implies
# @brief Stores potential subqueries (used when a query implies
# more than one datasource.
#
# Subqueries are tuple(target_class_ref_field, LeGetQuery)
@ -107,11 +112,11 @@ class LeFilteredQuery(LeQuery):
query_filters = [] if query_filters is None else query_filters
self.set_query_filter(query_filters)
##@brief Abstract FilteredQuery execution method
# @brief Abstract FilteredQuery execution method
#
# This method takes care to execute subqueries before calling super execute
def execute(self, datas=None):
#copy originals filters
def execute(self, data=None):
# copy originals filters
orig_filters = copy.copy(self._query_filter)
std_filters, rel_filters = self._query_filter
@ -123,17 +128,17 @@ class LeFilteredQuery(LeQuery):
try:
filters, rel_filters = self._query_filter
res = super().execute(datas)
res = super().execute(data)
except Exception as e:
#restoring filters even if an exception is raised
# restoring filters even if an exception is raised
self.__query_filter = orig_filters
raise e #reraise
#restoring filters
raise e # reraise
# restoring filters
self._query_filter = orig_filters
return res
##@brief Add filter(s) to the query
# @brief Add filter(s) to the query
#
# This method is also able to slice query if different datasources are
# implied in the request
@ -144,39 +149,39 @@ class LeFilteredQuery(LeQuery):
def set_query_filter(self, query_filter):
if isinstance(query_filter, str):
query_filter = [query_filter]
#Query filter prepration
# Query filter prepration
filters_orig, rel_filters = self._prepare_filters(query_filter)
# Here we now that each relational filter concern only one datasource
# thank's to _prepare_relational_fields
#Multiple datasources detection
# Multiple datasources detection
self_ds_name = self._target_class._datasource_name
result_rel_filters = list() # The filters that will stay in the query
result_rel_filters = list() # The filters that will stay in the query
other_ds_filters = dict()
for rfilter in rel_filters:
(rfield, ref_dict), op, value = rfilter
#rfield : the field in self._target_class
tmp_rel_filter = dict() #designed to stores rel_field of same DS
# rfield : the field in self._target_class
tmp_rel_filter = dict() # designed to stores rel_field of same DS
# First step : simplification
# Trying to delete relational filters done on referenced class uid
for tclass, tfield in copy.copy(ref_dict).items():
#tclass : reference target class
#tfield : referenced field from target class
# tclass : reference target class
# tfield : referenced field from target class
#
# !!!WARNING!!!
# The line below brake multi UID support
#
if tfield == tclass.uid_fieldname()[0]:
#This relational filter can be simplified as
# This relational filter can be simplified as
# ref_field, op, value
# Note : we will have to dedup filters_orig
filters_orig.append((rfield, op, value))
del(ref_dict[tclass])
if len(ref_dict) == 0:
continue
#Determine what to do with other relational filters given
# Determine what to do with other relational filters given
# referenced class datasource
#Remember : each class in a relational filter has the same
# Remember : each class in a relational filter has the same
# datasource
tclass = list(ref_dict.keys())[0]
cur_ds = tclass._datasource_name
@ -189,23 +194,23 @@ class LeFilteredQuery(LeQuery):
other_ds_filters[cur_ds] = list()
other_ds_filters[cur_ds].append(
((rfield, ref_dict), op, value))
#deduplication of std filters
# deduplication of std filters
filters_cp = set()
if not isinstance(filters_orig, set):
for i, cfilt in enumerate(filters_orig):
a, b, c = cfilt
if isinstance(c, list): #list are not hashable
if isinstance(c, list): # list are not hashable
newc = tuple(c)
else:
newc = c
old_len = len(filters_cp)
filters_cp |= set((a,b,newc))
filters_cp |= set((a, b, newc))
if len(filters_cp) == old_len:
del(filters_orig[i])
# Sets _query_filter attribute of self query
self._query_filter = (filters_orig, result_rel_filters)
#Sub queries creation
# Sub queries creation
subq = list()
for ds, rfilters in other_ds_filters.items():
for rfilter in rfilters:
@ -218,7 +223,7 @@ class LeFilteredQuery(LeQuery):
subq.append((rfield, query))
self.subqueries = subq
##@return informations
# @return informations
def dump_infos(self):
ret = super().dump_infos()
ret['query_filter'] = self._query_filter
@ -238,16 +243,16 @@ class LeFilteredQuery(LeQuery):
res += '>'
return res
## @brief Prepare filters for datasource
# @brief Prepare filters for datasource
#
#A filter can be a string or a tuple with len = 3.
# A filter can be a string or a tuple with len = 3.
#
#This method divide filters in two categories :
# This method divide filters in two categories :
#
#@par Simple filters
#
#Those filters concerns fields that represent object values (a title,
#the content, etc.) They are composed of three elements : FIELDNAME OP
# Those filters concerns fields that represent object values (a title,
# the content, etc.) They are composed of three elements : FIELDNAME OP
# VALUE . Where :
#- FIELDNAME is the name of the field
#- OP is one of the authorized comparison operands (see
@ -256,14 +261,14 @@ class LeFilteredQuery(LeQuery):
#
#@par Relational filters
#
#Those filters concerns on reference fields (see the corresponding
#abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
#The filter as quite the same composition than simple filters :
# Those filters concerns on reference fields (see the corresponding
# abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
# The filter as quite the same composition than simple filters :
# FIELDNAME[.REF_FIELD] OP VALUE . Where :
#- FIELDNAME is the name of the reference field
#- REF_FIELD is an optionnal addon to the base field. It indicate on wich
#field of the referenced object the comparison as to be done. If no
#REF_FIELD is indicated the comparison will be done on identifier.
# field of the referenced object the comparison as to be done. If no
# REF_FIELD is indicated the comparison will be done on identifier.
#
#@param cls
#@param filters_l list : This list of str or tuple (or both)
@ -271,11 +276,11 @@ class LeFilteredQuery(LeQuery):
#@todo move this doc in another place (a dedicated page ?)
#@warning Does not supports multiple UID for an EmClass
def _prepare_filters(self, filters_l):
filters=list()
filters = list()
res_filters = list()
rel_filters = list()
err_l = dict()
#Splitting in tuple if necessary
# Splitting in tuple if necessary
for i, fil in enumerate(filters_l):
if len(fil) == 3 and not isinstance(fil, str):
filters.append(tuple(fil))
@ -286,7 +291,7 @@ class LeFilteredQuery(LeQuery):
err_l["filter %d" % i] = e
for field, operator, value in filters:
err_key = "%s %s %s" % (field, operator, value) #to push in err_l
err_key = "%s %s %s" % (field, operator, value) # to push in err_l
# Spliting field name to be able to detect a relational field
field_spl = field.split('.')
if len(field_spl) == 2:
@ -310,12 +315,12 @@ field name" % field)
# inconsistency
err_l[field] = NameError("The field '%s' in %s is not \
a relational field, but %s.%s was present in the filter"
% (field,
self._target_class.__name__,
field,
ref_field))
% (field,
self._target_class.__name__,
field,
ref_field))
if field_datahandler.is_reference():
#Relationnal field
# Relationnal field
if ref_field is None:
# ref_field default value
#
@ -350,14 +355,14 @@ field to use for the relational filter"
value, error = field_datahandler.check_data_value(value)
if isinstance(error, Exception):
value = value_orig
res_filters.append((field,operator, value))
res_filters.append((field, operator, value))
if len(err_l) > 0:
raise LeApiDataCheckErrors(
"Error while preparing filters : ",
err_l)
"Error while preparing filters : ",
err_l)
return (res_filters, rel_filters)
## @brief Check and split a query filter
# @brief Check and split a query filter
# @note The query_filter format is "FIELD OPERATOR VALUE"
# @param query_filter str : A query_filter string
# @param cls
@ -382,18 +387,18 @@ field to use for the relational filter"
raise ValueError(msg % query_filter)
return result
## @brief Compile the regex for query_filter processing
# @brief Compile the regex for query_filter processing
# @note Set _LeObject._query_re
@classmethod
def __compile_query_re(cls):
op_re_piece = '(?P<operator>(%s)'
op_re_piece %= cls._query_operators[0].replace(' ', '\s')
for operator in cls._query_operators[1:]:
op_re_piece += '|(%s)'%operator.replace(' ', '\s')
op_re_piece += '|(%s)' % operator.replace(' ', '\s')
op_re_piece += ')'
re_full = '^\s*(?P<field>([a-z_][a-z0-9\-_]*\.)?[a-z_][a-z0-9\-_]*)\s*'
re_full += op_re_piece+'\s*(?P<value>.*)\s*$'
re_full += op_re_piece + '\s*(?P<value>.*)\s*$'
cls._query_re = re.compile(re_full, flags=re.IGNORECASE)
pass
@ -407,10 +412,10 @@ field to use for the relational filter"
msg %= (fieldname, target_class.__name__)
return NameError(msg)
##@brief Prepare a relational filter
# @brief Prepare a relational filter
#
#Relational filters are composed of a tuple like the simple filters
#but the first element of this tuple is a tuple to :
# Relational filters are composed of a tuple like the simple filters
# but the first element of this tuple is a tuple to :
#
#<code>((FIELDNAME, {REF_CLASS: REF_FIELD}), OP, VALUE)</code>
# Where :
@ -419,9 +424,9 @@ field to use for the relational filter"
# - REF_CLASS as key. It's a LeObject child class
# - REF_FIELD as value. The name of the referenced field in the REF_CLASS
#
#Visibly the REF_FIELD value of the dict will vary only when
#no REF_FIELD is explicitly given in the filter string notation
#and REF_CLASSES has differents uid
# Visibly the REF_FIELD value of the dict will vary only when
# no REF_FIELD is explicitly given in the filter string notation
# and REF_CLASSES has differents uid
#
#@par String notation examples
#<pre>contributeur IN (1,2,3,5)</pre> will be transformed into :
@ -439,7 +444,7 @@ field to use for the relational filter"
#
#@param fieldname str : The relational field name
#@param ref_field str|None : The referenced field name (if None use
#uniq identifiers as referenced field
# uniq identifiers as referenced field
#@return a well formed relational filter tuple or an Exception instance
def _prepare_relational_fields(self, fieldname, ref_field=None):
datahandler = self._target_class.field(fieldname)
@ -467,12 +472,12 @@ the relational filter %s"
logger.debug(msg)
if len(ref_dict) == 0:
return NameError("No field named '%s' in referenced objects [%s]"
% (ref_field,
','.join([rc.__name__ for rc in ref_classes])))
% (ref_field,
','.join([rc.__name__ for rc in ref_classes])))
return (fieldname, ref_dict)
##@brief A query to insert a new object
# @brief A query to insert a new object
class LeInsertQuery(LeQuery):
_hook_prefix = 'leapi_insert_'
_data_check_args = {'complete': True, 'allow_internal': False}
@ -483,49 +488,49 @@ class LeInsertQuery(LeQuery):
abstract LeObject : %s" % target_class)
super().__init__(target_class)
## @brief Implements an insert query operation, with only one insertion
# @param datas : datas to be inserted
def _query(self, datas):
datas = self._target_class.prepare_datas(datas, True, False)
id_inserted = self._rw_datasource.insert(self._target_class, datas)
#  @brief Implements an insert query operation, with only one insertion
# @param data : data to be inserted
def _query(self, data):
data = self._target_class.prepare_datas(data, True, False)
id_inserted = self._rw_datasource.insert(self._target_class, data)
return id_inserted
"""
## @brief Implements an insert query operation, with multiple insertions
# @param datas : list of **datas to be inserted
def _query(self, datas):
# @param data : list of **data to be inserted
def _query(self, data):
nb_inserted = self._datasource.insert_multi(
self._target_class,datas_list)
self._target_class,data_list)
if nb_inserted < 0:
raise LeApiQueryError("Multiple insertions error")
return nb_inserted
"""
## @brief Execute the insert query
def execute(self, datas):
return super().execute(datas=datas)
#  @brief Execute the insert query
def execute(self, data):
return super().execute(data=data)
##@brief A query to update datas for a given object
# @brief A query to update data for a given object
#
#@todo Change behavior, Huge optimization problem when updating using filters
#and not instance. We have to run a GET and then 1 update by fecthed object...
# and not instance. We have to run a GET and then 1 update by fecthed object...
class LeUpdateQuery(LeFilteredQuery):
_hook_prefix = 'leapi_update_'
_data_check_args = {'complete': False, 'allow_internal': False}
##@brief Instanciate an update query
# @brief Instanciate an update query
#
#If a class and not an instance is given, no query_filters are expected
#and the update will be fast and simple. Else we have to run a get query
#before updating (to fetch datas, update them and then, construct them
#and check their consistency)
# If a class and not an instance is given, no query_filters are expected
# and the update will be fast and simple. Else we have to run a get query
# before updating (to fetch data, update them and then, construct them
# and check their consistency)
#@param target LeObject clas or instance
#@param query_filters list|None
#@todo change strategy with instance update. We have to accept datas for
#the execute method
#@todo change strategy with instance update. We have to accept data for
# the execute method
def __init__(self, target, query_filters=None):
##@brief This attr is set only if the target argument is an
#instance of a LeObject subclass
# @brief This attr is set only if the target argument is an
# instance of a LeObject subclass
self.__leobject_instance_datas = None
target_class = target
@ -542,16 +547,16 @@ target to LeUpdateQuery constructor"
super().__init__(target_class, query_filters)
##@brief Implements an update query
#@param datas dict : datas to update
# @brief Implements an update query
#@param data dict : data to be updated
#@returns the number of updated items
#@todo change stategy for instance update. Datas should be allowed
#for execute method (and query)
def _query(self, datas):
#@todo change stategy for instance update. Data should be allowed
# for execute method (and query)
def _query(self, data):
uid_name = self._target_class._uid[0]
if self.__leobject_instance_datas is not None:
#Instance update
#Building query_filter
# Instance update
# Building query_filter
filters = [(
uid_name,
'=',
@ -560,59 +565,60 @@ target to LeUpdateQuery constructor"
self._target_class, filters, [],
self.__leobject_instance_datas)
else:
#Update by filters, we have to fetch datas before updating
# Update by filters, we have to fetch data before updating
res = self._ro_datasource.select(
self._target_class, self._target_class.fieldnames(True),
self._query_filter[0],
self._query_filter[1])
#Checking and constructing datas
upd_datas = dict()
# Checking and constructing data
upd_data = dict()
for res_data in res:
res_data.update(datas)
res_datas = self._target_class.prepare_datas(
res_data.update(data)
res_data = self._target_class.prepare_datas(
res_data, True, True)
filters = [(uid_name, '=', res_data[uid_name])]
res = self._rw_datasource.update(
self._target_class, filters, [],
res_datas)
res_data)
return res
## @brief Execute the update query
def execute(self, datas=None):
if self.__leobject_instance_datas is not None and datas is not None:
raise LeApiQueryError("No datas expected when running an update \
#  @brief Execute the update query
def execute(self, data=None):
if self.__leobject_instance_datas is not None and data is not None:
raise LeApiQueryError("No data expected when running an update \
query on an instance")
if self.__leobject_instance_datas is None and datas is None:
raise LeApiQueryError("Datas are mandatory when running an update \
if self.__leobject_instance_datas is None and data is None:
raise LeApiQueryError("Data are mandatory when running an update \
query on a class with filters")
return super().execute(datas=datas)
return super().execute(data=data)
##@brief A query to delete an object
# @brief A query to delete an object
class LeDeleteQuery(LeFilteredQuery):
_hook_prefix = 'leapi_delete_'
def __init__(self, target_class, query_filter):
super().__init__(target_class, query_filter)
## @brief Execute the delete query
# @param datas
def execute(self, datas=None):
#  @brief Execute the delete query
# @param data
def execute(self, data=None):
return super().execute()
##@brief Implements delete query operations
# @param datas
# @brief Implements delete query operations
# @param data
#@returns the number of deleted items
def _query(self, datas=None):
def _query(self, data=None):
filters, rel_filters = self._query_filter
nb_deleted = self._rw_datasource.delete(
self._target_class, filters, rel_filters)
return nb_deleted
class LeGetQuery(LeFilteredQuery):
_hook_prefix = 'leapi_get_'
##@brief Instanciate a new get query
# @brief Instanciate a new get query
#@param target_class LeObject : class of object the query is about
#@param query_filters dict : {OP, list of query filters}
# or tuple (FIELD, OPERATOR, VALUE) )
@ -624,33 +630,33 @@ class LeGetQuery(LeFilteredQuery):
# - offset int : offset
def __init__(self, target_class, query_filters, **kwargs):
super().__init__(target_class, query_filters)
##@brief The fields to get
# @brief The fields to get
self._field_list = None
##@brief An equivalent to the SQL ORDER BY
# @brief An equivalent to the SQL ORDER BY
self._order = None
##@brief An equivalent to the SQL GROUP BY
# @brief An equivalent to the SQL GROUP BY
self._group = None
##@brief An equivalent to the SQL LIMIT x
# @brief An equivalent to the SQL LIMIT x
self._limit = None
##@brief An equivalent to the SQL LIMIT x, OFFSET
# @brief An equivalent to the SQL LIMIT x, OFFSET
self._offset = 0
# Checking kwargs and assigning default values if there is some
for argname in kwargs:
if argname not in (
'field_list', 'order', 'group', 'limit', 'offset'):
'field_list', 'order', 'group', 'limit', 'offset'):
raise TypeError("Unexpected argument '%s'" % argname)
if 'field_list' not in kwargs:
self.set_field_list(target_class.fieldnames(include_ro = True))
self.set_field_list(target_class.fieldnames(include_ro=True))
else:
self.set_field_list(kwargs['field_list'])
if 'order' in kwargs:
#check kwargs['order']
# check kwargs['order']
self._order = kwargs['order']
if 'group' in kwargs:
#check kwargs['group']
# check kwargs['group']
self._group = kwargs['group']
if 'limit' in kwargs and kwargs['limit'] is not None:
try:
@ -669,7 +675,7 @@ class LeGetQuery(LeFilteredQuery):
msg = "offset argument expected to be an integer >= 0"
raise ValueError(msg)
##@brief Set the field list
# @brief Set the field list
# @param field_list list | None : If None use all fields
# @return None
# @throw LeApiQueryError if unknown field given
@ -682,41 +688,41 @@ class LeGetQuery(LeFilteredQuery):
msg = "No field named '%s' in %s"
msg %= (fieldname, self._target_class.__name__)
expt = NameError(msg)
err_l[fieldname] = expt
err_l[fieldname] = expt
if len(err_l) > 0:
msg = "Error while setting field_list in a get query"
raise LeApiQueryErrors(msg = msg, exceptions = err_l)
raise LeApiQueryErrors(msg=msg, exceptions=err_l)
self._field_list = list(set(field_list))
##@brief Execute the get query
def execute(self, datas=None):
# @brief Execute the get query
def execute(self, data=None):
return super().execute()
##@brief Implements select query operations
# @brief Implements select query operations
# @returns a list containing the item(s)
def _query(self, datas=None):
# select datas corresponding to query_filter
def _query(self, data=None):
# select data corresponding to query_filter
fl = list(self._field_list) if self._field_list is not None else None
l_datas=self._ro_datasource.select(
target = self._target_class,
field_list = fl,
filters = self._query_filter[0],
relational_filters = self._query_filter[1],
order = self._order,
group = self._group,
limit = self._limit,
offset = self._offset)
return l_datas
l_data = self._ro_datasource.select(
target=self._target_class,
field_list=fl,
filters=self._query_filter[0],
relational_filters=self._query_filter[1],
order=self._order,
group=self._group,
limit=self._limit,
offset=self._offset)
return l_data
##@return a dict with query infos
# @return a dict with query infos
def dump_infos(self):
ret = super().dump_infos()
ret.update({ 'field_list' : self._field_list,
'order' : self._order,
'group' : self._group,
'limit' : self._limit,
'offset': self._offset,
})
ret.update({'field_list': self._field_list,
'order': self._order,
'group': self._group,
'limit': self._limit,
'offset': self._offset,
})
return ret
def __repr__(self):
@ -725,7 +731,7 @@ field_list={field_list} order={order} group={group} limit={limit} \
offset={offset}"
res = res.format(**self.dump_infos())
if len(self.subqueries) > 0:
for n,subq in enumerate(self.subqueries):
for n, subq in enumerate(self.subqueries):
res += "\n\tSubquerie %d : %s"
res %= (n, subq)
res += ">"