OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # |
| 3 # Copyright 2015 Google Inc. |
| 4 # |
| 5 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 # you may not use this file except in compliance with the License. |
| 7 # You may obtain a copy of the License at |
| 8 # |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 # |
| 11 # Unless required by applicable law or agreed to in writing, software |
| 12 # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 # See the License for the specific language governing permissions and |
| 15 # limitations under the License. |
| 16 |
| 17 """Assorted utilities shared between parts of apitools.""" |
| 18 |
| 19 import collections |
| 20 import os |
| 21 import random |
| 22 |
| 23 import six |
| 24 from six.moves import http_client |
| 25 import six.moves.urllib.error as urllib_error |
| 26 import six.moves.urllib.parse as urllib_parse |
| 27 import six.moves.urllib.request as urllib_request |
| 28 |
| 29 from apitools.base.protorpclite import messages |
| 30 from apitools.base.py import encoding |
| 31 from apitools.base.py import exceptions |
| 32 |
| 33 __all__ = [ |
| 34 'DetectGae', |
| 35 'DetectGce', |
| 36 ] |
| 37 |
| 38 _RESERVED_URI_CHARS = r":/?#[]@!$&'()*+,;=" |
| 39 |
| 40 |
| 41 def DetectGae(): |
| 42 """Determine whether or not we're running on GAE. |
| 43 |
| 44 This is based on: |
| 45 https://developers.google.com/appengine/docs/python/#The_Environment |
| 46 |
| 47 Returns: |
| 48 True iff we're running on GAE. |
| 49 """ |
| 50 server_software = os.environ.get('SERVER_SOFTWARE', '') |
| 51 return (server_software.startswith('Development/') or |
| 52 server_software.startswith('Google App Engine/')) |
| 53 |
| 54 |
| 55 def DetectGce(): |
| 56 """Determine whether or not we're running on GCE. |
| 57 |
| 58 This is based on: |
| 59 https://cloud.google.com/compute/docs/metadata#runninggce |
| 60 |
| 61 Returns: |
| 62 True iff we're running on a GCE instance. |
| 63 """ |
| 64 try: |
| 65 o = urllib_request.build_opener(urllib_request.ProxyHandler({})).open( |
| 66 urllib_request.Request('http://metadata.google.internal')) |
| 67 except urllib_error.URLError: |
| 68 return False |
| 69 return (o.getcode() == http_client.OK and |
| 70 o.headers.get('metadata-flavor') == 'Google') |
| 71 |
| 72 |
| 73 def NormalizeScopes(scope_spec): |
| 74 """Normalize scope_spec to a set of strings.""" |
| 75 if isinstance(scope_spec, six.string_types): |
| 76 return set(scope_spec.split(' ')) |
| 77 elif isinstance(scope_spec, collections.Iterable): |
| 78 return set(scope_spec) |
| 79 raise exceptions.TypecheckError( |
| 80 'NormalizeScopes expected string or iterable, found %s' % ( |
| 81 type(scope_spec),)) |
| 82 |
| 83 |
| 84 def Typecheck(arg, arg_type, msg=None): |
| 85 if not isinstance(arg, arg_type): |
| 86 if msg is None: |
| 87 if isinstance(arg_type, tuple): |
| 88 msg = 'Type of arg is "%s", not one of %r' % ( |
| 89 type(arg), arg_type) |
| 90 else: |
| 91 msg = 'Type of arg is "%s", not "%s"' % (type(arg), arg_type) |
| 92 raise exceptions.TypecheckError(msg) |
| 93 return arg |
| 94 |
| 95 |
| 96 def ExpandRelativePath(method_config, params, relative_path=None): |
| 97 """Determine the relative path for request.""" |
| 98 path = relative_path or method_config.relative_path or '' |
| 99 |
| 100 for param in method_config.path_params: |
| 101 param_template = '{%s}' % param |
| 102 # For more details about "reserved word expansion", see: |
| 103 # http://tools.ietf.org/html/rfc6570#section-3.2.2 |
| 104 reserved_chars = '' |
| 105 reserved_template = '{+%s}' % param |
| 106 if reserved_template in path: |
| 107 reserved_chars = _RESERVED_URI_CHARS |
| 108 path = path.replace(reserved_template, param_template) |
| 109 if param_template not in path: |
| 110 raise exceptions.InvalidUserInputError( |
| 111 'Missing path parameter %s' % param) |
| 112 try: |
| 113 # TODO(craigcitro): Do we want to support some sophisticated |
| 114 # mapping here? |
| 115 value = params[param] |
| 116 except KeyError: |
| 117 raise exceptions.InvalidUserInputError( |
| 118 'Request missing required parameter %s' % param) |
| 119 if value is None: |
| 120 raise exceptions.InvalidUserInputError( |
| 121 'Request missing required parameter %s' % param) |
| 122 try: |
| 123 if not isinstance(value, six.string_types): |
| 124 value = str(value) |
| 125 path = path.replace(param_template, |
| 126 urllib_parse.quote(value.encode('utf_8'), |
| 127 reserved_chars)) |
| 128 except TypeError as e: |
| 129 raise exceptions.InvalidUserInputError( |
| 130 'Error setting required parameter %s to value %s: %s' % ( |
| 131 param, value, e)) |
| 132 return path |
| 133 |
| 134 |
| 135 def CalculateWaitForRetry(retry_attempt, max_wait=60): |
| 136 """Calculates amount of time to wait before a retry attempt. |
| 137 |
| 138 Wait time grows exponentially with the number of attempts. A |
| 139 random amount of jitter is added to spread out retry attempts from |
| 140 different clients. |
| 141 |
| 142 Args: |
| 143 retry_attempt: Retry attempt counter. |
| 144 max_wait: Upper bound for wait time [seconds]. |
| 145 |
| 146 Returns: |
| 147 Number of seconds to wait before retrying request. |
| 148 |
| 149 """ |
| 150 |
| 151 wait_time = 2 ** retry_attempt |
| 152 max_jitter = wait_time / 4.0 |
| 153 wait_time += random.uniform(-max_jitter, max_jitter) |
| 154 return max(1, min(wait_time, max_wait)) |
| 155 |
| 156 |
| 157 def AcceptableMimeType(accept_patterns, mime_type): |
| 158 """Return True iff mime_type is acceptable for one of accept_patterns. |
| 159 |
| 160 Note that this function assumes that all patterns in accept_patterns |
| 161 will be simple types of the form "type/subtype", where one or both |
| 162 of these can be "*". We do not support parameters (i.e. "; q=") in |
| 163 patterns. |
| 164 |
| 165 Args: |
| 166 accept_patterns: list of acceptable MIME types. |
| 167 mime_type: the mime type we would like to match. |
| 168 |
| 169 Returns: |
| 170 Whether or not mime_type matches (at least) one of these patterns. |
| 171 """ |
| 172 if '/' not in mime_type: |
| 173 raise exceptions.InvalidUserInputError( |
| 174 'Invalid MIME type: "%s"' % mime_type) |
| 175 unsupported_patterns = [p for p in accept_patterns if ';' in p] |
| 176 if unsupported_patterns: |
| 177 raise exceptions.GeneratedClientError( |
| 178 'MIME patterns with parameter unsupported: "%s"' % ', '.join( |
| 179 unsupported_patterns)) |
| 180 |
| 181 def MimeTypeMatches(pattern, mime_type): |
| 182 """Return True iff mime_type is acceptable for pattern.""" |
| 183 # Some systems use a single '*' instead of '*/*'. |
| 184 if pattern == '*': |
| 185 pattern = '*/*' |
| 186 return all(accept in ('*', provided) for accept, provided |
| 187 in zip(pattern.split('/'), mime_type.split('/'))) |
| 188 |
| 189 return any(MimeTypeMatches(pattern, mime_type) |
| 190 for pattern in accept_patterns) |
| 191 |
| 192 |
| 193 def MapParamNames(params, request_type): |
| 194 """Reverse parameter remappings for URL construction.""" |
| 195 return [encoding.GetCustomJsonFieldMapping(request_type, json_name=p) or p |
| 196 for p in params] |
| 197 |
| 198 |
| 199 def MapRequestParams(params, request_type): |
| 200 """Perform any renames/remappings needed for URL construction. |
| 201 |
| 202 Currently, we have several ways to customize JSON encoding, in |
| 203 particular of field names and enums. This works fine for JSON |
| 204 bodies, but also needs to be applied for path and query parameters |
| 205 in the URL. |
| 206 |
| 207 This function takes a dictionary from param names to values, and |
| 208 performs any registered mappings. We also need the request type (to |
| 209 look up the mappings). |
| 210 |
| 211 Args: |
| 212 params: (dict) Map from param names to values |
| 213 request_type: (protorpc.messages.Message) request type for this API call |
| 214 |
| 215 Returns: |
| 216 A new dict of the same size, with all registered mappings applied. |
| 217 """ |
| 218 new_params = dict(params) |
| 219 for param_name, value in params.items(): |
| 220 field_remapping = encoding.GetCustomJsonFieldMapping( |
| 221 request_type, python_name=param_name) |
| 222 if field_remapping is not None: |
| 223 new_params[field_remapping] = new_params.pop(param_name) |
| 224 if isinstance(value, messages.Enum): |
| 225 new_params[param_name] = encoding.GetCustomJsonEnumMapping( |
| 226 type(value), python_name=str(value)) or str(value) |
| 227 return new_params |
OLD | NEW |