Yann Weber před 8 roky
rodič
revize
61dd475e8c
3 změnil soubory, kde provedl 268 přidání a 299 odebrání
  1. 3
    0
      README.txt
  2. 98
    1
      lodel/leapi/leobject.py
  3. 167
    298
      lodel/leapi/query.py

+ 3
- 0
README.txt Zobrazit soubor

@@ -1,3 +1,6 @@
1
+Local configuration :
2
+	First of all copy the settings.ini to settings_local.ini and replace values by correct path
3
+
1 4
 Doxygen documentation generation :
2 5
 	doxygen
3 6
 

+ 98
- 1
lodel/leapi/leobject.py Zobrazit soubor

@@ -273,10 +273,107 @@ class LeObject(object):
273 273
     # at the end of the dyncode parse
274 274
     # @warning This method is deleted once the dynamic code loaded
275 275
     # @param field_list list : list of EmField instance
276
+    # @param cls
276 277
     @classmethod
277 278
     def _set__fields(cls, field_list):
278 279
         cls._fields = field_list
279 280
         
281
+    ## @brief Check that datas are valid for this type
282
+    # @param datas dict : key == field name value are field values
283
+    # @param complete bool : if True expect that datas provide values for all non internal fields
284
+    # @param allow_internal bool : if True don't raise an error if a field is internal
285
+    # @param cls
286
+    # @return Checked datas
287
+    # @throw LeApiDataCheckError if errors reported during check
288
+    @classmethod
289
+    def check_datas_value(cls, datas, complete = False, allow_internal = True):
290
+        err_l = dict() #Error storing
291
+        correct = set() #valid fields name
292
+        mandatory = set() #mandatory fields name
293
+        for fname, datahandler in cls._fields.items():
294
+            if allow_internal or not datahandler.is_internal():
295
+                correct.add(fname)
296
+                if complete and not hasattr(datahandler, 'default'):
297
+                    mandatory.add(fname)
298
+        provided = set(datas.keys())
299
+        # searching for unknow fields
300
+        for u_f in provided - correct:
301
+            #Here we can check if the field is invalid or rejected because
302
+            # it is internel
303
+            err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'" % u_f)
304
+        # searching for missing mandatory fieldsa
305
+        for missing in mandatory - provided:
306
+            err_l[miss_field] = AttributeError("The data for field '%s' is missing" % missing)
307
+        #Checks datas
308
+        checked_datas = dict()
309
+        for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
310
+            dh = cls._fields[name]
311
+            res = dh.check_data_value(value)
312
+            checked_datas[name], err = res
313
+            if err:
314
+                err_l[name] = err
280 315
 
316
+        if len(err_l) > 0:
317
+            raise LeApiDataCheckError("Error while checking datas", err_l)
318
+        return checked_datas
319
+
320
+    ##@brief Check and prepare datas
321
+    # 
322
+    # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
323
+    # 
324
+    # @param datas dict : {fieldname : fieldvalue, ...}
325
+    # @param complete bool : If True you MUST give all the datas
326
+    # @param allow_internal : Wether or not interal fields are expected in datas
327
+    # @param cls
328
+    # @return Datas ready for use
329
+    # @todo: complete is very unsafe, find a way to get rid of it
330
+    @classmethod
331
+    def prepare_datas(cls, datas, complete=False, allow_internal=True):
332
+        if not complete:
333
+            warnings.warn("\nActual implementation can make datas construction and consitency unsafe when datas are not complete\n")
334
+        ret_datas = cls.check_datas_value(datas, complete, allow_internal)
335
+        if isinstance(ret_datas, Exception):
336
+            raise ret_datas
337
+
338
+        if complete:
339
+            ret_datas = cls._construct_datas(ret_datas)
340
+            cls._check_datas_consistency(ret_datas)
341
+        return ret_datas
342
+
343
+    ## @brief Construct datas values
344
+    #
345
+    # @param cls
346
+    # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
347
+    # @return A new dict of datas
348
+    # @todo IMPLEMENTATION
349
+    @classmethod
350
+    def _construct_datas(cls, datas):
351
+        """
352
+        constructor = DatasConstructor(cls, datas, cls.fieldtypes())
353
+        ret = {
354
+                fname:constructor[fname]
355
+                for fname, ftype in cls.fieldtypes().items()
356
+                if not ftype.is_internal() or ftype.internal != 'autosql'
357
+        }
358
+        return ret
359
+        """
360
+        pass
361
+
362
+    ## @brief Check datas consistency
363
+    # 
364
+    # @warning assert that datas is complete
365
+    # @param cls
366
+    # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
367
+    # @throw LeApiDataCheckError if fails
368
+    @classmethod
369
+    def _check_datas_consistency(cls, datas):
370
+        err_l = []
371
+        err_l = dict()
372
+        for fname, dh in cls._fields.items():
373
+            ret = dh.check_data_consistency(cls, fname, datas)
374
+            if isinstance(ret, Exception):
375
+                err_l[fname] = ret
376
+
377
+        if len(err_l) > 0:
378
+            raise LeApiDataCheckError("Datas consistency checks fails", err_l)
281 379
 
