From 3b3c7fe833c2534030f51ce5a42446cc7d875efd Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Tue, 22 Mar 2022 19:39:05 +0100 Subject: [PATCH 01/14] Update websocket_client.py There should be no need to install the signal handlers to close the connection if the application terminates. If it does, the OS will close all the file handles owned by the terminating process, among which the sockets used by the connection. And upon closure, the TCP stack will properly issue a reset which will be noticed by the remote server. The issue is that, on top of what already underlined in the existing comments (override of user specified signals), setting a signal handler from outside the main thread will result with an error: ``` ER20220322 17:42:45.726997;root;utils: File "/usr/local/lib/python3.8/dist-packages/polygon/websocket/websocket_client.py", line 42, in __init__ ER20220322 17:42:45.726997;root;utils: signal.signal(signal.SIGINT, self._cleanup_signal_handler()) ER20220322 17:42:45.726997;root;utils: File "/usr/lib/python3.8/signal.py", line 47, in signal ER20220322 17:42:45.726997;root;utils: handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler)) ER20220322 17:42:45.726997;root;utils: ValueError: signal only works in main thread ``` This in turn leads to the inability to create a websocket connection from outside the main thread. --- polygon/websocket/websocket_client.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index 3c046485..20832a87 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -1,4 +1,3 @@ -import signal import threading from typing import Optional, Callable @@ -36,12 +35,6 @@ def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callab # self._run_thread is only set if the client is run asynchronously self._run_thread: Optional[threading.Thread] = None - # TODO: this probably isn't great design. - # If the user defines their own signal handler then this will gets overwritten. - # We still need to make sure that killing, terminating, interrupting the program closes the connection - signal.signal(signal.SIGINT, self._cleanup_signal_handler()) - signal.signal(signal.SIGTERM, self._cleanup_signal_handler()) - def run(self): self.ws.run_forever() @@ -68,9 +61,6 @@ def unsubscribe(self, *params): sub_message = '{"action":"unsubscribe","params":"%s"}' % self._format_params(params) self.ws.send(sub_message) - def _cleanup_signal_handler(self): - return lambda signalnum, frame: self.close_connection() - def _authenticate(self, ws): ws.send('{"action":"auth","params":"%s"}' % self.auth_key) self._authenticated.set() From 5bf764407185b5bc0507976f48e8d3ca31f3c70a Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 23 Mar 2022 12:49:52 +0100 Subject: [PATCH 02/14] Clear the event on close --- polygon/websocket/websocket_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index 3c046485..95d0d584 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -53,6 +53,7 @@ def close_connection(self): self.ws.close() if self._run_thread: self._run_thread.join() + self._authenticated.clear() def subscribe(self, *params): # TODO: make this a decorator or context manager From cadce08b25ef67cff7a6c2affd564e2b47c64f89 Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 23 Mar 2022 14:03:40 +0100 Subject: [PATCH 03/14] Specify the version within the setup.py file. --- setup.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 45095315..ce5898e9 100644 --- a/setup.py +++ b/setup.py @@ -2,13 +2,7 @@ from setuptools import setup, find_packages -import os -import sys - -version = os.getenv("VERSION") -if not version: - print("no version supplied") - sys.exit(1) +VERSION = "0.3" def get_readme_md_contents(): """read the contents of your README file""" @@ -18,7 +12,7 @@ def get_readme_md_contents(): setup( name="polygon-api-client", - version=version, + version=VERSION, description="Polygon API client", long_description=get_readme_md_contents(), long_description_content_type="text/markdown", From b3dac3169fae06442f57e9e3bde38151f102074e Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Fri, 25 Mar 2022 11:32:31 +0100 Subject: [PATCH 04/14] Update websocket_client.py --- polygon/websocket/websocket_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index a593670a..22c5b313 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -35,11 +35,11 @@ def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callab # self._run_thread is only set if the client is run asynchronously self._run_thread: Optional[threading.Thread] = None - def run(self): - self.ws.run_forever() + def run(self, **kwargs): + self.ws.run_forever(**kwargs) - def run_async(self): - self._run_thread = threading.Thread(target=self.run) + def run_async(self, **kwargs): + self._run_thread = threading.Thread(target=self.run, kwargs=kwargs) self._run_thread.start() def close_connection(self): From 652b8e32ccce4de9d30c221f626b06381d7cabc3 Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Tue, 19 Apr 2022 17:37:17 +0200 Subject: [PATCH 05/14] Simplify Code --- polygon/websocket/websocket_client.py | 35 +++------------------------ 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index 22c5b313..c598db42 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -21,15 +21,11 @@ def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callab self._host = self.DEFAULT_HOST self.url = f"wss://{self._host}/{cluster}" self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, on_open=self._default_on_open(), - on_close=self._default_on_close, - on_error=self._default_on_error, - on_message=self._default_on_message()) + on_close=on_close, + on_error=on_error, + on_message=process_message) self.auth_key = auth_key - self.process_message = process_message - self.ws.on_close = on_close - self.ws.on_error = on_error - # being authenticated is an event that must occur before any other action is sent to the server self._authenticated = threading.Event() # self._run_thread is only set if the client is run asynchronously @@ -70,33 +66,8 @@ def _authenticate(self, ws): def _format_params(params): return ",".join(params) - @property - def process_message(self): - return self.__process_message - - @process_message.setter - def process_message(self, pm): - if pm: - self.__process_message = pm - self.ws.on_message = lambda ws, message: self.__process_message(message) - - def _default_on_message(self): - return lambda ws, message: self._default_process_message(message) - - @staticmethod - def _default_process_message(message): - print(message) - def _default_on_open(self): def f(ws): self._authenticate(ws) return f - - @staticmethod - def _default_on_error(ws, error): - print("error:", error) - - @staticmethod - def _default_on_close(ws): - print("### closed ###") From 0f55d7def6ebaf20cbcb0ab5e6f96571cb34e65f Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Tue, 19 Apr 2022 17:45:49 +0200 Subject: [PATCH 06/14] Reorganize Code --- polygon/websocket/websocket_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index c598db42..759d0081 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -20,14 +20,15 @@ def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callab on_error: Optional[Callable[[websocket.WebSocketApp, str], None]] = None): self._host = self.DEFAULT_HOST self.url = f"wss://{self._host}/{cluster}" + self.auth_key = auth_key + # being authenticated is an event that must occur before any other action is sent to the server + self._authenticated = threading.Event() + self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, on_open=self._default_on_open(), on_close=on_close, on_error=on_error, on_message=process_message) - self.auth_key = auth_key - # being authenticated is an event that must occur before any other action is sent to the server - self._authenticated = threading.Event() # self._run_thread is only set if the client is run asynchronously self._run_thread: Optional[threading.Thread] = None From a0f39756a53c88ee89816a871eb41ddd7588bf6e Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Tue, 19 Apr 2022 18:37:15 +0200 Subject: [PATCH 07/14] Fix Signature --- polygon/websocket/websocket_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index 759d0081..e48babc9 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -15,7 +15,8 @@ class WebSocketClient: # the 3 possible clusters (I think I like client per, but then a problem is the user can make multiple clients for # the same cluster and that's not desirable behavior, # somehow keeping track with multiple Client instances will be the difficulty) - def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callable[[str], None]] = None, + def __init__(self, cluster: str, auth_key: str, + process_message: Optional[Callable[[websocket.WebSocketApp, str], None]] = None, on_close: Optional[Callable[[websocket.WebSocketApp], None]] = None, on_error: Optional[Callable[[websocket.WebSocketApp, str], None]] = None): self._host = self.DEFAULT_HOST From 703493b2195a2995eecf3cd16a0d583484052339 Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 20 Apr 2022 16:31:58 +0200 Subject: [PATCH 08/14] Minor reformat --- polygon/websocket/websocket_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index e48babc9..c1ca95fa 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -25,7 +25,8 @@ def __init__(self, cluster: str, auth_key: str, # being authenticated is an event that must occur before any other action is sent to the server self._authenticated = threading.Event() - self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, on_open=self._default_on_open(), + self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, + on_open=self._default_on_open(), on_close=on_close, on_error=on_error, on_message=process_message) From 5511df6bfd0f1435f47335901e9c904012181744 Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 20 Apr 2022 16:33:56 +0200 Subject: [PATCH 09/14] Fix example --- websocket_example/polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_example/polygon.py b/websocket_example/polygon.py index 760a27c0..3d8c6f1e 100644 --- a/websocket_example/polygon.py +++ b/websocket_example/polygon.py @@ -5,7 +5,7 @@ from polygon import WebSocketClient, STOCKS_CLUSTER -def my_custom_process_message(message): +def my_custom_process_message(ws, message): print("this is my custom message processing", message) From bd79e31c708fe3241b8a9da0187ac99798abc84b Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 6 Mar 2024 08:00:02 +0100 Subject: [PATCH 10/14] Move authentication handling to the user. --- polygon/websocket/websocket_client.py | 42 ++++++++------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index c1ca95fa..a440b62f 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -8,8 +8,12 @@ CRYPTO_CLUSTER = "crypto" +def _format_params(params): + return ','.join(params) + + class WebSocketClient: - DEFAULT_HOST = "socket.polygon.io" + DEFAULT_HOST = 'socket.polygon.io' # TODO: Either an instance of the client couples 1:1 with the cluster or an instance of the Client couples 1:3 with # the 3 possible clusters (I think I like client per, but then a problem is the user can make multiple clients for @@ -19,19 +23,14 @@ def __init__(self, cluster: str, auth_key: str, process_message: Optional[Callable[[websocket.WebSocketApp, str], None]] = None, on_close: Optional[Callable[[websocket.WebSocketApp], None]] = None, on_error: Optional[Callable[[websocket.WebSocketApp, str], None]] = None): - self._host = self.DEFAULT_HOST - self.url = f"wss://{self._host}/{cluster}" + self.url = f'wss://{self.DEFAULT_HOST}/{cluster}' self.auth_key = auth_key - # being authenticated is an event that must occur before any other action is sent to the server - self._authenticated = threading.Event() self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, - on_open=self._default_on_open(), on_close=on_close, on_error=on_error, on_message=process_message) - # self._run_thread is only set if the client is run asynchronously self._run_thread: Optional[threading.Thread] = None def run(self, **kwargs): @@ -45,32 +44,15 @@ def close_connection(self): self.ws.close() if self._run_thread: self._run_thread.join() - self._authenticated.clear() def subscribe(self, *params): - # TODO: make this a decorator or context manager - self._authenticated.wait() - - sub_message = '{"action":"subscribe","params":"%s"}' % self._format_params(params) - self.ws.send(sub_message) + fparams = _format_params(params) + self.ws.send(f'{{"action":"subscribe","params":"{fparams}"}}') def unsubscribe(self, *params): - # TODO: make this a decorator or context manager - self._authenticated.wait() - - sub_message = '{"action":"unsubscribe","params":"%s"}' % self._format_params(params) - self.ws.send(sub_message) - - def _authenticate(self, ws): - ws.send('{"action":"auth","params":"%s"}' % self.auth_key) - self._authenticated.set() - - @staticmethod - def _format_params(params): - return ",".join(params) + fparams = _format_params(params) + self.ws.send(f'{{"action":"unsubscribe","params":"{fparams}"}}') - def _default_on_open(self): - def f(ws): - self._authenticate(ws) + def authenticate(self): + self.ws.send(f'{{"action":"auth","params":"{self.auth_key}"}}') - return f From 89497483b941ea600462e05257dff2aa2e499e1a Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 6 Mar 2024 08:02:35 +0100 Subject: [PATCH 11/14] Bumped version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ce5898e9..9a1a28a2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = "0.3" +VERSION = "0.4" def get_readme_md_contents(): """read the contents of your README file""" From 946c56370a520dc1fc24a00b0c7995488eb1f85e Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 6 Mar 2024 09:26:19 +0100 Subject: [PATCH 12/14] Allow for passing delayd service. --- polygon/websocket/websocket_client.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index a440b62f..9c7ee3dd 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -13,17 +13,11 @@ def _format_params(params): class WebSocketClient: - DEFAULT_HOST = 'socket.polygon.io' - - # TODO: Either an instance of the client couples 1:1 with the cluster or an instance of the Client couples 1:3 with - # the 3 possible clusters (I think I like client per, but then a problem is the user can make multiple clients for - # the same cluster and that's not desirable behavior, - # somehow keeping track with multiple Client instances will be the difficulty) - def __init__(self, cluster: str, auth_key: str, + def __init__(self, cluster: str, auth_key: str, service: str = 'socket', process_message: Optional[Callable[[websocket.WebSocketApp, str], None]] = None, on_close: Optional[Callable[[websocket.WebSocketApp], None]] = None, on_error: Optional[Callable[[websocket.WebSocketApp, str], None]] = None): - self.url = f'wss://{self.DEFAULT_HOST}/{cluster}' + self.url = f'wss://{service}.polygon.io/{cluster}' self.auth_key = auth_key self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, From 37f4ccad7bb713cd2978974f155e8ed227e2004e Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Wed, 6 Mar 2024 09:28:44 +0100 Subject: [PATCH 13/14] Allow better None handling. --- polygon/websocket/websocket_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polygon/websocket/websocket_client.py b/polygon/websocket/websocket_client.py index 9c7ee3dd..9b18bf7d 100644 --- a/polygon/websocket/websocket_client.py +++ b/polygon/websocket/websocket_client.py @@ -13,11 +13,11 @@ def _format_params(params): class WebSocketClient: - def __init__(self, cluster: str, auth_key: str, service: str = 'socket', + def __init__(self, cluster: str, auth_key: str, service: str = None, process_message: Optional[Callable[[websocket.WebSocketApp, str], None]] = None, on_close: Optional[Callable[[websocket.WebSocketApp], None]] = None, on_error: Optional[Callable[[websocket.WebSocketApp, str], None]] = None): - self.url = f'wss://{service}.polygon.io/{cluster}' + self.url = f'wss://{service or "socket"}.polygon.io/{cluster}' self.auth_key = auth_key self.ws: websocket.WebSocketApp = websocket.WebSocketApp(self.url, From e069359f2c9a98716993b98c66701c9ac66d2e39 Mon Sep 17 00:00:00 2001 From: Davide Libenzi Date: Thu, 1 May 2025 19:04:39 +0200 Subject: [PATCH 14/14] Added project file. --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..242e366b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +# pyproject.toml +[build-system] +# XXX: If your project needs other packages to build properly, add them to this list. +requires = ["setuptools >= 42.0.0"] +build-backend = "setuptools.build_meta"