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.

lecrud.py 20KB

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