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