282
-    

+ 167
- 298
lodel/leapi/query.py Zobrazit soubor

@@ -2,322 +2,191 @@
2 2
 
3 3
 import re
4 4
 from .leobject import LeObject, LeApiErrors, LeApiDataCheckError
5
-
5
+from lodel.plugin.hooks import LodelHook
6 6
 
7 7
 class LeQueryError(Exception):
8 8
     pass
9 9
 
10 10
 class LeQuery(object):
11
-
12
-    ##@brief The datasource object used for this query
13
-    datasource = None
14
-
15
-    ##@brief The available operators used in query definitions
16
-    query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ', ' like ', ' not like ']
17
-
18
-    ##@brief Constructor
19
-    # @param target_class EmClass : class of the object to query about
11
+    
12
+    ##@brief Hookname preffix
13
+    _hook_prefix = None
14
+    ##@brief arguments for the LeObject.check_data_value()
15
+    _data_check_args = { 'complete': False, 'allow_internal': False }
16
+
17
+    ##@brief Abstract constructor
18
+    # @param target_class LeObject : class of object the query is about
20 19
     def __init__(self, target_class):
20
+        if hook_prefix is None:
21
+            raise NotImplementedError("Asbtract class")
21 22
         if not issubclass(target_class, LeObject):
22 23
             raise TypeError("target class has to be a child class of LeObject")
23
-        self.target_class = target_class
24
-
25
-
26
-##@brief Class representing an Insert query
27
-class LeInsertQuery(LeQuery):
28
-
29
-    ##@brief Constructor
30
-    # @param target_class EmClass: class corresponding to the inserted object
31
-    # @param datas dict : datas to insert
32
-    def __init__(self, target_class, datas):
33
-        super().__init__(target_class)
34
-        self.datas = datas
35
-
36
-    ##@brief executes the insert query
37
-    # @return bool
38
-    # @TODO reactivate the LodelHooks call when this class is implemented
39
-    def execute(self):
40
-        datas = self.datas  # LodelHooks.call_hook('leapi_insert_pre', self.target_class, self.datas)
41
-        ret = self.__insert(**datas)
42
-        # ret = LodelHook.call_hook('leapi_insert_post', self.target_class, ret)
24
+        self.__target_class = target_class
25
+    
26
+    ##@brief Execute a query and return the result
27
+    # @param **datas
28
+    # @return the query result
29
+    # @see LeQuery.__query()
30
+    #
31
+    # @note maybe the datasource in not an argument but should be determined
32
+    #elsewhere
33
+    def execute(self, datasource, **datas):
34
+        if len(datas) > 0:
35
+            self.__target_class.check_datas_value(datas, **self._data_check_args)
36
+            self.__target_class.prepare_datas() #not yet implemented
37
+        if self._hook_prefix is None:
38
+            raise NotImplementedError("Abstract method")
39
+        LodelHook.call_hook(    self._hook_prefix+'_pre',
40
+                                self.__target_class,
41
+                                datas)
42
+        ret = self.__query(datasource, **datas)
43
+        ret = LodelHook.call_hook(  self._hook_prefix+'_post',
44
+                                    self.__target_class,
45
+                                    ret)
43 46
         return ret
