Skip to content

Commit 4e18853

Browse files
committed
Merge branch 'DLPLAT-352-connection-module-change' into devel
* DLPLAT-352-connection-module-change: fix lint add test for session reuse remove debug print fix lint change module naming, fix test change initialization flow to allow configurate reuse adapter change connection from class to module Signed-off-by: Jamie Couture <[email protected]>
2 parents d8af6f1 + 9bc8dd0 commit 4e18853

16 files changed

+183
-160
lines changed

nasdaqdatalink/connection.py

Lines changed: 108 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -14,107 +14,116 @@
1414
AuthenticationError, ForbiddenError, InvalidRequestError,
1515
NotFoundError, ServiceUnavailableError)
1616

17+
# global session
18+
session = None
1719

18-
class Connection:
19-
@classmethod
20-
def request(cls, http_verb, url, **options):
21-
if 'headers' in options:
22-
headers = options['headers']
20+
21+
def request(http_verb, url, **options):
22+
if 'headers' in options:
23+
headers = options['headers']
24+
else:
25+
headers = {}
26+
27+
accept_value = 'application/json'
28+
if ApiConfig.api_version:
29+
accept_value += ", application/vnd.data.nasdaq+json;version=%s" % ApiConfig.api_version
30+
31+
headers = Util.merge_to_dicts({
32+
'accept': accept_value,
33+
'request-source': 'python',
34+
'request-source-version': VERSION
35+
}, headers)
36+
if ApiConfig.api_key:
37+
headers = Util.merge_to_dicts({'x-api-token': ApiConfig.api_key}, headers)
38+
39+
options['headers'] = headers
40+
41+
abs_url = '%s/%s' % (ApiConfig.api_base, url)
42+
43+
return execute_request(http_verb, abs_url, **options)
44+
45+
46+
def execute_request(http_verb, url, **options):
47+
session = get_session()
48+
49+
try:
50+
response = session.request(
51+
method=http_verb,
52+
url=url,
53+
verify=ApiConfig.verify_ssl,
54+
**options
55+
)
56+
if response.status_code < 200 or response.status_code >= 300:
57+
handle_api_error(response)
2358
else:
24-
headers = {}
25-
26-
accept_value = 'application/json'
27-
if ApiConfig.api_version:
28-
accept_value += ", application/vnd.data.nasdaq+json;version=%s" % ApiConfig.api_version
29-
30-
headers = Util.merge_to_dicts({'accept': accept_value,
31-
'request-source': 'python',
32-
'request-source-version': VERSION}, headers)
33-
if ApiConfig.api_key:
34-
headers = Util.merge_to_dicts({'x-api-token': ApiConfig.api_key}, headers)
35-
36-
options['headers'] = headers
37-
38-
abs_url = '%s/%s' % (ApiConfig.api_base, url)
39-
40-
return cls.execute_request(http_verb, abs_url, **options)
41-
42-
@classmethod
43-
def execute_request(cls, http_verb, url, **options):
44-
session = cls.get_session()
45-
46-
try:
47-
response = session.request(method=http_verb,
48-
url=url,
49-
verify=ApiConfig.verify_ssl,
50-
**options)
51-
if response.status_code < 200 or response.status_code >= 300:
52-
cls.handle_api_error(response)
53-
else:
54-
return response
55-
except requests.exceptions.RequestException as e:
56-
if e.response:
57-
cls.handle_api_error(e.response)
58-
raise e
59-
60-
@classmethod
61-
def get_session(cls):
62-
session = requests.Session()
63-
adapter = HTTPAdapter(max_retries=cls.get_retries())
64-
session.mount(ApiConfig.api_protocol, adapter)
65-
66-
proxies = urllib.request.getproxies()
67-
if proxies is not None:
68-
session.proxies.update(proxies)
59+
return response
60+
except requests.exceptions.RequestException as e:
61+
if e.response:
62+
handle_api_error(e.response)
63+
raise e
64+
65+
66+
def get_retries():
67+
if not ApiConfig.use_retries:
68+
return Retry(total=0)
69+
70+
Retry.BACKOFF_MAX = ApiConfig.max_wait_between_retries
71+
retries = Retry(total=ApiConfig.number_of_retries,
72+
connect=ApiConfig.number_of_retries,
73+
read=ApiConfig.number_of_retries,
74+
status_forcelist=ApiConfig.retry_status_codes,
75+
backoff_factor=ApiConfig.retry_backoff_factor,
76+
raise_on_status=False)
6977

