Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(65)

Side by Side Diff: third_party/google-endpoints/endpoints/api_config_manager.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 Google Inc. All Rights Reserved.
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 """Configuration manager to store API configurations."""
16
17 # pylint: disable=g-bad-name
18 import base64
19 import logging
20 import re
21 import threading
22 import urllib
23
24 import discovery_service
25
26
27 # Internal constants
28 _PATH_VARIABLE_PATTERN = r'[a-zA-Z_][a-zA-Z_.\d]*'
29 _PATH_VALUE_PATTERN = r'[^:/?#\[\]{}]*'
30
31
32 class ApiConfigManager(object):
33 """Manages loading api configs and method lookup."""
34
35 def __init__(self):
36 self._rpc_method_dict = {}
37 self._rest_methods = []
38 self._configs = {}
39 self._config_lock = threading.Lock()
40
41 @property
42 def configs(self):
43 """Return a dict with the current configuration mappings.
44
45 Returns:
46 A dict with the current configuration mappings.
47 """
48 with self._config_lock:
49 return self._configs.copy()
50
51 def process_api_config_response(self, config_json):
52 """Parses a JSON API config and registers methods for dispatch.
53
54 Side effects:
55 Parses method name, etc. for all methods and updates the indexing
56 data structures with the information.
57
58 Args:
59 config_json: A dict, the JSON body of the getApiConfigs response.
60 """
61 with self._config_lock:
62 self._add_discovery_config()
63 for config in config_json.get('items', []):
64 lookup_key = config.get('name', ''), config.get('version', '')
65 self._configs[lookup_key] = config
66
67 for config in self._configs.itervalues():
68 name = config.get('name', '')
69 version = config.get('version', '')
70 sorted_methods = self._get_sorted_methods(config.get('methods', {}))
71
72 for method_name, method in sorted_methods:
73 self._save_rpc_method(method_name, version, method)
74 self._save_rest_method(method_name, name, version, method)
75
76 def _get_sorted_methods(self, methods):
77 """Get a copy of 'methods' sorted the way they would be on the live server.
78
79 Args:
80 methods: JSON configuration of an API's methods.
81
82 Returns:
83 The same configuration with the methods sorted based on what order
84 they'll be checked by the server.
85 """
86 if not methods:
87 return methods
88
89 # Comparison function we'll use to sort the methods:
90 def _sorted_methods_comparison(method_info1, method_info2):
91 """Sort method info by path and http_method.
92
93 Args:
94 method_info1: Method name and info for the first method to compare.
95 method_info2: Method name and info for the method to compare to.
96
97 Returns:
98 Negative if the first method should come first, positive if the
99 first method should come after the second. Zero if they're
100 equivalent.
101 """
102
103 def _score_path(path):
104 """Calculate the score for this path, used for comparisons.
105
106 Higher scores have priority, and if scores are equal, the path text
107 is sorted alphabetically. Scores are based on the number and location
108 of the constant parts of the path. The server has some special handling
109 for variables with regexes, which we don't handle here.
110
111 Args:
112 path: The request path that we're calculating a score for.
113
114 Returns:
115 The score for the given path.
116 """
117 score = 0
118 parts = path.split('/')
119 for part in parts:
120 score <<= 1
121 if not part or part[0] != '{':
122 # Found a constant.
123 score += 1
124 # Shift by 31 instead of 32 because some (!) versions of Python like
125 # to convert the int to a long if we shift by 32, and the sorted()
126 # function that uses this blows up if it receives anything but an int.
127 score <<= 31 - len(parts)
128 return score
129
130 # Higher path scores come first.
131 path_score1 = _score_path(method_info1[1].get('path', ''))
132 path_score2 = _score_path(method_info2[1].get('path', ''))
133 if path_score1 != path_score2:
134 return path_score2 - path_score1
135
136 # Compare by path text next, sorted alphabetically.
137 path_result = cmp(method_info1[1].get('path', ''),
138 method_info2[1].get('path', ''))
139 if path_result != 0:
140 return path_result
141
142 # All else being equal, sort by HTTP method.
143 method_result = cmp(method_info1[1].get('httpMethod', ''),
144 method_info2[1].get('httpMethod', ''))
145 return method_result
146
147 return sorted(methods.items(), _sorted_methods_comparison)
148
149 @staticmethod
150 def _get_path_params(match):
151 """Gets path parameters from a regular expression match.
152
153 Args:
154 match: A regular expression Match object for a path.
155
156 Returns:
157 A dictionary containing the variable names converted from base64.
158 """
159 result = {}
160 for var_name, value in match.groupdict().iteritems():
161 actual_var_name = ApiConfigManager._from_safe_path_param_name(var_name)
162 result[actual_var_name] = urllib.unquote_plus(value)
163 return result
164
165 def lookup_rpc_method(self, method_name, version):
166 """Lookup the JsonRPC method at call time.
167
168 The method is looked up in self._rpc_method_dict, the dictionary that
169 it is saved in for SaveRpcMethod().
170
171 Args:
172 method_name: A string containing the name of the method.
173 version: A string containing the version of the API.
174
175 Returns:
176 Method descriptor as specified in the API configuration.
177 """
178 with self._config_lock:
179 method = self._rpc_method_dict.get((method_name, version))
180 return method
181
182 def lookup_rest_method(self, path, http_method):
183 """Look up the rest method at call time.
184
185 The method is looked up in self._rest_methods, the list it is saved
186 in for SaveRestMethod.
187
188 Args:
189 path: A string containing the path from the URL of the request.
190 http_method: A string containing HTTP method of the request.
191
192 Returns:
193 Tuple of (<method name>, <method>, <params>)
194 Where:
195 <method name> is the string name of the method that was matched.
196 <method> is the descriptor as specified in the API configuration. -and-
197 <params> is a dict of path parameters matched in the rest request.
198 """
199 with self._config_lock:
200 path = urllib.unquote(path)
201 for compiled_path_pattern, unused_path, methods in self._rest_methods:
202 match = compiled_path_pattern.match(path)
203 if match:
204 params = self._get_path_params(match)
205 method_key = http_method.lower()
206 method_name, method = methods.get(method_key, (None, None))
207 if method:
208 break
209 else:
210 logging.warn('No endpoint found for path: %s', path)
211 method_name = None
212 method = None
213 params = None
214 return method_name, method, params
215
216 def _add_discovery_config(self):
217 """Add the Discovery configuration to our list of configs.
218
219 This should only be called with self._config_lock. The code here assumes
220 the lock is held.
221 """
222 lookup_key = (discovery_service.DiscoveryService.API_CONFIG['name'],
223 discovery_service.DiscoveryService.API_CONFIG['version'])
224 self._configs[lookup_key] = discovery_service.DiscoveryService.API_CONFIG
225
226 def save_config(self, lookup_key, config):
227 """Save a configuration to the cache of configs.
228
229 Args:
230 lookup_key: A string containing the cache lookup key.
231 config: The dict containing the configuration to save to the cache.
232 """
233 with self._config_lock:
234 self._configs[lookup_key] = config
235
236 @staticmethod
237 def _to_safe_path_param_name(matched_parameter):
238 """Creates a safe string to be used as a regex group name.
239
240 Only alphanumeric characters and underscore are allowed in variable name
241 tokens, and numeric are not allowed as the first character.
242
243 We cast the matched_parameter to base32 (since the alphabet is safe),
244 strip the padding (= not safe) and prepend with _, since we know a token
245 can begin with underscore.
246
247 Args:
248 matched_parameter: A string containing the parameter matched from the URL
249 template.
250
251 Returns:
252 A string that's safe to be used as a regex group name.
253 """
254 return '_' + base64.b32encode(matched_parameter).rstrip('=')
255
256 @staticmethod
257 def _from_safe_path_param_name(safe_parameter):
258 """Takes a safe regex group name and converts it back to the original value.
259
260 Only alphanumeric characters and underscore are allowed in variable name
261 tokens, and numeric are not allowed as the first character.
262
263 The safe_parameter is a base32 representation of the actual value.
264
265 Args:
266 safe_parameter: A string that was generated by _to_safe_path_param_name.
267
268 Returns:
269 A string, the parameter matched from the URL template.
270 """
271 assert safe_parameter.startswith('_')
272 safe_parameter_as_base32 = safe_parameter[1:]
273
274 padding_length = - len(safe_parameter_as_base32) % 8
275 padding = '=' * padding_length
276 return base64.b32decode(safe_parameter_as_base32 + padding)
277
278 @staticmethod
279 def _compile_path_pattern(pattern):
280 r"""Generates a compiled regex pattern for a path pattern.
281
282 e.g. '/MyApi/v1/notes/{id}'
283 returns re.compile(r'/MyApi/v1/notes/(?P<id>[^:/?#\[\]{}]*)')
284
285 Args:
286 pattern: A string, the parameterized path pattern to be checked.
287
288 Returns:
289 A compiled regex object to match this path pattern.
290 """
291
292 def replace_variable(match):
293 """Replaces a {variable} with a regex to match it by name.
294
295 Changes the string corresponding to the variable name to the base32
296 representation of the string, prepended by an underscore. This is
297 necessary because we can have message variable names in URL patterns
298 (e.g. via {x.y}) but the character '.' can't be in a regex group name.
299
300 Args:
301 match: A regex match object, the matching regex group as sent by
302 re.sub().
303
304 Returns:
305 A string regex to match the variable by name, if the full pattern was
306 matched.
307 """
308 if match.lastindex > 1:
309 var_name = ApiConfigManager._to_safe_path_param_name(match.group(2))
310 return '%s(?P<%s>%s)' % (match.group(1), var_name,
311 _PATH_VALUE_PATTERN)
312 return match.group(0)
313
314 pattern = re.sub('(/|^){(%s)}(?=/|$)' % _PATH_VARIABLE_PATTERN,
315 replace_variable, pattern)
316 return re.compile(pattern + '/?$')
317
318 def _save_rpc_method(self, method_name, version, method):
319 """Store JsonRpc api methods in a map for lookup at call time.
320
321 (rpcMethodName, apiVersion) => method.
322
323 Args:
324 method_name: A string containing the name of the API method.
325 version: A string containing the version of the API.
326 method: A dict containing the method descriptor (as in the api config
327 file).
328 """
329 self._rpc_method_dict[(method_name, version)] = method
330
331 def _save_rest_method(self, method_name, api_name, version, method):
332 """Store Rest api methods in a list for lookup at call time.
333
334 The list is self._rest_methods, a list of tuples:
335 [(<compiled_path>, <path_pattern>, <method_dict>), ...]
336 where:
337 <compiled_path> is a compiled regex to match against the incoming URL
338 <path_pattern> is a string representing the original path pattern,
339 checked on insertion to prevent duplicates. -and-
340 <method_dict> is a dict of httpMethod => (method_name, method)
341
342 This structure is a bit complex, it supports use in two contexts:
343 Creation time:
344 - SaveRestMethod is called repeatedly, each method will have a path,
345 which we want to be compiled for fast lookup at call time
346 - We want to prevent duplicate incoming path patterns, so store the
347 un-compiled path, not counting on a compiled regex being a stable
348 comparison as it is not documented as being stable for this use.
349 - Need to store the method that will be mapped at calltime.
350 - Different methods may have the same path but different http method.
351 Call time:
352 - Quickly scan through the list attempting .match(path) on each
353 compiled regex to find the path that matches.
354 - When a path is matched, look up the API method from the request
355 and get the method name and method config for the matching
356 API method and method name.
357
358 Args:
359 method_name: A string containing the name of the API method.
360 api_name: A string containing the name of the API.
361 version: A string containing the version of the API.
362 method: A dict containing the method descriptor (as in the api config
363 file).
364 """
365 path_pattern = '/'.join((api_name, version, method.get('path', '')))
366 http_method = method.get('httpMethod', '').lower()
367 for _, path, methods in self._rest_methods:
368 if path == path_pattern:
369 methods[http_method] = method_name, method
370 break
371 else:
372 self._rest_methods.append(
373 (self._compile_path_pattern(path_pattern),
374 path_pattern,
375 {http_method: (method_name, method)}))
OLDNEW
« no previous file with comments | « third_party/google-endpoints/endpoints/api_config.py ('k') | third_party/google-endpoints/endpoints/api_exceptions.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698