-
Notifications
You must be signed in to change notification settings - Fork 309
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
Add requests transport #66
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
google.auth.transport.requests module | ||
===================================== | ||
|
||
.. automodule:: google.auth.transport.requests | ||
:members: | ||
:inherited-members: | ||
:show-inheritance: |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,5 +11,6 @@ Submodules | |
|
||
.. toctree:: | ||
|
||
google.auth.transport.requests | ||
google.auth.transport.urllib3 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# Copyright 2016 Google Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Transport adapter for Requests.""" | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
|
||
from __future__ import absolute_import | ||
|
||
import logging | ||
|
||
|
||
import requests | ||
import requests.exceptions | ||
|
||
from google.auth import exceptions | ||
from google.auth import transport | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class _Response(transport.Response): | ||
"""Requests transport response adapter. | ||
|
||
Args: | ||
response (requests.Response): The raw Requests response. | ||
""" | ||
def __init__(self, response): | ||
self._response = response | ||
|
||
@property | ||
def status(self): | ||
return self._response.status_code | ||
|
||
@property | ||
def headers(self): | ||
return self._response.headers | ||
|
||
@property | ||
def data(self): | ||
return self._response.content | ||
|
||
|
||
class Request(transport.Request): | ||
"""Requests request adapter. | ||
|
||
This class is used internally for making requests using various transports | ||
in a consistent way. If you use :class:`AuthorizedSession` you do not need | ||
to construct or use this class directly. | ||
|
||
This class can be useful if you want to manually refresh a | ||
:class:`~google.auth.credentials.Credentials` instance:: | ||
|
||
import google.auth.transport.requests | ||
import requests | ||
|
||
request = google.auth.transport.requests.Request() | ||
|
||
credentials.refresh(request) | ||
|
||
Args: | ||
session (requests.Session): An instance :class:`requests.Session` used | ||
to make HTTP requests. If not specified, a session will be created. | ||
|
||
.. automethod:: __call__ | ||
""" | ||
def __init__(self, session=None): | ||
if not session: | ||
session = requests.Session() | ||
|
||
self.session = session | ||
|
||
def __call__(self, url, method='GET', body=None, headers=None, | ||
timeout=None, **kwargs): | ||
"""Make an HTTP request using requests. | ||
|
||
Args: | ||
url (str): The URI to be requested. | ||
method (str): The HTTP method to use for the request. Defaults | ||
to 'GET'. | ||
body (bytes): The payload / body in HTTP request. | ||
headers (Mapping[str, str]): Request headers. | ||
timeout (Optional[int]): The number of seconds to wait for a | ||
response from the server. If not specified or if None, the | ||
requests default timeout will be used. | ||
kwargs: Additional arguments passed through to the underlying | ||
requests :meth:`~requests.Session.request` method. | ||
|
||
Returns: | ||
google.auth.transport.Response: The HTTP response. | ||
|
||
Raises: | ||
google.auth.exceptions.TransportError: If any exception occurred. | ||
""" | ||
try: | ||
_LOGGER.debug('Making request: %s %s', method, url) | ||
response = self.session.request( | ||
method, url, data=body, headers=headers, timeout=timeout, | ||
**kwargs) | ||
return _Response(response) | ||
except requests.exceptions.RequestException as exc: | ||
raise exceptions.TransportError(exc) | ||
|
||
|
||
class AuthorizedSession(requests.Session): | ||
"""A Requests Session class with credentials. | ||
|
||
This class is used to perform requests to API endpoints that require | ||
authorization:: | ||
|
||
from google.auth.transport.requests import AuthorizedSession | ||
|
||
authed_session = AuthorizedSession(credentials) | ||
|
||
response = authed_session.request( | ||
'GET', 'https://www.googleapis.com/storage/v1/b') | ||
|
||
The underlying :meth:`request` implementation handles adding the | ||
credentials' headers to the request and refreshing credentials as needed. | ||
|
||
Args: | ||
credentials (google.auth.credentials.Credentials): The credentials to | ||
add to the request. | ||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate | ||
that credentials should be refreshed and the request should be | ||
retried. | ||
max_refresh_attempts (int): The maximum number of times to attempt to | ||
refresh the credentials and retry the request. | ||
kwargs: Additional arguments passed to the :class:`requests.Session` | ||
constructor. | ||
""" | ||
def __init__(self, credentials, | ||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, | ||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, | ||
**kwargs): | ||
super(AuthorizedSession, self).__init__(**kwargs) | ||
self.credentials = credentials | ||
self._refresh_status_codes = refresh_status_codes | ||
self._max_refresh_attempts = max_refresh_attempts | ||
# Request instance used by internal methods (for example, | ||
# credentials.refresh). | ||
# Do not pass `self` as the session here, as it can lead to infinite | ||
# recursion. | ||
self._auth_request = Request() | ||
|
||
def request(self, method, url, data=None, headers=None, **kwargs): | ||
"""Implementation of Requests' request.""" | ||
|
||
# Use a kwarg for this instead of an attribute to maintain | ||
# thread-safety. | ||
_credential_refresh_attempt = kwargs.pop( | ||
'_credential_refresh_attempt', 0) | ||
|
||
# Make a copy of the headers. They will be modified by the credentials | ||
# and we want to pass the original headers if we recurse. | ||
request_headers = headers.copy() if headers is not None else {} | ||
|
||
self.credentials.before_request( | ||
self._auth_request, method, url, request_headers) | ||
|
||
response = super(AuthorizedSession, self).request( | ||
method, url, data=data, headers=request_headers, **kwargs) | ||
|
||
# If the response indicated that the credentials needed to be | ||
# refreshed, then refresh the credentials and re-attempt the | ||
# request. | ||
# A stored token may expire between the time it is retrieved and | ||
# the time the request is made, so we may need to try twice. | ||
if (response.status_code in self._refresh_status_codes | ||
and _credential_refresh_attempt < self._max_refresh_attempts): | ||
|
||
_LOGGER.info( | ||
'Refreshing credentials due to a %s response. Attempt %s/%s.', | ||
response.status_code, _credential_refresh_attempt + 1, | ||
self._max_refresh_attempts) | ||
|
||
self.credentials.refresh(self._auth_request) | ||
|
||
# Recurse. Pass in the original headers, not our modified set. | ||
return self.request( | ||
method, url, data=data, headers=headers, | ||
_credential_refresh_attempt=_credential_refresh_attempt + 1, | ||
**kwargs) | ||
|
||
return response |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# Copyright 2016 Google Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import mock | ||
import requests | ||
import requests.adapters | ||
from six.moves import http_client | ||
|
||
import google.auth.transport.requests | ||
from tests.transport import compliance | ||
|
||
|
||
class TestRequestResponse(compliance.RequestResponseTests): | ||
def make_request(self): | ||
return google.auth.transport.requests.Request() | ||
|
||
def test_timeout(self): | ||
http = mock.Mock() | ||
request = google.auth.transport.requests.Request(http) | ||
request(url='http://example.com', method='GET', timeout=5) | ||
|
||
assert http.request.call_args[1]['timeout'] == 5 | ||
|
||
|
||
class MockCredentials(object): | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
def __init__(self, token='token'): | ||
self.token = token | ||
|
||
def apply(self, headers): | ||
headers['authorization'] = self.token | ||
|
||
def before_request(self, request, method, url, headers): | ||
self.apply(headers) | ||
|
||
def refresh(self, request): | ||
self.token += '1' | ||
|
||
|
||
class MockAdapter(requests.adapters.BaseAdapter): | ||
def __init__(self, responses, headers=None): | ||
self.responses = responses | ||
self.requests = [] | ||
self.headers = headers or {} | ||
|
||
def send(self, request, **kwargs): | ||
self.requests.append(request) | ||
return self.responses.pop(0) | ||
|
||
|
||
def make_response(status=http_client.OK, data=None): | ||
response = requests.Response() | ||
response.status_code = status | ||
response._content = data | ||
return response | ||
|
||
|
||
class TestAuthorizedHttp(object): | ||
TEST_URL = 'http://example.com/' | ||
|
||
def test_constructor(self): | ||
authed_session = google.auth.transport.requests.AuthorizedSession( | ||
mock.sentinel.credentials) | ||
|
||
assert authed_session.credentials == mock.sentinel.credentials | ||
|
||
def test_request_no_refresh(self): | ||
mock_credentials = mock.Mock(wraps=MockCredentials()) | ||
mock_response = make_response() | ||
mock_adapter = MockAdapter([mock_response]) | ||
|
||
authed_session = google.auth.transport.requests.AuthorizedSession( | ||
mock_credentials) | ||
authed_session.mount(self.TEST_URL, mock_adapter) | ||
|
||
response = authed_session.request('GET', self.TEST_URL) | ||
|
||
assert response == mock_response | ||
assert mock_credentials.before_request.called | ||
assert not mock_credentials.refresh.called | ||
assert len(mock_adapter.requests) == 1 | ||
assert mock_adapter.requests[0].url == self.TEST_URL | ||
assert mock_adapter.requests[0].headers['authorization'] == 'token' | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
|
||
def test_request_refresh(self): | ||
mock_credentials = mock.Mock(wraps=MockCredentials()) | ||
mock_final_response = make_response(status=http_client.OK) | ||
# First request will 401, second request will succeed. | ||
mock_adapter = MockAdapter([ | ||
make_response(status=http_client.UNAUTHORIZED), | ||
mock_final_response]) | ||
|
||
authed_session = google.auth.transport.requests.AuthorizedSession( | ||
mock_credentials) | ||
authed_session.mount(self.TEST_URL, mock_adapter) | ||
|
||
response = authed_session.request('GET', self.TEST_URL) | ||
|
||
assert response == mock_final_response | ||
assert mock_credentials.before_request.call_count == 2 | ||
assert mock_credentials.refresh.called | ||
assert len(mock_adapter.requests) == 2 | ||
|
||
assert mock_adapter.requests[0].url == self.TEST_URL | ||
assert mock_adapter.requests[0].headers['authorization'] == 'token' | ||
|
||
assert mock_adapter.requests[1].url == self.TEST_URL | ||
assert mock_adapter.requests[1].headers['authorization'] == 'token1' |
This comment was marked as spam.
Sorry, something went wrong.