78+
return retries
79+
80+
81+
def get_session():
82+
global session
83+
if session is not None:
7084
return session
7185

72-
@classmethod
73-
def get_retries(cls):
74-
if not ApiConfig.use_retries:
75-
return Retry(total=0)
76-
77-
Retry.BACKOFF_MAX = ApiConfig.max_wait_between_retries
78-
retries = Retry(total=ApiConfig.number_of_retries,
79-
connect=ApiConfig.number_of_retries,
80-
read=ApiConfig.number_of_retries,
81-
status_forcelist=ApiConfig.retry_status_codes,
82-
backoff_factor=ApiConfig.retry_backoff_factor,
83-
raise_on_status=False)
84-
85-
return retries
86-
87-
@classmethod
88-
def parse(cls, response):
89-
try:
90-
return response.json()
91-
except ValueError:
92-
raise DataLinkError(http_status=response.status_code, http_body=response.text)
93-
94-
@classmethod
95-
def handle_api_error(cls, resp):
96-
error_body = cls.parse(resp)
97-
98-
# if our app does not form a proper data_link_error response
99-
# throw generic error
100-
if 'error' not in error_body:
101-
raise DataLinkError(http_status=resp.status_code, http_body=resp.text)
102-
103-
code = error_body['error']['code']
104-
message = error_body['error']['message']
105-
prog = re.compile('^QE([a-zA-Z])x')
106-
if prog.match(code):
107-
code_letter = prog.match(code).group(1)
108-
109-
d_klass = {
110-
'L': LimitExceededError,
111-
'M': InternalServerError,
112-
'A': AuthenticationError,
113-
'P': ForbiddenError,
114-
'S': InvalidRequestError,
115-
'C': NotFoundError,
116-
'X': ServiceUnavailableError
117-
}
118-
klass = d_klass.get(code_letter, DataLinkError)
119-
120-
raise klass(message, resp.status_code, resp.text, resp.headers, code)
86+
session = requests.Session()
87+
adapter = HTTPAdapter(max_retries=get_retries())
88+
session.mount(ApiConfig.api_protocol, adapter)
89+
90+
proxies = urllib.request.getproxies()
91+
if proxies is not None:
92+
session.proxies.update(proxies)
93+
94+
return session
95+
96+
97+
def parse(response):
98+
try:
99+
return response.json()
100+
except ValueError:
101+
raise DataLinkError(http_status=response.status_code, http_body=response.text)
102+
103+
104+
def handle_api_error(resp):
105+
error_body = parse(resp)
106+
107+
# if our app does not form a proper data_link_error response
108+
# throw generic error
109+
if 'error' not in error_body:
110+
raise DataLinkError(http_status=resp.status_code, http_body=resp.text)
111+
112+
code = error_body['error']['code']
113+
message = error_body['error']['message']
114+
prog = re.compile('^QE([a-zA-Z])x')
115+
if prog.match(code):
116+
code_letter = prog.match(code).group(1)
117+
118+
d_klass = {
119+
'L': LimitExceededError,
120+
'M': InternalServerError,
121+
'A': AuthenticationError,
122+
'P': ForbiddenError,
123+
'S': InvalidRequestError,
124+
'C': NotFoundError,
125+
'X': ServiceUnavailableError
126+
}
127+
klass = d_klass.get(code_letter, DataLinkError)
128+
129+
raise klass(message, resp.status_code, resp.text, resp.headers, code)

nasdaqdatalink/model/database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import nasdaqdatalink.model.dataset
66
from nasdaqdatalink.api_config import ApiConfig
7-
from nasdaqdatalink.connection import Connection
7+
import nasdaqdatalink.connection as connection
88
from nasdaqdatalink.errors.data_link_error import DataLinkError
99
from nasdaqdatalink.message import Message
1010
from nasdaqdatalink.operations.get import GetOperation
@@ -43,7 +43,7 @@ def bulk_download_to_file(self, file_or_folder_path, **options):
4343
path_url = self._bulk_download_path()
4444

