This repository was archived by the owner on Mar 6, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 351
Add requests transport #66
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,5 +11,6 @@ Submodules | |
|
|
||
| .. toctree:: | ||
|
|
||
| google.auth.transport.requests | ||
| google.auth.transport.urllib3 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This comment was marked as spam.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.