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

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