4545
options['stream'] = True
46-
r = Connection.request('get', path_url, **options)
46+
r = connection.request('get', path_url, **options)
4747
file_path = file_or_folder_path
4848
if os.path.isdir(file_or_folder_path):
4949
file_path = file_or_folder_path + '/' + os.path.basename(urlparse(r.url).path)

nasdaqdatalink/model/datatable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from six.moves.urllib.request import urlopen
55

6-
from nasdaqdatalink.connection import Connection
6+
import nasdaqdatalink.connection as connection
77
from nasdaqdatalink.errors.data_link_error import DataLinkError
88
from nasdaqdatalink.message import Message
99
from nasdaqdatalink.operations.get import GetOperation
@@ -51,7 +51,7 @@ def _request_file_info(self, file_or_folder_path, **options):
5151

5252
updated_options = Util.convert_options(request_type=request_type, **options)
5353

54-
r = Connection.request(request_type, url, **updated_options)
54+
r = connection.request(request_type, url, **updated_options)
5555

5656
response_data = r.json()
5757

nasdaqdatalink/operations/get.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from inflection import singularize
22

33
from .operation import Operation
4-
from nasdaqdatalink.connection import Connection
4+
import nasdaqdatalink.connection as connection
55
from nasdaqdatalink.util import Util
66

77

@@ -21,7 +21,7 @@ def __get_raw_data__(self):
2121

2222
path = Util.constructed_path(cls.get_path(), options['params'])
2323

24-
r = Connection.request('get', path, **options)
24+
r = connection.request('get', path, **options)
2525
response_data = r.json()
2626
Util.convert_to_dates(response_data)
2727
self._raw_data = response_data[singularize(cls.lookup_key())]

nasdaqdatalink/operations/list.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .operation import Operation
2-
from nasdaqdatalink.connection import Connection
2+
import nasdaqdatalink.connection as connection
33
from nasdaqdatalink.util import Util
44
from nasdaqdatalink.model.paginated_list import PaginatedList
55
from nasdaqdatalink.utils.request_type_util import RequestType
@@ -12,7 +12,7 @@ def all(cls, **options):
1212
if 'params' not in options:
1313
options['params'] = {}
1414
path = Util.constructed_path(cls.list_path(), options['params'])
15-
r = Connection.request('get', path, **options)
15+
r = connection.request('get', path, **options)
1616
response_data = r.json()
1717
Util.convert_to_dates(response_data)
1818
resource = cls.create_list_from_response(response_data)
@@ -27,7 +27,7 @@ def page(cls, datatable, **options):
2727

2828
updated_options = Util.convert_options(request_type=request_type, **options)
2929

30-
r = Connection.request(request_type, path, **updated_options)
30+
r = connection.request(request_type, path, **updated_options)
3131

3232
response_data = r.json()
3333
Util.convert_to_dates(response_data)

test/test_connection.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from nasdaqdatalink.connection import Connection
1+
import nasdaqdatalink.connection as connection
22
from nasdaqdatalink.api_config import ApiConfig
33
from nasdaqdatalink.errors.data_link_error import (
44
DataLinkError, LimitExceededError, InternalServerError,
@@ -42,7 +42,7 @@ def test_nasdaqdatalink_exceptions_no_retries(self, request_method):
4242

4343
for expected_error in data_link_errors:
4444
self.assertRaises(
45-
expected_error[2], lambda: Connection.request(request_method, 'databases'))
45+
expected_error[2], lambda: connection.request(request_method, 'databases'))
4646

4747
@parameterized.expand(['GET', 'POST'])
4848
def test_parse_error(self, request_method):
@@ -51,7 +51,7 @@ def test_parse_error(self, request_method):
5151
"https://data.nasdaq.com/api/v3/databases",
5252
body="not json", status=500)
5353
self.assertRaises(
54-
DataLinkError, lambda: Connection.request(request_method, 'databases'))
54+
DataLinkError, lambda: connection.request(request_method, 'databases'))
5555