47
+    
48
+    ##@brief Childs classes implements this method to execute the query
49
+    # @param **datas
50
+    # @return query result
51
+    def __query(self, **datas):
52
+        raise NotImplementedError("Asbtract method")
44 53
 
45
-    ##@brief calls the datasource to perform the insert command
46
-    # @param datas dict : formatted datas corresponding to the insert
47
-    # @return str : the uid of the inserted object
48
-    def __insert(self, **datas):
49
-        insert_datas = self.target_class.prepare_datas(datas, complete=True, allow_internal=True)
50
-        res = self.datasource.insert(self.target_class, **insert_datas)
51
-        return res
52
-
53
-
54
-##@brief Class representing an Abstract Filtered Query
55 54
 class LeFilteredQuery(LeQuery):
56
-
57
-    ##@brief Constructor
58
-    # @param target_class EmClass : Object of the query
59
-    def __init__(self, target_class):
55
+    
56
+    ##@brief The available operators used in query definitions
57
+    query_operators = [
58
+                        '=',
59
+                        '<=',
60
+                        '>=',
61
+                        '!=',
62
+                        '<',
63
+                        '>',
64
+                        ' in ',
65
+                        ' not in ',
66
+                        ' like ',
67
+                        ' not like ']
68
+
69
+    ##@brief Abtract constructor for queries with filter
70
+    # @param target_class LeObject : class of object the query is about
71
+    # @param query_filters list : list of string of query filters (or tuple 
72
+    #(FIELD, OPERATOR, VALUE) )
73
+    def __init__(self, target_class, query_filter):
60 74
         super().__init__(target_class)
75
+        ##@brief The query filter
76
+        self.__query_filter = None
77
+        self.set_qeury_filter(query_filter)
78
+    
79
+    ##@brief Set the query filter for a query
80
+    def set_query_filter(self, query_filter):
81
+        #
82
+        #   Query filter check & prepare should be done here
83
+        #
84
+        self.__query_filter = query_filter
85
+
86
+##@brief A query for insert a new object
87
+class LeInsertQuery(LeQuery):
88
+    
89
+    _hook_prefix = 'leapi_insert_'
90
+    _data_check_args = { 'complete': True, 'allow_internal': False }
61 91
 
62
-    ##@brief Validates the query filters
63
-    # @param query_filters list
64
-    # @return bool
65
-    # @raise LeQueryError if one of the filter is not valid
66
-    @classmethod
67
-    def validate_query_filters(cls, query_filters):
68
-        for query_filter in query_filters:
69
-            if query_filter[1] not in cls.query_operators:
70
-                raise LeQueryError("The operator %s is not valid." % query_filter[1])
71
-        return True
72
-
73
-    ##@brief Checks if a field is relational
74
-    # @param field str : Name of the field
75
-    # @return bool
76
-    @classmethod
77
-    def is_relational_field(cls, field):
78
-        return field.startswith('superior.') or field.startswith('subordinate.')
79
-
80
-
81
-##@brief Class representing a Get Query
82
-class LeGetQuery(LeFilteredQuery):
83
-
84
-    ##@brief Constructor
85
-    # @param target_class EmClass : main class
86
-    # @param query_filters
87
-    # @param field_list list
88
-    # @param order list : list of tuples corresponding to the fields used to sort the results
89
-    # @param group list : list of tuples corresponding to the fields used to group the results
90
-    # @param limit int : Maximum number of results to get
91
-    # @param offset int
92
-    # @param instanciate bool : if True, objects will be returned instead of dictionaries
93
-    def __init__(self, target_class, query_filters, field_list=None, order=None, group=None, limit=None, offset=0, instanciate=True):
92
+    def __init__(self, target_class):
94 93
         super().__init__(target_class)
