Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/api_core/path_template.py: 16%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

107 statements  

1# Copyright 2017 Google LLC 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Expand and validate URL path templates. 

16 

17This module provides the :func:`expand` and :func:`validate` functions for 

18interacting with Google-style URL `path templates`_ which are commonly used 

19in Google APIs for `resource names`_. 

20 

21.. _path templates: https://github.com/googleapis/googleapis/blob 

22 /57e2d376ac7ef48681554204a3ba78a414f2c533/google/api/http.proto#L212 

23.. _resource names: https://cloud.google.com/apis/design/resource_names 

24""" 

25 

26from __future__ import unicode_literals 

27 

28from collections import deque 

29import copy 

30import functools 

31import re 

32 

33# Regular expression for extracting variable parts from a path template. 

34# The variables can be expressed as: 

35# 

36# - "*": a single-segment positional variable, for example: "books/*" 

37# - "**": a multi-segment positional variable, for example: "shelf/**/book/*" 

38# - "{name}": a single-segment wildcard named variable, for example 

39# "books/{name}" 

40# - "{name=*}: same as above. 

41# - "{name=**}": a multi-segment wildcard named variable, for example 

42# "shelf/{name=**}" 

43# - "{name=/path/*/**}": a multi-segment named variable with a sub-template. 

44_VARIABLE_RE = re.compile( 

45 r""" 

46 ( # Capture the entire variable expression 

47 (?P<positional>\*\*?) # Match & capture * and ** positional variables. 

48 | 

49 # Match & capture named variables {name} 

50 { 

51 (?P<name>[^/]+?) 

52 # Optionally match and capture the named variable's template. 

53 (?:=(?P<template>.+?))? 

54 } 

55 ) 

56 """, 

57 re.VERBOSE, 

58) 

59 

60# Segment expressions used for validating paths against a template. 

61_SINGLE_SEGMENT_PATTERN = r"([^/]+)" 

62_MULTI_SEGMENT_PATTERN = r"(.+)" 

63 

64 

65def _expand_variable_match(positional_vars, named_vars, match): 

66 """Expand a matched variable with its value. 

67 

68 Args: 

69 positional_vars (list): A list of positional variables. This list will 

70 be modified. 

71 named_vars (dict): A dictionary of named variables. 

72 match (re.Match): A regular expression match. 

73 

74 Returns: 

75 str: The expanded variable to replace the match. 

76 

77 Raises: 

78 ValueError: If a positional or named variable is required by the 

79 template but not specified or if an unexpected template expression 

80 is encountered. 

81 """ 

82 positional = match.group("positional") 

83 name = match.group("name") 

84 if name is not None: 

85 try: 

86 return str(named_vars[name]) 

87 except KeyError: 

88 raise ValueError( 

89 "Named variable '{}' not specified and needed by template " 

90 "`{}` at position {}".format(name, match.string, match.start()) 

91 ) 

92 elif positional is not None: 

93 try: 

94 return str(positional_vars.pop(0)) 

95 except IndexError: 

96 raise ValueError( 

97 "Positional variable not specified and needed by template " 

98 "`{}` at position {}".format(match.string, match.start()) 

99 ) 

100 else: 

101 raise ValueError("Unknown template expression {}".format(match.group(0))) 

102 

103 

104def expand(tmpl, *args, **kwargs): 

105 """Expand a path template with the given variables. 

106 

107 .. code-block:: python 

108 

109 >>> expand('users/*/messages/*', 'me', '123') 

110 users/me/messages/123 

111 >>> expand('/v1/{name=shelves/*/books/*}', name='shelves/1/books/3') 

112 /v1/shelves/1/books/3 

113 

114 Args: 

115 tmpl (str): The path template. 

116 args: The positional variables for the path. 

117 kwargs: The named variables for the path. 

118 

119 Returns: 

120 str: The expanded path 

121 

122 Raises: 

123 ValueError: If a positional or named variable is required by the 

124 template but not specified or if an unexpected template expression 

125 is encountered. 

126 """ 

127 replacer = functools.partial(_expand_variable_match, list(args), kwargs) 

128 return _VARIABLE_RE.sub(replacer, tmpl) 

129 

130 

131def _replace_variable_with_pattern(match): 

132 """Replace a variable match with a pattern that can be used to validate it. 

133 

134 Args: 

135 match (re.Match): A regular expression match 

136 

137 Returns: 

138 str: A regular expression pattern that can be used to validate the 

139 variable in an expanded path. 

140 

141 Raises: 

142 ValueError: If an unexpected template expression is encountered. 

143 """ 

144 positional = match.group("positional") 

145 name = match.group("name") 

146 template = match.group("template") 

147 if name is not None: 

148 if not template: 

149 return _SINGLE_SEGMENT_PATTERN.format(name) 

150 elif template == "**": 

151 return _MULTI_SEGMENT_PATTERN.format(name) 

152 else: 

153 return _generate_pattern_for_template(template) 

154 elif positional == "*": 

155 return _SINGLE_SEGMENT_PATTERN 

156 elif positional == "**": 

157 return _MULTI_SEGMENT_PATTERN 

158 else: 

159 raise ValueError("Unknown template expression {}".format(match.group(0))) 

160 

161 

162def _generate_pattern_for_template(tmpl): 

163 """Generate a pattern that can validate a path template. 

164 

165 Args: 

166 tmpl (str): The path template 

167 

168 Returns: 

169 str: A regular expression pattern that can be used to validate an 

