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