Skip to content

Commit 1066b01

Browse files
committed
api_config: avoid duplicating initialization state logic
Currently the api_config.py is used as both a module containing methods as well as defining a class that, until recently, was used to obtain values from the class attributes instead of some instance state. To avoid global state and reduce logic duplication, specifically read_key(), move the module methods to class methods. The ApiConfig should be convenient on instantiation and should try to set some sane initial state. While we're at it reduce file stat(3) reading when calling get_config_from_kwargs(). Return default api config instead of creating new objects for every call an authorized session instance makes. Signed-off-by: Jamie Couture <[email protected]>
1 parent e87eee2 commit 1066b01

File tree

5 files changed

+140
-155
lines changed

5 files changed

+140
-155
lines changed

nasdaqdatalink/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
from .api_config import ApiConfig, save_key, read_key
3+
from .api_config import ApiConfig, get_config_from_kwargs
44

55
from .errors.data_link_error import *
66

@@ -16,6 +16,3 @@
1616
from .export_table import export_table
1717
from .get_table import get_table
1818
from .get_point_in_time import get_point_in_time
19-
20-
21-
read_key()

nasdaqdatalink/api_config.py

Lines changed: 72 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -17,111 +17,96 @@ class ApiConfig:
1717
retry_status_codes = [429] + list(range(500, 512))
1818
verify_ssl = True
1919

20-
def read_key(self, filename=None):
21-
if not os.path.isfile(filename):
22-
raise_empty_file(filename)
23-
24-
with open(filename, 'r') as f:
25-
apikey = get_first_non_empty(f)
26-
27-
if not apikey:
28-
raise_empty_file(filename)
29-
20+
def __init__(self):
21+
self.read_key()
22+
23+
def create_file(self, config_filename):
24+
# Create the file as well as the parent dir if needed.
25+
dirname = os.path.split(config_filename)[0]
26+
if not os.path.isdir(dirname):
27+
os.makedirs(dirname)
28+
with os.fdopen(os.open(config_filename,
29+
os.O_WRONLY | os.O_CREAT, 0o600), 'w'):
30+
pass
31+
32+
def create_file_if_necessary(self, config_filename):
33+
if not os.path.isfile(config_filename):
34+
self.create_file(config_filename)
35+
36+
def default_config_filename(self):
37+
config_file = os.path.join('~', '.nasdaq', 'data_link_apikey')
38+
return os.path.expanduser(config_file)
39+
40+
def config_file_exists(self, filename=None):
41+
if filename is None:
42+
filename = self.default_config_filename()
43+
44+
return os.path.isfile(filename)
45+
46+
def save_key(self, apikey, filename=None):
47+
if filename is None:
48+
filename = self.default_config_filename()
49+
self.create_file_if_necessary(filename)
50+
51+
fileptr = open(filename, 'w')
52+
fileptr.write(apikey)
53+
fileptr.close()
3054
self.api_key = apikey
3155

56+
def raise_empty_file(self, config_filename):
57+
raise ValueError("File '{:s}' is empty.".format(config_filename))
3258

33-
def create_file(config_filename):
34-
# Create the file as well as the parent dir if needed.
35-
dirname = os.path.split(config_filename)[0]
36-
if not os.path.isdir(dirname):
37-
os.makedirs(dirname)
38-
with os.fdopen(os.open(config_filename,
39-
os.O_WRONLY | os.O_CREAT, 0o600), 'w'):
40-
pass
41-
42-
43-
def create_file_if_necessary(config_filename):
44-
if not os.path.isfile(config_filename):
45-
create_file(config_filename)
46-
47-
48-
def default_config_filename():
49-
config_file = os.path.join('~', '.nasdaq', 'data_link_apikey')
50-
return os.path.expanduser(config_file)
51-
52-
53-
def config_file_exists(filename=None):
54-
if filename is None:
55-
filename = default_config_filename()
56-
57-
return os.path.isfile(filename)
58-
59-
60-
def save_key(apikey, filename=None):
61-
if filename is None:
62-
filename = default_config_filename()
63-
create_file_if_necessary(filename)
64-
65-
fileptr = open(filename, 'w')
66-
fileptr.write(apikey)
67-
fileptr.close()
68-
ApiConfig.api_key = apikey
69-
70-
71-
def raise_empty_file(config_filename):
72-
raise ValueError("File '{:s}' is empty.".format(config_filename))
59+
def raise_empty_environment_variable(self):
60+
raise ValueError("NASDAQ_DATA_LINK_API_KEY cannot be empty")
7361