95
-        self.query_filters = query_filters
96
-        self.default_field_list = []
97
-        self.field_list = field_list if field_list is not None else self.target_class.fieldnames()
98
-        self.order = order
99
-        self.group = group
100
-        self.limit = limit
101
-        self.offset = offset
102
-        self.instanciate = instanciate
103
-
104
-    ##@brief executes the query
105
-    # @return list
106
-    # @TODO activate LodelHook calls
107
-    def execute(self):
108
-        datas = self.datas  # LodelHook.call_hook('leapi_get_pre', self.target_object, self.datas)
109
-        ret = self.__get(**datas)
110
-        # ret = LodelHook.call_hook('leapi_get_post', self.target_object, ret)
111
-        return ret
112
-
113
-    def __get(self, **datas):
114
-        field_list = self.__prepare_field_list(self.field_list)
115
-
116
-        query_filters, relational_filters = self.__prepare_filters()
117
-
118
-        # Preparing the "order" parameters
119
-        if self.order:
120
-            order = self.__prepare_order()
121
-            if isinstance(order, Exception):
122
-                raise order  # can be buffered and raised later, but __prepare_filters raise when fails
123
-
124
-        # Preparing the "group" parameters
125
-        if self.group:
126
-            group = self.__prepare_order()
127
-            if isinstance(group, Exception):
128
-                raise group  # can be buffered and raised later
129
-
130
-        # checks the limit and offset values
131
-        if self.limit is not None and self.limit <= 0:
132
-            raise ValueError('Invalid limit given')
133
-
134
-        if self.offset is not None and self.offset < 0:
135
-            raise ValueError('Invalid offset given : %d' % self.offset)
136
-
137
-        results = self._datasource.select()  # TODO add the correct arguments for the datasource's method call
138
-        return results
139
-
140
-    ##@brief prepares the field list
141
-    # @return list
142
-    # @raise LeApiDataCheckError
143
-    def __prepare_field_list(self):
144
-        errors = dict()
145
-        ret_field_list = list()
146
-        for field in self.field_list:
147
-            if self.is_relational(field):
148
-                ret = self.__prepare_relational_field(field)
149
-            else:
150
-                ret = self.__check_field(field)
151
-
152
-            if isinstance(ret, Exception):
153
-                errors[field] = ret
154
-            else:
155
-                ret_field_list.append(ret)
156
-
157
-        if len(errors) > 0:
158
-            raise LeApiDataCheckError(errors)
159
-
160
-        return ret_field_list
161
-
162
-    ##@brief prepares a relational field
163
-    def __prepare_relational_field(self, field):
164
-        # TODO Implement the method
165
-        return field
166
-
167
-    ##@brief splits the filter string into a tuple (FIELD, OPERATOR, VALUE)
168
-    # @param filter str
169
-    # @return tuple
170
-    # @raise ValueError
171
-    def __split_filter(self, filter):
172
-        if self.query_re is None:
173
-            self.__compile_query_re()
174
-
175
-        matches = self.query_re.match(filter)
176
-        if not matches:
177
-            raise ValueError("The query_filter '%s' seems to be invalid" % filter)
178
-
179
-        result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator')), matches.group('value').strip())
180
-        for r in result:
181
-            if len(r) == 0:
182
-                raise ValueError("The query_filter '%s' seems to be invalid" % filter)
183
-
184
-        return result
185
-
186
-    def __compile_query_re(self):
187
-        op_re_piece = '(?P<operator>(%s)' % self._query_operators[0].replace(' ', '\s')
188
-        for operator in self._query_operators[1:]:
189
-            op_re_piece += '|(%s)' % operator.replace(' ', '\s')
190
-        op_re_piece += ')'
191
-        self.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)
192
-
193
-    ##@brief checks if a field is in the target class of the query
194
-    # @param field str
195
-    # @return str
196
-    # @raise ValueError
197
-    def __check_field(self, field):
198
-        if field not in self.target_class.fieldnames():
199
-            return ValueError("No such field '%s' in %s" % (field, self.target_class))
200
-        return field
201
-
202
-    ##@brief Prepares the filters (relational and others)
203
-    # @return tuple
204
-    def __prepare_filters(self):
205
-        filters = list()
206
-        errors = dict()
207
-        res_filters = list()
208
-        rel_filters = list()
209
-
210
-        # Splitting in tuple if necessary
211
-        for filter in self.query_filters:
212
-            if len(filter) == 3 and not isinstance(filter, str):
213
-                filters.append(tuple(filter))
214
-            else:
215
-                filters.append(self.__split_filter(filter))
216
-
217
-        for field, operator, value in filters:
218
-            # TODO check the relation filters
219
-            ret = self.__check_field(field)
220
-            if isinstance(ret, Exception):
221
-                errors[field] = ret
222
-            else:
223
-                res_filters.append((ret, operator, value))
224
-
225
-        if len(errors) > 0:
226
-            raise LeApiDataCheckError("Error while preparing filters : ", errors)
227
-
228
-        return (res_filters, rel_filters)
229
-
230
-
231
-        datas = dict()
232
-        if LeFilteredQuery.validate_query_filters(self.query_filters):
233
-            datas['query_filters'] = self.query_filters
234
-        datas['target_class'] = self.target_class
235
-        return datas
236
-
237
-    ##@brief prepares the "order" parameters
238
-    # @return list
239
-    def __prepare_order(self):
240
-        errors = dict()
241
-        result = []
242
-        for order_field in self.order:
243
-            if not isinstance(order_field, tuple):
244
-                order_field = (order_field, 'ASC')
245
-            if len(order_field) != 2 or order_field[1].upper() not in ['ASC', 'DESC']:
246
-                errors[order_field] = ValueError("Expected a string or a tuple with (FIELDNAME, ['ASC'|'DESC']) but got : %s" % order_field)
247
-            else:
248
-                ret = self.target_class.check_field(order_field[0])
249
-                if isinstance(ret, Exception):
250
-                    errors[order_field] = ret
251
-            order_field = (order_field[0], order_field[1].upper())
252
-            result.append(order_field)
253
-
254
-        if len(errors) > 0:
255
-            raise LeApiErrors("Errors when preparing ordering fields", errors)
256
-        return result
257
-
94
+    
95
+    ## @brief Implements an insert query operations
96
+    # @param **datas : datas to be inserted
97
+    def __query(self, datasource, **datas):
98
+        pass
258 99
 
