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