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

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