100
+##@brief A query to update datas for a given object
259 101
 class LeUpdateQuery(LeFilteredQuery):
260
-
261
-    def __init__(self, target_class, target_uid, query_filters):
262
-        super().__init__(target_class)
263
-        self.query_filters = query_filters
264
-        self.target_uid = target_uid
265
-
266
-    def execute(self):
267
-        # LodelHook.call_hook('leapi_update_pre', self.target_object, None)
268
-        ret = self.__update()
269
-        # ret = LodelHook.call_hook('leapi_update_post', self.target_object, ret)
270
-        return ret
271
-
272
-    ##@brief calls the datasource's update method and the corresponding hooks
273
-    # @return bool
274
-    # @TODO change the behavior in case of error in the update process
275
-    def __update(self):
276
-        updated_datas = self.__prepare()
277
-        ret = self.datasource.update(self.target_uid, **updated_datas)  # TODO add the correct arguments for the datasource's method
278
-        if ret == 1:
279
-            return True
280
-        else:
281
-            return False
282
-
283
-    ##@brief prepares the query_filters to be used as argument for the datasource's update method
284
-    def __prepare(self):
285
-        datas = dict()
286
-        if LeFilteredQuery.validate_query_filters(self.query_filters):
287
-            datas['query_filters'] = self.query_filters
288
-
289
-        datas['target_uid'] = self.target_uid
290
-        datas['target_class'] = self._target_class
291
-        return datas
292
-
293
-
102
+    
103
+    _hook_prefix = 'leapi_update_'
104
+    _data_check_args = { 'complete': True, 'allow_internal': False }
105
+
106
+    def __init__(self, target_class, query_filter):
107
+        super().__init__(target_class, query_filter)
108
+    
109
+    ##@brief Implements an update query
110
+    # @param **datas : datas to update
111
+    def __query(self, datasource, **datas):
112
+        pass
113
+
114
+##@brief A query to delete an object
294 115
 class LeDeleteQuery(LeFilteredQuery):
116
+    
117
+    _hook_prefix = 'leapi_delete_'
295 118
 