62+
def get_first_non_empty(self, file_handle):
63+
lines = [line.strip() for line in file_handle.readlines()]
64+
return next((line for line in lines if line), None)
7465

75-
def raise_empty_environment_variable():
76-
raise ValueError("NASDAQ_DATA_LINK_API_KEY cannot be empty")
66+
def read_key_from_file(self, filename=None):
67+
if filename is None:
68+
filename = self.default_config_filename()
7769

70+
if not os.path.isfile(filename):
71+
self.raise_empty_file(filename)
7872

79-
def get_first_non_empty(file_handle):
80-
lines = [line.strip() for line in file_handle.readlines()]
81-
return next((line for line in lines if line), None)
82-
83-
84-
def read_key_from_file(filename=None):
85-
if filename is None:
86-
filename = default_config_filename()
87-
88-
if not os.path.isfile(filename):
89-
raise_empty_file(filename)
90-
91-
with open(filename, 'r') as f:
92-
apikey = get_first_non_empty(f)
93-
94-
if not apikey:
95-
raise_empty_file(filename)
96-
97-
ApiConfig.api_key = apikey
98-
73+
with open(filename, 'r') as f:
74+
apikey = self.get_first_non_empty(f)
9975

100-
def api_key_environment_variable_exists():
101-
return NASDAQ_DATA_LINK_API_KEY in os.environ
76+
if not apikey:
77+
self.raise_empty_file(filename)
10278

79+
self.api_key = apikey
10380

104-
def read_key_from_environment_variable():
105-
apikey = os.environ.get(NASDAQ_DATA_LINK_API_KEY)
106-
if not apikey:
107-
raise_empty_environment_variable()
81+
def api_key_environment_variable_exists(self):
82+
return NASDAQ_DATA_LINK_API_KEY in os.environ
10883

109-
ApiConfig.api_key = apikey
84+
def read_key_from_environment_variable(self):
85+
apikey = os.environ.get(NASDAQ_DATA_LINK_API_KEY)
86+
if not apikey:
87+
self.raise_empty_environment_variable()
11088

89+
self.api_key = apikey
11190

112-
def read_key(filename=None):
113-
if api_key_environment_variable_exists():
114-
read_key_from_environment_variable()
115-
elif config_file_exists(filename):
116-
read_key_from_file(filename)
91+
def read_key(self, filename=None):
92+
if self.api_key_environment_variable_exists():
93+
self.read_key_from_environment_variable()
94+
elif self.config_file_exists(filename):
95+
self.read_key_from_file(filename)
11796

11897

11998
def get_config_from_kwargs(kwargs):
120-
result = ApiConfig
12199
if isinstance(kwargs, dict):
122100
params = kwargs.get('params')
101+
123102
if isinstance(params, dict):
124103
result = params.get('api_config')
104+
125105
if not isinstance(result, ApiConfig):
126-
result = ApiConfig
127-
return result
106+
return DEFAULT_API_CONFIG
107+
return result
108+
109+
return DEFAULT_API_CONFIG
110+
111+
112+
DEFAULT_API_CONFIG = ApiConfig()

nasdaqdatalink/model/authorized_session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import nasdaqdatalink
2-
from nasdaqdatalink.api_config import ApiConfig
2+
from nasdaqdatalink.api_config import ApiConfig, DEFAULT_API_CONFIG
33
from urllib3.util.retry import Retry
44
from requests.adapters import HTTPAdapter
55
import requests
66
import urllib
77

88

9-
def get_retries(api_config=nasdaqdatalink.ApiConfig):
9+
def get_retries(api_config=DEFAULT_API_CONFIG):
1010
retries = None
1111
if not api_config.use_retries:
1212
return Retry(total=0)
@@ -22,10 +22,10 @@ def get_retries(api_config=nasdaqdatalink.ApiConfig):
2222

