Skip to content

Commit 8993eb9

Browse files
author
Cary Cheng
authored
Implement Basic Proxy Support(#407)
1 parent c8c8a2e commit 8993eb9

File tree

6 files changed

+146
-6
lines changed

6 files changed

+146
-6
lines changed

boxsdk/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ class Client(object):
2626
py_version.minor,
2727
py_version.micro,
2828
)
29+
30+
31+
class Proxy(object):
32+
URL = None
33+
AUTH = None

boxsdk/network/default_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616

1717
class DefaultNetwork(Network):
18-
"""Implementats the network interface using the requests library."""
18+
"""Implements the network interface using the requests library."""
1919

2020
LOGGER_NAME = 'boxsdk.network'
2121
REQUEST_FORMAT = '\x1b[36m%(method)s %(url)s %(request_kwargs)s\x1b[0m'

boxsdk/session/session.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
from functools import partial
99
from logging import getLogger
1010

11+
from boxsdk.exception import BoxException
1112
from .box_request import BoxRequest as _BoxRequest
1213
from .box_response import BoxResponse as _BoxResponse
13-
from ..config import API, Client
14+
from ..config import API, Client, Proxy
1415
from ..exception import BoxAPIException
1516
from ..network.default_network import DefaultNetwork
1617
from ..util.json import is_json_response
@@ -35,6 +36,7 @@ def __init__(
3536
default_network_request_kwargs=None,
3637
api_config=None,
3738
client_config=None,
39+
proxy_config=None,
3840
):
3941
"""
4042
:param network_layer:
@@ -64,11 +66,16 @@ def __init__(
6466
Object containing client information, including user agent string.
6567
:type client_config:
6668
:class:`Client`
69+
:param proxy_config:
70+
Object containing proxy information.
71+
:type proxy_config:
72+
:class:`Proxy` or None
6773
"""
6874
if translator is None:
6975
translator = Translator(extend_default_translator=True, new_child=True)
7076
self._api_config = api_config or API()
7177
self._client_config = client_config or Client()
78+
self._proxy_config = proxy_config or Proxy()
7279
super(Session, self).__init__()
7380
self._network_layer = network_layer or DefaultNetwork()
7481
self._default_headers = {
@@ -184,6 +191,14 @@ def client_config(self):
184191
"""
185192
return self._client_config
186193

194+
@property
195+
def proxy_config(self):
196+
"""
197+
198+
:rtype: :class:`Proxy`
199+
"""
200+
return self._proxy_config
201+
187202
def get_url(self, endpoint, *args):
188203
"""
189204
Return the URL for the given Box API endpoint.
@@ -211,6 +226,7 @@ def get_constructor_kwargs(self):
211226
default_network_request_kwargs=self._default_network_request_kwargs.copy(),
212227
api_config=self._api_config,
213228
client_config=self._client_config,
229+
proxy_config=self._proxy_config,
214230
default_headers=self._default_headers.copy(),
215231
)
216232

@@ -425,6 +441,34 @@ def _get_retry_request_callable(self, network_response, attempt_number, request)
425441
def _get_request_headers(self):
426442
return self._default_headers.copy()
427443

