|
@@ -0,0 +1,227 @@
|
|
1
|
+#-*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+class LeApiErrors(Exception):
|
|
4
|
+ ## @brief Instanciate a new exceptions handling multiple exceptions
|
|
5
|
+ # @param msg str : Exception message
|
|
6
|
+ # @param exceptions dict : A list of data check Exception with concerned field (or stuff) as key
|
|
7
|
+ def __init__(self, msg = "Unknow error", exceptions = None):
|
|
8
|
+ self._msg = msg
|
|
9
|
+ self._exceptions = dict() if exceptions is None else exceptions
|
|
10
|
+
|
|
11
|
+ def __repr__(self):
|
|
12
|
+ return self.__str__()
|
|
13
|
+
|
|
14
|
+ def __str__(self):
|
|
15
|
+ msg = self._msg
|
|
16
|
+ for_iter = self._exceptions.items() if isinstance(self._exceptions, dict) else enumerate(self.__exceptions)
|
|
17
|
+ for obj, expt in for_iter:
|
|
18
|
+ msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
|
|
19
|
+ expt_obj = obj,
|
|
20
|
+ expt_name=expt.__class__.__name__,
|
|
21
|
+ expt_msg=str(expt)
|
|
22
|
+ )
|
|
23
|
+ return msg
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+## @brief When an error concern a query
|
|
27
|
+class LeApiQueryError(LeApiErrors): pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+## @brief When an error concerns a datas
|
|
31
|
+class LeApiDataCheckError(LeApiErrors): pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+## @brief Wrapper class for LeObject getter & setter
|
|
35
|
+#
|
|
36
|
+# This class intend to provide easy & friendly access to LeObject fields values
|
|
37
|
+# without name collision problems
|
|
38
|
+# @note Wrapped methods are : LeObject.data() & LeObject.set_data()
|
|
39
|
+class LeObjectValues(object):
|
|
40
|
+
|
|
41
|
+ ## @brief Construct a new LeObjectValues
|
|
42
|
+ # @param set_callback method : The LeObject.set_datas() method of corresponding LeObject class
|
|
43
|
+ # @param get_callback method : The LeObject.get_datas() method of corresponding LeObject class
|
|
44
|
+ def __init__(self, fieldnames_callback, set_callback, get_callback):
|
|
45
|
+ self.__setter = set_callback
|
|
46
|
+ self.__getter = get_callback
|
|
47
|
+
|
|
48
|
+ ## @brief Provide read access to datas values
|
|
49
|
+ # @note Read access should be provided for all fields
|
|
50
|
+ # @param fname str : Field name
|
|
51
|
+ def __getattribute__(self, fname):
|
|
52
|
+ return self.__getter(fname)
|
|
53
|
+
|
|
54
|
+ ## @brief Provide write access to datas values
|
|
55
|
+ # @note Write acces shouldn't be provided for internal or immutable fields
|
|
56
|
+ # @param fname str : Field name
|
|
57
|
+ # @param fval * : the field value
|
|
58
|
+ def __setattribute__(self, fname, fval):
|
|
59
|
+ return self.__setter(fname, fval)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+class LeObject(object):
|
|
63
|
+
|
|
64
|
+ ## @brief A dict that stores DataHandler instances indexed by field name
|
|
65
|
+ __fields = None
|
|
66
|
+ ## @brief A tuple of fieldname (or a uniq fieldname) representing uid
|
|
67
|
+ __uid = None
|
|
68
|
+
|
|
69
|
+ def __init__(self, **kwargs):
|
|
70
|
+ ## @brief A dict that stores fieldvalues indexed by fieldname
|
|
71
|
+ self.__datas = dict(fname:None for fname in self.__fields)
|
|
72
|
+ ## @brief Store a list of initianilized fields when instanciation not complete else store True
|
|
73
|
+ self.__initialized = list()
|
|
74
|
+ ## @brief Datas accessor. Instance of @ref LeObjectValues
|
|
75
|
+ self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
|
|
76
|
+
|
|
77
|
+ # Checks that uid is given
|
|
78
|
+ for uid_name in self.__uid:
|
|
79
|
+ if uid_name not in kwargs:
|
|
80
|
+ raise AttributeError("Cannot instanciate a LeObject without it's identifier")
|
|
81
|
+ self.__datas[uid_name] = kwargs[uid_name]
|
|
82
|
+ del(kwargs[uid_name])
|
|
83
|
+ self.__initialized.append(uid_name)
|
|
84
|
+
|
|
85
|
+ allowed_fieldnames = self.fieldnames(include_ro = False)
|
|
86
|
+ err_list = list()
|
|
87
|
+ for fieldname, fieldval in kwargs.items():
|
|
88
|
+ if fieldname not in allowed_fieldnames:
|
|
89
|
+ if fieldname in self.__fields:
|
|
90
|
+ err_list.append(
|
|
91
|
+ AttributeError("Value given for internal field : '%s'" % fieldname)
|
|
92
|
+ )
|
|
93
|
+ else:
|
|
94
|
+ err_list.append(
|
|
95
|
+ AttributeError("Unknown fieldname : '%s'" % fieldname)
|
|
96
|
+ )
|
|
97
|
+ else:
|
|
98
|
+ self.__datas[fieldame] = fieldval
|
|
99
|
+ self.__initialized = list()
|
|
100
|
+ self.set_initialized()
|
|
101
|
+
|
|
102
|
+ #-----------------------------------#
|
|
103
|
+ # Fields datas handling methods #
|
|
104
|
+ #-----------------------------------#
|
|
105
|
+
|
|
106
|
+ ## @brief @property True if LeObject is initialized else False
|
|
107
|
+ @property
|
|
108
|
+ def initialized(self):
|
|
109
|
+ return not isinstance(self.__initialized, list)
|
|
110
|
+
|
|
111
|
+ ## @brief Return a list of fieldnames
|
|
112
|
+ # @param include_ro bool : if True include read only field names
|
|
113
|
+ # @return a list of str
|
|
114
|
+ @classmethod
|
|
115
|
+ def fieldnames(cls, include_ro = False):
|
|
116
|
+ if not include_ro:
|
|
117
|
+ return [ fname for fname in self.__fields if not self.__fields[fname].is_internal() ]
|
|
118
|
+ else:
|
|
119
|
+ return list(self.__fields.keys())
|
|
120
|
+
|
|
121
|
+ ## @brief Read only access to all datas
|
|
122
|
+ # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
|
|
123
|
+ # @param name str : field name
|
|
124
|
+ # @return the Value
|
|
125
|
+ # @throw RuntimeError if the field is not initialized yet
|
|
126
|
+ # @throw NameError if name is not an existing field name
|
|
127
|
+ def data(self, field_name):
|
|
128
|
+ if field_name not in self.__fields.keys():
|
|
129
|
+ raise NameError("No such field in %s : %s" % (self.__class__.__name__, name)
|
|
130
|
+ if not self.initialized and name not in self.__initialized:
|
|
131
|
+ raise RuntimeError("The field %s is not initialized yet (and have no value)" % name)
|
|
132
|
+ return self.__datas[name]
|
|
133
|
+
|
|
134
|
+ ## @brief Datas setter
|
|
135
|
+ # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
|
|
136
|
+ # @param fname str : field name
|
|
137
|
+ # @param fval * : field value
|
|
138
|
+ # @return the value that is really set
|
|
139
|
+ # @throw NameError if fname is not valid
|
|
140
|
+ # @throw AttributeError if the field is not writtable
|
|
141
|
+ def set_data(self, fname, fval):
|
|
142
|
+ if field_name not in self.fieldnames(include_ro = False):
|
|
143
|
+ if field_name not in self.__fields.keys():
|
|
144
|
+ raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
|
|
145
|
+ else:
|
|
146
|
+ raise AttributeError("The field %s is read only" % fname)
|
|
147
|
+ self.__datas[fname] = fval
|
|
148
|
+ if not self.initialized and fname not in self.__initialized:
|
|
149
|
+ # Add field to initialized fields list
|
|
150
|
+ self.__initialized.append(fname)
|
|
151
|
+ self.__set_initialized()
|
|
152
|
+ if self.initialized:
|
|
153
|
+ # Running full value check
|
|
154
|
+ ret = self.__check_modified_values()
|
|
155
|
+ if ret is None:
|
|
156
|
+ return self.__datas[fname]
|
|
157
|
+ else:
|
|
158
|
+ raise LeApiErrors("Data check error", ret)
|
|
159
|
+ else:
|
|
160
|
+ # Doing value check on modified field
|
|
161
|
+ # We skip full validation here because the LeObject is not fully initialized yet
|
|
162
|
+ val, err = self.__fields[fname].check_data_value(fval)
|
|
163
|
+ if isinstance(err, Exception):
|
|
164
|
+ #Revert change to be in valid state
|
|
165
|
+ del(self.__datas[fname])
|
|
166
|
+ del(self.__initialized[-1])
|
|
167
|
+ raise LeApiErrors("Data check error", {fname:err})
|
|
168
|
+ else:
|
|
169
|
+ self.__datas[fname] = val
|
|
170
|
+
|
|
171
|
+ ## @brief Update the __initialized attribute according to LeObject internal state
|
|
172
|
+ #
|
|
173
|
+ # Check the list of initialized fields and set __initialized to True if all fields initialized
|
|
174
|
+ def __set_initialized(self):
|
|
175
|
+ if isinstance(self.__initialized, list):
|
|
176
|
+ expected_fields = self.fieldnames(include_ro = False) + self.__uid
|
|
177
|
+ if set(expected_fields) == set(self.__initialized):
|
|
178
|
+ self.__initialized = True
|
|
179
|
+
|
|
180
|
+ ## @brief Designed to be called when datas are modified
|
|
181
|
+ #
|
|
182
|
+ # Make different checks on the LeObject given it's state (fully initialized or not)
|
|
183
|
+ # @return None if checks succeded else return an exception list
|
|
184
|
+ def __check_modified_values(self):
|
|
185
|
+ err_list = dict()
|
|
186
|
+ if self.__initialized is True:
|
|
187
|
+ # Data value check
|
|
188
|
+ for fname in self.fieldnames(include_ro = False):
|
|
189
|
+ val, err = self.__fields[fname].check_data_value(self.__datas[fname])
|
|
190
|
+ if err is not None:
|
|
191
|
+ err_list[fname] = err
|
|
192
|
+ else:
|
|
193
|
+ self.__datas[fname] = val
|
|
194
|
+ # Data construction
|
|
195
|
+ if len(err_list) == 0:
|
|
196
|
+ for fname in self.fieldnames(include_ro = True):
|
|
197
|
+ try:
|
|
198
|
+ field = self.__fields[fname]
|
|
199
|
+ self.__datas[fname] = fields.construct_data( self,
|
|
200
|
+ fname,
|
|
201
|
+ self.__datas,
|
|
202
|
+ self.__datas[fname]
|
|
203
|
+ )
|
|
204
|
+ except Exception as e:
|
|
205
|
+ err_list[fname] = e
|
|
206
|
+ # Datas consistency check
|
|
207
|
+ if len(err_list) == 0:
|
|
208
|
+ for fname in self.fieldnames(include_ro = True):
|
|
209
|
+ field = self.__fields[fname]
|
|
210
|
+ ret = field.check_data_consistency(self, fname, self.__datas)
|
|
211
|
+ if isinstance(ret, Exception):
|
|
212
|
+ err_list[fname] = ret
|
|
213
|
+ else:
|
|
214
|
+ # Data value check for initialized datas
|
|
215
|
+ for fname in self.__initialized:
|
|
216
|
+ val, err = self.__fields[fname].check_data_value(self.__datas[fname])
|
|
217
|
+ if err is not None:
|
|
218
|
+ err_list[fname] = err
|
|
219
|
+ else:
|
|
220
|
+ self.__datas[fname] = val
|
|
221
|
+ return err_list if len(err_list) > 0 else None
|
|
222
|
+
|
|
223
|
+ #-------------------#
|
|
224
|
+ # Crud methods #
|
|
225
|
+ #-------------------#
|
|
226
|
+
|
|
227
|
+
|