split_by_op was returning data against which further error checking was then required. This change moves that error handling inside the method and simplifies the regular expression so that it splits (greedily) on the first operator it finds. If the split is not possible, it is a ValueError. If the field or value are empty, that is a ValueError. Both split_by_op and split_by_datatype were doing a findall() where a match() and split() do the right job and more efficiently. Regular expression compilation has been moved to the module level to insure they need only be compiled once. Operator keys must be sorted by length to ensure the point at which the split happens is most greedy. Using a split keeps the regex short and removes any statements about the left and right hand sides of the operator. Tests added to cover the method more completely, including testing for corner cases such as single character field or values or operators showing up in unexpected locations. 'string' variable renamed to 'query' and 'query_value' to avoid confusion. Named parameters on string substitution for clarity. Note that the tests which do self.assertRaises could more explicitly check the exception with self.assertRaisesRegexp but that would break compatibility with Python 2.6. Change-Id: Icd815ff65aba9eae3f76afee3bb33e85d85bea72 Closes-Bug: #1314544
130 lines
3.9 KiB
Python
130 lines
3.9 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import re
|
|
|
|
from six.moves.urllib import parse
|
|
|
|
OP_LOOKUP = {'!=': 'ne',
|
|
'>=': 'ge',
|
|
'<=': 'le',
|
|
'>': 'gt',
|
|
'<': 'lt',
|
|
'=': 'eq'}
|
|
|
|
OP_LOOKUP_KEYS = '|'.join(sorted(OP_LOOKUP.keys(), key=len, reverse=True))
|
|
OP_SPLIT_RE = re.compile(r'(%s)' % OP_LOOKUP_KEYS)
|
|
|
|
DATA_TYPE_RE = re.compile(r'^(string|integer|float|datetime|boolean)(::)(.+)$')
|
|
|
|
|
|
def build_url(path, q, params=None):
|
|
'''This converts from a list of dicts and a list of params to
|
|
what the rest api needs, so from:
|
|
"[{field=this,op=le,value=34},
|
|
{field=that,op=eq,value=foo,type=string}],
|
|
['foo=bar','sna=fu']"
|
|
to:
|
|
"?q.field=this&q.field=that&
|
|
q.op=le&q.op=eq&
|
|
q.type=&q.type=string&
|
|
q.value=34&q.value=foo&
|
|
foo=bar&sna=fu"
|
|
'''
|
|
if q:
|
|
query_params = {'q.field': [],
|
|
'q.value': [],
|
|
'q.op': [],
|
|
'q.type': []}
|
|
|
|
for query in q:
|
|
for name in ['field', 'op', 'value', 'type']:
|
|
query_params['q.%s' % name].append(query.get(name, ''))
|
|
|
|
# Transform the dict to a sequence of two-element tuples in fixed
|
|
# order, then the encoded string will be consistent in Python 2&3.
|
|
new_qparams = sorted(query_params.items(), key=lambda x: x[0])
|
|
path += "?" + parse.urlencode(new_qparams, doseq=True)
|
|
|
|
if params:
|
|
for p in params:
|
|
path += '&%s' % p
|
|
elif params:
|
|
path += '?%s' % params[0]
|
|
for p in params[1:]:
|
|
path += '&%s' % p
|
|
return path
|
|
|
|
|
|
def cli_to_array(cli_query):
|
|
"""This converts from the cli list of queries to what is required
|
|
by the python api.
|
|
so from:
|
|
"this<=34;that=string::foo"
|
|
to
|
|
"[{field=this,op=le,value=34,type=''},
|
|
{field=that,op=eq,value=foo,type=string}]"
|
|
|
|
"""
|
|
|
|
if cli_query is None:
|
|
return None
|
|
|
|
def split_by_op(query):
|
|
"""Split a single query string to field, operator, value.
|
|
"""
|
|
|
|
def _value_error(message):
|
|
raise ValueError('invalid query %(query)s: missing %(message)s' %
|
|
{'query': query, 'message': message})
|
|
|
|
try:
|
|
field, operator, value = OP_SPLIT_RE.split(query, maxsplit=1)
|
|
except ValueError:
|
|
_value_error('operator')
|
|
|
|
if not len(field):
|
|
_value_error('field')
|
|
|
|
if not len(value):
|
|
_value_error('value')
|
|
|
|
return (field, operator, value)
|
|
|
|
def split_by_data_type(query_value):
|
|
frags = DATA_TYPE_RE.match(query_value)
|
|
|
|
# The second match is the separator. Return a list without it if
|
|
# a type identifier was found.
|
|
return frags.group(1, 3) if frags else None
|
|
|
|
opts = []
|
|
queries = cli_query.split(';')
|
|
for q in queries:
|
|
query = split_by_op(q)
|
|
opt = {}
|
|
opt['field'] = query[0]
|
|
opt['op'] = OP_LOOKUP[query[1]]
|
|
|
|
# Allow the data type of the value to be specified via <type>::<value>,
|
|
# where type can be one of integer, string, float, datetime, boolean
|
|
value_frags = split_by_data_type(query[2])
|
|
if not value_frags:
|
|
opt['value'] = query[2]
|
|
opt['type'] = ''
|
|
else:
|
|
opt['type'] = value_frags[0]
|
|
opt['value'] = value_frags[1]
|
|
opts.append(opt)
|
|
return opts
|