|
@@ -3,11 +3,13 @@
|
3
|
3
|
import re
|
4
|
4
|
from .leobject import LeObject, LeApiErrors, LeApiDataCheckError
|
5
|
5
|
from lodel.plugin.hooks import LodelHook
|
|
6
|
+from lodel import logger
|
6
|
7
|
|
7
|
8
|
class LeQueryError(Exception):
|
8
|
9
|
##@brief Instanciate a new exceptions handling multiple exceptions
|
9
|
|
- # @param msg str : Exception message
|
10
|
|
- # @param exceptions dict : A list of data check Exception with concerned field (or stuff) as key
|
|
10
|
+ #@param msg str : Exception message
|
|
11
|
+ #@param exceptions dict : A list of data check Exception with concerned
|
|
12
|
+ # field (or stuff) as key
|
11
|
13
|
def __init__(self, msg = "Unknow error", exceptions = None):
|
12
|
14
|
self._msg = msg
|
13
|
15
|
self._exceptions = dict() if exceptions is None else exceptions
|
|
@@ -17,7 +19,10 @@ class LeQueryError(Exception):
|
17
|
19
|
|
18
|
20
|
def __str__(self):
|
19
|
21
|
msg = self._msg
|
20
|
|
- for_iter = self._exceptions.items() if isinstance(self._exceptions, dict) else enumerate(self.__exceptions)
|
|
22
|
+ if isinstance(self._exceptions, dict):
|
|
23
|
+ for_iter = self._exceptions.items()
|
|
24
|
+ else:
|
|
25
|
+ for_iter = enumerate(self.__exceptions)
|
21
|
26
|
for obj, expt in for_iter:
|
22
|
27
|
msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
|
23
|
28
|
expt_obj = obj,
|
|
@@ -49,9 +54,11 @@ class LeQuery(object):
|
49
|
54
|
#
|
50
|
55
|
# @note maybe the datasource in not an argument but should be determined
|
51
|
56
|
#elsewhere
|
52
|
|
- def execute(self, datasource, **datas = None):
|
|
57
|
+ def execute(self, datasource, datas = None):
|
53
|
58
|
if len(datas) > 0:
|
54
|
|
- self.__target_class.check_datas_value(datas, **self._data_check_args)
|
|
59
|
+ self.__target_class.check_datas_value(
|
|
60
|
+ datas,
|
|
61
|
+ **self._data_check_args)
|
55
|
62
|
self.__target_class.prepare_datas() #not yet implemented
|
56
|
63
|
if self._hook_prefix is None:
|
57
|
64
|
raise NotImplementedError("Abstract method")
|
|
@@ -90,48 +97,196 @@ class LeFilteredQuery(LeQuery):
|
90
|
97
|
# @param query_filters list : with a tuple (only one filter) or a list of tuple
|
91
|
98
|
# or a dict: {OP,list(filters)} with OP = 'OR' or 'AND
|
92
|
99
|
# For tuple (FIELD,OPERATOR,VALUE)
|
93
|
|
- def __init__(self, target_class, query_filter):
|
|
100
|
+ def __init__(self, target_class, query_filters = None):
|
94
|
101
|
super().__init__(target_class)
|
95
|
102
|
##@brief The query filter
|
96
|
103
|
self.__query_filter = None
|
97
|
104
|
self.set_query_filter(query_filter)
|
98
|
105
|
|
99
|
|
- ##@brief Set the query filter for a query
|
100
|
|
- def set_query_filter(self, query_filter):
|
101
|
|
- #
|
102
|
|
- # Query filter check & prepare
|
103
|
|
- # query_filters can be a tuple (only one filter), a list of tuple
|
104
|
|
- # or a dict: {OP,list(filters)} with OP = 'OR' or 'AND
|
105
|
|
- # For tuple (FIELD,OPERATOR,VALUE)
|
106
|
|
- # FIELD has to be in the field_names list of target class
|
107
|
|
- # OPERATOR in query_operator attribute
|
108
|
|
- # VALUE has to be a correct value for FIELD
|
|
106
|
+ ##@brief Add filter(s) to the query
|
|
107
|
+ #@param query_filter list|tuple|str : A single filter or a list of filters
|
|
108
|
+ #@see LeFilteredQuery._prepare_filters()
|
|
109
|
+ def filter(self, query_filter):
|
|
110
|
+ pass
|
|
111
|
+
|
|
112
|
+ ## @brief Prepare filters for datasource
|
|
113
|
+ #
|
|
114
|
+ #A filter can be a string or a tuple with len = 3.
|
|
115
|
+ #
|
|
116
|
+ #This method divide filters in two categories :
|
|
117
|
+ #
|
|
118
|
+ #@par Simple filters
|
|
119
|
+ #
|
|
120
|
+ #Those filters concerns fields that represent object values (a title,
|
|
121
|
+ #the content, etc.) They are composed of three elements : FIELDNAME OP
|
|
122
|
+ # VALUE . Where :
|
|
123
|
+ #- FIELDNAME is the name of the field
|
|
124
|
+ #- OP is one of the authorized comparison operands ( see
|
|
125
|
+ #@ref LeFilteredQuery.query_operators )
|
|
126
|
+ #- VALUE is... a value
|
|
127
|
+ #
|
|
128
|
+ #@par Relational filters
|
|
129
|
+ #
|
|
130
|
+ #Those filters concerns on reference fields ( see the corresponding
|
|
131
|
+ #abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
|
|
132
|
+ #The filter as quite the same composition than simple filters :
|
|
133
|
+ # FIELDNAME[.REF_FIELD] OP VALUE . Where :
|
|
134
|
+ #- FIELDNAME is the name of the reference field
|
|
135
|
+ #- REF_FIELD is an optionnal addon to the base field. It indicate on wich
|
|
136
|
+ #field of the referenced object the comparison as to be done. If no
|
|
137
|
+ #REF_FIELD is indicated the comparison will be done on identifier.
|
|
138
|
+ #
|
|
139
|
+ #@param cls
|
|
140
|
+ #@param filters_l list : This list of str or tuple (or both)
|
|
141
|
+ #@return a tuple(FILTERS, RELATIONNAL_FILTERS
|
|
142
|
+ #@todo move this doc in another place (a dedicated page ?)
|
|
143
|
+ @classmethod
|
|
144
|
+ def _prepare_filters(cls, filters_l):
|
|
145
|
+ filters = list()
|
|
146
|
+ res_filters = list()
|
|
147
|
+ rel_filters = list()
|
|
148
|
+ err_l = dict()
|
|
149
|
+ #Splitting in tuple if necessary
|
|
150
|
+ for fil in filters_l:
|
|
151
|
+ if len(fil) == 3 and not isinstance(fil, str):
|
|
152
|
+ filters.append(tuple(fil))
|
|
153
|
+ else:
|
|
154
|
+ filters.append(cls.split_filter(fil))
|
109
|
155
|
|
110
|
|
- fieldnames = self.__target_class.fieldnames()
|
111
|
|
- # Recursive method which checks filters
|
112
|
|
- def check_tuple(tupl, fieldnames, target_class):
|
113
|
|
- if isinstance(tupl, tuple):
|
114
|
|
- if tupl[0] not in fieldnames:
|
115
|
|
- return False
|
116
|
|
- if tupl[1] not in self.query_operators:
|
117
|
|
- return False
|
118
|
|
- if not isinstance(tupl[2], target_class.datahandler(tupl[0])):
|
119
|
|
- return False
|
120
|
|
- return True
|
121
|
|
- elif isinstance(tupl,dict):
|
122
|
|
- return check_tuple(tupl[1])
|
123
|
|
- elif isinstance(tupl,list):
|
124
|
|
- for tup in tupl:
|
125
|
|
- return check_tuple(tup)
|
126
|
|
- else:
|
127
|
|
- raise TypeError("Wrong filters for query")
|
|
156
|
+ for field, operator, value in filters:
|
|
157
|
+ # Spliting field name to be able to detect a relational field
|
|
158
|
+ field_spl = field.split('.')
|
|
159
|
+ if len(field_spl) == 2:
|
|
160
|
+ field, ref_field = field_spl
|
|
161
|
+ elif len(field_spl) == 1:
|
|
162
|
+ ref_field = None
|
|
163
|
+ else:
|
|
164
|
+ err_l[field] = NameError( "'%s' is not a valid relational \
|
|
165
|
+field name" % fieldname)
|
|
166
|
+ continue
|
|
167
|
+ # Checking field against target_class
|
|
168
|
+ ret = self.__check_field(self.__target_class, field)
|
|
169
|
+ if isinstance(ret, Exception):
|
|
170
|
+ err_l[field] = ret
|
|
171
|
+ continue
|
|
172
|
+ # Check that the field is relational if ref_field given
|
|
173
|
+ if ref_field is not None and not cls.field(field).is_reference():
|
|
174
|
+ # inconsistency
|
|
175
|
+ err_l[field] = NameError( "The field '%s' in %s is not\
|
|
176
|
+a relational field, but %s.%s was present in the filter"
|
|
177
|
+ % ( field,
|
|
178
|
+ field,
|
|
179
|
+ ref_field))
|
|
180
|
+ # Prepare relational field
|
|
181
|
+ if cls.field(field).is_reference():
|
|
182
|
+ ret = cls._prepare_relational_fields(field, ref_field)
|
|
183
|
+ if isinstance(ret, Exception):
|
|
184
|
+ err_l[field] = ret
|
|
185
|
+ else:
|
|
186
|
+ rel_filters.append((ret, operator, value))
|
|
187
|
+ else:
|
|
188
|
+ res_filters.append((field,operator, value))
|
|
189
|
+
|
|
190
|
+ if len(err_l) > 0:
|
|
191
|
+ raise LeApiDataCheckError(
|
|
192
|
+ "Error while preparing filters : ",
|
|
193
|
+ err_l)
|
|
194
|
+ return (res_filters, rel_filters)
|
128
|
195
|
|
129
|
|
- check_ok=check_tuple(query_filter, fieldnames, self.__target_class)
|
130
|
|
- if check_ok:
|
131
|
|
- self.__query_filter = query_filter
|
132
|
|
-
|
133
|
|
- def execute(self, datasource, **datas = None):
|
134
|
|
- super().execute(datasource, **datas)
|
|
196
|
+ ## @brief Check and split a query filter
|
|
197
|
+ # @note The query_filter format is "FIELD OPERATOR VALUE"
|
|
198
|
+ # @param query_filter str : A query_filter string
|
|
199
|
+ # @param cls
|
|
200
|
+ # @return a tuple (FIELD, OPERATOR, VALUE)
|
|
201
|
+ @classmethod
|
|
202
|
+ def split_filter(cls, query_filter):
|
|
203
|
+ if cls._query_re is None:
|
|
204
|
+ cls.__compile_query_re()
|
|
205
|
+ matches = cls._query_re.match(query_filter)
|
|
206
|
+ if not matches:
|
|
207
|
+ raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
|
|
208
|
+ result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
|
|
209
|
+ for r in result:
|
|
210
|
+ if len(r) == 0:
|
|
211
|
+ raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
|
|
212
|
+ return result
|
|
213
|
+
|
|
214
|
+ ## @brief Compile the regex for query_filter processing
|
|
215
|
+ # @note Set _LeObject._query_re
|
|
216
|
+ @classmethod
|
|
217
|
+ def __compile_query_re(cls):
|
|
218
|
+ op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
|
|
219
|
+ for operator in cls._query_operators[1:]:
|
|
220
|
+ op_re_piece += '|(%s)'%operator.replace(' ', '\s')
|
|
221
|
+ op_re_piece += ')'
|
|
222
|
+ cls._query_re = re.compile('^\s*(?P<field>(((superior)|(subordinate))\.)?[a-z_][a-z0-9\-_]*)\s*'+op_re_piece+'\s*(?P<value>[^<>=!].*)\s*$', flags=re.IGNORECASE)
|
|
223
|
+ pass
|
|
224
|
+
|
|
225
|
+ @classmethod
|
|
226
|
+ def __check_field(cls, target_class, fieldname):
|
|
227
|
+ try:
|
|
228
|
+ target_class.field(fieldname)
|
|
229
|
+ except NameError:
|
|
230
|
+ tc_name = target_class.__name__
|
|
231
|
+ return ValueError("No such field '%s' in %s" % ( fieldname,
|
|
232
|
+ tc_name))
|
|
233
|
+
|
|
234
|
+ ##@brief Prepare a relational filter
|
|
235
|
+ #
|
|
236
|
+ #Relational filters are composed of a tuple like the simple filters
|
|
237
|
+ #but the first element of this tuple is a tuple to :
|
|
238
|
+ #
|
|
239
|
+ #<code>( (FIELDNAME, {REF_CLASS: REF_FIELD}), OP, VALUE)</code>
|
|
240
|
+ # Where :
|
|
241
|
+ #- FIELDNAME is the field name is the target class
|
|
242
|
+ #- the second element is a dict with :
|
|
243
|
+ # - REF_CLASS as key. It's a LeObject child class
|
|
244
|
+ # - REF_FIELD as value. The name of the referenced field in the REF_CLASS
|
|
245
|
+ #
|
|
246
|
+ #Visibly the REF_FIELD value of the dict will vary only when
|
|
247
|
+ #no REF_FIELD is explicitly given in the filter string notation
|
|
248
|
+ #and REF_CLASSES has differents uid
|
|
249
|
+ #
|
|
250
|
+ #@par String notation examples
|
|
251
|
+ #<pre>contributeur IN (1,2,3,5)</pre> will be transformed into :
|
|
252
|
+ #<pre>(
|
|
253
|
+ # (
|
|
254
|
+ # contributeur,
|
|
255
|
+ # {
|
|
256
|
+ # auteur: 'lodel_id',
|
|
257
|
+ # traducteur: 'lodel_id'
|
|
258
|
+ # }
|
|
259
|
+ # ),
|
|
260
|
+ # ' IN ',
|
|
261
|
+ # [ 1,2,3,5 ])</pre>
|
|
262
|
+ #@todo move the documentation to another place
|
|
263
|
+ #
|
|
264
|
+ #@param fieldname str : The relational field name
|
|
265
|
+ #@param ref_field str|None : The referenced field name (if None use
|
|
266
|
+ #uniq identifiers as referenced field
|
|
267
|
+ #@return a well formed relational filter tuple or an Exception instance
|
|
268
|
+ @classmethod
|
|
269
|
+ def __prepare_relational_fields(cls, fieldname, ref_field = None):
|
|
270
|
+ datahandler = self.__target_class.field(fieldname)
|
|
271
|
+ # now we are going to fetch the referenced class to see if the
|
|
272
|
+ # reference field is valid
|
|
273
|
+ ref_classes = datahandler.linked_classes
|
|
274
|
+ ref_dict = dict()
|
|
275
|
+ if ref_field is None:
|
|
276
|
+ for ref_class in ref_classes:
|
|
277
|
+ ref_dict[ref_class] = ref_class.uid_fieldname
|
|
278
|
+ else:
|
|
279
|
+ for ref_class in ref_classes:
|
|
280
|
+ if ref_field in ref_class.fieldnames(True):
|
|
281
|
+ ref_dict[ref_class] = ref_field
|
|
282
|
+ else:
|
|
283
|
+ logger.debug("Warning the class %s is not considered in \
|
|
284
|
+the relational filter %s" % ref_class.__name__)
|
|
285
|
+ if len(ref_dict) == 0:
|
|
286
|
+ return NameError( "No field named '%s' in referenced objects"
|
|
287
|
+ % ref_field)
|
|
288
|
+ return ( (fieldname, ref_dict), op, value)
|
|
289
|
+
|
135
|
290
|
|
136
|
291
|
##@brief A query to insert a new object
|
137
|
292
|
class LeInsertQuery(LeQuery):
|
|
@@ -178,7 +333,7 @@ class LeUpdateQuery(LeFilteredQuery):
|
178
|
333
|
l_uids=datasource.select(self.__target_class,list(self.__target_class.getuid()),query_filter,None, None, None, None, 0, False)
|
179
|
334
|
# list of dict l_uids : _uid(s) of the objects to be updated, corresponding datas
|
180
|
335
|
nb_updated = datasource.update(self.__target_class,l_uids, **datas)
|
181
|
|
- if (nb_updated != len(l_uids):
|
|
336
|
+ if nb_updated != len(l_uids):
|
182
|
337
|
raise LeQueryError("Number of updated items: %d is not as expected: %d " % (nb_updated, len(l_uids)))
|
183
|
338
|
return nb_updated
|
184
|
339
|
|
|
@@ -206,7 +361,7 @@ class LeDeleteQuery(LeFilteredQuery):
|
206
|
361
|
l_uids=datasource.select(self.__target_class,list(self.__target_class.getuid()),query_filter,None, None, None, None, 0, False)
|
207
|
362
|
# list of dict l_uids : _uid(s) of the objects to be deleted
|
208
|
363
|
nb_deleted = datasource.update(self.__target_class,l_uids, **datas)
|
209
|
|
- if (nb_deleted != len(l_uids):
|
|
364
|
+ if nb_deleted != len(l_uids):
|
210
|
365
|
raise LeQueryError("Number of deleted items %d is not as expected %d " % (nb_deleted, len(l_uids)))
|
211
|
366
|
return nb_deleted
|
212
|
367
|
|