_helpers.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # Copyright 2015 Google Inc.
  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. """Helper functions for commonly used utilities."""
  15. import base64
  16. import calendar
  17. import datetime
  18. import six
  19. from six.moves import urllib
  20. CLOCK_SKEW_SECS = 10 # 10 seconds
  21. CLOCK_SKEW = datetime.timedelta(seconds=CLOCK_SKEW_SECS)
  22. def copy_docstring(source_class):
  23. """Decorator that copies a method's docstring from another class.
  24. Args:
  25. source_class (type): The class that has the documented method.
  26. Returns:
  27. Callable: A decorator that will copy the docstring of the same
  28. named method in the source class to the decorated method.
  29. """
  30. def decorator(method):
  31. """Decorator implementation.
  32. Args:
  33. method (Callable): The method to copy the docstring to.
  34. Returns:
  35. Callable: the same method passed in with an updated docstring.
  36. Raises:
  37. ValueError: if the method already has a docstring.
  38. """
  39. if method.__doc__:
  40. raise ValueError("Method already has a docstring.")
  41. source_method = getattr(source_class, method.__name__)
  42. method.__doc__ = source_method.__doc__
  43. return method
  44. return decorator
  45. def utcnow():
  46. """Returns the current UTC datetime.
  47. Returns:
  48. datetime: The current time in UTC.
  49. """
  50. return datetime.datetime.utcnow()
  51. def datetime_to_secs(value):
  52. """Convert a datetime object to the number of seconds since the UNIX epoch.
  53. Args:
  54. value (datetime): The datetime to convert.
  55. Returns:
  56. int: The number of seconds since the UNIX epoch.
  57. """
  58. return calendar.timegm(value.utctimetuple())
  59. def to_bytes(value, encoding="utf-8"):
  60. """Converts a string value to bytes, if necessary.
  61. Unfortunately, ``six.b`` is insufficient for this task since in
  62. Python 2 because it does not modify ``unicode`` objects.
  63. Args:
  64. value (Union[str, bytes]): The value to be converted.
  65. encoding (str): The encoding to use to convert unicode to bytes.
  66. Defaults to "utf-8".
  67. Returns:
  68. bytes: The original value converted to bytes (if unicode) or as
  69. passed in if it started out as bytes.
  70. Raises:
  71. ValueError: If the value could not be converted to bytes.
  72. """
  73. result = value.encode(encoding) if isinstance(value, six.text_type) else value
  74. if isinstance(result, six.binary_type):
  75. return result
  76. else:
  77. raise ValueError("{0!r} could not be converted to bytes".format(value))
  78. def from_bytes(value):
  79. """Converts bytes to a string value, if necessary.
  80. Args:
  81. value (Union[str, bytes]): The value to be converted.
  82. Returns:
  83. str: The original value converted to unicode (if bytes) or as passed in
  84. if it started out as unicode.
  85. Raises:
  86. ValueError: If the value could not be converted to unicode.
  87. """
  88. result = value.decode("utf-8") if isinstance(value, six.binary_type) else value
  89. if isinstance(result, six.text_type):
  90. return result
  91. else:
  92. raise ValueError("{0!r} could not be converted to unicode".format(value))
  93. def update_query(url, params, remove=None):
  94. """Updates a URL's query parameters.
  95. Replaces any current values if they are already present in the URL.
  96. Args:
  97. url (str): The URL to update.
  98. params (Mapping[str, str]): A mapping of query parameter
  99. keys to values.
  100. remove (Sequence[str]): Parameters to remove from the query string.
  101. Returns:
  102. str: The URL with updated query parameters.
  103. Examples:
  104. >>> url = 'http://example.com?a=1'
  105. >>> update_query(url, {'a': '2'})
  106. http://example.com?a=2
  107. >>> update_query(url, {'b': '3'})
  108. http://example.com?a=1&b=3
  109. >> update_query(url, {'b': '3'}, remove=['a'])
  110. http://example.com?b=3
  111. """
  112. if remove is None:
  113. remove = []
  114. # Split the URL into parts.
  115. parts = urllib.parse.urlparse(url)
  116. # Parse the query string.
  117. query_params = urllib.parse.parse_qs(parts.query)
  118. # Update the query parameters with the new parameters.
  119. query_params.update(params)
  120. # Remove any values specified in remove.
  121. query_params = {
  122. key: value for key, value in six.iteritems(query_params) if key not in remove
  123. }
  124. # Re-encoded the query string.
  125. new_query = urllib.parse.urlencode(query_params, doseq=True)
  126. # Unsplit the url.
  127. new_parts = parts._replace(query=new_query)
  128. return urllib.parse.urlunparse(new_parts)
  129. def scopes_to_string(scopes):
  130. """Converts scope value to a string suitable for sending to OAuth 2.0
  131. authorization servers.
  132. Args:
  133. scopes (Sequence[str]): The sequence of scopes to convert.
  134. Returns:
  135. str: The scopes formatted as a single string.
  136. """
  137. return " ".join(scopes)
  138. def string_to_scopes(scopes):
  139. """Converts stringifed scopes value to a list.
  140. Args:
  141. scopes (Union[Sequence, str]): The string of space-separated scopes
  142. to convert.
  143. Returns:
  144. Sequence(str): The separated scopes.
  145. """
  146. if not scopes:
  147. return []
  148. return scopes.split(" ")
  149. def padded_urlsafe_b64decode(value):
  150. """Decodes base64 strings lacking padding characters.
  151. Google infrastructure tends to omit the base64 padding characters.
  152. Args:
  153. value (Union[str, bytes]): The encoded value.
  154. Returns:
  155. bytes: The decoded value
  156. """
  157. b64string = to_bytes(value)
  158. padded = b64string + b"=" * (-len(b64string) % 4)
  159. return base64.urlsafe_b64decode(padded)
  160. def unpadded_urlsafe_b64encode(value):
  161. """Encodes base64 strings removing any padding characters.
  162. `rfc 7515`_ defines Base64url to NOT include any padding
  163. characters, but the stdlib doesn't do that by default.
  164. _rfc7515: https://tools.ietf.org/html/rfc7515#page-6
  165. Args:
  166. value (Union[str|bytes]): The bytes-like value to encode
  167. Returns:
  168. Union[str|bytes]: The encoded value
  169. """
  170. return base64.urlsafe_b64encode(value).rstrip(b"=")