444+
def _prepare_proxy(self):
445+
"""
446+
Prepares basic authenticated and unauthenticated proxies for requests.
447+
448+
:return:
449+
A prepared proxy dict to send along with the request. None if incorrect parameters were passed.
450+
:rtype:
451+
`dict` or None
452+
"""
453+
proxy = {}
454+
proxy_string = ''
455+
if self._proxy_config.URL is None:
456+
return None
457+
if self._proxy_config.AUTH and {'user', 'password'} <= set(self._proxy_config.AUTH):
458+
host = self._proxy_config.URL
459+
address = host.split('//')[1]
460+
proxy_string = 'http://{0}:{1}@{2}'.format(self._proxy_config.AUTH.get('user', None),
461+
self._proxy_config.AUTH.get('password', None),
462+
address)
463+
elif self._proxy_config.AUTH is None:
464+
proxy_string = self._proxy_config.URL
465+
else:
466+
raise BoxException("The proxy auth dict you provided does not match pattern {'user': 'example_user', 'password': 'example_password'}")
467+
proxy['http'] = proxy_string
468+
proxy['https'] = proxy['http']
469+
470+
return proxy
471+
428472
def _send_request(self, request, **kwargs):
429473
"""
430474
Make a request to the Box API.
@@ -443,6 +487,9 @@ def _send_request(self, request, **kwargs):
443487
files, file_stream_positions = kwargs.get('files'), kwargs.pop('file_stream_positions')
444488
request_kwargs = self._default_network_request_kwargs.copy()
445489
request_kwargs.update(kwargs)
490+
proxy_dict = self._prepare_proxy()
491+
if proxy_dict is not None:
492+
request_kwargs.update({'proxies': proxy_dict})
446493
if files and file_stream_positions:
447494
for name, position in file_stream_positions.items():
448495
files[name][1].seek(position)

docs/usage/configuration.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Configuration
2+
=============
3+
4+
The Python SDK has helpful custom config that you can set for a variety of use cases.
5+
6+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
7+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
8+
9+
10+
- [Proxy](#proxy)
11+
- [Unauthenticated Proxy](#unauthenticated-proxy)
12+
- [Basic Authentication Proxy](#basic-authentication-proxy)
13+
14+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
15+
16+
Proxy
17+
-----
18+
19+
### Unauthenticated Proxy
20+
21+
In order to set up configuration for basic proxy with the Python SDK, simply specify the proxy address for the `PROXY.URL` field.
22+
23+
```python
24+
from boxsdk.config import PROXY
25+
PROXY.URL = 'http://example-proxy-address.com'
26+
```
27+
28+
### Basic Authentication Proxy
29+
30+
The Python SDK also lets you set an authenticated proxy. To do this specify the `user` and `password` fields and pass set that on the `PROXY.AUTH` field.
31+
32+
```python
33+
from boxsdk.config import PROXY
34+
PROXY.AUTH = {
35+
'user': 'test_user',
36+
'password': 'test_password',
37+
}
38+
```
39+

test/unit/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from mock import Mock, MagicMock
99
import pytest
1010

11-
from boxsdk.config import API, Client
11+
from boxsdk.config import API, Client, Proxy
1212
from boxsdk.network import default_network
1313
from boxsdk.network.default_network import DefaultNetworkResponse, DefaultNetwork
1414
from boxsdk.session.box_response import BoxResponse
@@ -50,6 +50,7 @@ def mock_box_session(translator):
5050
# pylint:disable=protected-access
5151
mock_session._api_config = mock_session.api_config = API()
5252
mock_session._client_config = mock_session.client_config = Client()
53+
mock_session._proxy_config = mock_session.proxy_config = Proxy()
5354
# pylint:enable=protected-access
5455
mock_session.get_url.side_effect = lambda *args, **kwargs: Session.get_url(mock_session, *args, **kwargs)
5556
mock_session.translator = translator
@@ -62,6 +63,7 @@ def mock_box_session_2(translator):
6263
# pylint:disable=protected-access
6364
mock_session._api_config = API()
6465
mock_session._client_config = Client()
66+
mock_session._proxy_config = Proxy()
6567
# pylint:enable=protected-access
6668
mock_session.get_url.side_effect = lambda *args, **kwargs: Session.get_url(mock_session, *args, **kwargs)
6769
mock_session.translator = translator

test/unit/session/test_session.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from io import IOBase
77
from numbers import Number
88

9-
from mock import MagicMock, Mock, PropertyMock, call, patch
9+
from mock import MagicMock, Mock, PropertyMock, call, patch, ANY
1010
import pytest
1111

1212
from boxsdk.auth.oauth2 import OAuth2
13-
from boxsdk.config import API
14-
from boxsdk.exception import BoxAPIException
13+
from boxsdk.config import API, Proxy
14+
from boxsdk.exception import BoxAPIException, BoxException
1515
from boxsdk.network.default_network import DefaultNetwork, DefaultNetworkResponse
1616
from boxsdk.session.box_response import BoxResponse
1717
from boxsdk.session.session import Session, Translator, AuthorizedSession
@@ -293,3 +293,50 @@ def test_get_retry_after_time(box_session, attempt_number, retry_after_header, e
293293
retry_time = box_session._get_retry_after_time(attempt_number, retry_after_header) # pylint: disable=protected-access
294294
retry_time = round(retry_time, 4)
295295
assert retry_time == expected_result
296+
297+
298+
@pytest.mark.parametrize(
299+
'test_proxy_url,test_proxy_auth,expected_proxy_dict',
300+
[
301+
('http://example-proxy.com', {'user': 'test_user', 'password': 'test_password', },
302+
{'http': 'http://test_user:[email protected]', 'https': 'http://test_user:[email protected]'}),
303+
('http://example-proxy.com', None, {'http': 'http://example-proxy.com', 'https': 'http://example-proxy.com'}),
304+
]
305+
)
306+
def test_proxy_attaches_to_request_correctly(
307+
box_session,
308+
monkeypatch,
309+
mock_network_layer,
310+
generic_successful_response,
311+
test_proxy_url, test_proxy_auth,
312+
expected_proxy_dict):
313+
monkeypatch.setattr(Proxy, 'URL', test_proxy_url)
314+
monkeypatch.setattr(Proxy, 'AUTH', test_proxy_auth)
315+
mock_network_layer.request.side_effect = [generic_successful_response]
316+
box_session.request('GET', test_proxy_url)
317+
mock_network_layer.request.assert_called_once_with(
318+
'GET',
319+
test_proxy_url,
320+
access_token='fake_access_token',
321+
headers=ANY,
322+
proxies=expected_proxy_dict,
323+
)
324+
325+
326+
def test_proxy_malformed_dict_does_not_attach(box_session, monkeypatch, mock_network_layer, generic_successful_response):
327+
test_proxy_url = 'http://example.com'
328+
test_proxy_auth = {
329+
'foo': 'bar',
330+
}
331+
monkeypatch.setattr(Proxy, 'URL', test_proxy_url)
332+
monkeypatch.setattr(Proxy, 'AUTH', test_proxy_auth)
333+
mock_network_layer.request.side_effect = [generic_successful_response]
334+
with pytest.raises(BoxException) as exc_info:
335+
box_session.request('GET', test_proxy_url)
336+
assert isinstance(exc_info.value, BoxException)
337+
assert exc_info.value.args[0] == "The proxy auth dict you provided does not match pattern " \
338+
"{'user': 'example_user', 'password': 'example_password'}"
339+
340+
341+
def test_proxy_network_config_property(box_session):
342+
assert isinstance(box_session.proxy_config, Proxy)

0 commit comments

Comments
 (0)