Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

appengine.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """
  2. This module provides a pool manager that uses Google App Engine's
  3. `URLFetch Service <https://cloud.google.com/appengine/docs/python/urlfetch>`_.
  4. Example usage::
  5. from pip._vendor.urllib3 import PoolManager
  6. from pip._vendor.urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
  7. if is_appengine_sandbox():
  8. # AppEngineManager uses AppEngine's URLFetch API behind the scenes
  9. http = AppEngineManager()
  10. else:
  11. # PoolManager uses a socket-level API behind the scenes
  12. http = PoolManager()
  13. r = http.request('GET', 'https://google.com/')
  14. There are `limitations <https://cloud.google.com/appengine/docs/python/\
  15. urlfetch/#Python_Quotas_and_limits>`_ to the URLFetch service and it may not be
  16. the best choice for your application. There are three options for using
  17. urllib3 on Google App Engine:
  18. 1. You can use :class:`AppEngineManager` with URLFetch. URLFetch is
  19. cost-effective in many circumstances as long as your usage is within the
  20. limitations.
  21. 2. You can use a normal :class:`~urllib3.PoolManager` by enabling sockets.
  22. Sockets also have `limitations and restrictions
  23. <https://cloud.google.com/appengine/docs/python/sockets/\
  24. #limitations-and-restrictions>`_ and have a lower free quota than URLFetch.
  25. To use sockets, be sure to specify the following in your ``app.yaml``::
  26. env_variables:
  27. GAE_USE_SOCKETS_HTTPLIB : 'true'
  28. 3. If you are using `App Engine Flexible
  29. <https://cloud.google.com/appengine/docs/flexible/>`_, you can use the standard
  30. :class:`PoolManager` without any configuration or special environment variables.
  31. """
  32. from __future__ import absolute_import
  33. import logging
  34. import os
  35. import warnings
  36. from ..packages.six.moves.urllib.parse import urljoin
  37. from ..exceptions import (
  38. HTTPError,
  39. HTTPWarning,
  40. MaxRetryError,
  41. ProtocolError,
  42. TimeoutError,
  43. SSLError
  44. )
  45. from ..packages.six import BytesIO
  46. from ..request import RequestMethods
  47. from ..response import HTTPResponse
  48. from ..util.timeout import Timeout
  49. from ..util.retry import Retry
  50. try:
  51. from google.appengine.api import urlfetch
  52. except ImportError:
  53. urlfetch = None
  54. log = logging.getLogger(__name__)
  55. class AppEnginePlatformWarning(HTTPWarning):
  56. pass
  57. class AppEnginePlatformError(HTTPError):
  58. pass
  59. class AppEngineManager(RequestMethods):
  60. """
  61. Connection manager for Google App Engine sandbox applications.
  62. This manager uses the URLFetch service directly instead of using the
  63. emulated httplib, and is subject to URLFetch limitations as described in
  64. the App Engine documentation `here
  65. <https://cloud.google.com/appengine/docs/python/urlfetch>`_.
  66. Notably it will raise an :class:`AppEnginePlatformError` if:
  67. * URLFetch is not available.
  68. * If you attempt to use this on App Engine Flexible, as full socket
  69. support is available.
  70. * If a request size is more than 10 megabytes.
  71. * If a response size is more than 32 megabtyes.
  72. * If you use an unsupported request method such as OPTIONS.
  73. Beyond those cases, it will raise normal urllib3 errors.
  74. """
  75. def __init__(self, headers=None, retries=None, validate_certificate=True,
  76. urlfetch_retries=True):
  77. if not urlfetch:
  78. raise AppEnginePlatformError(
  79. "URLFetch is not available in this environment.")
  80. if is_prod_appengine_mvms():
  81. raise AppEnginePlatformError(
  82. "Use normal urllib3.PoolManager instead of AppEngineManager"
  83. "on Managed VMs, as using URLFetch is not necessary in "
  84. "this environment.")
  85. warnings.warn(
  86. "urllib3 is using URLFetch on Google App Engine sandbox instead "
  87. "of sockets. To use sockets directly instead of URLFetch see "
  88. "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.",
  89. AppEnginePlatformWarning)
  90. RequestMethods.__init__(self, headers)
  91. self.validate_certificate = validate_certificate
  92. self.urlfetch_retries = urlfetch_retries
  93. self.retries = retries or Retry.DEFAULT
  94. def __enter__(self):
  95. return self
  96. def __exit__(self, exc_type, exc_val, exc_tb):
  97. # Return False to re-raise any potential exceptions
  98. return False
  99. def urlopen(self, method, url, body=None, headers=None,
  100. retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT,
  101. **response_kw):
  102. retries = self._get_retries(retries, redirect)
  103. try:
  104. follow_redirects = (
  105. redirect and
  106. retries.redirect != 0 and
  107. retries.total)
  108. response = urlfetch.fetch(
  109. url,
  110. payload=body,
  111. method=method,
  112. headers=headers or {},
  113. allow_truncated=False,
  114. follow_redirects=self.urlfetch_retries and follow_redirects,
  115. deadline=self._get_absolute_timeout(timeout),
  116. validate_certificate=self.validate_certificate,
  117. )
  118. except urlfetch.DeadlineExceededError as e:
  119. raise TimeoutError(self, e)
  120. except urlfetch.InvalidURLError as e:
  121. if 'too large' in str(e):
  122. raise AppEnginePlatformError(
  123. "URLFetch request too large, URLFetch only "
  124. "supports requests up to 10mb in size.", e)
  125. raise ProtocolError(e)
  126. except urlfetch.DownloadError as e:
  127. if 'Too many redirects' in str(e):
  128. raise MaxRetryError(self, url, reason=e)
  129. raise ProtocolError(e)
  130. except urlfetch.ResponseTooLargeError as e:
  131. raise AppEnginePlatformError(
  132. "URLFetch response too large, URLFetch only supports"
  133. "responses up to 32mb in size.", e)
  134. except urlfetch.SSLCertificateError as e:
  135. raise SSLError(e)
  136. except urlfetch.InvalidMethodError as e:
  137. raise AppEnginePlatformError(
  138. "URLFetch does not support method: %s" % method, e)
  139. http_response = self._urlfetch_response_to_http_response(
  140. response, retries=retries, **response_kw)
  141. # Handle redirect?
  142. redirect_location = redirect and http_response.get_redirect_location()
  143. if redirect_location:
  144. # Check for redirect response
  145. if (self.urlfetch_retries and retries.raise_on_redirect):
  146. raise MaxRetryError(self, url, "too many redirects")
  147. else:
  148. if http_response.status == 303:
  149. method = 'GET'
  150. try:
  151. retries = retries.increment(method, url, response=http_response, _pool=self)
  152. except MaxRetryError:
  153. if retries.raise_on_redirect:
  154. raise MaxRetryError(self, url, "too many redirects")
  155. return http_response
  156. retries.sleep_for_retry(http_response)
  157. log.debug("Redirecting %s -> %s", url, redirect_location)
  158. redirect_url = urljoin(url, redirect_location)
  159. return self.urlopen(
  160. method, redirect_url, body, headers,
  161. retries=retries, redirect=redirect,
  162. timeout=timeout, **response_kw)
  163. # Check if we should retry the HTTP response.
  164. has_retry_after = bool(http_response.getheader('Retry-After'))
  165. if retries.is_retry(method, http_response.status, has_retry_after):
  166. retries = retries.increment(
  167. method, url, response=http_response, _pool=self)
  168. log.debug("Retry: %s", url)
  169. retries.sleep(http_response)
  170. return self.urlopen(
  171. method, url,
  172. body=body, headers=headers,
  173. retries=retries, redirect=redirect,
  174. timeout=timeout, **response_kw)
  175. return http_response
  176. def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw):
  177. if is_prod_appengine():
  178. # Production GAE handles deflate encoding automatically, but does
  179. # not remove the encoding header.
  180. content_encoding = urlfetch_resp.headers.get('content-encoding')
  181. if content_encoding == 'deflate':
  182. del urlfetch_resp.headers['content-encoding']
  183. transfer_encoding = urlfetch_resp.headers.get('transfer-encoding')
  184. # We have a full response's content,
  185. # so let's make sure we don't report ourselves as chunked data.
  186. if transfer_encoding == 'chunked':
  187. encodings = transfer_encoding.split(",")
  188. encodings.remove('chunked')
  189. urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings)
  190. original_response = HTTPResponse(
  191. # In order for decoding to work, we must present the content as
  192. # a file-like object.
  193. body=BytesIO(urlfetch_resp.content),
  194. msg=urlfetch_resp.header_msg,
  195. headers=urlfetch_resp.headers,
  196. status=urlfetch_resp.status_code,
  197. **response_kw
  198. )
  199. return HTTPResponse(
  200. body=BytesIO(urlfetch_resp.content),
  201. headers=urlfetch_resp.headers,
  202. status=urlfetch_resp.status_code,
  203. original_response=original_response,
  204. **response_kw
  205. )
  206. def _get_absolute_timeout(self, timeout):
  207. if timeout is Timeout.DEFAULT_TIMEOUT:
  208. return None # Defer to URLFetch's default.
  209. if isinstance(timeout, Timeout):
  210. if timeout._read is not None or timeout._connect is not None:
  211. warnings.warn(
  212. "URLFetch does not support granular timeout settings, "
  213. "reverting to total or default URLFetch timeout.",
  214. AppEnginePlatformWarning)
  215. return timeout.total
  216. return timeout
  217. def _get_retries(self, retries, redirect):
  218. if not isinstance(retries, Retry):
  219. retries = Retry.from_int(
  220. retries, redirect=redirect, default=self.retries)
  221. if retries.connect or retries.read or retries.redirect:
  222. warnings.warn(
  223. "URLFetch only supports total retries and does not "
  224. "recognize connect, read, or redirect retry parameters.",
  225. AppEnginePlatformWarning)
  226. return retries
  227. def is_appengine():
  228. return (is_local_appengine() or
  229. is_prod_appengine() or
  230. is_prod_appengine_mvms())
  231. def is_appengine_sandbox():
  232. return is_appengine() and not is_prod_appengine_mvms()
  233. def is_local_appengine():
  234. return ('APPENGINE_RUNTIME' in os.environ and
  235. 'Development/' in os.environ['SERVER_SOFTWARE'])
  236. def is_prod_appengine():
  237. return ('APPENGINE_RUNTIME' in os.environ and
  238. 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and
  239. not is_prod_appengine_mvms())
  240. def is_prod_appengine_mvms():
  241. return os.environ.get('GAE_VM', False) == 'true'