Ingen beskrivning
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.

query.py 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. #-*- coding: utf-8 -*-
  2. import re
  3. from .leobject import LeObject, LeApiErrors, LeApiDataCheckError
  4. from lodel.plugin.hooks import LodelHook
  5. from lodel import logger
  6. class LeQueryError(Exception):
  7. ##@brief Instanciate a new exceptions handling multiple exceptions
  8. #@param msg str : Exception message
  9. #@param exceptions dict : A list of data check Exception with concerned
  10. # 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. if isinstance(self._exceptions, dict):
  19. for_iter = self._exceptions.items()
  20. else:
  21. for_iter = enumerate(self.__exceptions)
  22. for obj, expt in for_iter:
  23. msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
  24. expt_obj = obj,
  25. expt_name=expt.__class__.__name__,
  26. expt_msg=str(expt)
  27. )
  28. return msg
  29. class LeQuery(object):
  30. ##@brief Hookname prefix
  31. _hook_prefix = None
  32. ##@brief arguments for the LeObject.check_data_value()
  33. _data_check_args = { 'complete': False, 'allow_internal': False }
  34. ##@brief Abstract constructor
  35. # @param target_class LeObject : class of object the query is about
  36. def __init__(self, target_class):
  37. if self._hook_prefix is None:
  38. raise NotImplementedError("Abstract class")
  39. if not issubclass(target_class, LeObject):
  40. raise TypeError("target class has to be a child class of LeObject")
  41. self._target_class = target_class
  42. self._datasource = target_class._datasource
  43. ##@brief Execute a query and return the result
  44. # @param **datas
  45. # @return the query result
  46. # @see LeQuery.__query()
  47. #
  48. def execute(self, datas = None):
  49. if len(datas) > 0:
  50. self._target_class.check_datas_value(
  51. datas,
  52. **self._data_check_args)
  53. self._target_class.prepare_datas() #not yet implemented
  54. if self._hook_prefix is None:
  55. raise NotImplementedError("Abstract method")
  56. LodelHook.call_hook( self._hook_prefix+'_pre',
  57. self._target_class,
  58. datas)
  59. ret = self.__query(self._datasource, **datas)
  60. ret = LodelHook.call_hook( self._hook_prefix+'_post',
  61. self._target_class,
  62. ret)
  63. return ret
  64. ##@brief Childs classes implements this method to execute the query
  65. # @param **datas
  66. # @return query result
  67. def __query(self, **datas):
  68. raise NotImplementedError("Asbtract method")
  69. ##@return a dict with query infos
  70. def dump_infos(self):
  71. return {'target_class': self._target_class}
  72. def __repr__(self):
  73. ret = "<{classname} target={target_class}>"
  74. return ret.format(
  75. classname=self.__class__.__name__,
  76. target_class = self._target_class)
  77. ##@brief Abstract class handling query with filters
  78. #
  79. #@todo add handling of inter-datasource queries
  80. class LeFilteredQuery(LeQuery):
  81. ##@brief The available operators used in query definitions
  82. _query_operators = [
  83. ' = ',
  84. ' <= ',
  85. ' >= ',
  86. ' != ',
  87. ' < ',
  88. ' > ',
  89. ' in ',
  90. ' not in ',
  91. ' like ',
  92. ' not like ']
  93. ##@brief Regular expression to process filters
  94. _query_re = None
  95. ##@brief Abtract constructor for queries with filter
  96. # @param target_class LeObject : class of object the query is about
  97. # @param query_filters list : with a tuple (only one filter) or a list of tuple
  98. # or a dict: {OP,list(filters)} with OP = 'OR' or 'AND
  99. # For tuple (FIELD,OPERATOR,VALUE)
  100. def __init__(self, target_class, query_filters = None):
  101. super().__init__(target_class)
  102. ##@brief The query filter tuple(std_filter, relational_filters)
  103. self.__query_filter = None
  104. self.set_query_filter(query_filters)
  105. ##@brief Add filter(s) to the query
  106. #@param query_filter list|tuple|str : A single filter or a list of filters
  107. #@see LeFilteredQuery._prepare_filters()
  108. def set_query_filter(self, query_filter):
  109. if isinstance(query_filter, str):
  110. query_filter = [query_filter]
  111. self.__query_filter = self._prepare_filters(query_filter)
  112. def dump_infos(self):
  113. ret = super().dump_infos()
  114. ret['query_filter'] = self.__query_filter
  115. return ret
  116. def __repr__(self):
  117. ret = "<{classname} target={target_class} query_filter={query_filter}>"
  118. return ret.format(
  119. classname=self.__class__.__name__,
  120. query_filter = self.__query_filter,
  121. target_class = self._target_class)
  122. ## @brief Prepare filters for datasource
  123. #
  124. #A filter can be a string or a tuple with len = 3.
  125. #
  126. #This method divide filters in two categories :
  127. #
  128. #@par Simple filters
  129. #
  130. #Those filters concerns fields that represent object values (a title,
  131. #the content, etc.) They are composed of three elements : FIELDNAME OP
  132. # VALUE . Where :
  133. #- FIELDNAME is the name of the field
  134. #- OP is one of the authorized comparison operands ( see
  135. #@ref LeFilteredQuery.query_operators )
  136. #- VALUE is... a value
  137. #
  138. #@par Relational filters
  139. #
  140. #Those filters concerns on reference fields ( see the corresponding
  141. #abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
  142. #The filter as quite the same composition than simple filters :
  143. # FIELDNAME[.REF_FIELD] OP VALUE . Where :
  144. #- FIELDNAME is the name of the reference field
  145. #- REF_FIELD is an optionnal addon to the base field. It indicate on wich
  146. #field of the referenced object the comparison as to be done. If no
  147. #REF_FIELD is indicated the comparison will be done on identifier.
  148. #
  149. #@param cls
  150. #@param filters_l list : This list of str or tuple (or both)
  151. #@return a tuple(FILTERS, RELATIONNAL_FILTERS
  152. #@todo move this doc in another place (a dedicated page ?)
  153. def _prepare_filters(self, filters_l):
  154. filters = list()
  155. res_filters = list()
  156. rel_filters = list()
  157. err_l = dict()
  158. #Splitting in tuple if necessary
  159. for i,fil in enumerate(filters_l):
  160. if len(fil) == 3 and not isinstance(fil, str):
  161. filters.append(tuple(fil))
  162. else:
  163. try:
  164. filters.append(self.split_filter(fil))
  165. except ValueError as e:
  166. err_l["filter %d" % i] = e
  167. for field, operator, value in filters:
  168. err_key = "%s %s %s" % (field, operator, value) #to push in err_l
  169. # Spliting field name to be able to detect a relational field
  170. field_spl = field.split('.')
  171. if len(field_spl) == 2:
  172. field, ref_field = field_spl
  173. elif len(field_spl) == 1:
  174. ref_field = None
  175. else:
  176. err_l[field] = NameError( "'%s' is not a valid relational \
  177. field name" % fieldname)
  178. continue
  179. # Checking field against target_class
  180. ret = self._check_field(self._target_class, field)
  181. if isinstance(ret, Exception):
  182. err_l[field] = ret
  183. continue
  184. field_datahandler = self._target_class.field(field)
  185. if ref_field is not None and not field_datahandler.is_reference():
  186. # inconsistency
  187. err_l[field] = NameError( "The field '%s' in %s is not \
  188. a relational field, but %s.%s was present in the filter"
  189. % ( field,
  190. self._target_class.__name__,
  191. field,
  192. ref_field))
  193. if field_datahandler.is_reference():
  194. #Relationnal field
  195. if ref_field is None:
  196. # ref_field default value
  197. ref_uid = set([ lc._uid for lc in field_datahandler.linked_classes])
  198. if len(ref_uid) == 1:
  199. ref_field = ref_uid[0]
  200. else:
  201. if len(ref_uid) > 1:
  202. err_l[err_key] = RuntimeError("The referenced classes are identified by fields with different name. Unable to determine wich field to use for the reference")
  203. else:
  204. err_l[err_key] = RuntimeError("Unknow error when trying to determine wich field to use for the relational filter")
  205. continue
  206. # Prepare relational field
  207. ret = self._prepare_relational_fields(field, ref_field)
  208. if isinstance(ret, Exception):
  209. err_l[err_key] = ret
  210. continue
  211. else:
  212. rel_filters.append((ret, operator, value))
  213. else:
  214. res_filters.append((field,operator, value))
  215. if len(err_l) > 0:
  216. raise LeApiDataCheckError(
  217. "Error while preparing filters : ",
  218. err_l)
  219. return (res_filters, rel_filters)
  220. ## @brief Prepare & check relational field
  221. #
  222. # The result is a tuple with (field, ref_field, concerned_classes), with :
  223. # - field the target_class field name
  224. # - ref_field the concerned_classes field names
  225. # - concerned_classes a set of concerned LeObject classes
  226. # @param field str : The target_class field name
  227. # @param ref_field str : The referenced class field name
  228. # @return a tuple(field, concerned_classes, ref_field) or an Exception class instance
  229. def _prepare_relational_fields(self,field, ref_field):
  230. field_dh = self._target_class.field(field)
  231. concerned_classes = []
  232. linked_classes = [] if field_dh.linked_classes is None else field_dh.linked_classes
  233. for l_class in linked_classes:
  234. try:
  235. l_class.field(ref_field)
  236. concerned_classes.append(l_class)
  237. except KeyError:
  238. pass
  239. if len(concerned_classes) > 0:
  240. return (field, ref_field, concerned_classes)
  241. else:
  242. return ValueError("None of the linked class of field %s has a field named '%s'" % (field, ref_field))
  243. ## @brief Check and split a query filter
  244. # @note The query_filter format is "FIELD OPERATOR VALUE"
  245. # @param query_filter str : A query_filter string
  246. # @param cls
  247. # @return a tuple (FIELD, OPERATOR, VALUE)
  248. @classmethod
  249. def split_filter(cls, query_filter):
  250. if cls._query_re is None:
  251. cls.__compile_query_re()
  252. matches = cls._query_re.match(query_filter)
  253. if not matches:
  254. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  255. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  256. result = [r.strip() for r in result]
  257. for r in result:
  258. if len(r) == 0:
  259. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  260. return result
  261. ## @brief Compile the regex for query_filter processing
  262. # @note Set _LeObject._query_re
  263. @classmethod
  264. def __compile_query_re(cls):
  265. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  266. for operator in cls._query_operators[1:]:
  267. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  268. op_re_piece += ')'
  269. cls._query_re = re.compile('^\s*(?P<field>([a-z_][a-z0-9\-_]*\.)?[a-z_][a-z0-9\-_]*)\s*'+op_re_piece+'\s*(?P<value>.*)\s*$', flags=re.IGNORECASE)
  270. pass
  271. @classmethod
  272. def _check_field(cls, target_class, fieldname):
  273. try:
  274. target_class.field(fieldname)
  275. except NameError:
  276. tc_name = target_class.__name__
  277. return ValueError("No such field '%s' in %s" % ( fieldname,
  278. tc_name))
  279. ##@brief Prepare a relational filter
  280. #
  281. #Relational filters are composed of a tuple like the simple filters
  282. #but the first element of this tuple is a tuple to :
  283. #
  284. #<code>( (FIELDNAME, {REF_CLASS: REF_FIELD}), OP, VALUE)</code>
  285. # Where :
  286. #- FIELDNAME is the field name is the target class
  287. #- the second element is a dict with :
  288. # - REF_CLASS as key. It's a LeObject child class
  289. # - REF_FIELD as value. The name of the referenced field in the REF_CLASS
  290. #
  291. #Visibly the REF_FIELD value of the dict will vary only when
  292. #no REF_FIELD is explicitly given in the filter string notation
  293. #and REF_CLASSES has differents uid
  294. #
  295. #@par String notation examples
  296. #<pre>contributeur IN (1,2,3,5)</pre> will be transformed into :
  297. #<pre>(
  298. # (
  299. # contributeur,
  300. # {
  301. # auteur: 'lodel_id',
  302. # traducteur: 'lodel_id'
  303. # }
  304. # ),
  305. # ' IN ',
  306. # [ 1,2,3,5 ])</pre>
  307. #@todo move the documentation to another place
  308. #
  309. #@param fieldname str : The relational field name
  310. #@param ref_field str|None : The referenced field name (if None use
  311. #uniq identifiers as referenced field
  312. #@return a well formed relational filter tuple or an Exception instance
  313. @classmethod
  314. def __prepare_relational_fields(cls, fieldname, ref_field = None):
  315. datahandler = self._target_class.field(fieldname)
  316. # now we are going to fetch the referenced class to see if the
  317. # reference field is valid
  318. ref_classes = datahandler.linked_classes
  319. ref_dict = dict()
  320. if ref_field is None:
  321. for ref_class in ref_classes:
  322. ref_dict[ref_class] = ref_class.uid_fieldname
  323. else:
  324. for ref_class in ref_classes:
  325. if ref_field in ref_class.fieldnames(True):
  326. ref_dict[ref_class] = ref_field
  327. else:
  328. logger.debug("Warning the class %s is not considered in \
  329. the relational filter %s" % ref_class.__name__)
  330. if len(ref_dict) == 0:
  331. return NameError( "No field named '%s' in referenced objects"
  332. % ref_field)
  333. return ( (fieldname, ref_dict), op, value)
  334. ##@brief A query to insert a new object
  335. class LeInsertQuery(LeQuery):
  336. _hook_prefix = 'leapi_insert_'
  337. _data_check_args = { 'complete': True, 'allow_internal': False }
  338. def __init__(self, target_class):
  339. super().__init__(target_class)
  340. ## @brief Implements an insert query operation, with only one insertion
  341. # @param **datas : datas to be inserted
  342. def __query(self, **datas):
  343. nb_inserted = self._datasource.insert(self._target_class,**datas)
  344. if nb_inserted < 0:
  345. raise LeQueryError("Insertion error")
  346. return nb_inserted
  347. ## @brief Implements an insert query operation, with multiple insertions
  348. # @param datas : list of **datas to be inserted
  349. def __query(self, datas):
  350. nb_inserted = self._datasource.insert_multi(self._target_class,datas_list)
  351. if nb_inserted < 0:
  352. raise LeQueryError("Multiple insertions error")
  353. return nb_inserted
  354. ## @brief Execute the insert query
  355. def execute(self, **datas):
  356. super().execute(self._datasource, **datas)
  357. ##@brief A query to update datas for a given object
  358. class LeUpdateQuery(LeFilteredQuery):
  359. _hook_prefix = 'leapi_update_'
  360. _data_check_args = { 'complete': True, 'allow_internal': False }
  361. def __init__(self, target_class, query_filter):
  362. super().__init__(target_class, query_filter)
  363. ##@brief Implements an update query
  364. # @param **datas : datas to update
  365. # @returns the number of updated items
  366. # @exception when the number of updated items is not as expected
  367. def __query(self, **datas):
  368. # select _uid corresponding to query_filter
  369. l_uids=self._datasource.select( self._target_class,
  370. list(self._target_class.getuid()),
  371. query_filter,
  372. None,
  373. None,
  374. None,
  375. None,
  376. 0,
  377. False)
  378. # list of dict l_uids : _uid(s) of the objects to be updated, corresponding datas
  379. nb_updated = self._datasource.update( self._target_class,
  380. l_uids,
  381. **datas)
  382. if nb_updated != len(l_uids):
  383. msg = "Number of updated items: %d is not as expected: %d "
  384. msg %= (nb_updated, len(l_uids))
  385. raise LeQueryError(msg)
  386. return nb_updated
  387. ## @brief Execute the update query
  388. def execute(self, **datas):
  389. super().execute(self._datasource, **datas)
  390. ##@brief A query to delete an object
  391. class LeDeleteQuery(LeFilteredQuery):
  392. _hook_prefix = 'leapi_delete_'
  393. def __init__(self, target_class, query_filter):
  394. super().__init__(target_class, query_filter)
  395. ## @brief Execute the delete query
  396. def execute(self):
  397. super().execute()
  398. ##@brief Implements delete query operations
  399. # @returns the number of deleted items
  400. # @exception when the number of deleted items is not as expected
  401. def __query(self):
  402. # select _uid corresponding to query_filter
  403. l_uids = self._datasource.select( self._target_class,
  404. list(self._target_class.getuid()),
  405. query_filter,
  406. None,
  407. None,
  408. None,
  409. None,
  410. 0,
  411. False)
  412. # list of dict l_uids : _uid(s) of the objects to be deleted
  413. nb_deleted = datasource.update(self._target_class,l_uids, **datas)
  414. if nb_deleted != len(l_uids):
  415. msg = "Number of deleted items %d is not as expected %d "
  416. msg %= (nb_deleted, len(l_uids))
  417. raise LeQueryError(msg)
  418. return nb_deleted
  419. class LeGetQuery(LeFilteredQuery):
  420. _hook_prefix = 'leapi_get_'
  421. ##@brief Instanciate a new get query
  422. # @param target_class LeObject : class of object the query is about
  423. # @param query_filters dict : {OP, list of query filters }
  424. # or tuple (FIELD, OPERATOR, VALUE) )
  425. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  426. # @param order list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
  427. # @param group list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
  428. # @param limit int : The maximum number of returned results
  429. # @param offset int : offset
  430. def __init__(self, target_class, query_filter, **kwargs):
  431. super().__init__(target_class, query_filter)
  432. ##@brief The fields to get
  433. self.__field_list = None
  434. ##@brief An equivalent to the SQL ORDER BY
  435. self.__order = None
  436. ##@brief An equivalent to the SQL GROUP BY
  437. self.__group = None
  438. ##@brief An equivalent to the SQL LIMIT x
  439. self.__limit = None
  440. ##@brief An equivalent to the SQL LIMIT x, OFFSET
  441. self.__offset = 0
  442. # Checking kwargs and assigning default values if there is some
  443. for argname in kwargs:
  444. if argname not in ('field_list', 'order', 'group', 'limit', 'offset'):
  445. raise TypeError("Unexpected argument '%s'" % argname)
  446. if 'field_list' not in kwargs:
  447. self.set_field_list(target_class.fieldnames(include_ro = True))
  448. else:
  449. self.set_field_list(kwargs['field_list'])
  450. if 'order' in kwargs:
  451. #check kwargs['order']
  452. self.__order = kwargs['order']
  453. if 'group' in kwargs:
  454. #check kwargs['group']
  455. self.__group = kwargs['group']
  456. if 'limit' in kwargs:
  457. try:
  458. self.__limit = int(kwargs[limit])
  459. if self.__limit <= 0:
  460. raise ValueError()
  461. except ValueError:
  462. raise ValueError("limit argument expected to be an interger > 0")
  463. if 'offset' in kwargs:
  464. try:
  465. self.__offset = int(kwargs['offset'])
  466. if self.__offset < 0:
  467. raise ValueError()
  468. except ValueError:
  469. raise ValueError("offset argument expected to be an integer >= 0")
  470. ##@brief Set the field list
  471. # @param field_list list | None : If None use all fields
  472. # @return None
  473. # @throw LeQueryError if unknown field given
  474. def set_field_list(self, field_list):
  475. err_l = dict()
  476. for fieldname in field_list:
  477. ret = self._check_field(self._target_class, fieldname)
  478. if isinstance(ret, Exception):
  479. expt = NameError( "No field named '%s' in %s" % ( fieldname,
  480. self._target_class.__name__))
  481. err_l[fieldname] = expt
  482. if len(err_l) > 0:
  483. raise LeQueryError( msg = "Error while setting field_list in a get query",
  484. exceptions = err_l)
  485. self.__field_list = list(set(field_list))
  486. ##@brief Execute the get query
  487. def execute(self, datasource):
  488. super().execute(datasource)
  489. ##@brief Implements select query operations
  490. # @returns a list containing the item(s)
  491. def __query(self, datasource):
  492. # select datas corresponding to query_filter
  493. l_datas=datasource.select( self._target_class,
  494. list(self.field_list),
  495. self.query_filter,
  496. None,
  497. self.__order,
  498. self.__group,
  499. self.__limit,
  500. self.offset,
  501. False)
  502. return l_datas
  503. ##@return a dict with query infos
  504. def dump_infos(self):
  505. ret = super().dump_infos()
  506. ret.update( { 'field_list' : self.__field_list,
  507. 'order' : self.__order,
  508. 'group' : self.__group,
  509. 'limit' : self.__limit,
  510. 'offset': self.__offset,
  511. })
  512. return ret
  513. def __repr__(self):
  514. ret = "<LeGetQuery target={target_class} filter={query_filter} field_list={field_list} order={order} group={group} limit={limit} offset={offset}>"
  515. return ret.format(**self.dump_infos())