2323

2424
class AuthorizedSession:
25-
def __init__(self, api_config=ApiConfig) -> None:
25+
def __init__(self, api_config=DEFAULT_API_CONFIG) -> None:
2626
super(AuthorizedSession, self).__init__()
2727
if not isinstance(api_config, ApiConfig):
28-
api_config = ApiConfig
28+
api_config = DEFAULT_API_CONFIG
2929
self._api_config = api_config
3030
self._auth_session = requests.Session()
3131
retries = get_retries(self._api_config)

test/test_api_config.py

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,70 +43,75 @@ def tearDown(self):
4343

4444
def test_read_key_when_environment_variable_set(self):
4545
os.environ['NASDAQ_DATA_LINK_API_KEY'] = 'setinenv'
46-
ApiConfig.api_key = None
47-
read_key()
48-
self.assertEqual(ApiConfig.api_key, "setinenv")
46+
api_config = ApiConfig()
47+
self.assertEqual(api_config.api_key, "setinenv")
4948

5049

5150
def test_read_key_environment_variable_takes_precedence(self):
5251
os.environ['NASDAQ_DATA_LINK_API_KEY'] = 'setinenvprecedence'
53-
save_key("keyforfilenot", TEST_KEY_FILE)
54-
ApiConfig.api_key = None
55-
read_key()
56-
self.assertEqual(ApiConfig.api_key, "setinenvprecedence")
52+
api_config = ApiConfig()
53+
api_config.save_key("keyforfilenot", TEST_KEY_FILE)
54+
api_config.api_key = None
55+
api_config.read_key()
56+
self.assertEqual(api_config.api_key, "setinenvprecedence")
5757

5858

5959
def test_read_key_when_environment_variable_not_set(self):
60-
save_key("keyforfile", TEST_KEY_FILE)
61-
ApiConfig.api_key = None # Set None, we are not testing save_key
62-
read_key(TEST_KEY_FILE)
63-
self.assertEqual(ApiConfig.api_key, 'keyforfile')
60+
api_config = ApiConfig()
61+
api_config.save_key("keyforfile", TEST_KEY_FILE)
62+
api_config.api_key = None # Set None, we are not testing save_key
63+
api_config.read_key(TEST_KEY_FILE)
64+
self.assertEqual(api_config.api_key, 'keyforfile')
6465

6566

6667
def test_read_key_empty_file(self):
67-
with mock.patch("nasdaqdatalink.api_config.default_config_filename") as mock_default_config_filename:
68+
with mock.patch("nasdaqdatalink.api_config.ApiConfig.default_config_filename") as mock_default_config_filename:
6869
mock_default_config_filename.return_value = TEST_DEFAULT_FILE
69-
save_key("")
70+
api_config = ApiConfig()
71+
api_config.save_key("")
7072
with self.assertRaises(ValueError):
71-
read_key()
73+
api_config.read_key()
7274

7375

7476
def test_read_key_when_env_key_empty(self):
7577
os.environ['NASDAQ_DATA_LINK_API_KEY'] = ''
7678
with self.assertRaises(ValueError):
77-
read_key()
79+
ApiConfig().read_key()
7880

7981

8082
def test_read_key_when_files_not_set(self):
81-
ApiConfig.api_key = None
82-
with mock.patch("nasdaqdatalink.api_config.default_config_filename") as mock_default_config_filename:
83+
api_config = ApiConfig()
84+
api_config.api_key = None
85+
with mock.patch("nasdaqdatalink.api_config.ApiConfig.default_config_filename") as mock_default_config_filename:
8386
mock_default_config_filename.return_value = TEST_DEFAULT_FILE
84-
read_key()
87+
api_config.read_key()
8588

8689
mock_default_config_filename.assert_called_once
87-
self.assertEqual(ApiConfig.api_key, None)
90+
self.assertEqual(api_config.api_key, None)
8891

8992

