1#!/usr/bin/env python 2 3""" 4URI Template (RFC6570) Processor 5""" 6 7__copyright__ = """\ 8Copyright 2011-2013 Joe Gregorio 9 10Licensed under the Apache License, Version 2.0 (the "License"); 11you may not use this file except in compliance with the License. 12You may obtain a copy of the License at 13 14 http://www.apache.org/licenses/LICENSE-2.0 15 16Unless required by applicable law or agreed to in writing, software 17distributed under the License is distributed on an "AS IS" BASIS, 18WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19See the License for the specific language governing permissions and 20limitations under the License. 21""" 22 23import re 24try: 25 from urllib.parse import quote 26except ImportError: 27 from urllib import quote 28 29 30 31__version__ = "0.6" 32 33RESERVED = ":/?#[]@!$&'()*+,;=" 34OPERATOR = "+#./;?&|!@" 35MODIFIER = ":^" 36TEMPLATE = re.compile(r"{([^\}]+)}") 37 38 39def variables(template): 40 '''Returns the set of keywords in a uri template''' 41 vars = set() 42 for varlist in TEMPLATE.findall(template): 43 if varlist[0] in OPERATOR: 44 varlist = varlist[1:] 45 varspecs = varlist.split(',') 46 for var in varspecs: 47 # handle prefix values 48 var = var.split(':')[0] 49 # handle composite values 50 if var.endswith('*'): 51 var = var[:-1] 52 vars.add(var) 53 return vars 54 55 56def _quote(value, safe, prefix=None): 57 if prefix is not None: 58 return quote(str(value)[:prefix], safe) 59 return quote(str(value), safe) 60 61 62def _tostring(varname, value, explode, prefix, operator, safe=""): 63 if isinstance(value, list): 64 return ",".join([_quote(x, safe) for x in value]) 65 if isinstance(value, dict): 66 keys = sorted(value.keys()) 67 if explode: 68 return ",".join([_quote(key, safe) + "=" + \ 69 _quote(value[key], safe) for key in keys]) 70 else: 71 return ",".join([_quote(key, safe) + "," + \ 72 _quote(value[key], safe) for key in keys]) 73 elif value is None: 74 return 75 else: 76 return _quote(value, safe, prefix) 77 78 79def _tostring_path(varname, value, explode, prefix, operator, safe=""): 80 joiner = operator 81 if isinstance(value, list): 82 if explode: 83 out = [_quote(x, safe) for x in value if value is not None] 84 else: 85 joiner = "," 86 out = [_quote(x, safe) for x in value if value is not None] 87 if out: 88 return joiner.join(out) 89 else: 90 return 91 elif isinstance(value, dict): 92 keys = sorted(value.keys()) 93 if explode: 94 out = [_quote(key, safe) + "=" + \ 95 _quote(value[key], safe) for key in keys \ 96 if value[key] is not None] 97 else: 98 joiner = "," 99 out = [_quote(key, safe) + "," + \ 100 _quote(value[key], safe) \ 101 for key in keys if value[key] is not None] 102 if out: 103 return joiner.join(out) 104 else: 105 return 106 elif value is None: 107 return 108 else: 109 return _quote(value, safe, prefix) 110 111 112def _tostring_semi(varname, value, explode, prefix, operator, safe=""): 113 joiner = operator 114 if operator == "?": 115 joiner = "&" 116 if isinstance(value, list): 117 if explode: 118 out = [varname + "=" + _quote(x, safe) \ 119 for x in value if x is not None] 120 if out: 121 return joiner.join(out) 122 else: 123 return 124 else: 125 return varname + "=" + ",".join([_quote(x, safe) \ 126 for x in value]) 127 elif isinstance(value, dict): 128 keys = sorted(value.keys()) 129 if explode: 130 return joiner.join([_quote(key, safe) + "=" + \ 131 _quote(value[key], safe) \ 132 for key in keys if key is not None]) 133 else: 134 return varname + "=" + ",".join([_quote(key, safe) + "," + \ 135 _quote(value[key], safe) for key in keys \ 136 if key is not None]) 137 else: 138 if value is None: 139 return 140 elif value: 141 return (varname + "=" + _quote(value, safe, prefix)) 142 else: 143 return varname 144 145 146def _tostring_query(varname, value, explode, prefix, operator, safe=""): 147 joiner = operator 148 if operator in ["?", "&"]: 149 joiner = "&" 150 if isinstance(value, list): 151 if 0 == len(value): 152 return None 153 if explode: 154 return joiner.join([varname + "=" + _quote(x, safe) \ 155 for x in value]) 156 else: 157 return (varname + "=" + ",".join([_quote(x, safe) \ 158 for x in value])) 159 elif isinstance(value, dict): 160 if 0 == len(value): 161 return None 162 keys = sorted(value.keys()) 163 if explode: 164 return joiner.join([_quote(key, safe) + "=" + \ 165 _quote(value[key], safe) \ 166 for key in keys]) 167 else: 168 return varname + "=" + \ 169 ",".join([_quote(key, safe) + "," + \ 170 _quote(value[key], safe) for key in keys]) 171 else: 172 if value is None: 173 return 174 elif value: 175 return (varname + "=" + _quote(value, safe, prefix)) 176 else: 177 return (varname + "=") 178 179 180TOSTRING = { 181 "" : _tostring, 182 "+": _tostring, 183 "#": _tostring, 184 ";": _tostring_semi, 185 "?": _tostring_query, 186 "&": _tostring_query, 187 "/": _tostring_path, 188 ".": _tostring_path, 189 } 190 191 192def expand(template, variables): 193 """ 194 Expand template as a URI Template using variables. 195 """ 196 def _sub(match): 197 expression = match.group(1) 198 operator = "" 199 if expression[0] in OPERATOR: 200 operator = expression[0] 201 varlist = expression[1:] 202 else: 203 varlist = expression 204 205 safe = "" 206 if operator in ["+", "#"]: 207 safe = RESERVED 208 varspecs = varlist.split(",") 209 varnames = [] 210 defaults = {} 211 for varspec in varspecs: 212 default = None 213 explode = False 214 prefix = None 215 if "=" in varspec: 216 varname, default = tuple(varspec.split("=", 1)) 217 else: 218 varname = varspec 219 if varname[-1] == "*": 220 explode = True 221 varname = varname[:-1] 222 elif ":" in varname: 223 try: 224 prefix = int(varname[varname.index(":")+1:]) 225 except ValueError: 226 raise ValueError("non-integer prefix '{0}'".format( 227 varname[varname.index(":")+1:])) 228 varname = varname[:varname.index(":")] 229 if default: 230 defaults[varname] = default 231 varnames.append((varname, explode, prefix)) 232 233 retval = [] 234 joiner = operator 235 start = operator 236 if operator == "+": 237 start = "" 238 joiner = "," 239 if operator == "#": 240 joiner = "," 241 if operator == "?": 242 joiner = "&" 243 if operator == "&": 244 start = "&" 245 if operator == "": 246 joiner = "," 247 for varname, explode, prefix in varnames: 248 if varname in variables: 249 value = variables[varname] 250 if not value and value != "" and varname in defaults: 251 value = defaults[varname] 252 elif varname in defaults: 253 value = defaults[varname] 254 else: 255 continue 256 expanded = TOSTRING[operator]( 257 varname, value, explode, prefix, operator, safe=safe) 258 if expanded is not None: 259 retval.append(expanded) 260 if len(retval) > 0: 261 return start + joiner.join(retval) 262 else: 263 return "" 264 265 return TEMPLATE.sub(_sub, template) 266