2828# =================================================================
2929
3030import logging
31- import re # noqa
31+ from functools import reduce
32+ import operator
3233import os
34+ import re
35+ from typing import Union
3336import uuid
3437
3538from 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