From 4ce17b807921dd1be6c2349eb99170eec8f384ee Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Wed, 2 Sep 2020 03:17:11 +0200 Subject: [PATCH 01/14] Added barebone support for websockets API --- docs/usage.rst | 17 +++++++++ setup.py | 1 + tiingo/__init__.py | 1 + tiingo/wsclient.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 tiingo/wsclient.py diff --git a/docs/usage.rst b/docs/usage.rst index 60ef7e2b..71cc0cda 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -61,6 +61,23 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a startDate='2017-01-01', endDate='2017-08-31') +Websocket support:: + +.. code-block:: python + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + 'eventData': { + 'thresholdLevel':5 + } + } + + client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) Further Docs -------- diff --git a/setup.py b/setup.py index 5ac90986..a08e6316 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def read(*filenames, **kwargs): requirements = [ 'requests', + 'websocket-client' ] setup_requirements = [ diff --git a/tiingo/__init__.py b/tiingo/__init__.py index be6190be..21594440 100644 --- a/tiingo/__init__.py +++ b/tiingo/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from tiingo.api import TiingoClient +from tiingo.wsclient import TiingoWebsocketClient __author__ = """Cameron Yick""" __email__ = 'cameron.yick@enigma.com' diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py new file mode 100644 index 00000000..a8ee9447 --- /dev/null +++ b/tiingo/wsclient.py @@ -0,0 +1,93 @@ +import os +import websocket +try: + import thread +except ImportError: + import _thread as thread +import time +import json + +GLOB_config=None +GLOB_on_msg_cb=None + +class genericWebsocketClient: + ''' + the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly + see websocket-client/#471 + ''' + def on_message(ws, message): + GLOB_on_msg_cb(message) + def on_error(ws, error): + print(error) + def on_close(ws): + pass + def on_open(ws): + def run(*args): + print(GLOB_config) + ws.send(json.dumps(GLOB_config)) + thread.start_new_thread(run, ()) + def __init__(self,config,on_msg_cb): + global GLOB_config + global GLOB_on_msg_cb + GLOB_config=config + GLOB_on_msg_cb=on_msg_cb + return + +class TiingoWebsocketClient: + ''' + from tiingo import TiingoWebsocketClient + def cb_fn(msg): + print(msg) + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + 'eventData': { + 'thresholdLevel':5 + } + } + client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + ''' + + def __init__(self,config={},endpoint=None,on_msg_cb=None): + + self._base_url = "wss://api.tiingo.com" + self.config=config + + try: + api_key = self.config['authorization'] + except KeyError: + api_key = os.environ.get('TIINGO_API_KEY') + self.config.update({"authorization":api_key}) + + self._api_key = api_key + if not(api_key): + raise RuntimeError("Tiingo API Key not provided. Please provide" + " via environment variable or config argument." + "Notice that this config dict ticks the API Key as authorization ") + + try: + self.endpoint = endpoint + if self.endpoint==None: + raise KeyError + if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"): + raise KeyError + except KeyError: + raise AttributeError("Endpoint must be defined as either (iex,fx,crypto) ") + + self.on_msg_cb = on_msg_cb + if self.on_msg_cb==None: + raise AttributeError("please define on_msg_cb ") + + ws_client = genericWebsocketClient(config=self.config,on_msg_cb=self.on_msg_cb) + + + websocket.enableTrace(True) + + ws = websocket.WebSocketApp("{0}/{1}".format(self._base_url,self.endpoint), + on_message = genericWebsocketClient.on_message, + on_error = genericWebsocketClient.on_error, + on_close = genericWebsocketClient.on_close, + on_open = genericWebsocketClient.on_open) + ws.run_forever() + From 1910c4e7c10fed39b924d9b495f75f57b3731003 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Wed, 2 Sep 2020 03:37:45 +0200 Subject: [PATCH 02/14] added docs to README --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 355d65a3..f7f5b435 100644 --- a/README.rst +++ b/README.rst @@ -135,6 +135,23 @@ To receive results in ``pandas`` format, use the ``get_dataframe()`` method: startDate='2017-01-01', endDate='2018-05-31') +Websocket support:: + +.. code-block:: python + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + 'eventData': { + 'thresholdLevel':5 + } + } + + client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) You can specify any of the end of day frequencies (daily, weekly, monthly, and annually) or any intraday frequency for both the ``get_ticker_price`` and ``get_dataframe`` methods. Weekly frequencies resample to the end of day on Friday, monthly frequencies resample to the last day of the month, and annually frequencies resample to the end of day on 12-31 of each year. The intraday frequencies are specified using an integer followed by "Min" or "Hour", for example "30Min" or "1Hour". From febcd8a5f313a452f89c04317697617bcc0c8012 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Sun, 6 Sep 2020 15:33:10 +0200 Subject: [PATCH 03/14] added a more informative docstring with response examples;more verbose error msg for missing on_msg_cb --- README.rst | 45 +++++++++++++++++++++++++++++++-------------- docs/usage.rst | 45 +++++++++++++++++++++++++++++++-------------- tiingo/wsclient.py | 39 +++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index f7f5b435..50b537b4 100644 --- a/README.rst +++ b/README.rst @@ -138,20 +138,37 @@ To receive results in ``pandas`` format, use the ``get_dataframe()`` method: Websocket support:: .. code-block:: python - from tiingo import TiingoWebsocketClient - - def cb_fn(msg): - print(msg) - - subscribe = { - 'eventName':'subscribe', - 'authorization':'API_KEY_GOES_HERE', - 'eventData': { - 'thresholdLevel':5 - } - } - - client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + You can specify any of the end of day frequencies (daily, weekly, monthly, and annually) or any intraday frequency for both the ``get_ticker_price`` and ``get_dataframe`` methods. Weekly frequencies resample to the end of day on Friday, monthly frequencies resample to the last day of the month, and annually frequencies resample to the end of day on 12-31 of each year. The intraday frequencies are specified using an integer followed by "Min" or "Hour", for example "30Min" or "1Hour". diff --git a/docs/usage.rst b/docs/usage.rst index 71cc0cda..138c485a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -64,20 +64,37 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a Websocket support:: .. code-block:: python - from tiingo import TiingoWebsocketClient - - def cb_fn(msg): - print(msg) - - subscribe = { - 'eventName':'subscribe', - 'authorization':'API_KEY_GOES_HERE', - 'eventData': { - 'thresholdLevel':5 - } - } - - client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + Further Docs -------- diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index a8ee9447..5d564190 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -1,11 +1,9 @@ import os import websocket -try: - import thread -except ImportError: - import _thread as thread +import thread import time import json +from exceptions import MissingRequiredArgument GLOB_config=None GLOB_on_msg_cb=None @@ -36,16 +34,34 @@ def __init__(self,config,on_msg_cb): class TiingoWebsocketClient: ''' from tiingo import TiingoWebsocketClient + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + print(msg) + subscribe = { 'eventName':'subscribe', 'authorization':'API_KEY_GOES_HERE', - 'eventData': { + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { 'thresholdLevel':5 } } - client=TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) while True:pass ''' @@ -64,11 +80,11 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): if not(api_key): raise RuntimeError("Tiingo API Key not provided. Please provide" " via environment variable or config argument." - "Notice that this config dict ticks the API Key as authorization ") + "Notice that this config dict takes the API Key as authorization ") try: self.endpoint = endpoint - if self.endpoint==None: + if not self.endpoint: raise KeyError if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"): raise KeyError @@ -76,8 +92,11 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): raise AttributeError("Endpoint must be defined as either (iex,fx,crypto) ") self.on_msg_cb = on_msg_cb - if self.on_msg_cb==None: - raise AttributeError("please define on_msg_cb ") + if not self.on_msg_cb: + raise MissingRequiredArgument("please define on_msg_cb It's a callback that gets called when new messages arrive " + "Example:" + "def cb_fn(msg):" + " print(msg)") ws_client = genericWebsocketClient(config=self.config,on_msg_cb=self.on_msg_cb) From 111dcb043d848954a3f08e37412aa463fe938a02 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Sun, 6 Sep 2020 15:47:26 +0200 Subject: [PATCH 04/14] ammends to the last commit --- tiingo/wsclient.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 5d564190..2cb585c5 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -1,9 +1,12 @@ import os import websocket -import thread +try: + import thread +except ImportError: + import _thread as thread import time import json -from exceptions import MissingRequiredArgument +from tiingo.exceptions import MissingRequiredArgumentError GLOB_config=None GLOB_on_msg_cb=None @@ -93,7 +96,7 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): self.on_msg_cb = on_msg_cb if not self.on_msg_cb: - raise MissingRequiredArgument("please define on_msg_cb It's a callback that gets called when new messages arrive " + raise MissingRequiredArgumentError("please define on_msg_cb It's a callback that gets called when new messages arrive " "Example:" "def cb_fn(msg):" " print(msg)") @@ -108,5 +111,4 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): on_error = genericWebsocketClient.on_error, on_close = genericWebsocketClient.on_close, on_open = genericWebsocketClient.on_open) - ws.run_forever() - + ws.run_forever() \ No newline at end of file From a76fa92d60e7feb55aa7dd7336019e886aa91f11 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Thu, 10 Sep 2020 11:25:05 +0200 Subject: [PATCH 05/14] minor comments and doc changes --- README.rst | 3 ++- docs/usage.rst | 5 +++-- tiingo/wsclient.py | 13 +++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 50b537b4..fa7954d3 100644 --- a/README.rst +++ b/README.rst @@ -144,7 +144,8 @@ Websocket support:: # Example response # msg = { - # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. # # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. diff --git a/docs/usage.rst b/docs/usage.rst index 138c485a..408f572c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -70,8 +70,9 @@ Websocket support:: # Example response # msg = { - # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". - # + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. + # # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. # diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 2cb585c5..c6ee6679 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -1,5 +1,6 @@ import os import websocket +#to import the correct version of thread regardless of python version try: import thread except ImportError: @@ -42,7 +43,8 @@ def cb_fn(msg): # Example response # msg = { - # "service":"iex" # An identifier telling you this is IEX data. The value returned by this will always be "iex". + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. # # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. @@ -85,13 +87,8 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): " via environment variable or config argument." "Notice that this config dict takes the API Key as authorization ") - try: - self.endpoint = endpoint - if not self.endpoint: - raise KeyError - if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"): - raise KeyError - except KeyError: + self.endpoint = endpoint + if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"): raise AttributeError("Endpoint must be defined as either (iex,fx,crypto) ") self.on_msg_cb = on_msg_cb From e5b89b3b33b55cac105b49fc8cf167402bf8c4a9 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Thu, 10 Sep 2020 12:01:53 +0200 Subject: [PATCH 06/14] Added tests to check for valid initialization arguments for TiingoWebsocketClient --- tests/test_wsclient.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_wsclient.py diff --git a/tests/test_wsclient.py b/tests/test_wsclient.py new file mode 100644 index 00000000..3fb8c121 --- /dev/null +++ b/tests/test_wsclient.py @@ -0,0 +1,42 @@ +from unittest import TestCase +from tiingo.wsclient import TiingoWebsocketClient +from tiingo.exceptions import MissingRequiredArgumentError + +class TestRestClientWithSession(TestCase): + def setUp(self): + + def msg_cb(msg): + print(msg) + + self.cb=msg_cb + + self.config = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + + # test for missing or incorrectly supplied endpoints + def test_missing_or_wrong_endpoint(self): + with self.assertRaises(AttributeError) as ex: + TiingoWebsocketClient(config=self.config,on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==AttributeError) + + with self.assertRaises(AttributeError) as ex: + TiingoWebsocketClient(config=self.config,endpoint='wq',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==AttributeError) + + # test for missing API keys in config dict + def test_missing_api_key(self): + with self.assertRaises(RuntimeError) as ex: + TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==RuntimeError) + + # test for missing callback argument + def test_missing_msg_cb(self): + with self.assertRaises(MissingRequiredArgumentError) as ex: + TiingoWebsocketClient(config=self.config,endpoint='iex') + self.assertTrue(type(ex.exception)==MissingRequiredArgumentError) \ No newline at end of file From fa6b010e9a52975445b04799c1a292233df78b03 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Thu, 10 Sep 2020 12:02:00 +0200 Subject: [PATCH 07/14] Added tests to check for valid initialization arguments for TiingoWebsocketClient --- tiingo/wsclient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index c6ee6679..4a1b6f92 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -5,7 +5,6 @@ import thread except ImportError: import _thread as thread -import time import json from tiingo.exceptions import MissingRequiredArgumentError From d9cb8947fbc1a8754f5c8197131347797959ac3b Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Sat, 3 Oct 2020 20:45:55 +0200 Subject: [PATCH 08/14] ditched the Globals and used a higher order func instead;also ditched the send thread on_open() --- tiingo/wsclient.py | 60 +++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 4a1b6f92..46d01489 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -8,32 +8,6 @@ import json from tiingo.exceptions import MissingRequiredArgumentError -GLOB_config=None -GLOB_on_msg_cb=None - -class genericWebsocketClient: - ''' - the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly - see websocket-client/#471 - ''' - def on_message(ws, message): - GLOB_on_msg_cb(message) - def on_error(ws, error): - print(error) - def on_close(ws): - pass - def on_open(ws): - def run(*args): - print(GLOB_config) - ws.send(json.dumps(GLOB_config)) - thread.start_new_thread(run, ()) - def __init__(self,config,on_msg_cb): - global GLOB_config - global GLOB_on_msg_cb - GLOB_config=config - GLOB_on_msg_cb=on_msg_cb - return - class TiingoWebsocketClient: ''' from tiingo import TiingoWebsocketClient @@ -97,14 +71,30 @@ def __init__(self,config={},endpoint=None,on_msg_cb=None): "def cb_fn(msg):" " print(msg)") - ws_client = genericWebsocketClient(config=self.config,on_msg_cb=self.on_msg_cb) - - - websocket.enableTrace(True) + websocket.enableTrace(False) ws = websocket.WebSocketApp("{0}/{1}".format(self._base_url,self.endpoint), - on_message = genericWebsocketClient.on_message, - on_error = genericWebsocketClient.on_error, - on_close = genericWebsocketClient.on_close, - on_open = genericWebsocketClient.on_open) - ws.run_forever() \ No newline at end of file + on_message = self.get_on_msg_cb(), + on_error = self.on_error, + on_close = self.on_close, + on_open = self.get_on_open(self.config)) + ws.run_forever() + + def get_on_open(self,config): + # the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly + # see websocket-client/#471 + def on_open(ws): + ws.send(json.dumps(config)) + return on_open + + def get_on_msg_cb(self): + def on_msg_cb_local(ws,msg): + self.on_msg_cb(msg) + return + return on_msg_cb_local + + def on_error(ws, error): + print(error) + + def on_close(ws): + pass \ No newline at end of file From 716a5b6b486b488f67210db91ef22a4d268c0da9 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Sun, 4 Oct 2020 06:33:53 +0200 Subject: [PATCH 09/14] removed uneeded import --- tiingo/wsclient.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 46d01489..01f89281 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -1,10 +1,5 @@ import os import websocket -#to import the correct version of thread regardless of python version -try: - import thread -except ImportError: - import _thread as thread import json from tiingo.exceptions import MissingRequiredArgumentError From f157b0c8414ccd4c3a44bba720a107311481fbbf Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Tue, 6 Oct 2020 04:26:36 +0200 Subject: [PATCH 10/14] removed config as a default arg;added comment to explain why on_error() and on_close() don't get a self --- tiingo/wsclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 01f89281..5747e360 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -38,7 +38,7 @@ def cb_fn(msg): while True:pass ''' - def __init__(self,config={},endpoint=None,on_msg_cb=None): + def __init__(self,config,endpoint=None,on_msg_cb=None): self._base_url = "wss://api.tiingo.com" self.config=config @@ -88,6 +88,7 @@ def on_msg_cb_local(ws,msg): return return on_msg_cb_local + # since methods need to be unbound in order for websocketClient these methods don't have a self as their first parameter def on_error(ws, error): print(error) From be25aa24c3dc2551de164e9ef8bb03a7a30c1f93 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Sun, 3 Jan 2021 12:59:22 +0200 Subject: [PATCH 11/14] better error handling for config option --- tiingo/wsclient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index 5747e360..ca722d29 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -38,10 +38,10 @@ def cb_fn(msg): while True:pass ''' - def __init__(self,config,endpoint=None,on_msg_cb=None): + def __init__(self,config=None,endpoint=None,on_msg_cb=None): self._base_url = "wss://api.tiingo.com" - self.config=config + self.config = {} if config is None else config try: api_key = self.config['authorization'] From a15986feca47a3afa03fb8c6caaebb30a835957d Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Tue, 12 Jan 2021 15:46:02 +0200 Subject: [PATCH 12/14] test of missing API key in os env and config dict --- tests/test_wsclient.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_wsclient.py b/tests/test_wsclient.py index 3fb8c121..961a2eaa 100644 --- a/tests/test_wsclient.py +++ b/tests/test_wsclient.py @@ -1,3 +1,4 @@ +import os from unittest import TestCase from tiingo.wsclient import TiingoWebsocketClient from tiingo.exceptions import MissingRequiredArgumentError @@ -12,7 +13,7 @@ def msg_cb(msg): self.config = { 'eventName':'subscribe', - 'authorization':'API_KEY_GOES_HERE', + 'authorization':os.getenv("TIINGO_API_KEY"), #see https://api.tiingo.com/documentation/websockets/iex > Request for more info 'eventData': { 'thresholdLevel':5 @@ -29,14 +30,15 @@ def test_missing_or_wrong_endpoint(self): TiingoWebsocketClient(config=self.config,endpoint='wq',on_msg_cb=self.cb) self.assertTrue(type(ex.exception)==AttributeError) - # test for missing API keys in config dict - def test_missing_api_key(self): - with self.assertRaises(RuntimeError) as ex: - TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) - self.assertTrue(type(ex.exception)==RuntimeError) - # test for missing callback argument def test_missing_msg_cb(self): with self.assertRaises(MissingRequiredArgumentError) as ex: TiingoWebsocketClient(config=self.config,endpoint='iex') - self.assertTrue(type(ex.exception)==MissingRequiredArgumentError) \ No newline at end of file + self.assertTrue(type(ex.exception)==MissingRequiredArgumentError) + + # test for missing API keys in config dict and in os env + def test_missing_api_key(self): + del os.environ['TIINGO_API_KEY'] + with self.assertRaises(RuntimeError) as ex: + TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==RuntimeError) From 0be8171d0ca981638cea8a86065d45f5210985ac Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Thu, 1 Jul 2021 14:16:57 +0200 Subject: [PATCH 13/14] ignore lgtm [py/not-named-self] --- tiingo/wsclient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py index ca722d29..ffeefe30 100644 --- a/tiingo/wsclient.py +++ b/tiingo/wsclient.py @@ -89,8 +89,8 @@ def on_msg_cb_local(ws,msg): return on_msg_cb_local # since methods need to be unbound in order for websocketClient these methods don't have a self as their first parameter - def on_error(ws, error): + def on_error(ws, error): # lgtm[py/not-named-self] print(error) - def on_close(ws): - pass \ No newline at end of file + def on_close(ws): # lgtm[py/not-named-self] + pass From b162391cef691377527432c45a12237ac310dab6 Mon Sep 17 00:00:00 2001 From: mohamedemad4 Date: Wed, 7 Jul 2021 13:16:52 +0200 Subject: [PATCH 14/14] use unittest.mock to remove TIINGO_API_KEY --- tests/test_wsclient.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_wsclient.py b/tests/test_wsclient.py index 961a2eaa..af3b7b8b 100644 --- a/tests/test_wsclient.py +++ b/tests/test_wsclient.py @@ -1,5 +1,5 @@ import os -from unittest import TestCase +from unittest import TestCase,mock from tiingo.wsclient import TiingoWebsocketClient from tiingo.exceptions import MissingRequiredArgumentError @@ -38,7 +38,7 @@ def test_missing_msg_cb(self): # test for missing API keys in config dict and in os env def test_missing_api_key(self): - del os.environ['TIINGO_API_KEY'] - with self.assertRaises(RuntimeError) as ex: - TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) - self.assertTrue(type(ex.exception)==RuntimeError) + with mock.patch.dict(os.environ, {}, clear=True): #clear env vars including the TIINGO_API_KEY + with self.assertRaises(RuntimeError) as ex: + TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==RuntimeError)