296
-    def __init__(self, target_class, target_uid, query_filters):
297
-        super().__init__(self._target_class)
298
-        self.target_uid = target_uid
299
-        self.query_filters = query_filters
300
-
301
-    def execute(self):
302
-        # LodelHook.call_hook('leapi_delete_pre', self.target_uid, None)
303
-        ret = self.__delete()
304
-        # ret = LodelHook.call('leapi_delete_post', self.target_object, ret)
305
-        return ret
306
-
307
-    ##@brief calls the datasource's delete method
308
-    # @return bool
309
-    # @TODO change the behavior in case of error in the update process
310
-    def __delete(self):
311
-        delete_datas = self.__prepare()
312
-        ret = self._datasource.delete(**delete_datas)
313
-        return ret
119
+    def __init__(self, target_class, query_filter):
120
+        super().__init__(target_class, query_filter)
314 121
 
315
-    def __prepare(self):
316
-        datas = dict()
317
-        if LeFilteredQuery.validate_query_filters(self.query_filters):
318
-            datas['query_filters'] = self.query_filters
122
+    ## @brief Execute the delete query
123
+    def execute(self, datasource):
124
+        super().execute()
125
+    
126
+    ##@brief Implements delete query operations
127
+    def __query(self, datasource):
128
+        pass
319 129
 
320
-        datas['target_uid'] = self.target_uid
321
-        datas['target_class'] = self._target_class
130
+class LeGetQuery(LeFilteredQuery):
131
+    
132
+    _hook_prefix = 'leapi_get_'
133
+
134
+    ##@brief Instanciate a new get query
135
+    # @param target_class LeObject : class of object the query is about
136
+    # @param query_filters list : list of string of query filters (or tuple 
137
+    #(FIELD, OPERATOR, VALUE) )
138
+    # @param field_list list|None : list of string representing fields see @ref leobject_filters
139
+    # @param order list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
140
+    # @param group list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
141
+    # @param limit int : The maximum number of returned results
142
+    # @param offset int : offset
143
+    def __init__(self, target_class, query_filter, **kwargs):
144
+        super().__init__(target_class, query_filter)
145
+        
146
+        ##@brief The fields to get
147
+        self.__field_list = None
148
+        ##@brief An equivalent to the SQL ORDER BY
149
+        self.__order = None
150
+        ##@brief An equivalent to the SQL GROUP BY
151
+        self.__group = None
152
+        ##@brief An equivalent to the SQL LIMIT x
153
+        self.__limit = None
154
+        ##@brief An equivalent to the SQL LIMIT x, OFFSET
155
+        self.__offset = 0
156
+        
157
+        # Checking kwargs and assigning default values if there is some
158
+        for argname in kwargs:
159
+            if argname not in ('order', 'group', 'limit', 'offset'):
160
+                raise TypeError("Unexpected argument '%s'" % argname)
161
+
162
+        if 'field_list' not in kwargs:
163
+            #field_list = target_class.get_field_list
164
+            pass
165
+        else:
166
+            #target_class.check_fields(kwargs['field_list'])
167
+            field_list = kwargs['field_list']
168
+        if 'order' in kwargs:
169
+            #check kwargs['order']
170
+            self.__order = kwargs['order']
171
+        if 'group' in kwargs:
172
+            #check kwargs['group']
173
+            self.__group = kwargs['group']
174
+        if 'limit' in kwargs:
175
+            try:
176
+                self.__limit = int(kwargs[limit])
177
+                if self.__limit <= 0:
178
+                    raise ValueError()
179
+            except ValueError:
180
+                raise ValueError("limit argument expected to be an interger > 0")
181
+        if 'offset' in kwargs:
182
+            try:
183
+                self.__offset = int(kwargs['offset'])
184
+                if self.__offset < 0:
185
+                    raise ValueError()
186
+            except ValueError:
187
+                raise ValueError("offset argument expected to be an integer >= 0")
188
+    
189
+    ##@brief Execute the get query
190
+    def execute(self, datasource):
191
+        super().execute(datasource)
322 192
 
323
-        return datas

Loading…
Zrušit
Uložit