5656
@parameterized.expand(['GET', 'POST'])
5757
def test_non_data_link_error(self, request_method):
@@ -62,16 +62,16 @@ def test_non_data_link_error(self, request_method):
6262
{'foobar':
6363
{'code': 'blah', 'message': 'something went wrong'}}), status=500)
6464
self.assertRaises(
65-
DataLinkError, lambda: Connection.request(request_method, 'databases'))
65+
DataLinkError, lambda: connection.request(request_method, 'databases'))
6666

6767
@parameterized.expand(['GET', 'POST'])
68-
@patch('nasdaqdatalink.connection.Connection.execute_request')
68+
@patch('nasdaqdatalink.connection.execute_request')
6969
def test_build_request(self, request_method, mock):
7070
ApiConfig.api_key = 'api_token'
7171
ApiConfig.api_version = '2015-04-09'
7272
params = {'per_page': 10, 'page': 2}
7373
headers = {'x-custom-header': 'header value'}
74-
Connection.request(request_method, 'databases', headers=headers, params=params)
74+
connection.request(request_method, 'databases', headers=headers, params=params)
7575
expected = call(request_method, 'https://data.nasdaq.com/api/v3/databases',
7676
headers={'x-custom-header': 'header value',
7777
'x-api-token': 'api_token',
@@ -81,3 +81,15 @@ def test_build_request(self, request_method, mock):
8181
'request-source-version': VERSION},
8282
params={'per_page': 10, 'page': 2})
8383
self.assertEqual(mock.call_args, expected)
84+
85+
def test_session_reuse(self):
86+
session1 = connection.get_session()
87+
session2 = connection.get_session()
88+
areSessionsSame = session1 is session2
89+
90+
adapter1 = connection.get_session().get_adapter(ApiConfig.api_protocol)
91+
adapter2 = connection.get_session().get_adapter(ApiConfig.api_protocol)
92+
areAdaptersSame = adapter1 is adapter2
93+
94+
self.assertEqual(areAdaptersSame, True)
95+
self.assertEqual(areSessionsSame, True)

test/test_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def tearDownClass(cls):
7777
httpretty.disable()
7878
httpretty.reset()
7979

80-
@patch('nasdaqdatalink.connection.Connection.request')
80+
@patch('nasdaqdatalink.connection.request')
8181
def test_data_calls_connection(self, mock):
8282
Data.all(params={'database_code': 'NSE', 'dataset_code': 'OIL'})
8383
expected = call('get', 'datasets/NSE/OIL/data', params={})

test/test_database.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from six.moves.urllib.parse import parse_qs, urlparse
88

99
from nasdaqdatalink.api_config import ApiConfig
10-
from nasdaqdatalink.connection import Connection
10+
import nasdaqdatalink.connection as connection
1111
from nasdaqdatalink.errors.data_link_error import (InternalServerError, DataLinkError)
1212
from nasdaqdatalink.model.database import Database
1313
from test.factories.database import DatabaseFactory
@@ -34,7 +34,7 @@ def tearDownClass(cls):
3434
httpretty.disable()
3535
httpretty.reset()
3636

37-
@patch('nasdaqdatalink.connection.Connection.request')
37+
@patch('nasdaqdatalink.connection.request')
3838
def test_database_calls_connection(self, mock):
3939
database = Database('NSE')
4040
database.data_fields()
@@ -80,7 +80,7 @@ def tearDownClass(cls):
8080
httpretty.disable()
8181
httpretty.reset()
8282

83-
@patch('nasdaqdatalink.connection.Connection.request')
83+
@patch('nasdaqdatalink.connection.request')
8484
def test_databases_calls_connection(self, mock):
8585
Database.all()
8686
expected = call('get', 'databases', params={})
@@ -148,7 +148,7 @@ def test_get_bulk_download_url_without_download_type(self):
148148

149149
def test_bulk_download_to_fileaccepts_download_type(self):
150150
m = mock_open()
151-
with patch.object(Connection, 'request') as mock_method:
151+
with patch.object(connection, 'request') as mock_method:
152152
mock_method.return_value.url = 'https://www.blah.com/download/db.zip'
153153
with patch('nasdaqdatalink.model.database.open', m, create=True):
154154
self.database.bulk_download_to_file(

0 commit comments

Comments
 (0)