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 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. #-*- coding: utf-8 -*-
  2. import importlib
  3. import warnings
  4. import copy
  5. from lodel import logger
  6. from lodel.settings import Settings
  7. from lodel.settings.utils import SettingsError
  8. from .query import LeInsertQuery, LeUpdateQuery, LeDeleteQuery, LeGetQuery
  9. from .exceptions import *
  10. from lodel.plugin.exceptions import *
  11. from lodel.plugin.hooks import LodelHook
  12. from lodel.plugin import Plugin, DatasourcePlugin
  13. from lodel.leapi.datahandlers.base_classes import DatasConstructor
  14. ##@brief Stores the name of the field present in each LeObject that indicates
  15. #the name of LeObject subclass represented by this object
  16. CLASS_ID_FIELDNAME = "classname"
  17. ##@brief Wrapper class for LeObject getter & setter
  18. #
  19. # This class intend to provide easy & friendly access to LeObject fields values
  20. # without name collision problems
  21. # @note Wrapped methods are : LeObject.data() & LeObject.set_data()
  22. class LeObjectValues(object):
  23. ##@brief Construct a new LeObjectValues
  24. # @param set_callback method : The LeObject.set_datas() method of corresponding LeObject class
  25. # @param get_callback method : The LeObject.get_datas() method of corresponding LeObject class
  26. def __init__(self, fieldnames_callback, set_callback, get_callback):
  27. self._setter = set_callback
  28. self._getter = get_callback
  29. ##@brief Provide read access to datas values
  30. # @note Read access should be provided for all fields
  31. # @param fname str : Field name
  32. def __getattribute__(self, fname):
  33. getter = super().__getattribute__('_getter')
  34. return getter(fname)
  35. ##@brief Provide write access to datas values
  36. # @note Write acces shouldn't be provided for internal or immutable fields
  37. # @param fname str : Field name
  38. # @param fval * : the field value
  39. def __setattribute__(self, fname, fval):
  40. setter = super().__getattribute__('_setter')
  41. return setter(fname, fval)
  42. class LeObject(object):
  43. ##@brief boolean that tells if an object is abtract or not
  44. _abstract = None
  45. ##@brief A dict that stores DataHandler instances indexed by field name
  46. _fields = None
  47. ##@brief A tuple of fieldname (or a uniq fieldname) representing uid
  48. _uid = None
  49. ##@brief Read only datasource ( see @ref lodel2_datasources )
  50. _ro_datasource = None
  51. ##@brief Read & write datasource ( see @ref lodel2_datasources )
  52. _rw_datasource = None
  53. ##@brief Store the list of child classes
  54. _child_classes = None
  55. def __new__(cls, **kwargs):
  56. self = object.__new__(cls)
  57. ##@brief A dict that stores fieldvalues indexed by fieldname
  58. self.__datas = { fname:None for fname in self._fields }
  59. ##@brief Store a list of initianilized fields when instanciation not complete else store True
  60. self.__initialized = list()
  61. ##@brief Datas accessor. Instance of @ref LeObjectValues
  62. self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
  63. for fieldname, fieldval in kwargs.items():
  64. self.__datas[fieldname] = fieldval
  65. self.__initialized.append(fieldname)
  66. self.__is_initialized = False
  67. self.__set_initialized()
  68. return self
  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. # Checks that uid is given
  75. for uid_name in self._uid:
  76. if uid_name not in kwargs:
  77. raise LeApiError("Cannot instanciate a LeObject without it's identifier")
  78. self.__datas[uid_name] = kwargs[uid_name]
  79. del(kwargs[uid_name])
  80. self.__initialized.append(uid_name)
  81. # Processing given fields
  82. allowed_fieldnames = self.fieldnames(include_ro = False)
  83. err_list = dict()
  84. for fieldname, fieldval in kwargs.items():
  85. if fieldname not in allowed_fieldnames:
  86. if fieldname in self._fields:
  87. err_list[fieldname] = LeApiError(
  88. "Value given but the field is internal")
  89. else:
  90. err_list[fieldname] = LeApiError(
  91. "Unknown fieldname : '%s'" % fieldname)
  92. else:
  93. self.__datas[fieldname] = fieldval
  94. self.__initialized.append(fieldname)
  95. if len(err_list) > 0:
  96. raise LeApiErrors(msg = "Unable to __init__ %s" % self.__class__,
  97. exceptions = err_list)
  98. self.__set_initialized()
  99. #-----------------------------------#
  100. # Fields datas handling methods #
  101. #-----------------------------------#
  102. ##@brief Property method True if LeObject is initialized else False
  103. @property
  104. def initialized(self):
  105. return self.__is_initialized
  106. ##@return The uid field name
  107. @classmethod
  108. def uid_fieldname(cls):
  109. return cls._uid
  110. ##@brief Return a list of fieldnames
  111. # @param include_ro bool : if True include read only field names
  112. # @return a list of str
  113. @classmethod
  114. def fieldnames(cls, include_ro = False):
  115. if not include_ro:
  116. return [ fname for fname in cls._fields if not cls._fields[fname].is_internal() ]
  117. else:
  118. return list(cls._fields.keys())
  119. @classmethod
  120. def name2objname(cls, name):
  121. return name.title()
  122. ##@brief Return the datahandler asssociated with a LeObject field
  123. # @param fieldname str : The fieldname
  124. # @return A data handler instance
  125. @classmethod
  126. def data_handler(cls, fieldname):
  127. if not fieldname in cls._fields:
  128. raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
  129. return cls._fields[fieldname]
  130. ##@brief Return a LeObject child class from a name
  131. # @warning This method has to be called from dynamically generated LeObjects
  132. # @param leobject_name str : LeObject name
  133. # @return A LeObject child class
  134. # @throw NameError if invalid name given
  135. @classmethod
  136. def name2class(cls, leobject_name):
  137. if cls.__module__ == 'lodel.leapi.leobject':
  138. raise NotImplementedError("Abstract method")
  139. mod = importlib.import_module(cls.__module__)
  140. try:
  141. return getattr(mod, leobject_name)
  142. except (AttributeError, TypeError) :
  143. raise LeApiError("No LeObject named '%s'" % leobject_name)
  144. @classmethod
  145. def is_abstract(cls):
  146. return cls._abstract
  147. ##@brief Field data handler getter
  148. #@param fieldname str : The field name
  149. #@return A datahandler instance
  150. #@throw NameError if the field doesn't exist
  151. @classmethod
  152. def field(cls, fieldname):
  153. try:
  154. return cls._fields[fieldname]
  155. except KeyError:
  156. raise NameError("No field named '%s' in %s" % ( fieldname,
  157. cls.__name__))
  158. ##@return A dict with fieldname as key and datahandler as instance
  159. @classmethod
  160. def fields(cls, include_ro = False):
  161. if include_ro:
  162. return copy.copy(cls._fields)
  163. else:
  164. return {fname:cls._fields[fname] for fname in cls._fields if not cls._fields[fname].is_internal()}
  165. ##@brief Return the list of parents classes
  166. #
  167. #@note the first item of the list is the current class, the second is it's
  168. #parent etc...
  169. #@param cls
  170. #@warning multiple inheritance broken by this method
  171. #@return a list of LeObject child classes
  172. #@todo multiple parent capabilities implementation
  173. @classmethod
  174. def hierarch(cls):
  175. res = [cls]
  176. cur = cls
  177. while True:
  178. cur = cur.__bases__[0] # Multiple inheritance broken HERE
  179. if cur in (LeObject, object):
  180. break
  181. else:
  182. res.append(cur)
  183. return res
  184. ##@brief Return a tuple a child classes
  185. #@return a tuple of child classes
  186. @classmethod
  187. def child_classes(cls):
  188. return copy.copy(cls._child_classes)
  189. ##@brief Return the parent class that is the "source" of uid
  190. #
  191. #The method goal is to return the parent class that defines UID.
  192. #@return a LeObject child class or false if no UID defined
  193. @classmethod
  194. def uid_source(cls):
  195. if cls._uid is None or len(cls._uid) == 0:
  196. return False
  197. hierarch = cls.hierarch()
  198. prev = hierarch[0]
  199. uid_handlers = set( cls._fields[name] for name in cls._uid )
  200. for pcls in cls.hierarch()[1:]:
  201. puid_handlers = set(cls._fields[name] for name in pcls._uid)
  202. if set(pcls._uid) != set(prev._uid) \
  203. or puid_handlers != uid_handlers:
  204. break
  205. prev = pcls
  206. return prev
  207. ##@brief Initialise both datasources (ro and rw)
  208. #
  209. #This method is used once at dyncode load to replace the datasource string
  210. #by a datasource instance to avoid doing this operation for each query
  211. #@see LeObject::_init_datasource()
  212. @classmethod
  213. def _init_datasources(cls):
  214. if isinstance(cls._datasource_name, str):
  215. rw_ds = ro_ds = cls._datasource_name
  216. else:
  217. ro_ds, rw_ds = cls._datasource_name
  218. #Read only datasource initialisation
  219. cls._ro_datasource = DatasourcePlugin.init_datasource(ro_ds, True)
  220. if cls._ro_datasource is None:
  221. log_msg = "No read only datasource set for LeObject %s"
  222. log_msg %= cls.__name__
  223. logger.debug(log_msg)
  224. else:
  225. log_msg = "Read only datasource '%s' initialized for LeObject %s"
  226. log_msg %= (ro_ds, cls.__name__)
  227. logger.debug(log_msg)
  228. #Read write datasource initialisation
  229. cls._rw_datasource = DatasourcePlugin.init_datasource(rw_ds, False)
  230. if cls._ro_datasource is None:
  231. log_msg = "No read/write datasource set for LeObject %s"
  232. log_msg %= cls.__name__
  233. logger.debug(log_msg)
  234. else:
  235. log_msg = "Read/write datasource '%s' initialized for LeObject %s"
  236. log_msg %= (ro_ds, cls.__name__)
  237. logger.debug(log_msg)
  238. ##@brief Return the uid of the current LeObject instance
  239. #@return the uid value
  240. #@warning Broke multiple uid capabilities
  241. def uid(self):
  242. return self.data(self._uid[0])
  243. ##@brief Read only access to all datas
  244. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  245. # @param name str : field name
  246. # @return the Value
  247. # @throw RuntimeError if the field is not initialized yet
  248. # @throw NameError if name is not an existing field name
  249. def data(self, field_name):
  250. if field_name not in self._fields.keys():
  251. raise NameError("No such field in %s : %s" % (self.__class__.__name__, field_name))
  252. if not self.initialized and field_name not in self.__initialized:
  253. raise RuntimeError("The field %s is not initialized yet (and have no value)" % field_name)
  254. return self.__datas[field_name]
  255. ##@brief Read only access to all datas
  256. #@return a dict representing datas of current instance
  257. def datas(self, internal = False):
  258. return {fname:self.data(fname) for fname in self.fieldnames(internal)}
  259. ##@brief Datas setter
  260. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  261. # @param fname str : field name
  262. # @param fval * : field value
  263. # @return the value that is really set
  264. # @throw NameError if fname is not valid
  265. # @throw AttributeError if the field is not writtable
  266. def set_data(self, fname, fval):
  267. if fname not in self.fieldnames(include_ro = False):
  268. if fname not in self._fields.keys():
  269. raise NameError("No such field in %s : %s" % (self.__class__.__name__, fname))
  270. else:
  271. raise AttributeError("The field %s is read only" % fname)
  272. self.__datas[fname] = fval
  273. if not self.initialized and fname not in self.__initialized:
  274. # Add field to initialized fields list
  275. self.__initialized.append(fname)
  276. self.__set_initialized()
  277. if self.initialized:
  278. # Running full value check
  279. ret = self.__check_modified_values()
  280. if ret is None:
  281. return self.__datas[fname]
  282. else:
  283. raise LeApiErrors("Data check error", ret)
  284. else:
  285. # Doing value check on modified field
  286. # We skip full validation here because the LeObject is not fully initialized yet
  287. val, err = self._fields[fname].check_data_value(fval)
  288. if isinstance(err, Exception):
  289. #Revert change to be in valid state
  290. del(self.__datas[fname])
  291. del(self.__initialized[-1])
  292. raise LeApiErrors("Data check error", {fname:err})
  293. else:
  294. self.__datas[fname] = val
  295. ##@brief Update the __initialized attribute according to LeObject internal state
  296. #
  297. # Check the list of initialized fields and set __initialized to True if all fields initialized
  298. def __set_initialized(self):
  299. if isinstance(self.__initialized, list):
  300. expected_fields = self.fieldnames(include_ro = False) + self._uid
  301. if set(expected_fields) == set(self.__initialized):
  302. self.__is_initialized = True
  303. ##@brief Designed to be called when datas are modified
  304. #
  305. # Make different checks on the LeObject given it's state (fully initialized or not)
  306. # @return None if checks succeded else return an exception list
  307. def __check_modified_values(self):
  308. err_list = dict()
  309. if self.__initialized is True:
  310. # Data value check
  311. for fname in self.fieldnames(include_ro = False):
  312. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  313. if err is not None:
  314. err_list[fname] = err
  315. else:
  316. self.__datas[fname] = val
  317. # Data construction
  318. if len(err_list) == 0:
  319. for fname in self.fieldnames(include_ro = True):
  320. try:
  321. field = self._fields[fname]
  322. self.__datas[fname] = fields.construct_data( self,
  323. fname,
  324. self.__datas,
  325. self.__datas[fname]
  326. )
  327. except Exception as e:
  328. err_list[fname] = e
  329. # Datas consistency check
  330. if len(err_list) == 0:
  331. for fname in self.fieldnames(include_ro = True):
  332. field = self._fields[fname]
  333. ret = field.check_data_consistency(self, fname, self.__datas)
  334. if isinstance(ret, Exception):
  335. err_list[fname] = ret
  336. else:
  337. # Data value check for initialized datas
  338. for fname in self.__initialized:
  339. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  340. if err is not None:
  341. err_list[fname] = err
  342. else:
  343. self.__datas[fname] = val
  344. return err_list if len(err_list) > 0 else None
  345. #--------------------#
  346. # Other methods #
  347. #--------------------#
  348. ##@brief Temporary method to set private fields attribute at dynamic code generation
  349. #
  350. # This method is used in the generated dynamic code to set the _fields attribute
  351. # at the end of the dyncode parse
  352. # @warning This method is deleted once the dynamic code loaded
  353. # @param field_list list : list of EmField instance
  354. # @param cls
  355. @classmethod
  356. def _set__fields(cls, field_list):
  357. cls._fields = field_list
  358. ## @brief Check that datas are valid for this type
  359. # @param datas dict : key == field name value are field values
  360. # @param complete bool : if True expect that datas provide values for all non internal fields
  361. # @param allow_internal bool : if True don't raise an error if a field is internal
  362. # @param cls
  363. # @return Checked datas
  364. # @throw LeApiDataCheckError if errors reported during check
  365. @classmethod
  366. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  367. err_l = dict() #Error storing
  368. correct = set() #valid fields name
  369. mandatory = set() #mandatory fields name
  370. for fname, datahandler in cls._fields.items():
  371. if allow_internal or not datahandler.is_internal():
  372. correct.add(fname)
  373. if complete and not hasattr(datahandler, 'default'):
  374. mandatory.add(fname)
  375. provided = set(datas.keys())
  376. # searching for unknow fields
  377. for u_f in provided - correct:
  378. #Here we can check if the field is invalid or rejected because
  379. # it is internel
  380. err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'" % u_f)
  381. # searching for missing mandatory fieldsa
  382. for missing in mandatory - provided:
  383. err_l[missing] = AttributeError("The data for field '%s' is missing" % missing)
  384. #Checks datas
  385. checked_datas = dict()
  386. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  387. dh = cls._fields[name]
  388. res = dh.check_data_value(value)
  389. checked_datas[name], err = res
  390. if err:
  391. err_l[name] = err
  392. if len(err_l) > 0:
  393. raise LeApiDataCheckErrors("Error while checking datas", err_l)
  394. return checked_datas
  395. ##@brief Check and prepare datas
  396. #
  397. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  398. #
  399. # @param datas dict : {fieldname : fieldvalue, ...}
  400. # @param complete bool : If True you MUST give all the datas
  401. # @param allow_internal : Wether or not interal fields are expected in datas
  402. # @param cls
  403. # @return Datas ready for use
  404. # @todo: complete is very unsafe, find a way to get rid of it
  405. @classmethod
  406. def prepare_datas(cls, datas, complete=False, allow_internal=True):
  407. if not complete:
  408. warnings.warn("\nActual implementation can make broken datas \
  409. construction and consitency when datas are not complete\n")
  410. ret_datas = cls.check_datas_value(datas, complete, allow_internal)
  411. if isinstance(ret_datas, Exception):
  412. raise ret_datas
  413. if complete:
  414. ret_datas = cls._construct_datas(ret_datas)
  415. cls._check_datas_consistency(ret_datas)
  416. return ret_datas
  417. ## @brief Construct datas values
  418. #
  419. # @param cls
  420. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  421. # @return A new dict of datas
  422. # @todo IMPLEMENTATION
  423. @classmethod
  424. def _construct_datas(cls, datas):
  425. constructor = DatasConstructor(cls, datas, cls._fields)
  426. ret = {
  427. fname:constructor[fname]
  428. for fname, ftype in cls._fields.items()
  429. if not ftype.is_internal() or ftype.internal != 'autosql'
  430. }
  431. return ret
  432. ## @brief Check datas consistency
  433. # @warning assert that datas is complete
  434. # @param cls
  435. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  436. # @throw LeApiDataCheckError if fails
  437. @classmethod
  438. def _check_datas_consistency(cls, datas):
  439. err_l = []
  440. err_l = dict()
  441. for fname, dh in cls._fields.items():
  442. ret = dh.check_data_consistency(cls, fname, datas)
  443. if isinstance(ret, Exception):
  444. err_l[fname] = ret
  445. if len(err_l) > 0:
  446. raise LeApiDataCheckError("Datas consistency checks fails", err_l)
  447. ## @brief Check datas consistency
  448. # @warning assert that datas is complete
  449. # @param cls
  450. # @param datas dict : Datas that have been returned by LeCrud.prepare_datas() method
  451. @classmethod
  452. def make_consistency(cls, datas):
  453. for fname, dh in cls._fields.items():
  454. ret = dh.make_consistency(cls, fname, datas)
  455. ## @brief Add a new instance of LeObject
  456. # @return a new uid en case of success, False otherwise
  457. @classmethod
  458. def insert(cls, datas):
  459. query = LeInsertQuery(cls)
  460. return query.execute(datas)
  461. ## @brief Update an instance of LeObject
  462. #
  463. #@param datas : list of new datas
  464. def update(self, datas = None):
  465. datas = self.datas(internal=False) if datas is None else datas
  466. uids = self._uid
  467. query_filter = list()
  468. for uid in uids:
  469. query_filter.append((uid, '=', self.data(uid)))
  470. try:
  471. query = LeUpdateQuery(self.__class__, query_filter)
  472. except Exception as err:
  473. raise err
  474. try:
  475. result = query.execute(datas)
  476. except Exception as err:
  477. raise err
  478. return result
  479. ## @brief Delete an instance of LeObject
  480. #
  481. #@return 1 if the objet has been deleted
  482. def delete(self):
  483. uids = self._uid
  484. query_filter = list()
  485. for uid in uids:
  486. query_filter.append((uid, '=', self.data(uid)))
  487. query = LeDeleteQuery(self.__class__, query_filter)
  488. result = query.execute()
  489. return result
  490. ## @brief Delete instances of LeObject
  491. #@param uids a list: lists of (fieldname, fieldvalue), with fieldname in cls._uids
  492. #@returns the
  493. @classmethod
  494. def delete_bundle(cls, query_filters):
  495. deleted = 0
  496. try:
  497. query = LeDeleteQuery(cls, query_filters)
  498. except Exception as err:
  499. raise err
  500. try:
  501. result = query.execute()
  502. except Exception as err:
  503. raise err
  504. if not result is None:
  505. deleted += result
  506. return deleted
  507. ## @brief Get instances of LeObject
  508. #
  509. #@param target_class LeObject : class of object the query is about
  510. #@param query_filters dict : (filters, relational filters), with filters is a list of tuples : (FIELD, OPERATOR, VALUE) )
  511. #@param field_list list|None : list of string representing fields see
  512. #@ref leobject_filters
  513. #@param order list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  514. #@param group list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  515. #@param limit int : The maximum number of returned results
  516. #@param offset int : offset
  517. #@param Inst
  518. #@return a list of items (lists of (fieldname, fieldvalue))
  519. @classmethod
  520. def get(cls, query_filters, field_list=None, order=None, group=None, limit=None, offset=0):
  521. if field_list is not None:
  522. for uid in [ uidname
  523. for uidname in cls.uid_fieldname()
  524. if uidname not in field_list ]:
  525. field_list.append(uid)
  526. if CLASS_ID_FIELDNAME not in field_list:
  527. field_list.append(CLASS_ID_FIELDNAME)
  528. try:
  529. query = LeGetQuery(
  530. cls, query_filters = query_filters, field_list = field_list,
  531. order = order, group = group, limit = limit, offset = offset)
  532. except ValueError as err:
  533. raise err
  534. try:
  535. result = query.execute()
  536. except Exception as err:
  537. raise err
  538. objects = list()
  539. for res in result:
  540. res_cls = cls.name2class(res[CLASS_ID_FIELDNAME])
  541. inst = res_cls.__new__(res_cls,**res)
  542. objects.append(inst)
  543. return objects