1# Copyright 2015 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Helper functions for commonly used utilities."""
16
17import base64
18import functools
19import inspect
20import json
21import logging
22import os
23import warnings
24
25import six
26from six.moves import urllib
27
28
29logger = logging.getLogger(__name__)
30
31POSITIONAL_WARNING = 'WARNING'
32POSITIONAL_EXCEPTION = 'EXCEPTION'
33POSITIONAL_IGNORE = 'IGNORE'
34POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
35 POSITIONAL_IGNORE])
36
37positional_parameters_enforcement = POSITIONAL_WARNING
38
39_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
40_IS_DIR_MESSAGE = '{0}: Is a directory'
41_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
42
43
44def positional(max_positional_args):
45 """A decorator to declare that only the first N arguments my be positional.
46
47 This decorator makes it easy to support Python 3 style keyword-only
48 parameters. For example, in Python 3 it is possible to write::
49
50 def fn(pos1, *, kwonly1=None, kwonly1=None):
51 ...
52
53 All named parameters after ``*`` must be a keyword::
54
55 fn(10, 'kw1', 'kw2') # Raises exception.
56 fn(10, kwonly1='kw1') # Ok.
57
58 Example
59 ^^^^^^^
60
61 To define a function like above, do::
62
63 @positional(1)
64 def fn(pos1, kwonly1=None, kwonly2=None):
65 ...
66
67 If no default value is provided to a keyword argument, it becomes a
68 required keyword argument::
69
70 @positional(0)
71 def fn(required_kw):
72 ...
73
74 This must be called with the keyword parameter::
75
76 fn() # Raises exception.
77 fn(10) # Raises exception.
78 fn(required_kw=10) # Ok.
79
80 When defining instance or class methods always remember to account for
81 ``self`` and ``cls``::
82
83 class MyClass(object):
84
85 @positional(2)
86 def my_method(self, pos1, kwonly1=None):
87 ...
88
89 @classmethod
90 @positional(2)
91 def my_method(cls, pos1, kwonly1=None):
92 ...
93
94 The positional decorator behavior is controlled by
95 ``_helpers.positional_parameters_enforcement``, which may be set to
96 ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
97 ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
98 nothing, respectively, if a declaration is violated.
99
100 Args:
101 max_positional_arguments: Maximum number of positional arguments. All
102 parameters after the this index must be
103 keyword only.
104
105 Returns:
106 A decorator that prevents using arguments after max_positional_args
107 from being used as positional parameters.
108
109 Raises:
110 TypeError: if a key-word only argument is provided as a positional
111 parameter, but only if
112 _helpers.positional_parameters_enforcement is set to
113 POSITIONAL_EXCEPTION.
114 """
115
116 def positional_decorator(wrapped):
117 @functools.wraps(wrapped)
118 def positional_wrapper(*args, **kwargs):
119 if len(args) > max_positional_args:
120 plural_s = ''
121 if max_positional_args != 1:
122 plural_s = 's'
123 message = ('{function}() takes at most {args_max} positional '
124 'argument{plural} ({args_given} given)'.format(
125 function=wrapped.__name__,
126 args_max=max_positional_args,
127 args_given=len(args),
128 plural=plural_s))
129 if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
130 raise TypeError(message)
131 elif positional_parameters_enforcement == POSITIONAL_WARNING:
132 logger.warning(message)
133 return wrapped(*args, **kwargs)
134 return positional_wrapper
135
136 if isinstance(max_positional_args, six.integer_types):
137 return positional_decorator
138 else:
139 args, _, _, defaults = inspect.getargspec(max_positional_args)
140 return positional(len(args) - len(defaults))(max_positional_args)
141
142
143def scopes_to_string(scopes):
144 """Converts scope value to a string.
145
146 If scopes is a string then it is simply passed through. If scopes is an
147 iterable then a string is returned that is all the individual scopes
148 concatenated with spaces.
149
150 Args:
151 scopes: string or iterable of strings, the scopes.
152
153 Returns:
154 The scopes formatted as a single string.
155 """
156 if isinstance(scopes, six.string_types):
157 return scopes
158 else:
159 return ' '.join(scopes)
160
161
162def string_to_scopes(scopes):
163 """Converts stringifed scope value to a list.
164
165 If scopes is a list then it is simply passed through. If scopes is an
166 string then a list of each individual scope is returned.
167
168 Args:
169 scopes: a string or iterable of strings, the scopes.
170
171 Returns:
172 The scopes in a list.
173 """
174 if not scopes:
175 return []
176 elif isinstance(scopes, six.string_types):
177 return scopes.split(' ')
178 else:
179 return scopes
180
181
182def parse_unique_urlencoded(content):
183 """Parses unique key-value parameters from urlencoded content.
184
185 Args:
186 content: string, URL-encoded key-value pairs.
187
188 Returns:
189 dict, The key-value pairs from ``content``.
190
191 Raises:
192 ValueError: if one of the keys is repeated.
193 """
194 urlencoded_params = urllib.parse.parse_qs(content)
195 params = {}
196 for key, value in six.iteritems(urlencoded_params):
197 if len(value) != 1:
198 msg = ('URL-encoded content contains a repeated value:'
199 '%s -> %s' % (key, ', '.join(value)))
200 raise ValueError(msg)
201 params[key] = value[0]
202 return params
203
204
205def update_query_params(uri, params):
206 """Updates a URI with new query parameters.
207
208 If a given key from ``params`` is repeated in the ``uri``, then
209 the URI will be considered invalid and an error will occur.
210
211 If the URI is valid, then each value from ``params`` will
212 replace the corresponding value in the query parameters (if
213 it exists).
214
215 Args:
216 uri: string, A valid URI, with potential existing query parameters.
217 params: dict, A dictionary of query parameters.
218
219 Returns:
220 The same URI but with the new query parameters added.
221 """
222 parts = urllib.parse.urlparse(uri)
223 query_params = parse_unique_urlencoded(parts.query)
224 query_params.update(params)
225 new_query = urllib.parse.urlencode(query_params)
226 new_parts = parts._replace(query=new_query)
227 return urllib.parse.urlunparse(new_parts)
228
229
230def _add_query_parameter(url, name, value):
231 """Adds a query parameter to a url.
232
233 Replaces the current value if it already exists in the URL.
234
235 Args:
236 url: string, url to add the query parameter to.
237 name: string, query parameter name.
238 value: string, query parameter value.
239
240 Returns:
241 Updated query parameter. Does not update the url if value is None.
242 """
243 if value is None:
244 return url
245 else:
246 return update_query_params(url, {name: value})
247
248
249def validate_file(filename):
250 if os.path.islink(filename):
251 raise IOError(_SYM_LINK_MESSAGE.format(filename))
252 elif os.path.isdir(filename):
253 raise IOError(_IS_DIR_MESSAGE.format(filename))
254 elif not os.path.isfile(filename):
255 warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
256
257
258def _parse_pem_key(raw_key_input):
259 """Identify and extract PEM keys.
260
261 Determines whether the given key is in the format of PEM key, and extracts
262 the relevant part of the key if it is.
263
264 Args:
265 raw_key_input: The contents of a private key file (either PEM or
266 PKCS12).
267
268 Returns:
269 string, The actual key if the contents are from a PEM file, or
270 else None.
271 """
272 offset = raw_key_input.find(b'-----BEGIN ')
273 if offset != -1:
274 return raw_key_input[offset:]
275
276
277def _json_encode(data):
278 return json.dumps(data, separators=(',', ':'))
279
280
281def _to_bytes(value, encoding='ascii'):
282 """Converts a string value to bytes, if necessary.
283
284 Unfortunately, ``six.b`` is insufficient for this task since in
285 Python2 it does not modify ``unicode`` objects.
286
287 Args:
288 value: The string/bytes value to be converted.
289 encoding: The encoding to use to convert unicode to bytes. Defaults
290 to "ascii", which will not allow any characters from ordinals
291 larger than 127. Other useful values are "latin-1", which
292 which will only allows byte ordinals (up to 255) and "utf-8",
293 which will encode any unicode that needs to be.
294
295 Returns:
296 The original value converted to bytes (if unicode) or as passed in
297 if it started out as bytes.
298
299 Raises:
300 ValueError if the value could not be converted to bytes.
301 """
302 result = (value.encode(encoding)
303 if isinstance(value, six.text_type) else value)
304 if isinstance(result, six.binary_type):
305 return result
306 else:
307 raise ValueError('{0!r} could not be converted to bytes'.format(value))
308
309
310def _from_bytes(value):
311 """Converts bytes to a string value, if necessary.
312
313 Args:
314 value: The string/bytes value to be converted.
315
316 Returns:
317 The original value converted to unicode (if bytes) or as passed in
318 if it started out as unicode.
319
320 Raises:
321 ValueError if the value could not be converted to unicode.
322 """
323 result = (value.decode('utf-8')
324 if isinstance(value, six.binary_type) else value)
325 if isinstance(result, six.text_type):
326 return result
327 else:
328 raise ValueError(
329 '{0!r} could not be converted to unicode'.format(value))
330
331
332def _urlsafe_b64encode(raw_bytes):
333 raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
334 return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
335
336
337def _urlsafe_b64decode(b64string):
338 # Guard against unicode strings, which base64 can't handle.
339 b64string = _to_bytes(b64string)
340 padded = b64string + b'=' * (4 - len(b64string) % 4)
341 return base64.urlsafe_b64decode(padded)