Skip to content

Commit 4eb97ea

Browse files
committed
stab at getting query builder to get query construction correct
1 parent 64cc0af commit 4eb97ea

File tree

3 files changed

+129
-22
lines changed

3 files changed

+129
-22
lines changed

pyral/query_builder.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,22 @@ def _encode(condition):
197197
##
198198
## print("RallyQueryFormatter parts: %s" % repr(parts))
199199
##
200-
201200
# if no CONJUNCTION is in parts, use the condition as is (simple case)
202201
conjunctions = [p for p in parts if p in RallyQueryFormatter.CONJUNCTIONS]
203202
if not conjunctions:
203+
if mo := re.search(r'^(\w+)\s+(!?in)\s+(.+)$', criteria, flags=re.I):
204+
attr_name, cond, values = mo.group(1), mo.group(2), mo.group(3)
205+
# Rally WSAPI supports attr_name in value1,value2,... directly but not so with !in
206+
if cond.lower() == '!in': # we must construct an OR'ed express with != for each listed value
207+
# Rally WSAPI supports attr_name in value1,value2,... directly but not so with !in
208+
criteria = RallyQueryFormatter.constructORedExpression(attr_name, cond, values)
209+
elif mo := re.search(r'^(\w+) (!?between)\s+(.+)\s+and\s+(.+)$', criteria, flags=re.I):
210+
attr_name, cond, lesser, greater = mo.group(1), mo.group(2), mo.group(3), mo.group(4)
211+
rlns = ['>=', '<='] if cond.lower() == 'between' else ['<', '>']
212+
rln_op = 'AND' if cond.lower() == 'between' else 'OR'
213+
lcond = '%s %s %s' % (attr_name, rlns[0], lesser)
214+
gcond = '%s %s %s' % (attr_name, rlns[1], greater)
215+
criteria = "(%s) %s (%s)" % (lcond, rln_op, gcond)
204216
expression = quote(criteria.strip()).replace('%28', '(').replace('%29', ')')
205217
##
206218
## print("RallyQueryFormatter.no_conjunctions: |%s|" % expression)
@@ -218,13 +230,12 @@ def _encode(condition):
218230
cond = quote(item)
219231
binary_expression = "(%s) %s" % (cond, binary_expression)
220232

221-
final_expression = binary_expression.replace('%28', '(')
222-
final_expression = final_expression.replace('%29', ')')
233+
encoded_parened_expression = binary_expression.replace('%28', '(').replace('%29', ')')
223234
##
224-
## print("RallyQueryFormatter.final_expression: |%s|" % final_expression)
225-
## print("==============================================")
235+
## print("RallyQueryFormatter.encoded_parened_expression: |{0}|".format(encoded_parened_expression))
236+
## print("=============================================================")
226237
##
227-
final_expression = final_expression.replace(' ', '%20')
238+
final_expression = encoded_parened_expression.replace(' ', '%20')
228239
return final_expression
229240

230241
@staticmethod
@@ -235,11 +246,37 @@ def validatePartsSyntax(parts):
235246
criteria_pattern = re.compile(r'^(%s) (%s) (%s)$' % (attr_ident, relationship, attr_value))
236247
quoted_value_pattern = re.compile(r'^(%s) (%s) ("[^"]+")$' % (attr_ident, relationship))
237248
unquoted_value_pattern = re.compile(r'^(%s) (%s) ([^"].+[^"])$' % (attr_ident, relationship))
249+
subset_pattern = re.compile(r'(%s) (in|!in) (%s)' % (attr_ident, attr_value), flags=re.I)
250+
range_pattern = re.compile(r'(%s) (between|!between) (%s) and (%s)' % (attr_ident, attr_value, attr_value), flags=re.I)
238251

