|
| 1 | +#!/usr/bin/python |
| 2 | + |
| 3 | +################################################################################################### |
| 4 | +# |
| 5 | +# pyral.query_builder - module to build Rally WSAPI compliant query clause |
| 6 | +# |
| 7 | +################################################################################################### |
| 8 | + |
| 9 | +__version__ = (0, 9, 4) |
| 10 | + |
| 11 | +import re |
| 12 | +import types |
| 13 | +import urllib |
| 14 | + |
| 15 | +################################################################################################### |
| 16 | + |
| 17 | +JSON_FORMAT = ".js" |
| 18 | + |
| 19 | +class RallyUrlBuilder(object): |
| 20 | + """ |
| 21 | + An instance of this class is used to collect information needed to construct a |
| 22 | + valid URL that can be issued in a REST Request to Rally. |
| 23 | + The sequence of use is to obtain a RallyUrlBuilder for a named entity, |
| 24 | + provide qualifying criteria, augments, scoping criteria and any provision |
| 25 | + for a pretty response, and then call build to return the resulting resource URL. |
| 26 | + An instance can be re-used (for the same entity) by simply re-calling the |
| 27 | + specification methods with differing values and then re-calling the build method. |
| 28 | + """ |
| 29 | + parts = ['fetch', 'query', 'order', |
| 30 | + 'workspace', 'project', 'projectScopeUp', 'projectScopeDown', |
| 31 | + 'pagesize', 'start', 'pretty' |
| 32 | + ] |
| 33 | + |
| 34 | + def __init__(self, entity): |
| 35 | + self.entity = entity |
| 36 | + |
| 37 | + def qualify(self, fetch, query, order, pagesize, startIndex): |
| 38 | + self.fetch = fetch |
| 39 | + self.query = query |
| 40 | + self.order = order |
| 41 | + self.pagesize = pagesize |
| 42 | + self.startIndex = startIndex |
| 43 | + self.workspace = None |
| 44 | + self.project = None |
| 45 | + self.scopeUp = None |
| 46 | + self.scopeDown = None |
| 47 | + self.pretty = False |
| 48 | + |
| 49 | + |
| 50 | + def build(self, pretty=None): |
| 51 | + if pretty: |
| 52 | + self.pretty = True |
| 53 | + |
| 54 | + resource = "%s%s?" % (self.entity, JSON_FORMAT) |
| 55 | + |
| 56 | + qualifiers = ['fetch=%s' % self.fetch] |
| 57 | + if self.query: |
| 58 | + #encodedQuery = self._prepQuery(self.query) |
| 59 | + #qualifiers.append('%s=%s' % ('query', encodedQuery if encodedQuery else "")) |
| 60 | + query_string = RallyQueryFormatter.parenGroups(self.query) |
| 61 | +## |
| 62 | +## print "query_string: |query=(%s)|" % query_string |
| 63 | +## |
| 64 | + qualifiers.append("query=(%s)" % query_string) |
| 65 | + if self.order: |
| 66 | + qualifiers.append("order=%s" % urllib.quote(self.order)) |
| 67 | + if self.workspace: |
| 68 | + qualifiers.append(self.workspace) |
| 69 | + if self.project: |
| 70 | + qualifiers.append(self.project) |
| 71 | + if self.scopeUp: |
| 72 | + qualifiers.append(self.scopeUp) |
| 73 | + if self.scopeDown: |
| 74 | + qualifiers.append(self.scopeDown) |
| 75 | + |
| 76 | + qualifiers.append('pagesize=%s' % self.pagesize) |
| 77 | + qualifiers.append('start=%s' % self.startIndex) |
| 78 | + |
| 79 | + if self.pretty: |
| 80 | + qualifiers.append('pretty=true') |
| 81 | + |
| 82 | + resource += "&".join(qualifiers) |
| 83 | +## |
| 84 | +## print "RallyUrlBuilder.build: resource= %s" % resource |
| 85 | +## |
| 86 | + return resource |
| 87 | + |
| 88 | + def augmentWorkspace(self, augments, workspace_ref): |
| 89 | + wksp_augment = [aug for aug in augments if aug.startswith('workspace=')] |
| 90 | + self.workspace = "workspace=%s" % workspace_ref |
| 91 | + if wksp_augment: |
| 92 | + self.workspace = wksp_augment[0] |
| 93 | + |
| 94 | + def augmentProject(self, augments, project_ref): |
| 95 | + proj_augment = [aug for aug in augments if aug.startswith('project=')] |
| 96 | + self.project = "project=%s" % project_ref |
| 97 | + if proj_augment: |
| 98 | + self.project = proj_augment[0] |
| 99 | + |
| 100 | + def augmentScoping(self, augments): |
| 101 | + scopeUp = [aug for aug in augments if aug.startswith('projectScopeUp=')] |
| 102 | + if scopeUp: |
| 103 | + self.scopeUp = scopeUp[0] |
| 104 | + scopeDown = [aug for aug in augments if aug.startswith('projectScopeDown=')] |
| 105 | + if scopeDown: |
| 106 | + self.scopeDown = scopeDown[0] |
| 107 | + |
| 108 | + def beautifyResponse(self): |
| 109 | + self.pretty = True |
| 110 | + |
| 111 | +################################################################################################## |
| 112 | + |
| 113 | +class RallyQueryFormatter(object): |
| 114 | + CONJUNCTIONS = ['and', 'AND', 'or', 'OR'] |
| 115 | + CONJUNCTION_PATT = re.compile('\s+(AND|OR)\s+', re.I | re.M) |
| 116 | + |
| 117 | + @staticmethod |
| 118 | + def parenGroups(criteria): |
| 119 | + """ |
| 120 | + Keep in mind that Rally WSAPI only supports a binary expression of (x) op (y) |
| 121 | + as in "(foo) and (bar)" |
| 122 | + or (foo) and ((bar) and (egg)) |
| 123 | + Note that Rally doesn't handle (x and y and z) directly. |
| 124 | + Look at the criteria to see if there are any parens other than begin and end |
| 125 | + if the only parens are at begin and end, strip them and subject the criteria to our |
| 126 | + clause grouper and binary expression confabulator. |
| 127 | + Otherwise, we'll naively assume the caller knows what they are doing, ie., they are |
| 128 | + aware of the binary expression requirement. |
| 129 | + """ |
| 130 | + def _encode(condition): |
| 131 | + """ |
| 132 | + if cond has pattern of 'thing relation value', then urllib.quote it and return it |
| 133 | + if cond has pattern of '(thing relation value)', then urllib.quote content inside parens |
| 134 | + then pass that result enclosed in parens back to the caller |
| 135 | + """ |
| 136 | + first_last = "%s%s" % (condition[0], condition[-1]) |
| 137 | + if first_last == "()": |
| 138 | + url_encoded = urllib.quote(condition) |
| 139 | + else: |
| 140 | + url_encoded = '(%s)' % urllib.quote(condition) |
| 141 | + |
| 142 | + # replace the %xx encodings for '=', '(', ')', '!', and double quote characters |
| 143 | + readable_encoded = url_encoded.replace("%3D", '=') |
| 144 | + readable_encoded = readable_encoded.replace("%22", '"') |
| 145 | + readable_encoded = readable_encoded.replace("%28", '(') |
| 146 | + readable_encoded = readable_encoded.replace("%29", ')') |
| 147 | + readable_encoded = readable_encoded.replace("%21", '!') |
| 148 | + return readable_encoded |
| 149 | +## |
| 150 | +## print "RQF.parenGroups criteria parm: |%s|" % repr(criteria) |
| 151 | +## |
| 152 | + |
| 153 | + if type(criteria) in [types.ListType, types.TupleType]: |
| 154 | + # by fiat (and until requested by a paying customer), we assume the criteria expressions are AND'ed |
| 155 | + #conditions = [_encode(expression) for expression in criteria] |
| 156 | + conditions = [expression for expression in criteria] |
| 157 | + criteria = " AND ".join(conditions) |
| 158 | +## |
| 159 | +## print "RallyQueryFormatter: criteria is sequence type resulting in |%s|" % criteria |
| 160 | +## |
| 161 | + |
| 162 | + if type(criteria) == types.DictType: |
| 163 | + expressions = [] |
| 164 | + for field, value in criteria.items(): |
| 165 | + # have to enclose string value in double quotes, otherwise turn whatever the value is into a string |
| 166 | + tval = '"%s"' % value if type(value) == types.StringType else '%s' % value |
| 167 | + expression = ('%s = %s' % (field, tval)).replace(' ', '%20') |
| 168 | + if len(criteria) == 1: |
| 169 | + return expression |
| 170 | + expressions.append(expression) |
| 171 | + criteria = " AND ".join(expressions) |
| 172 | + |
| 173 | + # if the caller has a simple query in the form "(something = a_value)" |
| 174 | + # then return the query as is (after stripping off the surrounding parens) |
| 175 | + if criteria.count('(') == 1 and criteria.count(')') == 1 and \ |
| 176 | + criteria.strip()[0] == '(' and criteria.strip()[-1] == ')': |
| 177 | + return criteria.strip()[1:-1].replace(' ', '%20') |
| 178 | + |
| 179 | + # if caller has more than one opening paren, summarily return the query |
| 180 | + # essentially untouched (except for encoding space chars). |
| 181 | + # The assumption is that the caller has correctly done the parenthisized grouping |
| 182 | + # to end up in a binary form |
| 183 | + if criteria.count('(') > 1: |
| 184 | + return criteria.strip().replace(' ', '%20') |
| 185 | + |
| 186 | + parts = RallyQueryFormatter.CONJUNCTION_PATT.split(criteria.strip()) |
| 187 | +## |
| 188 | +## print "RQF parts: %s" % repr(parts) |
| 189 | +## |
| 190 | + |
| 191 | + # if no CONJUNCTION is in parts, use the condition as is (simple case) |
| 192 | + conjunctions = [p for p in parts if p in RallyQueryFormatter.CONJUNCTIONS] |
| 193 | + if not conjunctions: |
| 194 | + expression = criteria.strip().replace(' ', '%20') |
| 195 | + #expression = urllib.quote(criteria.strip()).replace('%28', '(').replace('%29', ')') |
| 196 | +## |
| 197 | +## print "RQF.no_conjunctions: |%s|" % expression |
| 198 | +## |
| 199 | + return expression |
| 200 | + |
| 201 | + binary_expression = parts.pop() |
| 202 | + while parts: |
| 203 | + item = parts.pop() |
| 204 | + if item in RallyQueryFormatter.CONJUNCTIONS: |
| 205 | + conj = item |
| 206 | + binary_expression = "%s (%s)" % (conj, binary_expression) |
| 207 | + #binary_expression = "%s%%20(%s)" % (conj, binary_expression) |
| 208 | + else: |
| 209 | + cond = item |
| 210 | + binary_expression = "(%s) %s" % (cond, binary_expression) |
| 211 | + #binary_expression = "%s%%20(%s)" % (conj, binary_expression) |
| 212 | + |
| 213 | + #final_expression = urllib.quote(binary_expression).replace('%28', '(').replace('%29', ')') |
| 214 | + final_expression = binary_expression.replace('%28', '(') |
| 215 | + final_expression = final_expression.replace('%29', ')') |
| 216 | +## |
| 217 | +## print "RQF.final_expression: |%s|" % final_expression |
| 218 | +## print "==============================================" |
| 219 | +## |
| 220 | + final_expression = final_expression.replace(' ', '%20') |
| 221 | + return final_expression |
| 222 | + |
| 223 | +################################################################################################## |
0 commit comments