Ei kuvausta
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.

lecrud.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. #-*- coding: utf-8 -*-
  2. ## @package leapi.lecrud
  3. # @brief This package contains the abstract class representing Lodel Editorial components
  4. #
  5. import warnings
  6. import importlib
  7. import re
  8. class LeApiErrors(Exception):
  9. ## @brief Instanciate a new exceptions handling multiple exceptions
  10. # @param exptexptions dict : A list of data check Exception with concerned field (or stuff) as key
  11. def __init__(self, msg = "Unknow error", exceptions = None):
  12. self._msg = msg
  13. self._exceptions = list() if exceptions is None else exceptions
  14. def __str__(self):
  15. msg = self._msg
  16. for obj, expt in self._exceptions.items():
  17. msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
  18. expt_obj = obj,
  19. expt_name=expt.__class__.__name__,
  20. expt_msg=str(expt)
  21. )
  22. return msg
  23. ## @brief When an error concern a query
  24. class LeApiQueryError(LeApiErrors): pass
  25. ## @brief When an error concerns a datas
  26. class LeApiDataCheckError(LeApiErrors): pass
  27. ## @brief Main class to handler lodel editorial components (relations and objects)
  28. class _LeCrud(object):
  29. ## @brief The datasource
  30. _datasource = None
  31. ## @brief abstract property to store the fieldtype representing the component identifier
  32. _uid_fieldtype = None #Will be a dict fieldname => fieldtype
  33. ## @brief will store all the fieldtypes (child classes handle it)
  34. _fieldtypes_all = None
  35. ## @brief Stores a regular expression to parse query filters strings
  36. _query_re = None
  37. ## @brief Stores Query filters operators
  38. _query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ']
  39. def __init__(self):
  40. raise NotImplementedError("Abstract class")
  41. ## @brief Given a dynamically generated class name return the corresponding python Class
  42. # @param name str : a concrete class name
  43. # @return False if no such component
  44. @classmethod
  45. def name2class(cls, name):
  46. mod = importlib.import_module(cls.__module__)
  47. try:
  48. return getattr(mod, name)
  49. except AttributeError:
  50. return False
  51. ## @return LeObject class
  52. @classmethod
  53. def leobject(cls):
  54. return cls.name2class('LeObject')
  55. ## @return A dict with key field name and value a fieldtype instance
  56. @classmethod
  57. def fieldtypes(cls):
  58. raise NotImplementedError("Abstract method") #child classes should return their uid fieldtype
  59. ## @return A dict with fieldtypes marked as internal
  60. # @todo check if this method is in use, else delete it
  61. @classmethod
  62. def fieldtypes_internal(self):
  63. return { fname: ft for fname, ft in cls.fieldtypes().items() if hasattr(ft, 'internal') and ft.internal }
  64. ## @return A list of field name
  65. @classmethod
  66. def fieldlist(cls):
  67. return cls.fieldtypes().keys()
  68. ## @return The name of the uniq id field
  69. # @todo test for abstract method !!!
  70. @classmethod
  71. def uidname(cls):
  72. if len(cls._uid_fieldtype) == 0:
  73. raise NotImplementedError("Abstract method uid_name for %s!"%cls.__name__)
  74. return list(cls._uid_fieldtype.keys())[0]
  75. ## @return maybe Bool: True if cls implements LeType
  76. # @param cls Class: a Class or instanciated object
  77. @classmethod
  78. def implements_letype(cls):
  79. return hasattr(cls, '_leclass')
  80. ## @return maybe Bool: True if cls implements LeClass
  81. # @param cls Class: a Class or instanciated object
  82. @classmethod
  83. def implements_leclass(cls):
  84. return hasattr(cls, '_class_id')
  85. ## @return maybe Bool: True if cls implements LeObject
  86. # @param cls Class: a Class or instanciated object
  87. @classmethod
  88. def implements_leobject(cls):
  89. return hasattr(cls, '_me_uid')
  90. ## @return maybe Bool: True if cls is a LeType or an instance of LeType
  91. # @param cls Class: a Class or instanciated object
  92. @classmethod
  93. def is_letype(cls):
  94. return cls.implements_letype()
  95. ## @return maybe Bool: True if cls is a LeClass or an instance of LeClass
  96. # @param cls Class: a Class or instanciated object
  97. @classmethod
  98. def is_leclass(cls):
  99. return cls.implements_leclass() and not cls.implements_letype()
  100. ## @return maybe Bool: True if cls is a LeClass or an instance of LeClass
  101. # @param cls Class: a Class or instanciated object
  102. @classmethod
  103. def is_leobject(cls):
  104. return cls.implements_leobject() and not cls.implements_leclass()
  105. ## @return maybe Bool: True if cls implements LeRelation
  106. # @param cls Class: a Class or instanciated object
  107. @classmethod
  108. def implements_lerelation(cls):
  109. return hasattr(cls, '_lesup_fieldtype')
  110. ## @return maybe Bool: True if cls implements LeRel2Type
  111. # @param cls Class: a Class or instanciated object
  112. @classmethod
  113. def implements_lerel2type(cls):
  114. return hasattr(cls, '_rel_attr_fieldtypes')
  115. ## @return maybe Bool: True if cls is a LeHierarch or an instance of LeHierarch
  116. # @param cls Class: a Class or instanciated object
  117. @classmethod
  118. def is_lehierarch(cls):
  119. return cls.implements_lerelation() and not cls.implements_lerel2type()
  120. ## @return maybe Bool: True if cls is a LeRel2Type or an instance of LeRel2Type
  121. # @param cls Class: a Class or instanciated object
  122. @classmethod
  123. def is_lerel2type(cls):
  124. return cls.implements_lerel2type()
  125. ## @brief Returns object datas
  126. # @param
  127. # @return a dict of fieldname : value
  128. def datas(self, internal = False):
  129. res = dict()
  130. for fname, ftt in self.fieldtypes().items():
  131. if (internal or (not internal and ftt.is_internal)) and hasattr(self, fname):
  132. res[fname] = getattr(self, fname)
  133. ## @brief Update a component in DB
  134. # @param datas dict : If None use instance attributes to update de DB
  135. # @return True if success
  136. # @todo better error handling
  137. def update(self, datas = None):
  138. datas = self.datas(internal=False) if datas is None else datas
  139. upd_datas = self.prepare_datas(datas, complete = False, allow_internal = False)
  140. filters = [self._id_filter()]
  141. rel_filters = []
  142. ret = self._datasource.update(self.__class__, filters, rel_filters, **upd_datas)
  143. if ret == 1:
  144. return True
  145. else:
  146. #ERROR HANDLING
  147. return False
  148. ## @brief Delete a component (instance method)
  149. # @return True if success
  150. # @todo better error handling
  151. def _delete(self):
  152. filters = [self._id_filter()]
  153. ret = _LeCrud.delete(self.__class__, filters)
  154. if ret == 1:
  155. return True
  156. else:
  157. #ERROR HANDLING
  158. return False
  159. ## @brief Check that datas are valid for this type
  160. # @param datas dict : key == field name value are field values
  161. # @param complete bool : if True expect that datas provide values for all non internal fields
  162. # @param allow_internal bool : if True don't raise an error if a field is internal
  163. # @return Checked datas
  164. # @throw LeApiDataCheckError if errors reported during check
  165. @classmethod
  166. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  167. err_l = dict() #Stores errors
  168. correct = [] #Valid fields name
  169. mandatory = [] #mandatory fields name
  170. for fname, ftt in cls.fieldtypes().items():
  171. if allow_internal or not ftt.is_internal():
  172. correct.append(fname)
  173. if complete and not hasattr(ftt, 'default'):
  174. mandatory.append(fname)
  175. mandatory = set(mandatory)
  176. correct = set(correct)
  177. provided = set(datas.keys())
  178. #searching unknow fields
  179. unknown = provided - correct
  180. for u_f in unknown:
  181. #here we can check if the field is unknown or rejected because it is internal
  182. err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'"%u_f)
  183. #searching missings fields
  184. missings = mandatory - provided
  185. for miss_field in missings:
  186. err_l[miss_field] = AttributeError("The data for field '%s' is missing"%miss_field)
  187. #Checks datas
  188. checked_datas = dict()
  189. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  190. ft = cls.fieldtypes()
  191. ft = ft[name]
  192. r = ft.check_data_value(value)
  193. checked_datas[name], err = r
  194. #checked_datas[name], err = cls.fieldtypes()[name].check_data_value(value)
  195. if err:
  196. err_l[name] = err
  197. if len(err_l) > 0:
  198. raise LeApiDataCheckError("Error while checking datas", err_l)
  199. return checked_datas
  200. ## @brief Given filters delete editorial components
  201. # @param filters list :
  202. # @return The number of deleted components
  203. @staticmethod
  204. def delete(cls, filters):
  205. filters, rel_filters = cls._prepare_filters(filters)
  206. return cls._datasource.delete(cls, filters, rel_filters)
  207. ## @brief Retrieve a collection of lodel editorial components
  208. #
  209. # @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  210. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  211. # @return A list of lodel editorial components instance
  212. # @todo think about LeObject and LeClass instanciation (partial instanciation, etc)
  213. @classmethod
  214. def get(cls, query_filters, field_list = None):
  215. if field_list is None or len(field_list) == 0:
  216. #default field_list
  217. field_list = cls.fieldlist()
  218. field_list = cls._prepare_field_list(field_list) #Can raise LeApiDataCheckError
  219. #preparing filters
  220. filters, relational_filters = cls._prepare_filters(query_filters)
  221. #Fetching editorial components from datasource
  222. results = cls._datasource.select(cls, field_list, filters, relational_filters)
  223. return results
  224. ## @brief Insert a new component
  225. # @param datas dict : The value of object we want to insert
  226. # @return A new id if success else False
  227. @classmethod
  228. def insert(cls, datas = datas, classname = None):
  229. callcls = cls if classname is None else cls.name2class(classname)
  230. if not callcls.is_letype() and not callcls.implements_lerelation():
  231. raise ValueError("You can only insert relations and LeTypes objects but tying to insert a '%s'"%callcls.__name__)
  232. insert_datas = callcls.prepare_datas(datas, complete = True, allow_internal = False)
  233. return callcls._datasource.insert(callcls, **insert_datas)
  234. ## @brief Check and prepare datas
  235. #
  236. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  237. #
  238. # @param datas dict : {fieldname : fieldvalue, ...}
  239. # @param complete bool : If True you MUST give all the datas
  240. # @param allow_internal : Wether or not interal fields are expected in datas
  241. # @return Datas ready for use
  242. @classmethod
  243. def prepare_datas(cls, datas, complete = False, allow_internal = True):
  244. if not complete:
  245. warnings.warn("Actual implementation can make datas construction and consitency checks fails when datas are not complete")
  246. ret_datas = cls.check_datas_value(datas, complete, allow_internal)
  247. if isinstance(ret_datas, Exception):
  248. raise ret_datas
  249. ret_datas = cls._construct_datas(ret_datas)
  250. cls._check_datas_consistency(ret_datas)
  251. return ret_datas
  252. #-###################-#
  253. # Private methods #
  254. #-###################-#
  255. ## @brief Build a filter to select an object with a specific ID
  256. # @warning assert that the uid is not composed with multiple fieldtypes
  257. # @return A filter of the form tuple(UID, '=', self.UID)
  258. def _id_filter(self):
  259. id_name = self.uidname()
  260. return ( id_name, '=', getattr(self, id_name) )
  261. ## @brief Construct datas values
  262. #
  263. # @warning assert that datas is complete
  264. #
  265. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  266. # @return A new dict of datas
  267. # @todo Decide wether or not the datas are modifed inplace or returned in a new dict (second solution for the moment)
  268. @classmethod
  269. def _construct_datas(cls, datas):
  270. res_datas = dict()
  271. for fname, ftype in cls.fieldtypes().items():
  272. if fname in datas:
  273. res_datas[fname] = ftype.construct_data(cls, fname, datas)
  274. return res_datas
  275. ## @brief Check datas consistency
  276. # @warning assert that datas is complete
  277. #
  278. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  279. # @throw LeApiDataCheckError if fails
  280. @classmethod
  281. def _check_datas_consistency(cls, datas):
  282. err_l = []
  283. err_l = dict()
  284. for fname, ftype in cls.fieldtypes().items():
  285. ret = ftype.check_data_consistency(cls, fname, datas)
  286. if isinstance(ret, Exception):
  287. err_l[fname] = ret
  288. if len(err_l) > 0:
  289. raise LeApiDataCheckError("Datas consistency checks fails", err_l)
  290. ## @brief Prepare a field_list
  291. # @param field_list list : List of string representing fields
  292. # @return A well formated field list
  293. # @throw LeApiDataCheckError if invalid field given
  294. @classmethod
  295. def _prepare_field_list(cls, field_list):
  296. err_l = dict()
  297. ret_field_list = list()
  298. for field in field_list:
  299. if cls._field_is_relational(field):
  300. ret = cls._prepare_relational_field(field)
  301. else:
  302. ret = cls._check_field(field)
  303. if isinstance(ret, Exception):
  304. err_l[field] = ret
  305. else:
  306. ret_field_list.append(ret)
  307. if len(err_l) > 0:
  308. raise LeApiDataCheckError(err_l)
  309. return ret_field_list
  310. ## @brief Check that a relational field is valid
  311. # @param field str : a relational field
  312. # @return a nature
  313. @classmethod
  314. def _prepare_relational_fields(cls, field):
  315. raise NotImplementedError("Abstract method")
  316. ## @brief Check that the field list only contains fields that are in the current class
  317. # @return None if no problem, else returns a list of exceptions that occurs during the check
  318. @classmethod
  319. def _check_field(cls, field):
  320. if field not in cls.fieldlist():
  321. return ValueError("No such field '%s' in %s"%(field, cls.__name__))
  322. return field
  323. ## @brief Prepare filters for datasource
  324. #
  325. # This method divide filters in two categories :
  326. # - filters : standart FIELDNAME OP VALUE filter
  327. # - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
  328. #
  329. # Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
  330. #
  331. # @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
  332. # @return a tuple(FILTERS, RELATIONNAL_FILTERS
  333. #
  334. # @see @ref datasource_side
  335. @classmethod
  336. def _prepare_filters(cls, filters_l):
  337. filters = list()
  338. res_filters = list()
  339. rel_filters = list()
  340. err_l = dict()
  341. #Splitting in tuple if necessary
  342. for fil in filters_l:
  343. if len(fil) == 3 and not isinstance(fil, str):
  344. filters.append(tuple(fil))
  345. else:
  346. filters.append(cls._split_filter(fil))
  347. for field, operator, value in filters:
  348. if cls._field_is_relational(field):
  349. #Checks relational fields
  350. ret = cls._prepare_relational_field(field)
  351. if isinstance(ret, Exception):
  352. err_l[field] = ret
  353. else:
  354. rel_filters.append((ret, operator, value))
  355. else:
  356. #Checks other fields
  357. ret = cls._check_field(field)
  358. if isinstance(ret, Exception):
  359. err_l[field] = ret
  360. else:
  361. res_filters.append((field,operator, value))
  362. if len(err_l) > 0:
  363. raise LeApiDataCheckError(err_l)
  364. return (res_filters, rel_filters)
  365. ## @brief Check and split a query filter
  366. # @note The query_filter format is "FIELD OPERATOR VALUE"
  367. # @param query_filter str : A query_filter string
  368. # @param cls
  369. # @return a tuple (FIELD, OPERATOR, VALUE)
  370. @classmethod
  371. def _split_filter(cls, query_filter):
  372. if cls._query_re is None:
  373. cls._compile_query_re()
  374. matches = cls._query_re.match(query_filter)
  375. if not matches:
  376. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  377. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  378. for r in result:
  379. if len(r) == 0:
  380. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  381. return result
  382. ## @brief Compile the regex for query_filter processing
  383. # @note Set _LeObject._query_re
  384. @classmethod
  385. def _compile_query_re(cls):
  386. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  387. for operator in cls._query_operators[1:]:
  388. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  389. op_re_piece += ')'
  390. cls._query_re = re.compile('^\s*(?P<field>(((superior)|(subordinate))\.)?[a-z_][a-z0-9\-_]*)\s*'+op_re_piece+'\s*(?P<value>[^<>=!].*)\s*$', flags=re.IGNORECASE)
  391. pass
  392. ## @brief Check if a field is relational or not
  393. # @param field str : the field to test
  394. # @return True if the field is relational else False
  395. @staticmethod
  396. def _field_is_relational(field):
  397. return field.startswith('superior.') or field.startswith('subordinate')