No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

leobject.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. #-*- coding: utf-8 -*-
  2. import importlib
  3. from lodel.plugin import Plugin
  4. from lodel import logger
  5. from lodel.settings import Settings
  6. from lodel.settings.utils import SettingsError
  7. ##@brief Stores the name of the field present in each LeObject that indicates
  8. #the name of LeObject subclass represented by this object
  9. CLASS_ID_FIELDNAME = "classname"
  10. class LeApiErrors(Exception):
  11. ##@brief Instanciate a new exceptions handling multiple exceptions
  12. # @param msg str : Exception message
  13. # @param exceptions dict : A list of data check Exception with concerned field (or stuff) as key
  14. def __init__(self, msg = "Unknow error", exceptions = None):
  15. self._msg = msg
  16. self._exceptions = dict() if exceptions is None else exceptions
  17. def __repr__(self):
  18. return self.__str__()
  19. def __str__(self):
  20. msg = self._msg
  21. for_iter = self._exceptions.items() if isinstance(self._exceptions, dict) else enumerate(self.__exceptions)
  22. for obj, expt in for_iter:
  23. msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
  24. expt_obj = obj,
  25. expt_name=expt.__class__.__name__,
  26. expt_msg=str(expt)
  27. )
  28. return msg
  29. ##@brief When an error concern a query
  30. class LeApiQueryError(LeApiErrors):
  31. pass
  32. ##@brief When an error concerns a datas
  33. class LeApiDataCheckError(LeApiErrors):
  34. pass
  35. ##@brief Wrapper class for LeObject getter & setter
  36. #
  37. # This class intend to provide easy & friendly access to LeObject fields values
  38. # without name collision problems
  39. # @note Wrapped methods are : LeObject.data() & LeObject.set_data()
  40. class LeObjectValues(object):
  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. ##@brief Provide read access to datas values
  48. # @note Read access should be provided for all fields
  49. # @param fname str : Field name
  50. def __getattribute__(self, fname):
  51. return self.__getter(fname)
  52. ##@brief Provide write access to datas values
  53. # @note Write acces shouldn't be provided for internal or immutable fields
  54. # @param fname str : Field name
  55. # @param fval * : the field value
  56. def __setattribute__(self, fname, fval):
  57. return self.__setter(fname, fval)
  58. class LeObject(object):
  59. ##@brief boolean that tells if an object is abtract or not
  60. _abstract = None
  61. ##@brief A dict that stores DataHandler instances indexed by field name
  62. _fields = None
  63. ##@brief A tuple of fieldname (or a uniq fieldname) representing uid
  64. _uid = None
  65. ##@brief Read only datasource ( see @ref lodel2_datasources )
  66. _ro_datasource = None
  67. ##@breif Read & write datasource ( see @ref lodel2_datasources )
  68. _rw_datasource = None
  69. ##@brief Construct an object representing an Editorial component
  70. # @note Can be considered as EmClass instance
  71. def __init__(self, **kwargs):
  72. if self._abstract:
  73. raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ )
  74. ##@brief A dict that stores fieldvalues indexed by fieldname
  75. self.__datas = { fname:None for fname in self._fields }
  76. ##@brief Store a list of initianilized fields when instanciation not complete else store True
  77. self.__initialized = list()
  78. ##@brief Datas accessor. Instance of @ref LeObjectValues
  79. self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
  80. # Checks that uid is given
  81. for uid_name in self._uid:
  82. if uid_name not in kwargs:
  83. raise AttributeError("Cannot instanciate a LeObject without it's identifier")
  84. self.__datas[uid_name] = kwargs[uid_name]
  85. del(kwargs[uid_name])
  86. self.__initialized.append(uid_name)
  87. # Processing given fields
  88. allowed_fieldnames = self.fieldnames(include_ro = False)
  89. err_list = list()
  90. for fieldname, fieldval in kwargs.items():
  91. if fieldname not in allowed_fieldnames:
  92. if fieldname in self._fields:
  93. err_list.append(
  94. AttributeError("Value given for internal field : '%s'" % fieldname)
  95. )
  96. else:
  97. err_list.append(
  98. AttributeError("Unknown fieldname : '%s'" % fieldname)
  99. )
  100. else:
  101. self.__datas[fieldname] = fieldval
  102. self.__initialized = list()
  103. self.set_initialized()
  104. #-----------------------------------#
  105. # Fields datas handling methods #
  106. #-----------------------------------#
  107. ##@brief @property True if LeObject is initialized else False
  108. @property
  109. def initialized(self):
  110. return not isinstance(self.__initialized, list)
  111. ##@return The uid field name
  112. @classmethod
  113. def uid_fieldname(cls):
  114. return cls._uid
  115. ##@brief Return a list of fieldnames
  116. # @param include_ro bool : if True include read only field names
  117. # @return a list of str
  118. @classmethod
  119. def fieldnames(cls, include_ro = False):
  120. if not include_ro:
  121. return [ fname for fname in cls._fields if not cls._fields[fname].is_internal() ]
  122. else:
  123. return list(cls._fields.keys())
  124. @classmethod
  125. def name2objname(cls, name):
  126. return name.title()
  127. ##@brief Return the datahandler asssociated with a LeObject field
  128. # @param fieldname str : The fieldname
  129. # @return A data handler instance
  130. @classmethod
  131. def data_handler(cls, fieldname):
  132. if not fieldname in cls._fields:
  133. raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
  134. return cls._fields[fieldname]
  135. ##@brief Return a LeObject child class from a name
  136. # @warning This method has to be called from dynamically generated LeObjects
  137. # @param leobject_name str : LeObject name
  138. # @return A LeObject child class
  139. # @throw NameError if invalid name given
  140. @classmethod
  141. def name2class(cls, leobject_name):
  142. if cls.__module__ == 'lodel.leapi.leobject':
  143. raise NotImplementedError("Abstract method")
  144. mod = importlib.import_module(cls.__module__)
  145. try:
  146. return getattr(mod, leobject_name)
  147. except AttributeError:
  148. raise NameError("No LeObject named '%s'" % leobject_name)
  149. @classmethod
  150. def is_abstract(cls):
  151. return cls._abstract
  152. ##@brief Field data handler getter
  153. #@param fieldname str : The field name
  154. #@return A datahandler instance
  155. #@throw NameError if the field doesn't exist
  156. @classmethod
  157. def field(cls, fieldname):
  158. try:
  159. return cls._fields[fieldname]
  160. except KeyError:
  161. raise NameError("No field named '%s' in %s" % ( fieldname,
  162. cls.__name__))
  163. ##@brief Initialise both datasources (ro and rw)
  164. #
  165. #This method is used once at dyncode load to replace the datasource string
  166. #by a datasource instance to avoid doing this operation for each query
  167. #@see LeObject::_init_datasource()
  168. @classmethod
  169. def _init_datasources(cls):
  170. if isinstance(cls._datasource_name, str):
  171. rw_ds = ro_ds = cls._datasource_name
  172. else:
  173. ro_ds, rw_ds = cls._datasource_name
  174. #Read only datasource initialisation
  175. cls._ro_datasource = cls._init_datasource(ro_ds, True)
  176. log_msg = "Read only datasource %s initialized for LeObject %s"
  177. log_msg %= (ro_ds, cls.__name__)
  178. logger.debug(log_msg)
  179. #Read write datasource initialisation
  180. cls._rw_datasource = cls._init_datasource(rw_ds, False)
  181. log_msg = "Read&write only datasource %s initialized for LeObject %s"
  182. log_msg %= (rw_ds, cls.__name__)
  183. logger.debug(log_msg)
  184. ##@brief Replace the _datasource attribute value by a datasource instance
  185. #
  186. #This method is used once at dyncode load to replace the datasource string
  187. #by a datasource instance to avoid doing this operation for each query
  188. #@param ds_name str : The name of the datasource to instanciate
  189. #@param ro bool : if true initialise the _ro_datasource attribute else
  190. #initialise _rw_datasource attribute
  191. @classmethod
  192. def _init_datasource(cls, ds_name, ro):
  193. expt_msg = "In LeAPI class '%s' " % cls.__name__
  194. datasource_orig_name = ds_name
  195. if ds_name not in Settings.datasources._fields:
  196. expt_msg += "Unknow or unconfigured datasource %s"
  197. expt_msg %= (ds_name, cls.__name__)
  198. raise SettingsError(expt_msg)
  199. ds_identifier = getattr(Settings.datasources, ds_name)
  200. read_only = getattr(ds_identifier, 'read_only')
  201. try:
  202. ds_identifier = getattr(ds_identifier, 'identifier')
  203. except NameError:
  204. expt_msg += "Datasource %s is missconfigured, missing identifier."
  205. expt_msg %= ds_name
  206. raise SettingsError(expt_msg)
  207. if read_only and not ro:
  208. expt_msg += "Error in datasource %s configuration. Trying to use \
  209. a read only as a read&write datasource"
  210. expt_msg %= ds_name
  211. raise SettingsError(expt_msg)
  212. ds_plugin, ds_name = ds_identifier.split('.')
  213. #Checks that the datasource is configured
  214. if ds_plugin not in Settings.datasource._fields:
  215. expt_msg += "Unknown or unconfigured datasource plugin %s"
  216. expt_msg %= ds_plugin
  217. raise SettingsError(expt_msg)
  218. ds_conf = getattr(Settings.datasource, ds_plugin)
  219. if ds_name not in ds_conf._fields:
  220. expt_msg += "Unknown or unconfigured datasource instance %s"
  221. expt_msg %= ds_identifier
  222. raise SettingsError(expt_msg)
  223. ds_conf = getattr(ds_conf, ds_name)
  224. #Checks that the datasource plugin exists
  225. ds_plugin_module = Plugin.get(ds_plugin).module
  226. try:
  227. datasource_class = getattr(ds_plugin_module, "Datasource")
  228. except AttributeError as e:
  229. raise e
  230. expt_msg += "The datasource plugin %s seems to be invalid. Error \
  231. raised when trying to import Datasource"
  232. expt_msg %= ds_identifier
  233. raise SettingsError(expt_msg)
  234. ds_conf_old = ds_conf
  235. ds_conf = dict()
  236. for k in ds_conf_old._fields:
  237. ds_conf[k] = getattr(ds_conf_old, k)
  238. return datasource_class(**ds_conf)
  239. ##@brief Read only access to all datas
  240. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  241. # @param name str : field name
  242. # @return the Value
  243. # @throw RuntimeError if the field is not initialized yet
  244. # @throw NameError if name is not an existing field name
  245. def data(self, field_name):
  246. if field_name not in self._fields.keys():
  247. raise NameError("No such field in %s : %s" % (self.__class__.__name__, field_name))
  248. if not self.initialized and field_name not in self.__initialized:
  249. raise RuntimeError("The field %s is not initialized yet (and have no value)" % field_name)
  250. return self.__datas[field_name]
  251. ##@brief Datas setter
  252. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  253. # @param fname str : field name
  254. # @param fval * : field value
  255. # @return the value that is really set
  256. # @throw NameError if fname is not valid
  257. # @throw AttributeError if the field is not writtable
  258. def set_data(self, fname, fval):
  259. if fname not in self.fieldnames(include_ro = False):
  260. if fname not in self._fields.keys():
  261. raise NameError("No such field in %s : %s" % (self.__class__.__name__, fname))
  262. else:
  263. raise AttributeError("The field %s is read only" % fname)
  264. self.__datas[fname] = fval
  265. if not self.initialized and fname not in self.__initialized:
  266. # Add field to initialized fields list
  267. self.__initialized.append(fname)
  268. self.__set_initialized()
  269. if self.initialized:
  270. # Running full value check
  271. ret = self.__check_modified_values()
  272. if ret is None:
  273. return self.__datas[fname]
  274. else:
  275. raise LeApiErrors("Data check error", ret)
  276. else:
  277. # Doing value check on modified field
  278. # We skip full validation here because the LeObject is not fully initialized yet
  279. val, err = self._fields[fname].check_data_value(fval)
  280. if isinstance(err, Exception):
  281. #Revert change to be in valid state
  282. del(self.__datas[fname])
  283. del(self.__initialized[-1])
  284. raise LeApiErrors("Data check error", {fname:err})
  285. else:
  286. self.__datas[fname] = val
  287. ##@brief Update the __initialized attribute according to LeObject internal state
  288. #
  289. # Check the list of initialized fields and set __initialized to True if all fields initialized
  290. def __set_initialized(self):
  291. if isinstance(self.__initialized, list):
  292. expected_fields = self.fieldnames(include_ro = False) + self._uid
  293. if set(expected_fields) == set(self.__initialized):
  294. self.__initialized = True
  295. ##@brief Designed to be called when datas are modified
  296. #
  297. # Make different checks on the LeObject given it's state (fully initialized or not)
  298. # @return None if checks succeded else return an exception list
  299. def __check_modified_values(self):
  300. err_list = dict()
  301. if self.__initialized is True:
  302. # Data value check
  303. for fname in self.fieldnames(include_ro = False):
  304. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  305. if err is not None:
  306. err_list[fname] = err
  307. else:
  308. self.__datas[fname] = val
  309. # Data construction
  310. if len(err_list) == 0:
  311. for fname in self.fieldnames(include_ro = True):
  312. try:
  313. field = self._fields[fname]
  314. self.__datas[fname] = fields.construct_data( self,
  315. fname,
  316. self.__datas,
  317. self.__datas[fname]
  318. )
  319. except Exception as e:
  320. err_list[fname] = e
  321. # Datas consistency check
  322. if len(err_list) == 0:
  323. for fname in self.fieldnames(include_ro = True):
  324. field = self._fields[fname]
  325. ret = field.check_data_consistency(self, fname, self.__datas)
  326. if isinstance(ret, Exception):
  327. err_list[fname] = ret
  328. else:
  329. # Data value check for initialized datas
  330. for fname in self.__initialized:
  331. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  332. if err is not None:
  333. err_list[fname] = err
  334. else:
  335. self.__datas[fname] = val
  336. return err_list if len(err_list) > 0 else None
  337. #--------------------#
  338. # Other methods #
  339. #--------------------#
  340. ##@brief Temporary method to set private fields attribute at dynamic code generation
  341. #
  342. # This method is used in the generated dynamic code to set the _fields attribute
  343. # at the end of the dyncode parse
  344. # @warning This method is deleted once the dynamic code loaded
  345. # @param field_list list : list of EmField instance
  346. # @param cls
  347. @classmethod
  348. def _set__fields(cls, field_list):
  349. cls._fields = field_list
  350. ## @brief Check that datas are valid for this type
  351. # @param datas dict : key == field name value are field values
  352. # @param complete bool : if True expect that datas provide values for all non internal fields
  353. # @param allow_internal bool : if True don't raise an error if a field is internal
  354. # @param cls
  355. # @return Checked datas
  356. # @throw LeApiDataCheckError if errors reported during check
  357. @classmethod
  358. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  359. err_l = dict() #Error storing
  360. correct = set() #valid fields name
  361. mandatory = set() #mandatory fields name
  362. for fname, datahandler in cls._fields.items():
  363. if allow_internal or not datahandler.is_internal():
  364. correct.add(fname)
  365. if complete and not hasattr(datahandler, 'default'):
  366. mandatory.add(fname)
  367. provided = set(datas.keys())
  368. # searching for unknow fields
  369. for u_f in provided - correct:
  370. #Here we can check if the field is invalid or rejected because
  371. # it is internel
  372. err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'" % u_f)
  373. # searching for missing mandatory fieldsa
  374. for missing in mandatory - provided:
  375. err_l[miss_field] = AttributeError("The data for field '%s' is missing" % missing)
  376. #Checks datas
  377. checked_datas = dict()
  378. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  379. dh = cls._fields[name]
  380. res = dh.check_data_value(value)
  381. checked_datas[name], err = res
  382. if err:
  383. err_l[name] = err
  384. if len(err_l) > 0:
  385. raise LeApiDataCheckError("Error while checking datas", err_l)
  386. return checked_datas
  387. ##@brief Check and prepare datas
  388. #
  389. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  390. #
  391. # @param datas dict : {fieldname : fieldvalue, ...}
  392. # @param complete bool : If True you MUST give all the datas
  393. # @param allow_internal : Wether or not interal fields are expected in datas
  394. # @param cls
  395. # @return Datas ready for use
  396. # @todo: complete is very unsafe, find a way to get rid of it
  397. @classmethod
  398. def prepare_datas(cls, datas, complete=False, allow_internal=True):
  399. if not complete:
  400. warnings.warn("\nActual implementation can make datas construction and consitency unsafe when datas are not complete\n")
  401. ret_datas = cls.check_datas_value(datas, complete, allow_internal)
  402. if isinstance(ret_datas, Exception):
  403. raise ret_datas
  404. if complete:
  405. ret_datas = cls._construct_datas(ret_datas)
  406. cls._check_datas_consistency(ret_datas)
  407. return ret_datas
  408. ## @brief Construct datas values
  409. #
  410. # @param cls
  411. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  412. # @return A new dict of datas
  413. # @todo IMPLEMENTATION
  414. @classmethod
  415. def _construct_datas(cls, datas):
  416. """
  417. constructor = DatasConstructor(cls, datas, cls.fieldtypes())
  418. ret = {
  419. fname:constructor[fname]
  420. for fname, ftype in cls.fieldtypes().items()
  421. if not ftype.is_internal() or ftype.internal != 'autosql'
  422. }
  423. return ret
  424. """
  425. pass
  426. ## @brief Check datas consistency
  427. # @warning assert that datas is complete
  428. # @param cls
  429. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  430. # @throw LeApiDataCheckError if fails
  431. @classmethod
  432. def _check_datas_consistency(cls, datas):
  433. err_l = []
  434. err_l = dict()
  435. for fname, dh in cls._fields.items():
  436. ret = dh.check_data_consistency(cls, fname, datas)
  437. if isinstance(ret, Exception):
  438. err_l[fname] = ret
  439. if len(err_l) > 0:
  440. raise LeApiDataCheckError("Datas consistency checks fails", err_l)
  441. ## @brief Add a new instance
  442. def new():
  443. pass