説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

query.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. #-*- coding: utf-8 -*-
  2. import re
  3. import copy
  4. import inspect
  5. import warnings
  6. from lodel.context import LodelContext
  7. LodelContext.expose_modules(globals(), {
  8. 'lodel.leapi.exceptions': ['LeApiError', 'LeApiErrors',
  9. 'LeApiDataCheckError', 'LeApiDataCheckErrors', 'LeApiQueryError',
  10. 'LeApiQueryErrors'],
  11. 'lodel.plugin.hooks': ['LodelHook'],
  12. 'lodel.logger': ['logger']})
  13. # @todo check data when running query
  14. class LeQuery(object):
  15. # @brief Hookname prefix
  16. _hook_prefix = None
  17. # @brief arguments for the LeObject.check_data_value()
  18. _data_check_args = {'complete': False, 'allow_internal': False}
  19. # @brief Abstract constructor
  20. # @param target_class LeObject : class of object the query is about
  21. def __init__(self, target_class):
  22. from .leobject import LeObject
  23. if self._hook_prefix is None:
  24. raise NotImplementedError("Abstract class")
  25. if not inspect.isclass(target_class) or \
  26. not issubclass(target_class, LeObject):
  27. raise TypeError(
  28. "target class has to be a child class of LeObject but %s given" % target_class)
  29. self._target_class = target_class
  30. self._ro_datasource = target_class._ro_datasource
  31. self._rw_datasource = target_class._rw_datasource
  32. # @brief Executes a query and returns the result
  33. #@param **data
  34. #@return the query result
  35. #@see LeQuery._query()
  36. #@todo check that the check_datas_value is not duplicated/useless
  37. def execute(self, data):
  38. if data is not None:
  39. self._target_class.check_datas_value(
  40. data,
  41. **self._data_check_args)
  42. self._target_class.prepare_datas(data) # not yet implemented
  43. if self._hook_prefix is None:
  44. raise NotImplementedError("Abstract method")
  45. LodelHook.call_hook(self._hook_prefix + 'pre',
  46. self._target_class,
  47. data)
  48. ret = self._query(data=data)
  49. ret = LodelHook.call_hook(self._hook_prefix + 'post',
  50. self._target_class,
  51. ret)
  52. return ret
  53. # @brief Child classes implement this method to execute the query
  54. #@param **data
  55. #@return query result
  56. def _query(self, **data):
  57. raise NotImplementedError("Asbtract method")
  58. # @return a dict with query infos
  59. def dump_infos(self):
  60. return {'target_class': self._target_class}
  61. def __repr__(self):
  62. ret = "<{classname} target={target_class}>"
  63. return ret.format(
  64. classname=self.__class__.__name__,
  65. target_class=self._target_class)
  66. # @brief Abstract class handling query with filters
  67. class LeFilteredQuery(LeQuery):
  68. # @brief The available operators used in query definitions
  69. _query_operators = [
  70. ' = ',
  71. ' <= ',
  72. ' >= ',
  73. ' != ',
  74. ' < ',
  75. ' > ',
  76. ' in ',
  77. ' not in ',
  78. ' like ',
  79. ' not like ']
  80. # @brief Regular expression to process filters
  81. _query_re = None
  82. # @brief Abtract constructor for queries with filter
  83. #@param target_class LeObject : class of object the query is about
  84. #@param query_filters list : with a tuple (only one filter) or a list of
  85. # tuple or a dict: {OP,list(filters)} with OP = 'OR' or 'AND for tuple
  86. # (FIELD,OPERATOR,VALUE)
  87. def __init__(self, target_class, query_filters=None):
  88. super().__init__(target_class)
  89. # @brief The query filter tuple(std_filter, relational_filters)
  90. self._query_filter = None
  91. # @brief Stores potential subqueries (used when a query implies
  92. # more than one datasource.
  93. #
  94. # Subqueries are tuple(target_class_ref_field, LeGetQuery)
  95. self.subqueries = None
  96. query_filters = [] if query_filters is None else query_filters
  97. self.set_query_filter(query_filters)
  98. # @brief Abstract FilteredQuery execution method
  99. #
  100. # This method takes care to execute subqueries before calling super execute
  101. def execute(self, data=None):
  102. # copy originals filters
  103. orig_filters = copy.copy(self._query_filter)
  104. std_filters, rel_filters = self._query_filter
  105. for rfield, subq in self.subqueries:
  106. subq_res = subq.execute()
  107. std_filters.append(
  108. (rfield, ' in ', subq_res))
  109. self._query_filter = (std_filters, rel_filters)
  110. try:
  111. filters, rel_filters = self._query_filter
  112. res = super().execute(data)
  113. except Exception as e:
  114. # restoring filters even if an exception is raised
  115. self.__query_filter = orig_filters
  116. raise e # reraise
  117. # restoring filters
  118. self._query_filter = orig_filters
  119. return res
  120. # @brief Add filter(s) to the query
  121. #
  122. # This method is also able to slice query if different datasources are
  123. # implied in the request
  124. #
  125. #@param query_filter list|tuple|str : A single filter or a list of filters
  126. #@see LeFilteredQuery._prepare_filters()
  127. #@warning Does not support multiple UID
  128. def set_query_filter(self, query_filter):
  129. if isinstance(query_filter, str):
  130. query_filter = [query_filter]
  131. # Query filter prepration
  132. filters_orig, rel_filters = self._prepare_filters(query_filter)
  133. # Here we now that each relational filter concern only one datasource
  134. # thank's to _prepare_relational_fields
  135. # Multiple datasources detection
  136. self_ds_name = self._target_class._datasource_name
  137. result_rel_filters = list() # The filters that will stay in the query
  138. other_ds_filters = dict()
  139. for rfilter in rel_filters:
  140. (rfield, ref_dict), op, value = rfilter
  141. # rfield : the field in self._target_class
  142. tmp_rel_filter = dict() # designed to stores rel_field of same DS
  143. # First step : simplification
  144. # Trying to delete relational filters done on referenced class uid
  145. for tclass, tfield in copy.copy(ref_dict).items():
  146. # tclass : reference target class
  147. # tfield : referenced field from target class
  148. #
  149. # !!!WARNING!!!
  150. # The line below brake multi UID support
  151. #
  152. if tfield == tclass.uid_fieldname()[0]:
  153. # This relational filter can be simplified as
  154. # ref_field, op, value
  155. # Note : we will have to dedup filters_orig
  156. filters_orig.append((rfield, op, value))
  157. del(ref_dict[tclass])
  158. if len(ref_dict) == 0:
  159. continue
  160. # Determine what to do with other relational filters given
  161. # referenced class datasource
  162. # Remember : each class in a relational filter has the same
  163. # datasource
  164. tclass = list(ref_dict.keys())[0]
  165. cur_ds = tclass._datasource_name
  166. if cur_ds == self_ds_name:
  167. # Same datasource, the filter stay is self query
  168. result_rel_filters.append(((rfield, ref_dict), op, value))
  169. else:
  170. # Different datasource, we will have to create a subquery
  171. if cur_ds not in other_ds_filters:
  172. other_ds_filters[cur_ds] = list()
  173. other_ds_filters[cur_ds].append(
  174. ((rfield, ref_dict), op, value))
  175. # deduplication of std filters
  176. filters_cp = set()
  177. if not isinstance(filters_orig, set):
  178. for i, cfilt in enumerate(filters_orig):
  179. a, b, c = cfilt
  180. if isinstance(c, list): # list are not hashable
  181. newc = tuple(c)
  182. else:
  183. newc = c
  184. old_len = len(filters_cp)
  185. filters_cp |= set((a, b, newc))
  186. if len(filters_cp) == old_len:
  187. del(filters_orig[i])
  188. # Sets _query_filter attribute of self query
  189. self._query_filter = (filters_orig, result_rel_filters)
  190. # Sub queries creation
  191. subq = list()
  192. for ds, rfilters in other_ds_filters.items():
  193. for rfilter in rfilters:
  194. (rfield, ref_dict), op, value = rfilter
  195. for tclass, tfield in ref_dict.items():
  196. query = LeGetQuery(
  197. target_class=tclass,
  198. query_filters=[(tfield, op, value)],
  199. field_list=[tfield])
  200. subq.append((rfield, query))
  201. self.subqueries = subq
  202. # @return informations
  203. def dump_infos(self):
  204. ret = super().dump_infos()
  205. ret['query_filter'] = self._query_filter
  206. ret['subqueries'] = self.subqueries
  207. return ret
  208. def __repr__(self):
  209. res = "<{classname} target={target_class} query_filter={query_filter}"
  210. res = res.format(
  211. classname=self.__class__.__name__,
  212. query_filter=self._query_filter,
  213. target_class=self._target_class)
  214. if len(self.subqueries) > 0:
  215. for n, subq in enumerate(self.subqueries):
  216. res += "\n\tSubquerie %d : %s"
  217. res %= (n, subq)
  218. res += '>'
  219. return res
  220. # @brief Prepare filters for datasource
  221. #
  222. # A filter can be a string or a tuple with len = 3.
  223. #
  224. # This method divide filters in two categories :
  225. #
  226. #@par Simple filters
  227. #
  228. # Those filters concerns fields that represent object values (a title,
  229. # the content, etc.) They are composed of three elements : FIELDNAME OP
  230. # VALUE . Where :
  231. #- FIELDNAME is the name of the field
  232. #- OP is one of the authorized comparison operands (see
  233. #@ref LeFilteredQuery.query_operators )
  234. #- VALUE is... a value
  235. #
  236. #@par Relational filters
  237. #
  238. # Those filters concerns on reference fields (see the corresponding
  239. # abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
  240. # The filter as quite the same composition than simple filters :
  241. # FIELDNAME[.REF_FIELD] OP VALUE . Where :
  242. #- FIELDNAME is the name of the reference field
  243. #- REF_FIELD is an optionnal addon to the base field. It indicate on wich
  244. # field of the referenced object the comparison as to be done. If no
  245. # REF_FIELD is indicated the comparison will be done on identifier.
  246. #
  247. #@param cls
  248. #@param filters_l list : This list of str or tuple (or both)
  249. #@return a tuple(FILTERS, RELATIONNAL_FILTERS
  250. #@todo move this doc in another place (a dedicated page ?)
  251. #@warning Does not supports multiple UID for an EmClass
  252. def _prepare_filters(self, filters_l):
  253. filters = list()
  254. res_filters = list()
  255. rel_filters = list()
  256. err_l = dict()
  257. # Splitting in tuple if necessary
  258. for i, fil in enumerate(filters_l):
  259. if len(fil) == 3 and not isinstance(fil, str):
  260. filters.append(tuple(fil))
  261. else:
  262. try:
  263. filters.append(self.split_filter(fil))
  264. except ValueError as e:
  265. err_l["filter %d" % i] = e
  266. for field, operator, value in filters:
  267. err_key = "%s %s %s" % (field, operator, value) # to push in err_l
  268. # Spliting field name to be able to detect a relational field
  269. field_spl = field.split('.')
  270. if len(field_spl) == 2:
  271. field, ref_field = field_spl
  272. elif len(field_spl) == 1:
  273. ref_field = None
  274. else:
  275. err_l[field] = NameError("'%s' is not a valid relational \
  276. field name" % field)
  277. continue
  278. # Checking field against target_class
  279. ret = self._check_field(self._target_class, field)
  280. if isinstance(ret, Exception):
  281. err_l[field] = ret
  282. continue
  283. field_datahandler = self._target_class.field(field)
  284. if isinstance(field_datahandler, Exception):
  285. err_l[field] = field_datahandler
  286. continue
  287. if ref_field is not None and not field_datahandler.is_reference():
  288. # inconsistency
  289. err_l[field] = NameError("The field '%s' in %s is not \
  290. a relational field, but %s.%s was present in the filter"
  291. % (field,
  292. self._target_class.__name__,
  293. field,
  294. ref_field))
  295. if field_datahandler.is_reference():
  296. # Relationnal field
  297. if ref_field is None:
  298. # ref_field default value
  299. #
  300. # !!! WARNING !!!
  301. # This piece of code does not supports multiple UID for an
  302. # emclass
  303. #
  304. ref_uid = [
  305. lc._uid[0] for lc in field_datahandler.linked_classes]
  306. if len(set(ref_uid)) == 1:
  307. ref_field = ref_uid[0]
  308. else:
  309. if len(ref_uid) > 1:
  310. msg = "The referenced classes are identified by \
  311. fields with different name. Unable to determine wich field to use for the \
  312. reference"
  313. else:
  314. msg = "Unknow error when trying to determine wich \
  315. field to use for the relational filter"
  316. err_l[err_key] = RuntimeError(msg)
  317. continue
  318. # Prepare relational field
  319. ret = self._prepare_relational_fields(field, ref_field)
  320. if isinstance(ret, Exception):
  321. err_l[err_key] = ret
  322. continue
  323. else:
  324. rel_filters.append((ret, operator, value))
  325. else:
  326. value_orig = value
  327. value, error = field_datahandler.check_data_value(value)
  328. if isinstance(error, Exception):
  329. value = value_orig
  330. res_filters.append((field, operator, value))
  331. if len(err_l) > 0:
  332. raise LeApiDataCheckErrors(
  333. "Error while preparing filters : ",
  334. err_l)
  335. return (res_filters, rel_filters)
  336. # @brief Check and split a query filter
  337. # @note The query_filter format is "FIELD OPERATOR VALUE"
  338. # @param query_filter str : A query_filter string
  339. # @param cls
  340. # @return a tuple (FIELD, OPERATOR, VALUE)
  341. @classmethod
  342. def split_filter(cls, query_filter):
  343. if cls._query_re is None:
  344. cls.__compile_query_re()
  345. matches = cls._query_re.match(query_filter)
  346. if not matches:
  347. msg = "The query_filter '%s' seems to be invalid"
  348. raise ValueError(msg % query_filter)
  349. result = (
  350. matches.group('field'),
  351. re.sub(r'\s', ' ', matches.group('operator'), count=0),
  352. matches.group('value').strip())
  353. result = [r.strip() for r in result]
  354. for r in result:
  355. if len(r) == 0:
  356. msg = "The query_filter '%s' seems to be invalid"
  357. raise ValueError(msg % query_filter)
  358. return result
  359. # @brief Compile the regex for query_filter processing
  360. # @note Set _LeObject._query_re
  361. @classmethod
  362. def __compile_query_re(cls):
  363. op_re_piece = '(?P<operator>(%s)'
  364. op_re_piece %= cls._query_operators[0].replace(' ', '\s')
  365. for operator in cls._query_operators[1:]:
  366. op_re_piece += '|(%s)' % operator.replace(' ', '\s')
  367. op_re_piece += ')'
  368. re_full = '^\s*(?P<field>([a-z_][a-z0-9\-_]*\.)?[a-z_][a-z0-9\-_]*)\s*'
  369. re_full += op_re_piece + '\s*(?P<value>.*)\s*$'
  370. cls._query_re = re.compile(re_full, flags=re.IGNORECASE)
  371. pass
  372. @classmethod
  373. def _check_field(cls, target_class, fieldname):
  374. try:
  375. target_class.field(fieldname)
  376. except NameError as e:
  377. msg = "No field named '%s' in %s'"
  378. msg %= (fieldname, target_class.__name__)
  379. return NameError(msg)
  380. # @brief Prepare a relational filter
  381. #
  382. # Relational filters are composed of a tuple like the simple filters
  383. # but the first element of this tuple is a tuple to :
  384. #
  385. #<code>((FIELDNAME, {REF_CLASS: REF_FIELD}), OP, VALUE)</code>
  386. # Where :
  387. #- FIELDNAME is the field name is the target class
  388. #- the second element is a dict with :
  389. # - REF_CLASS as key. It's a LeObject child class
  390. # - REF_FIELD as value. The name of the referenced field in the REF_CLASS
  391. #
  392. # Visibly the REF_FIELD value of the dict will vary only when
  393. # no REF_FIELD is explicitly given in the filter string notation
  394. # and REF_CLASSES has differents uid
  395. #
  396. #@par String notation examples
  397. #<pre>contributeur IN (1,2,3,5)</pre> will be transformed into :
  398. #<pre>(
  399. # (
  400. # contributeur,
  401. # {
  402. # auteur: 'lodel_id',
  403. # traducteur: 'lodel_id'
  404. # }
  405. # ),
  406. # ' IN ',
  407. # [ 1,2,3,5 ])</pre>
  408. #@todo move the documentation to another place
  409. #
  410. #@param fieldname str : The relational field name
  411. #@param ref_field str|None : The referenced field name (if None use
  412. # uniq identifiers as referenced field
  413. #@return a well formed relational filter tuple or an Exception instance
  414. def _prepare_relational_fields(self, fieldname, ref_field=None):
  415. datahandler = self._target_class.field(fieldname)
  416. # now we are going to fetch the referenced class to see if the
  417. # reference field is valid
  418. ref_classes = datahandler.linked_classes
  419. ref_dict = dict()
  420. if ref_field is None:
  421. for ref_class in ref_classes:
  422. ref_dict[ref_class] = ref_class.uid_fieldname
  423. else:
  424. r_ds = None
  425. for ref_class in ref_classes:
  426. if r_ds is None:
  427. r_ds = ref_class._datasource_name
  428. elif ref_class._datasource_name != r_ds:
  429. return RuntimeError("All referenced class doesn't have the\
  430. same datasource. Query not possible")
  431. if ref_field in ref_class.fieldnames(True):
  432. ref_dict[ref_class] = ref_field
  433. else:
  434. msg = "Warning the class %s is not considered in \
  435. the relational filter %s"
  436. msg %= (ref_class.__name__, ref_field)
  437. logger.debug(msg)
  438. if len(ref_dict) == 0:
  439. return NameError("No field named '%s' in referenced objects [%s]"
  440. % (ref_field,
  441. ','.join([rc.__name__ for rc in ref_classes])))
  442. return (fieldname, ref_dict)
  443. # @brief A query to insert a new object
  444. class LeInsertQuery(LeQuery):
  445. _hook_prefix = 'leapi_insert_'
  446. _data_check_args = {'complete': True, 'allow_internal': False}
  447. def __init__(self, target_class):
  448. if target_class.is_abstract():
  449. raise LeApiQueryError("Trying to create an insert query on an \
  450. abstract LeObject : %s" % target_class)
  451. super().__init__(target_class)
  452. #  @brief Implements an insert query operation, with only one insertion
  453. # @param data : data to be inserted
  454. def _query(self, data):
  455. data = self._target_class.prepare_datas(data, True, False)
  456. id_inserted = self._rw_datasource.insert(self._target_class, data)
  457. return id_inserted
  458. """
  459. ## @brief Implements an insert query operation, with multiple insertions
  460. # @param data : list of **data to be inserted
  461. def _query(self, data):
  462. nb_inserted = self._datasource.insert_multi(
  463. self._target_class,data_list)
  464. if nb_inserted < 0:
  465. raise LeApiQueryError("Multiple insertions error")
  466. return nb_inserted
  467. """
  468. #  @brief Execute the insert query
  469. def execute(self, data):
  470. return super().execute(data=data)
  471. # @brief A query to update data for a given object
  472. #
  473. #@todo Change behavior, Huge optimization problem when updating using filters
  474. # and not instance. We have to run a GET and then 1 update by fecthed object...
  475. class LeUpdateQuery(LeFilteredQuery):
  476. _hook_prefix = 'leapi_update_'
  477. _data_check_args = {'complete': False, 'allow_internal': False}
  478. # @brief Instanciate an update query
  479. #
  480. # If a class and not an instance is given, no query_filters are expected
  481. # and the update will be fast and simple. Else we have to run a get query
  482. # before updating (to fetch data, update them and then, construct them
  483. # and check their consistency)
  484. #@param target LeObject clas or instance
  485. #@param query_filters list|None
  486. #@todo change strategy with instance update. We have to accept data for
  487. # the execute method
  488. def __init__(self, target, query_filters=None):
  489. # @brief This attr is set only if the target argument is an
  490. # instance of a LeObject subclass
  491. self.__leobject_instance_datas = None
  492. target_class = target
  493. if not inspect.isclass(target):
  494. if query_filters is not None:
  495. msg = "No query_filters accepted when an instance is given as \
  496. target to LeUpdateQuery constructor"
  497. raise AttributeError(msg)
  498. target_class = target.__class__
  499. if target_class.initialized:
  500. self.__leobject_instance_datas = target.datas(True)
  501. else:
  502. query_filters = [(target._uid[0], '=', target.uid())]
  503. super().__init__(target_class, query_filters)
  504. # @brief Implements an update query
  505. #@param data dict : data to be updated
  506. #@returns the number of updated items
  507. #@todo change stategy for instance update. Data should be allowed
  508. # for execute method (and query)
  509. def _query(self, data):
  510. uid_name = self._target_class._uid[0]
  511. if self.__leobject_instance_datas is not None:
  512. # Instance update
  513. # Building query_filter
  514. filters = [(
  515. uid_name,
  516. '=',
  517. str(self.__leobject_instance_datas[uid_name]))]
  518. res = self._rw_datasource.update(
  519. self._target_class, filters, [],
  520. self.__leobject_instance_datas)
  521. else:
  522. # Update by filters, we have to fetch data before updating
  523. res = self._ro_datasource.select(
  524. self._target_class, self._target_class.fieldnames(True),
  525. self._query_filter[0],
  526. self._query_filter[1])
  527. # Checking and constructing data
  528. upd_data = dict()
  529. for res_data in res:
  530. res_data.update(data)
  531. res_data = self._target_class.prepare_datas(
  532. res_data, True, True)
  533. filters = [(uid_name, '=', res_data[uid_name])]
  534. res = self._rw_datasource.update(
  535. self._target_class, filters, [],
  536. res_data)
  537. return res
  538. #  @brief Execute the update query
  539. def execute(self, data=None):
  540. if self.__leobject_instance_datas is not None and data is not None:
  541. raise LeApiQueryError("No data expected when running an update \
  542. query on an instance")
  543. if self.__leobject_instance_datas is None and data is None:
  544. raise LeApiQueryError("Data are mandatory when running an update \
  545. query on a class with filters")
  546. return super().execute(data=data)
  547. # @brief A query to delete an object
  548. class LeDeleteQuery(LeFilteredQuery):
  549. _hook_prefix = 'leapi_delete_'
  550. def __init__(self, target_class, query_filter):
  551. super().__init__(target_class, query_filter)
  552. #  @brief Execute the delete query
  553. # @param data
  554. def execute(self, data=None):
  555. return super().execute()
  556. # @brief Implements delete query operations
  557. # @param data
  558. #@returns the number of deleted items
  559. def _query(self, data=None):
  560. filters, rel_filters = self._query_filter
  561. nb_deleted = self._rw_datasource.delete(
  562. self._target_class, filters, rel_filters)
  563. return nb_deleted
  564. class LeGetQuery(LeFilteredQuery):
  565. _hook_prefix = 'leapi_get_'
  566. # @brief Instanciate a new get query
  567. #@param target_class LeObject : class of object the query is about
  568. #@param query_filters dict : {OP, list of query filters}
  569. # or tuple (FIELD, OPERATOR, VALUE) )
  570. #@param kwargs dict : other query-related arguments and options
  571. # - field_list list|None : list of string representing fields see @ref leobject_filters
  572. # - order list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  573. # - group list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  574. # - limit int : The maximum number of returned results
  575. # - offset int : offset
  576. def __init__(self, target_class, query_filters, **kwargs):
  577. super().__init__(target_class, query_filters)
  578. # @brief The fields to get
  579. self._field_list = None
  580. # @brief An equivalent to the SQL ORDER BY
  581. self._order = None
  582. # @brief An equivalent to the SQL GROUP BY
  583. self._group = None
  584. # @brief An equivalent to the SQL LIMIT x
  585. self._limit = None
  586. # @brief An equivalent to the SQL LIMIT x, OFFSET
  587. self._offset = 0
  588. # Checking kwargs and assigning default values if there is some
  589. for argname in kwargs:
  590. if argname not in (
  591. 'field_list', 'order', 'group', 'limit', 'offset'):
  592. raise TypeError("Unexpected argument '%s'" % argname)
  593. if 'field_list' not in kwargs:
  594. self.set_field_list(target_class.fieldnames(include_ro=True))
  595. else:
  596. self.set_field_list(kwargs['field_list'])
  597. if 'order' in kwargs:
  598. # check kwargs['order']
  599. self._order = kwargs['order']
  600. if 'group' in kwargs:
  601. # check kwargs['group']
  602. self._group = kwargs['group']
  603. if 'limit' in kwargs and kwargs['limit'] is not None:
  604. try:
  605. self._limit = int(kwargs['limit'])
  606. if self._limit <= 0:
  607. raise ValueError()
  608. except ValueError:
  609. msg = "limit argument expected to be an interger > 0"
  610. raise ValueError(msg)
  611. if 'offset' in kwargs:
  612. try:
  613. self._offset = int(kwargs['offset'])
  614. if self._offset < 0:
  615. raise ValueError()
  616. except ValueError:
  617. msg = "offset argument expected to be an integer >= 0"
  618. raise ValueError(msg)
  619. # @brief Set the field list
  620. # @param field_list list | None : If None use all fields
  621. # @return None
  622. # @throw LeApiQueryError if unknown field given
  623. def set_field_list(self, field_list):
  624. err_l = dict()
  625. if field_list is not None:
  626. for fieldname in field_list:
  627. ret = self._check_field(self._target_class, fieldname)
  628. if isinstance(ret, Exception):
  629. msg = "No field named '%s' in %s"
  630. msg %= (fieldname, self._target_class.__name__)
  631. expt = NameError(msg)
  632. err_l[fieldname] = expt
  633. if len(err_l) > 0:
  634. msg = "Error while setting field_list in a get query"
  635. raise LeApiQueryErrors(msg=msg, exceptions=err_l)
  636. self._field_list = list(set(field_list))
  637. # @brief Execute the get query
  638. def execute(self, data=None):
  639. return super().execute()
  640. # @brief Implements select query operations
  641. # @returns a list containing the item(s)
  642. def _query(self, data=None):
  643. # select data corresponding to query_filter
  644. fl = list(self._field_list) if self._field_list is not None else None
  645. l_data = self._ro_datasource.select(
  646. target=self._target_class,
  647. field_list=fl,
  648. filters=self._query_filter[0],
  649. relational_filters=self._query_filter[1],
  650. order=self._order,
  651. group=self._group,
  652. limit=self._limit,
  653. offset=self._offset)
  654. return l_data
  655. # @return a dict with query infos
  656. def dump_infos(self):
  657. ret = super().dump_infos()
  658. ret.update({'field_list': self._field_list,
  659. 'order': self._order,
  660. 'group': self._group,
  661. 'limit': self._limit,
  662. 'offset': self._offset,
  663. })
  664. return ret
  665. def __repr__(self):
  666. res = "<LeGetQuery target={target_class} filter={query_filter} \
  667. field_list={field_list} order={order} group={group} limit={limit} \
  668. offset={offset}"
  669. res = res.format(**self.dump_infos())
  670. if len(self.subqueries) > 0:
  671. for n, subq in enumerate(self.subqueries):
  672. res += "\n\tSubquerie %d : %s"
  673. res %= (n, subq)
  674. res += ">"
  675. return res