-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature(websocket): Add websocket client #508
Changes from 7 commits
4ce17b8
1910c4e
febcd8a
111dcb0
a76fa92
e5b89b3
fa6b010
d9cb894
716a5b6
f157b0c
be25aa2
a15986f
0be8171
b162391
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
from tiingo.api import TiingoClient | ||
from tiingo.wsclient import TiingoWebsocketClient | ||
|
||
__author__ = """Cameron Yick""" | ||
__email__ = '[email protected]' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import os | ||
import websocket | ||
#to import the correct version of thread regardless of python version | ||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wondering, what is this try-except import for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to import the correct version of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, that makes sense. Could we add a comment explaining this is the reason? |
||
import thread | ||
except ImportError: | ||
import _thread as thread | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, thanks for investigating this! I'm a little bit concerned by the use of I was wondering if there were any existing Websocket clients that this implementation was inspired by that I could take a look at. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have tracked this down to this line of code in the library. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unclear about why If we're not actually going to read the value of |
||
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): | ||
|
||
# Example response | ||
# msg = { | ||
# "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. | ||
# | ||
# # 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 | ||
''' | ||
|
||
def __init__(self,config={},endpoint=None,on_msg_cb=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about documenting the expected.permitted options for the config dictionary + the expected values for endpoint + the function signature for |
||
|
||
self._base_url = "wss://api.tiingo.com" | ||
self.config=config | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're getting a warning from LGTM about mutating a basic object. I think we can solve it by doing https://lgtm.com/rules/4840097/
self.config = {} if config is None else config This should address the warning. |
||
|
||
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 takes the API Key as authorization ") | ||
|
||
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 | ||
if not self.on_msg_cb: | ||
raise MissingRequiredArgumentError("please define on_msg_cb It's a callback that gets called when new messages arrive " | ||
"Example:" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idea to put an example function in the error message for how to fix the bug, developers really appreciate that! :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. glad you liked it |
||
"def cb_fn(msg):" | ||
" print(msg)") | ||
|
||
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work on the documentation for "service" and "messageType"!
Can we provide a sample of what one of these objects would look like? 1 or 2 would be plenty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think adding examples would work out for stylistic reasons. anyways the link provided gives a lot of in-depth details