9093
def test_read_key_when_default_file_set(self):
91-
save_key("keyfordefaultfile", TEST_DEFAULT_FILE)
92-
ApiConfig.api_key = None # Set None, we are not testing save_key
94+
api_config = ApiConfig()
95+
api_config.save_key("keyfordefaultfile", TEST_DEFAULT_FILE)
96+
api_config.api_key = None # Set None, we are not testing save_key
9397

94-
with mock.patch("nasdaqdatalink.api_config.default_config_filename") as mock_default_config_filename:
98+
with mock.patch("nasdaqdatalink.api_config.ApiConfig.default_config_filename") as mock_default_config_filename:
9599
mock_default_config_filename.return_value = TEST_DEFAULT_FILE
96-
read_key()
100+
api_config.read_key()
97101

98-
self.assertEqual(ApiConfig.api_key, 'keyfordefaultfile')
102+
self.assertEqual(api_config.api_key, 'keyfordefaultfile')
99103

100104

101105
def _read_key_from_file_helper(self, given, expected):
102-
save_key(given, TEST_DEFAULT_FILE)
103-
ApiConfig.api_key = None # Set None, we are not testing save_key
106+
api_config = ApiConfig()
107+
api_config.save_key(given, TEST_DEFAULT_FILE)
108+
api_config.api_key = None # Set None, we are not testing save_key
104109

105-
with mock.patch("nasdaqdatalink.api_config.default_config_filename") as mock_default_config_filename:
110+
with mock.patch("nasdaqdatalink.api_config.ApiConfig.default_config_filename") as mock_default_config_filename:
106111
mock_default_config_filename.return_value = TEST_DEFAULT_FILE
107-
read_key()
112+
api_config.read_key()
108113

109-
self.assertEqual(ApiConfig.api_key, expected)
114+
self.assertEqual(api_config.api_key, expected)
110115

111116

112117
def test_read_key_from_file_with_newline(self):
@@ -136,12 +141,12 @@ def test_read_key_from_file_with_multi_newline(self):
136141
def test_default_instance_will_have_share_values_with_singleton(self):
137142
os.environ['NASDAQ_DATA_LINK_API_KEY'] = 'setinenv'
138143
ApiConfig.api_key = None
139-
read_key()
140144
api_config = ApiConfig()
145+
api_config.read_key()
141146
self.assertEqual(api_config.api_key, "setinenv")
142147
# make sure change in instance will not affect the singleton
143-
api_config.api_key = None
144-
self.assertEqual(ApiConfig.api_key, "setinenv")
148+
api_config.api_key = 'foo'
149+
self.assertIsNone(ApiConfig.api_key)
145150

146151
def test_get_config_from_kwargs_return_api_config_if_present(self):
147152
api_config = get_config_from_kwargs({
@@ -164,26 +169,17 @@ def test_get_config_from_kwargs_return_singleton_if_not_present_or_wrong_type(se
164169
self.assertTrue(issubclass(api_config, ApiConfig))
165170
self.assertFalse(isinstance(api_config, ApiConfig))
166171

167-
def test_instance_read_key_should_raise_error(self):
168-
api_config = ApiConfig()
169-
with self.assertRaises(TypeError):
170-
api_config.read_key(None)
171-
with self.assertRaises(ValueError):
172-
api_config.read_key('')
173-
174172
def test_instance_read_key_should_raise_error_when_empty(self):
175-
save_key("", TEST_KEY_FILE)
176173
api_config = ApiConfig()
174+
api_config.save_key("", TEST_KEY_FILE)
177175
with self.assertRaises(ValueError):
178176
# read empty file
179177
api_config.read_key(TEST_KEY_FILE)
180178

181179
def test_instance_read_the_right_key(self):
182180
expected_key = 'ilovepython'
183-
save_key(expected_key, TEST_KEY_FILE)
184181
api_config = ApiConfig()
182+
api_config.save_key(expected_key, TEST_KEY_FILE)
185183
api_config.api_key = ''
186184
api_config.read_key(TEST_KEY_FILE)
187-
self.assertEqual(ApiConfig.api_key, expected_key)
188-
189-
185+
self.assertEqual(api_config.api_key, expected_key)

0 commit comments

Comments
 (0)