239252
valid_parts = []
240253
front = ""
241254
while parts:
242255
part = "%s%s" % (front, parts.pop(0))
256+
mo = subset_pattern.match(part)
257+
if mo:
258+
rln = '!=' if mo.group(1).startswith('!') else '='
259+
attr_name, values = mo.group(0), mo.group(2)
260+
# if rln == '=':
261+
# binary_expr = constructORedExpression(attr_name, rln, values)
262+
# else:
263+
# binary_expr = constructANDedExpression(attr_name, rln, values)
264+
valid_parts.append(binary_expr)
265+
continue
266+
mo = range_pattern.match(part)
267+
if mo:
268+
attr_name, cond = mo.group(0), mo.group(1)
269+
lesser_val, greater_val = mo.group(2), mo.group(3)
270+
rlns = ['<', '>'] if cond.startswith('!') else ['>=', '<=']
271+
# in range
272+
if cond.lower() == 'between':
273+
binary_expr = '((%s %s %s) AND (%s %s %s))' % \
274+
(attr_name, rlns[0], lesser_val, attr_name, rlns[1], greater_val)
275+
else:
276+
binary_expr = '((%s %s %s) OR (%s %s %s))' % \
277+
(attr_name, rlns[0], lesser_val, attr_name, rlns[1], greater_val)
278+
valid_parts.append(binary_expr)
279+
continue
243280
mo = criteria_pattern.match(part)
244281
if mo:
245282
valid_parts.append(part)
@@ -260,4 +297,44 @@ def validatePartsSyntax(parts):
260297

261298
return valid_parts
262299

300+
# subset and range related ops for building queries
301+
302+
@staticmethod
303+
def constructORedExpression(field, relation, values):
304+
"""
305+
intended for use when a subset operator (in or !in) is in play
306+
State in Defined, Accepted, Relased
307+
needs an ORed expression ((f = D OR f = A) OR ((f = R)))
308+
State !in Working, Fixed, Testing
309+
needs an ANDed expression ((f != W AND f != F) AND ((f != T)))
310+
"""
311+
operator = "="
312+
if relation == '!in':
313+
operator = "!="
314+
if len(values) == 1:
315+
return f'({field} {operator} "{values[0]})"'
316+
binary_expression = f'(({field} {operator} "{values[0]}") OR ({field} {operator} "{values[1]}"))'
317+
for value in values[2:]:
318+
binary_expression = f'({binary_expression} OR ({field} {operator} "{value}"))'
319+
return binary_expression
320+
321+
@staticmethod
322+
def constructANDedExpression(field, relation, values):
323+
"""
324+
intended for use when a range operator (between or !between) is in play
325+
DevPhase between 2021-05-23 and 2021-07-09
326+
needs a single ANDed expression ((dp >= d1) AND (dp <= d1)))
327+
DevPhase !between 2021-12-19 and 2022-01-03
328+
needs a single ORed expression ((dp < d1) OR (dp > d1)))
329+
"""
330+
operator = "="
331+
if relation == '!in':
332+
operator = "!="
333+
if len(values) == 1:
334+
return f'({field} {operator} "{values[0]})"'
335+
binary_expression = f'(({field} {operator} "{values[0]}") AND ({field} {operator} "{values[1]}"))'
336+
for value in values[2:]:
337+
binary_expression = f'({binary_expression} AND ({field} {operator} "{value}"))'
338+
return binary_expression
339+
263340
##################################################################################################

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
desc_file = path.join(path.abspath(path.dirname(__file__)), FULL_DESCRIPTION)
2222
with open(desc_file, encoding='utf-8') as df: long_description = df.read()
2323

24-
MINIMUM_REQUESTS_VERSION = '2.12.5' # although 2.22.x is recommended
24+
MINIMUM_REQUESTS_VERSION = '2.25.1' # although 2.25.x is recommended
2525
REQUIRES = ['six',
2626
'requests>=%s' % MINIMUM_REQUESTS_VERSION
2727
]
@@ -33,10 +33,9 @@
3333
'License :: OSI Approved :: BSD License',
3434
'Operating System :: OS Independent',
3535
'Programming Language :: Python',
36-
'Programming Language :: Python :: 3.5',
37-
'Programming Language :: Python :: 3.6',
3836
'Programming Language :: Python :: 3.7',
3937
'Programming Language :: Python :: 3.8',
38+
'Programming Language :: Python :: 3.9',
4039
'Topic :: Internet :: WWW/HTTP',
4140
'Topic :: Software Development :: Libraries',
4241
]

