Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a monkeypatch() function that ensures all Sessions use the AppEngineAdapter compatibility class. #119

Merged
merged 1 commit into from
Jan 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion requests_toolbelt/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@
from urllib3 import filepost
from urllib3 import poolmanager

if requests.__build__ < 0x020800:
if requests.__build__ < 0x020300:
timeout = None
else:
try:
from requests.packages.urllib3.util import timeout
except ImportError:
from urllib3.util import timeout

if requests.__build__ < 0x021000:
gaecontrib = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah we already have something like that here. We also need to update the version of urllib3 here because we're only going to properly support this for versions >= 0x021000

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiousity, where do you find the build information. When I do requests.__build__ on the latest version of requests (2.9.1) I get 133377. Also curious what build signifies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonathan-s 133377 == 0x020901

else:
try:
Expand Down Expand Up @@ -282,6 +290,7 @@ def from_httplib(cls, message): # Python 2
'fields',
'filepost',
'poolmanager',
'timeout',
'HTTPHeaderDict',
'queue',
'urlencode',
Expand Down
136 changes: 113 additions & 23 deletions requests_toolbelt/adapters/appengine.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,139 @@
# -*- coding: utf-8 -*-
"""The App Engine Transport Adapter for requests.

This requires a version of requests >= 2.8.0.
This requires a version of requests >= 2.10.0.
"""
import requests
from requests import adapters
from requests import sessions

from .. import exceptions as exc
from .._compat import gaecontrib
from .._compat import timeout

"""
.. versionadded:: 0.6.0

There are two ways to use this library.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to document that it was added in v0.6.0 of the toolbelt and that this is a submodule.

.. versionadded:: 0.6.0


If you're using requests directly, you can use code like:
>>> import requests
>>> import ssl
>>> from requests.packages.urllib3.contrib import appengine as ul_appengine
>>> from requests_toolbelt.adapters import appengine
>>> s = requests.Session()
>>> if ul_appengine.is_appengine_sandbox():
... s.mount('http://', appengine.AppEngineAdapter())
... s.mount('https://', appengine.AppEngineAdapter())

If you depend on external libraries which use requests, you can use code like:
>>> from requests_toolbelt.adapters import appengine
>>> appengine.monkeypatch()
which will ensure all requests.Session objects use AppEngineAdapter properly.
"""


class AppEngineAdapter(adapters.HTTPAdapter):
"""A transport adapter for Requests to use urllib3's GAE support.

Implements request's HTTPAdapter API.

When deploying to Google's App Engine service, some of Requests'
functionality is broken. There is underlying support for GAE in urllib3.
This functionality, however, is opt-in and needs to be enabled explicitly
for Requests to be able to use it.

Example usage:

.. code-block:: python

>>> import requests
>>> import ssl
>>> from requests_toolbelt.adapters import appengine
>>> s = requests.Session()
>>> if using_appengine():
... s.mount('https://', appengine.AppEngineAdapter())
...
>>>
"""

def __init__(self, validate_certificate=True, *args, **kwargs):
if gaecontrib is None:
raise exc.VersionMismatchError(
"The toolbelt requires at least Requests 2.8.0 to be "
"installed. Version {0} was found instead.".format(
requests.__version__
)
)
_check_version()
self._validate_certificate = validate_certificate
super(AppEngineAdapter, self).__init__(self, *args, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a check that raises an exception for versions of requests < 2.10.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, why does it need the check? Is this how we would check that urllib3/urllib3#763 had been merged properly into requests library?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is why we need that check.


def init_poolmanager(self, connections, maxsize, block=False):
self.poolmanager = gaecontrib.AppEngineManager(
validate_certificate=self._validate_certificate
self.poolmanager = _AppEnginePoolManager(self._validate_certificate)


class _AppEnginePoolManager(object):
"""Implements urllib3's PoolManager API expected by requests.

While a real PoolManager map hostnames to reusable Connections,
AppEngine has no concept of a reusable connection to a host.
So instead, this class constructs a small Connection per request,
that is returned to the Adapter and used to access the URL.
"""

def __init__(self, validate_certificate=True):
self.appengine_manager = gaecontrib.AppEngineManager(
validate_certificate=validate_certificate)

def connection_from_url(self, url):
return _AppEngineConnection(self.appengine_manager, url)

def clear(self):
pass


class _AppEngineConnection(object):
"""Implements urllib3's HTTPConnectionPool API's urlopen().

This Connection's urlopen() is called with a host-relative path,
so in order to properly support opening the URL, we need to store
the full URL when this Connection is constructed from the PoolManager.

This code wraps AppEngineManager.urlopen(), which exposes a different
API than in the original urllib3 urlopen(), and thus needs this adapter.
"""

def __init__(self, appengine_manager, url):
self.appengine_manager = appengine_manager
self.url = url

def urlopen(self, method, url, body=None, headers=None, retries=None,
redirect=True, assert_same_host=True,
timeout=timeout.Timeout.DEFAULT_TIMEOUT,
pool_timeout=None, release_conn=None, **response_kw):
# This function's url argument is a host-relative URL,
# but the AppEngineManager expects an absolute URL.
# So we saved out the self.url when the AppEngineConnection
# was constructed, which we then can use down below instead.
# Let's verify our assumptions here though, just in case.
assert self.url.endswith(url), (
"AppEngineConnection constructed "
"with (%s), and called urlopen with (%s). Expected the latter "
"to be the host-relative equivalent of the former." %
(self.url, url))

# Jump through the hoops necessary to call AppEngineManager's API.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document why this is necessary? See earlier comment along the same lines.

return self.appengine_manager.urlopen(
method,
self.url,
body=body,
headers=headers,
retries=retries,
redirect=redirect,
timeout=timeout,
**response_kw)


def monkeypatch():
"""Sets up all Sessions to use AppEngineAdapter by default.

If you don't want to deal with configuring your own Sessions,
or if you use libraries that use requests directly (ie requests.post),
then you may prefer to monkeypatch and auto-configure all Sessions.
"""
_check_version()
# HACK: We should consider modifying urllib3 to support this cleanly,
# so that we can set a module-level variable in the sessions module,
# instead of overriding an imported HTTPAdapter as is done here.
sessions.HTTPAdapter = AppEngineAdapter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function also needs to check requests' version before doing anything.



def _check_version():
if gaecontrib is None:
raise exc.VersionMismatchError(
"The toolbelt requires at least Requests 2.10.0 to be "
"installed. Version {0} was found instead.".format(
requests.__version__
)
)