170 expanded path template. 

171 """ 

172 return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl) 

173 

174 

175def get_field(request, field): 

176 """Get the value of a field from a given dictionary. 

177 

178 Args: 

179 request (dict | Message): A dictionary or a Message object. 

180 field (str): The key to the request in dot notation. 

181 

182 Returns: 

183 The value of the field. 

184 """ 

185 parts = field.split(".") 

186 value = request 

187 

188 for part in parts: 

189 if not isinstance(value, dict): 

190 value = getattr(value, part, None) 

191 else: 

192 value = value.get(part) 

193 if isinstance(value, dict): 

194 return 

195 return value 

196 

197 

198def delete_field(request, field): 

199 """Delete the value of a field from a given dictionary. 

200 

201 Args: 

202 request (dict | Message): A dictionary object or a Message. 

203 field (str): The key to the request in dot notation. 

204 """ 

205 parts = deque(field.split(".")) 

206 while len(parts) > 1: 

207 part = parts.popleft() 

208 if not isinstance(request, dict): 

209 if hasattr(request, part): 

210 request = getattr(request, part, None) 

211 else: 

212 return 

213 else: 

214 request = request.get(part) 

215 part = parts.popleft() 

216 if not isinstance(request, dict): 

217 if hasattr(request, part): 

218 request.ClearField(part) 

219 else: 

220 return 

221 else: 

222 request.pop(part, None) 

223 

224 

225def validate(tmpl, path): 

226 """Validate a path against the path template. 

227 

228 .. code-block:: python 

229 

230 >>> validate('users/*/messages/*', 'users/me/messages/123') 

231 True 

232 >>> validate('users/*/messages/*', 'users/me/drafts/123') 

233 False 

234 >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/books/3) 

235 True 

236 >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/tapes/3) 

237 False 

238 

239 Args: 

240 tmpl (str): The path template. 

241 path (str): The expanded path. 

242 

243 Returns: 

244 bool: True if the path matches. 

245 """ 

246 pattern = _generate_pattern_for_template(tmpl) + "$" 

247 return True if re.match(pattern, path) is not None else False 

248 

249 

250def transcode(http_options, message=None, **request_kwargs): 

251 """Transcodes a grpc request pattern into a proper HTTP request following the rules outlined here, 

252 https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312 

253 

254 Args: 

255 http_options (list(dict)): A list of dicts which consist of these keys, 

256 'method' (str): The http method 

257 'uri' (str): The path template 

258 'body' (str): The body field name (optional) 

259 (This is a simplified representation of the proto option `google.api.http`) 

260 

261 message (Message) : A request object (optional) 

262 request_kwargs (dict) : A dict representing the request object 

263 

264 Returns: 

265 dict: The transcoded request with these keys, 

266 'method' (str) : The http method 

267 'uri' (str) : The expanded uri 

268 'body' (dict | Message) : A dict or a Message representing the body (optional) 

269 'query_params' (dict | Message) : A dict or Message mapping query parameter variables and values 

270 

271 Raises: 

272 ValueError: If the request does not match the given template. 

273 """ 

274 transcoded_value = message or request_kwargs 

275 bindings = [] 

276 for http_option in http_options: 

277 request = {} 

278 

279 # Assign path 

280 uri_template = http_option["uri"] 

281 fields = [ 

282 (m.group("name"), m.group("template")) 

283 for m in _VARIABLE_RE.finditer(uri_template) 

284 ] 

285 bindings.append((uri_template, fields)) 

286 

287 path_args = {field: get_field(transcoded_value, field) for field, _ in fields} 

288 request["uri"] = expand(uri_template, **path_args) 

289 

290 if not validate(uri_template, request["uri"]) or not all(path_args.values()): 

291 continue 

292 

293 # Remove fields used in uri path from request 

294 leftovers = copy.deepcopy(transcoded_value) 

295 for path_field, _ in fields: 

296 delete_field(leftovers, path_field) 

297 

298 # Assign body and query params 

299 body = http_option.get("body") 

300 

301 if body: 

302 if body == "*": 

303 request["body"] = leftovers 

304 if message: 

305 request["query_params"] = message.__class__() 

306 else: 

307 request["query_params"] = {} 

308 else: 

309 try: 

310 if message: 

311 request["body"] = getattr(leftovers, body) 

312 delete_field(leftovers, body) 

313 else: 

314 request["body"] = leftovers.pop(body) 

315 except (KeyError, AttributeError): 

316 continue 

317 request["query_params"] = leftovers 

318 else: 

319 request["query_params"] = leftovers 

320 request["method"] = http_option["method"] 

321 return request 

322 

323 bindings_description = [ 

324 '\n\tURI: "{}"' 

325 "\n\tRequired request fields:\n\t\t{}".format( 

326 uri, 

327 "\n\t\t".join( 

328 [ 

329 'field: "{}", pattern: "{}"'.format(n, p if p else "*") 

330 for n, p in fields 

331 ] 

332 ), 

333 ) 

334 for uri, fields in bindings 

335 ] 

336 

337 raise ValueError( 

338 "Invalid request." 

339 "\nSome of the fields of the request message are either not initialized or " 

340 "initialized with an invalid value." 

341 "\nPlease make sure your request matches at least one accepted HTTP binding." 

342 "\nTo match a binding the request message must have all the required fields " 

343 "initialized with values matching their patterns as listed below:{}".format( 

344 "\n".join(bindings_description) 

345 ) 

346 )