Skip to content

Commit b61e14a

Browse files
committed
update TinyDB query builder (#2296)
* fix TinyDB query handling to use query builder * flake8
1 parent aedfc6e commit b61e14a

File tree

1 file changed

+76
-48
lines changed

1 file changed

+76
-48
lines changed

pygeoapi/provider/tinydb_.py

Lines changed: 76 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
# =================================================================
2929

3030
import logging
31-
import re # noqa
31+
from functools import reduce
32+
import operator
3233
import os
34+
import re
35+
from typing import Union
3336
import uuid
3437

3538
from dateutil.parser import parse as parse_date
@@ -157,9 +160,7 @@ def query(self, offset=0, limit=10, resulttype='results',
157160
"""
158161

159162
Q = Query()
160-
LOGGER.debug(f'Query initiated: {Q}')
161-
162-
QUERY = []
163+
predicates = []
163164

164165
feature_collection = {
165166
'type': 'FeatureCollection',
@@ -173,58 +174,60 @@ def query(self, offset=0, limit=10, resulttype='results',
173174
if bbox:
174175
LOGGER.debug('processing bbox parameter')
175176
bbox_as_string = ','.join(str(s) for s in bbox)
176-
QUERY.append(f"Q.geometry.test(bbox_intersects, '{bbox_as_string}')") # noqa
177+
predicates.append(Q.geometry.test(bbox_intersects, bbox_as_string))
177178

178179
if datetime_ is not None:
179180
LOGGER.debug('processing datetime parameter')
180181
if self.time_field is None:
181182
LOGGER.error('time_field not enabled for collection')
182183
LOGGER.error('Using default time property')
183-
time_field2 = 'time'
184+
time_field2 = Q.time
184185
else:
185186
LOGGER.error(f'Using properties.{self.time_field}')
186-
time_field2 = f"properties['{self.time_field}']"
187+
time_field2 = getattr(Q.properties, self.time_field)
187188

188189
if '/' in datetime_: # envelope
189190
LOGGER.debug('detected time range')
190191
time_begin, time_end = datetime_.split('/')
191192

192193
if time_begin != '..':
193-
QUERY.append(f"(Q.{time_field2}>='{time_begin}')") # noqa
194+
predicates.append(time_field2 >= time_begin)
194195
if time_end != '..':
195-
QUERY.append(f"(Q.{time_field2}<='{time_end}')") # noqa
196+
predicates.append(time_field2 <= time_end)
196197

197198
else: # time instant
198199
LOGGER.debug('detected time instant')
199-
QUERY.append(f"(Q.{time_field2}=='{datetime_}')") # noqa
200+
predicates.append(getattr(Q, time_field2) == datetime_)
200201

201202
if properties:
202203
LOGGER.debug('processing properties')
203204
for prop in properties:
204-
if isinstance(prop[1], str):
205-
value = f"'{prop[1]}'"
206-
else:
207-
value = prop[1]
208-
QUERY.append(f"(Q.properties['{prop[0]}']=={value})")
209-
210-
QUERY = self._add_search_query(QUERY, q)
211-
212-
QUERY_STRING = '&'.join(QUERY)
213-
LOGGER.debug(f'QUERY_STRING: {QUERY_STRING}')
214-
SEARCH_STRING = f'self.db.search({QUERY_STRING})'
215-
LOGGER.debug(f'SEARCH_STRING: {SEARCH_STRING}')
216-
217-
LOGGER.debug('querying database')
218-
if len(QUERY) > 0:
219-
LOGGER.debug(f'running eval on {SEARCH_STRING}')
220-
try:
221-
results = eval(SEARCH_STRING)
222-
except SyntaxError as err:
223-
msg = 'Invalid query'
224-
LOGGER.error(f'{msg}: {err}')
225-
raise ProviderInvalidQueryError(msg)
205+
if prop[0] not in self.fields:
206+
msg = 'Invalid query: invalid property name'
207+
LOGGER.error(msg)
208+
raise ProviderInvalidQueryError(msg)
209+
210+
predicates.append(getattr(Q.properties, prop[0]) == prop[1])
211+
212+
PQ = reduce(operator.and_, predicates) if predicates else None
213+
if q:
214+
SQ = self._add_search_query(Q, q)
226215
else:
227-
results = self.db.all()
216+
SQ = None
217+
218+
try:
219+
if PQ and SQ:
220+
results = self.db.search(PQ & SQ)
221+
elif PQ and not SQ:
222+
results = self.db.search(PQ)
223+
elif not PQ and SQ is not None:
224+
results = self.db.search(SQ)
225+
else:
226+
results = self.db.all()
227+
except SyntaxError as err:
228+
msg = 'Invalid query'
229+
LOGGER.error(f'{msg}: {err}')
230+
raise ProviderInvalidQueryError(msg)
228231

229232
feature_collection['numberMatched'] = len(results)
230233

@@ -355,17 +358,29 @@ def _add_extra_fields(self, json_data: dict) -> dict:
355358

356359
return json_data
357360

358-
def _add_search_query(self, query: list, search_term: str = None) -> str:
361+
def _add_search_query(self, search_object,
362+
search_term: str = None) -> Union[str, None]:
359363
"""
360-
Helper function to add extra query predicates
364+
Create a search query according to the OGC API - Records specification.
365+
366+
https://docs.ogc.org/is/20-004r1/20-004r1.html (Listing 14)
367+
368+
Examples (f is shorthand for Q.properties["_metadata-anytext"]):
369+
+-------------+-----------------------------------+
370+
| search term | TinyDB search |
371+
+-------------+-----------------------------------+
372+
| 'aa' | f.search('aa') |
373+
| 'aa,bb' | f.search('aa')|f.search('bb') |
374+
| 'aa,bb cc' | f.search('aa')|f.search('bb +cc') |
375+
+-------------+-----------------------------------+
361376
362-
:param query: `list` of query predicates
363-
:param search_term: `str` of search term
377+
:param Q: TinyDB search object
378+
:param s: `str` of q parameter value
364379
365-
:returns: `list` of updated query predicates
380+
:returns: `Query` object or `None`
366381
"""
367382

368-
return query
383+
return search_object
369384

370385
def __repr__(self):
371386
return f'<TinyDBProvider> {self.data}'
@@ -402,7 +417,7 @@ def _add_extra_fields(self, json_data: dict) -> dict:
402417

403418
return json_data
404419

405-
def _prepare_q_param_with_spaces(self, s: str) -> str:
420+
def _prepare_q_param_with_spaces(self, Q: Query, s: str) -> str:
406421
"""
407422
Prepare a search statement for the search term `s`.
408423
The term `s` might have spaces.
@@ -415,12 +430,18 @@ def _prepare_q_param_with_spaces(self, s: str) -> str:
415430
| 'aa bb' | f.search('aa +bb') |
416431
| ' aa bb ' | f.search('aa +bb') |
417432
+---------------+--------------------+
433+
434+
:param Q: TinyDB `Query` object
435+
:param s: `str` of q parameter value
436+
437+
:returns: `Query` object
418438
"""
419-
return 'Q.properties["_metadata-anytext"].search("' \
420-
+ ' +'.join(s.split()) \
421-
+ '", flags=re.IGNORECASE)'
422439

423-
def _add_search_query(self, query: list, search_term: str = None) -> str:
440+
return Q.properties["_metadata-anytext"].search(
441+
' +'.join(s.split()), flags=re.IGNORECASE)
442+
443+
def _add_search_query(self, search_object,
444+
search_term: str = None) -> Union[str, None]:
424445
"""
425446
Create a search query according to the OGC API - Records specification.
426447
@@ -434,15 +455,22 @@ def _add_search_query(self, query: list, search_term: str = None) -> str:
434455
| 'aa,bb' | f.search('aa')|f.search('bb') |
435456
| 'aa,bb cc' | f.search('aa')|f.search('bb +cc') |
436457
+-------------+-----------------------------------+
458+
459+
:param Q: TinyDB search object
460+
:param s: `str` of q parameter value
461+
462+
:returns: `Query` object or `None`
437463
"""
464+
438465
if search_term is not None and len(search_term) > 0:
439466
LOGGER.debug('catalogue q= query')
440467
terms = [s for s in search_term.split(',') if len(s) > 0]
441-
query.append('|'.join(
442-
[self._prepare_q_param_with_spaces(t) for t in terms]
443-
))
468+
terms2 = [self._prepare_q_param_with_spaces(search_object, t)
469+
for t in terms]
444470

445-
return query
471+
return reduce(operator.or_, terms2)
472+
else:
473+
return None
446474

447475
def __repr__(self):
448476
return f'<TinyDBCatalogueProvider> {self.data}'

0 commit comments

Comments
 (0)