• 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 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