Browse Source

Add some utility function + addition in query.py

Yann Weber 8 years ago
parent
commit
251fc6d324
5 changed files with 220 additions and 42 deletions
  1. 1
    0
      globconf.d/global.ini
  2. 4
    0
      lodel/leapi/datahandlers/base_classes.py
  3. 17
    0
      lodel/leapi/leobject.py
  4. 197
    42
      lodel/leapi/query.py
  5. 1
    0
      requirements.txt

+ 1
- 0
globconf.d/global.ini View File

@@ -8,3 +8,4 @@ emtranslator = picklefile
8 8
 dyncode = lodel/leapi/dyncode.py
9 9
 editormode = True
10 10
 groups = 
11
+

+ 4
- 0
lodel/leapi/datahandlers/base_classes.py View File

@@ -203,6 +203,10 @@ class Reference(DataHandler):
203 203
     def back_reference(self):
204 204
         return copy.copy(self.__back_reference)
205 205
 
206
+    @property
207
+    def linked_classes(self):
208
+        return copy.copy(self.__allowed_classes)
209
+
206 210
     ##@brief Set the back reference for this field.
207 211
     def _set_back_reference(self, back_reference):
208 212
         self.__back_reference = back_reference

+ 17
- 0
lodel/leapi/leobject.py View File

@@ -118,6 +118,11 @@ class LeObject(object):
118 118
     @property
119 119
     def initialized(self):
120 120
         return not isinstance(self.__initialized, list)
121
+    
122
+    ##@return The uid field name
123
+    @classmethod
124
+    def uid_fieldname(cls):
125
+        return cls._uid
121 126
 
122 127
     ##@brief Return a list of fieldnames
123 128
     # @param include_ro bool : if True include read only field names
@@ -160,6 +165,18 @@ class LeObject(object):
160 165
     @classmethod
161 166
     def is_abstract(cls):
162 167
         return cls._abstract
168
+    
169
+    ##@brief Field data handler gettet
170
+    #@param fieldname str : The field name
171
+    #@return A datahandler instance
172
+    #@throw NameError if the field doesn't exist
173
+    @classmethod
174
+    def field(cls, fieldname):
175
+        try:
176
+            return cls._fields[field_uid]
177
+        except KeyError:
178
+            raise NameError("No field named '%s' in %s" % ( field_uid,
179
+                                                            cls.__name__))
163 180
 
164 181
     ##@brief Read only access to all datas
165 182
     # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance

+ 197
- 42
lodel/leapi/query.py View File

@@ -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
 

+ 1
- 0
requirements.txt View File

@@ -10,3 +10,4 @@ pymongo==3.2.2
10 10
 six==1.10.0
11 11
 Werkzeug==0.11.4
12 12
 wrapt==1.10.6
13
+lxml==3.6.0

Loading…
Cancel
Save