• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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