def parse_query_string(self, query_string): # pylint: disable=too-many-locals """ Function that parse the querystring, extracting infos for limit, offset, ordering, filters, attribute and extra projections. :param query_string (as obtained from request.query_string) :return: parsed values for the querykeys """ from pyparsing import Word, alphas, nums, alphanums, printables, \ ZeroOrMore, OneOrMore, Suppress, Optional, Literal, Group, \ QuotedString, Combine, \ StringStart as SS, StringEnd as SE, \ WordEnd as WE, \ ParseException from pyparsing import pyparsing_common as ppc from dateutil import parser as dtparser from psycopg2.tz import FixedOffsetTimezone ## Define grammar # key types key = Word(f'{alphas}_', f'{alphanums}_') # operators operator = (Literal('=like=') | Literal('=ilike=') | Literal('=in=') | Literal('=notin=') | Literal('=') | Literal('!=') | Literal('>=') | Literal('>') | Literal('<=') | Literal('<')) # Value types value_num = ppc.number value_bool = ( Literal('true') | Literal('false')).addParseAction(lambda toks: bool(toks[0])) value_string = QuotedString('"', escQuote='""') value_orderby = Combine(Optional(Word('+-', exact=1)) + key) ## DateTimeShift value. First, compose the atomic values and then # combine # them and convert them to datetime objects # Date value_date = Combine( Word(nums, exact=4) + Literal('-') + Word(nums, exact=2) + Literal('-') + Word(nums, exact=2)) # Time value_time = Combine( Literal('T') + Word(nums, exact=2) + Optional(Literal(':') + Word(nums, exact=2)) + Optional(Literal(':') + Word(nums, exact=2))) # Shift value_shift = Combine( Word('+-', exact=1) + Word(nums, exact=2) + Optional(Literal(':') + Word(nums, exact=2))) # Combine atomic values value_datetime = Combine( value_date + Optional(value_time) + Optional(value_shift) + WE(printables.replace('&', '')) # To us the # word must end with '&' or end of the string # Adding WordEnd only here is very important. This makes atomic # values for date, time and shift not really # usable alone individually. ) ######################################################################## def validate_time(toks): """ Function to convert datetime string into datetime object. The format is compliant with ParseAction requirements :param toks: datetime string passed in tokens :return: datetime object """ datetime_string = toks[0] # Check the precision precision = len(datetime_string.replace('T', ':').split(':')) # Parse try: dtobj = dtparser.parse(datetime_string) except ValueError: raise RestInputValidationError( 'time value has wrong format. The ' 'right format is ' '<date>T<time><offset>, ' 'where <date> is expressed as ' '[YYYY]-[MM]-[DD], ' '<time> is expressed as [HH]:[MM]:[' 'SS], ' '<offset> is expressed as +/-[HH]:[' 'MM] ' 'given with ' 'respect to UTC') if dtobj.tzinfo is not None and dtobj.utcoffset() is not None: tzoffset_minutes = int(dtobj.utcoffset().total_seconds() // 60) return DatetimePrecision( dtobj.replace(tzinfo=FixedOffsetTimezone( offset=tzoffset_minutes, name=None)), precision) return DatetimePrecision( dtobj.replace(tzinfo=FixedOffsetTimezone(offset=0, name=None)), precision) ######################################################################## # Convert datetime value to datetime object value_datetime.setParseAction(validate_time) # More General types value = (value_string | value_bool | value_datetime | value_num | value_orderby) # List of values (I do not check the homogeneity of the types of values, # query builder will do it somehow) value_list = Group(value + OneOrMore(Suppress(',') + value) + Optional(Suppress(','))) # Fields single_field = Group(key + operator + value) list_field = Group(key + (Literal('=in=') | Literal('=notin=')) + value_list) orderby_field = Group(key + Literal('=') + value_list) field = (list_field | orderby_field | single_field) # Fields separator separator = Suppress(Literal('&')) # General query string general_grammar = SS() + Optional(field) + ZeroOrMore( separator + field) + \ Optional(separator) + SE() ## Parse the query string try: fields = general_grammar.parseString(query_string) # JQuery adds _=timestamp a parameter to not use cached data/response. # To handle query, remove this "_" parameter from the query string # For more details check issue #789 # (https://github.com/aiidateam/aiida-core/issues/789) in aiida-core field_list = [ entry for entry in fields.asList() if entry[0] != '_' ] except ParseException as err: raise RestInputValidationError( 'The query string format is invalid. ' "Parser returned this massage: \"{" "}.\" Please notice that the column " 'number ' 'is counted from ' 'the first character of the query ' 'string.'.format(err)) ## return the translator instructions elaborated from the field_list return self.build_translator_parameters(field_list)
def parse_query_string(query_string): """ Function that parse the querystring, extracting infos for limit, offset, ordering, filters, attribute and extra projections. :param query_string (as obtained from request.query_string) :return: parsed values for the querykeys """ from pyparsing import Word, alphas, nums, alphanums, printables, \ ZeroOrMore, OneOrMore, Suppress, Optional, Literal, Group, \ QuotedString, Combine, \ StringStart as SS, StringEnd as SE, \ WordStart as WS, WordEnd as WE, \ ParseException from pyparsing import pyparsing_common as ppc from dateutil import parser as dtparser from psycopg2.tz import FixedOffsetTimezone ## Define grammar # key types key = Word(alphas + '_', alphanums + '_') # operators operator = (Literal('=like=') | Literal('=ilike=') | Literal('=in=') | Literal('=notin=') | Literal('=') | Literal('!=') | Literal('>=') | Literal('>') | Literal('<=') | Literal('<')) # Value types valueNum = ppc.number valueBool = (Literal('true') | Literal('false')).addParseAction(lambda toks: bool(toks[0])) valueString = QuotedString('"', escQuote='""') valueOrderby = Combine(Optional(Word('+-', exact=1)) + key) ## DateTimeShift value. First, compose the atomic values and then combine # them and convert them to datetime objects # Date valueDate = Combine( Word(nums, exact=4) + Literal('-') + Word(nums, exact=2) + Literal('-') + Word(nums, exact=2)) # Time valueTime = Combine( Literal('T') + Word(nums, exact=2) + Optional(Literal(':') + Word(nums, exact=2)) + Optional(Literal(':') + Word(nums, exact=2))) # Shift valueShift = Combine( Word('+-', exact=1) + Word(nums, exact=2) + Optional(Literal(':') + Word(nums, exact=2))) # Combine atomic values valueDateTime = Combine( valueDate + Optional(valueTime) + Optional(valueShift) + WE(printables.translate(None, '&')) # To us the # word must end with '&' or end of the string # Adding WordEnd only here is very important. This makes atomic # values for date, time and shift not really # usable alone individually. ) ############################################################################ # Function to convert datetime string into datetime object. The format is # compliant with ParseAction requirements def validate_time(s, loc, toks): datetime_string = toks[0] # Check the precision precision = len(datetime_string.replace('T', ':').split(':')) # Parse try: dt = dtparser.parse(datetime_string) except ValueError: raise RestInputValidationError("time value has wrong format. The " "right format is " "<date>T<time><offset>, " "where <date> is expressed as " "[YYYY]-[MM]-[DD], " "<time> is expressed as [HH]:[MM]:[" "SS], " "<offset> is expressed as +/-[HH]:[" "MM] " "given with " "respect to UTC") if dt.tzinfo is not None: tzoffset_minutes = int( dt.tzinfo.utcoffset(None).total_seconds() / 60) return datetime_precision( dt.replace(tzinfo=FixedOffsetTimezone(offset=tzoffset_minutes, name=None)), precision) else: return datetime_precision( dt.replace(tzinfo=FixedOffsetTimezone(offset=0, name=None)), precision) ######################################################################## # Convert datetime value to datetime object valueDateTime.setParseAction(validate_time) # More General types value = (valueString | valueBool | valueDateTime | valueNum | valueOrderby) # List of values (I do not check the homogeneity of the types of values, # query builder will do it in a sense) valueList = Group(value + OneOrMore(Suppress(',') + value) + Optional(Suppress(','))) # Fields singleField = Group(key + operator + value) listField = Group(key + (Literal('=in=') | Literal('=notin=')) + valueList) orderbyField = Group(key + Literal('=') + valueList) Field = (listField | orderbyField | singleField) # Fields separator separator = Suppress(Literal('&')) # General query string generalGrammar = SS() + Optional(Field) + ZeroOrMore(separator + Field) + \ Optional(separator) + SE() ## Parse the query string try: fields = generalGrammar.parseString(query_string) field_dict = fields.asDict() field_list = fields.asList() except ParseException as e: raise RestInputValidationError("The query string format is invalid. " "Parser returned this massage: \"{" "}.\" Please notice that the column " "number " "is counted from " "the first character of the query " "string.".format(e)) ## return the translator instructions elaborated from the field_list return build_translator_parameters(field_list)