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.

query.py 30KB

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