test/test_query.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from rally_targets import DEFAULT_WORKSPACE, DEFAULT_PROJECT, NON_DEFAULT_PROJECT
2323
from rally_targets import BOONDOCKS_WORKSPACE, BOONDOCKS_PROJECT
2424
from rally_targets import PROJECT_SCOPING_TREE
25+
TLP_DICT = PROJECT_SCOPING_TREE['TOP_LEVEL_PROJECT']
26+
TLP_DICT_keys = [key for key in TLP_DICT.keys()]
27+
COLD_PROJECT = TLP_DICT_keys[0]
2528

2629
##################################################################################################
2730

@@ -535,29 +538,57 @@ def test_query_in_subset_operator():
535538
Query for State in the subset of {'Defined', 'In-Progress'}
536539
"""
537540
rally = Rally(server=RALLY, user=RALLY_USER, password=RALLY_PSWD)
538-
qualifier = "State in Defined, In-Progress"
541+
#qualifier = '(ScheduleState In "Defined,In-Progress")' # works cuz expression is parenned
542+
#qualifier = 'ScheduleState In "Defined,In-Progress"' # works when subset is quoted
543+
qualifier = 'ScheduleState In Defined,In-Progress' # works when no space after comma
539544
response = rally.get('Defect', fetch=True, query=qualifier, pagesize=100, limit=100)
540545
print(response.status_code)
541546
assert response.status_code == 200
542-
print(response.errors)
543-
print(response.warnings)
547+
assert len(response.errors) == 0
548+
assert len(response.warnings) == 0
544549

545550
items = [item for item in response]
546551

547552
assert len(items) > 10
548-
defined = [item for item in items if item.ScheduleState == 'Defined']
549-
inprog = [item for item in items if item.ScheduleState == 'In-Progress']
550-
cmplted = [item for item in items if item.ScheduleState == 'Completed']
551-
accpted = [item for item in items if item.ScheduleState == 'Accepted']
553+
defined = [item for item in items if item.ScheduleState == 'Defined']
554+
inprog = [item for item in items if item.ScheduleState == 'In-Progress']
555+
completed = [item for item in items if item.ScheduleState == 'Completed']
556+
accepted = [item for item in items if item.ScheduleState == 'Accepted']
552557
assert len(defined) > 0
553558
assert len(inprog) > 0
554-
assert len(cmplted) == 0
555-
assert len(accpted) == 0
559+
assert len(completed) == 0
560+
assert len(accepted) == 0
561+
562+
def test_query_not_in_subset_operator():
563+
"""
564+
Query for Priority not in the subset of {'Trivial', 'Low'}
565+
"""
566+
rally = Rally(server=RALLY, user=RALLY_USER, password=RALLY_PSWD,
567+
project=COLD_PROJECT)
568+
qualifier = 'ScheduleState !in Defined,Completed'
569+
#qualifier = '((ScheduleState != Defined) AND (ScheduleState != Completed))'
570+
response = rally.get('Story', fetch=True, query=qualifier, pagesize=100, limit=100, projectScopeDown=True)
571+
assert response.status_code == 200
572+
assert len(response.errors) == 0
573+
assert len(response.warnings) == 0
574+
575+
items = [item for item in response]
576+
sched_states = [item.ScheduleState for item in items]
577+
ss = list(set(sorted(sched_states)))
578+
assert len(ss) == 2
579+
580+
defined = [item for item in items if item.ScheduleState == 'Defined']
581+
inprog = [item for item in items if item.ScheduleState == 'In-Progress']
582+
completed = [item for item in items if item.ScheduleState == 'Completed']
583+
accepted = [item for item in items if item.ScheduleState == 'Accepted']
584+
585+
assert len(defined) == 0
586+
assert len(inprog) > 0
587+
assert len(completed) == 0
588+
assert len(accepted) > 0
589+
590+
556591

557-
#def test_query_not_in_subset_operator():
558-
# """
559-
# Query for Priority not in the subset of {'Trivial', 'Low'}
560-
# """
561592
#
562593
# assert result of query has no items with Trivial or Low and has some items with different Priority values
563594
#

0 commit comments

Comments
 (0)