Skip to content

Commit 2cccd8c

Browse files
authored
Merge pull request #1 from Polygon-io/websocket-mvp
Initial take on the websocket client
2 parents 1398f19 + ebf492f commit 2cccd8c

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-0
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
11
# A Python client library for Polgyon's WebSocket and RESTful APIs
2+
3+
Currently this repo only supports the WebSocket API
4+
5+
## Getting Started
6+
7+
For a basic product overview, check out our [setup and use documentation](https://polygon.io/sockets)
8+
9+
10+
## Simple Demo
11+
```python
12+
import time
13+
14+
15+
from polygon_client import WebSocketClient, STOCKS_CLUSTER
16+
17+
18+
def my_customer_process_message(message):
19+
print("this is my custom message processing", message)
20+
21+
22+
def main():
23+
key = 'your api key'
24+
my_client = WebSocketClient(STOCKS_CLUSTER, key, my_customer_process_message)
25+
my_client.run_async()
26+
27+
my_client.subscribe("T.MSFT", "T.AAPL", "T.AMD", "T.NVDA")
28+
time.sleep(2)
29+
30+
my_client.close_connection()
31+
32+
33+
if __name__ == "__main__":
34+
main()
35+
36+
```

example.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import time
2+
3+
from polygon_client import WebSocketClient, STOCKS_CLUSTER
4+
5+
6+
def my_customer_process_message(message):
7+
print("this is my custom message processing", message)
8+
9+
10+
def main():
11+
key = 'your api key'
12+
my_client = WebSocketClient(STOCKS_CLUSTER, key, my_customer_process_message)
13+
my_client.run_async()
14+
15+
my_client.subscribe("T.MSFT", "T.AAPL", "T.AMD", "T.NVDA")
16+
time.sleep(1)
17+
18+
my_client.close_connection()
19+
20+
21+
if __name__ == "__main__":
22+
main()

polygon_client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .websocket_client import WebSocketClient, STOCKS_CLUSTER, FOREX_CLUSTER, CRYPTO_CLUSTER

polygon_client/websocket_client.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import signal
2+
import threading
3+
from typing import Optional, Callable
4+
5+
import websocket
6+
7+
STOCKS_CLUSTER = "stocks"
8+
FOREX_CLUSTER = "forex"
9+
CRYPTO_CLUSTER = "crypto"
10+
11+
12+
class WebSocketClient(object):
13+
DEFAULT_HOST = 'socket.polygon.io'
14+
15+
# TODO: Either an instance of the client couples 1:1 with the cluster or an instance of the Client couples 1:3 with
16+
# the 3 possible clusters (I think I like client per, but then a problem is the user can make multiple clients for
17+
# the same cluster and that's not desirable behavior,
18+
# somehow keeping track with multiple Client instances will be the difficulty)
19+
def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callable[[str], None]] = None):
20+
self._host = self.DEFAULT_HOST
21+
self.url = f"wss://{self._host}/{cluster}"
22+
self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, on_open=self._default_on_open(),
23+
on_close=self._default_on_close,
24+
on_error=self._default_on_error,
25+
on_message=self._default_on_message())
26+
self.auth_key = auth_key
27+
28+
self.process_message = process_message
29+
30+
# being authenticated is an event that must occur before any other action is sent to the server
31+
self._authenticated = threading.Event()
32+
# self._run_thread is only set if the client is run asynchronously
33+
self._run_thread: Optional[threading.Thread] = None
34+
35+
signal.signal(signal.SIGINT, self._cleanup_signal_handler())
36+
signal.signal(signal.SIGTERM, self._cleanup_signal_handler())
37+
38+
def run(self):
39+
self.ws.run_forever()
40+
41+
def run_async(self):
42+
self._run_thread = threading.Thread(target=self.run)
43+
self._run_thread.start()
44+
45+
def close_connection(self):
46+
self.ws.close()
47+
if self._run_thread:
48+
self._run_thread.join()
49+
50+
def subscribe(self, *params):
51+
# TODO: make this a decorator or context manager
52+
self._authenticated.wait()
53+
54+
sub_message = '{"action":"subscribe","params":"%s"}' % self._format_params(params)
55+
self.ws.send(sub_message)
56+
57+
def unsubscribe(self, *params):
58+
# TODO: make this a decorator or context manager
59+
self._authenticated.wait()
60+
61+
sub_message = '{"action":"unsubscribe","params":"%s"}' % self._format_params(params)
62+
self.ws.send(sub_message)
63+
64+
def _cleanup_signal_handler(self):
65+
return lambda signalnum, frame: self.close_connection()
66+
67+
def _authenticate(self, ws):
68+
ws.send('{"action":"auth","params":"%s"}' % self.auth_key)
69+
self._authenticated.set()
70+
71+
@staticmethod
72+
def _format_params(params):
73+
return ",".join(params)
74+
75+
@property
76+
def process_message(self):
77+
return self.__process_message
78+
79+
@process_message.setter
80+
def process_message(self, pm):
81+
if pm:
82+
self.__process_message = pm
83+
self.ws.on_message = lambda ws, message: self.__process_message(message)
84+
85+
def _default_on_message(self):
86+
return lambda ws, message: self._default_process_message(message)
87+
88+
@staticmethod
89+
def _default_process_message(message):
90+
print(message)
91+
92+
def _default_on_open(self):
93+
def f(ws):
94+
self._authenticate(ws)
95+
96+
@staticmethod
97+
def _default_on_error(ws, error):
98+
print("error:", error)
99+
100+
@staticmethod
101+
def _default_on_close(ws):
102+
print("### closed ###")

requirements.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
aiofiles==0.4.0
2+
aiohttp==3.6.2
3+
async-timeout==3.0.1
4+
attrs==19.3.0
5+
certifi==2019.9.11
6+
chardet==3.0.4
7+
idna==2.8
8+
multidict==4.5.2
9+
six==1.12.0
10+
websocket-client==0.56.0
11+
websockets==8.0.2
12+
yarl==1.3.0

0 commit comments

Comments
 (0)