wiz-trader 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wiz_trader/__init__.py CHANGED
@@ -3,6 +3,6 @@
3
3
  from .quotes import QuotesClient
4
4
  from .apis import WizzerClient
5
5
 
6
- __version__ = "0.7.0"
6
+ __version__ = "0.9.0"
7
7
 
8
8
  __all__ = ["QuotesClient", "WizzerClient"]
@@ -3,7 +3,7 @@ import json
3
3
  import os
4
4
  import logging
5
5
  import random
6
- from typing import Callable, List, Optional
6
+ from typing import Callable, List, Optional, Any, Iterator
7
7
 
8
8
  import websockets
9
9
  from websockets.exceptions import ConnectionClosed
@@ -33,7 +33,9 @@ class QuotesClient:
33
33
  self,
34
34
  base_url: Optional[str] = None,
35
35
  token: Optional[str] = None,
36
- log_level: str = "error" # default only errors
36
+ log_level: str = "error", # default only errors
37
+ max_message_size: int = 10 * 1024 * 1024, # 10MB default max size
38
+ batch_size: int = 20 # Max number of instruments to subscribe to at once
37
39
  ):
38
40
  # Configure logger based on log_level.
39
41
  valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
@@ -42,6 +44,8 @@ class QuotesClient:
42
44
  logger.setLevel(valid_levels[log_level])
43
45
 
44
46
  self.log_level = log_level
47
+ self.max_message_size = max_message_size
48
+ self.batch_size = batch_size
45
49
  # System env vars take precedence over .env
46
50
  self.base_url = base_url or os.environ.get("WZ__QUOTES_BASE_URL")
47
51
  self.token = token or os.environ.get("WZ__TOKEN")
@@ -63,6 +67,20 @@ class QuotesClient:
63
67
 
64
68
  logger.debug("Initialized QuotesClient with URL: %s", self.url)
65
69
 
70
+ def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
71
+ """
72
+ Split a list into smaller chunks.
73
+
74
+ Args:
75
+ data (List[Any]): The list to split.
76
+ chunk_size (int): Maximum size of each chunk.
77
+
78
+ Returns:
79
+ Iterator[List[Any]]: Iterator of list chunks.
80
+ """
81
+ for i in range(0, len(data), chunk_size):
82
+ yield data[i:i + chunk_size]
83
+
66
84
  async def connect(self) -> None:
67
85
  """
68
86
  Continuously connect to the quotes server and process incoming messages.
@@ -73,18 +91,23 @@ class QuotesClient:
73
91
  while True:
74
92
  try:
75
93
  logger.info("Connecting to %s ...", self.url)
76
- async with websockets.connect(self.url) as websocket:
94
+ async with websockets.connect(self.url, max_size=self.max_message_size) as websocket:
77
95
  self.ws = websocket
78
96
  logger.info("Connected to the quotes server.")
79
97
 
80
98
  # On reconnection, re-subscribe if needed.
81
99
  if self.subscribed_instruments:
82
- subscribe_msg = {
83
- "action": "subscribe",
84
- "instruments": list(self.subscribed_instruments)
85
- }
86
- await self.ws.send(json.dumps(subscribe_msg))
87
- logger.info("Re-subscribed to instruments: %s", list(self.subscribed_instruments))
100
+ # Split into batches to avoid message size issues
101
+ instruments_list = list(self.subscribed_instruments)
102
+ for batch in self._chunk_list(instruments_list, self.batch_size):
103
+ subscribe_msg = {
104
+ "action": "subscribe",
105
+ "instruments": batch
106
+ }
107
+ await self.ws.send(json.dumps(subscribe_msg))
108
+ logger.info("Re-subscribed to batch of %d instruments", len(batch))
109
+ # Small delay between batches to avoid overwhelming the server
110
+ await asyncio.sleep(0.1)
88
111
 
89
112
  # Reset backoff after a successful connection.
90
113
  backoff = self._backoff_base
@@ -106,23 +129,53 @@ class QuotesClient:
106
129
  async def _handle_messages(self) -> None:
107
130
  """
108
131
  Handle incoming messages and dispatch them via the on_tick callback.
132
+ Handles newline-delimited JSON objects in a single message.
109
133
  """
110
134
  try:
111
135
  async for message in self.ws: # type: ignore
112
- try:
113
- tick = json.loads(message)
114
- if self.on_tick:
115
- self.on_tick(tick)
136
+ # Log message size for debugging large message issues
137
+ if self.log_level == "debug" and isinstance(message, str):
138
+ message_size = len(message.encode("utf-8"))
139
+ if message_size > 1024 * 1024: # Over 1MB
140
+ logger.debug("Received large message: %d bytes", message_size)
141
+ # Log the beginning of the message for debugging
142
+ logger.debug("Message starts with: %s", message[:100])
143
+
144
+ if isinstance(message, str):
145
+ # Special handling for newline-delimited JSON
146
+ if '\n' in message:
147
+ # Split by newlines and process each JSON object separately
148
+ for json_str in message.strip().split('\n'):
149
+ if not json_str:
150
+ continue
151
+
152
+ try:
153
+ tick = json.loads(json_str)
154
+ if self.on_tick:
155
+ self.on_tick(tick)
156
+ except json.JSONDecodeError as e:
157
+ logger.error("Failed to parse JSON object: %s", str(e))
158
+ logger.error("Invalid JSON: %s...", json_str[:100])
116
159
  else:
117
- logger.debug("Received tick (no on_tick callback set): %s", tick)
118
- except json.JSONDecodeError:
119
- logger.debug("Received non-JSON message: %s", message)
160
+ # Single JSON object
161
+ try:
162
+ tick = json.loads(message)
163
+ if self.on_tick:
164
+ self.on_tick(tick)
165
+ except json.JSONDecodeError as e:
166
+ logger.error("Failed to parse JSON: %s", str(e))
167
+ logger.error("Invalid JSON message: %s...", message[:100])
168
+ else:
169
+ logger.warning("Received non-string message: %s", type(message))
120
170
  except ConnectionClosed as e:
121
171
  logger.info("Connection closed during message handling: %s", e)
172
+ except Exception as e:
173
+ logger.error("Error processing message: %s", str(e), exc_info=True)
122
174
 
123
175
  async def subscribe(self, instruments: List[str]) -> None:
124
176
  """
125
177
  Subscribe to a list of instruments and update the subscription list.
178
+ Splits large subscription requests into batches to avoid message size issues.
126
179
 
127
180
  Args:
128
181
  instruments (List[str]): List of instrument identifiers.
@@ -131,32 +184,55 @@ class QuotesClient:
131
184
  new_instruments = set(instruments) - self.subscribed_instruments
132
185
  if new_instruments:
133
186
  self.subscribed_instruments.update(new_instruments)
134
- message = {"action": "subscribe", "instruments": list(new_instruments)}
135
- await self.ws.send(json.dumps(message))
136
- logger.info("Sent subscription message for instruments: %s", list(new_instruments))
187
+
188
+ # Split into batches to avoid message size issues
189
+ new_instruments_list = list(new_instruments)
190
+ for batch in self._chunk_list(new_instruments_list, self.batch_size):
191
+ logger.info("Subscribing to batch of %d instruments", len(batch))
192
+ message = {"action": "subscribe", "instruments": batch}
193
+ await self.ws.send(json.dumps(message))
194
+ # Small delay between batches to avoid overwhelming the server
195
+ await asyncio.sleep(0.1)
196
+
197
+ logger.info("Completed subscription for %d new instruments", len(new_instruments))
137
198
  else:
138
199
  logger.info("Instruments already subscribed: %s", instruments)
139
200
  else:
140
201
  logger.info("Cannot subscribe: WebSocket is not connected.")
202
+ # Still update the subscription list so we can subscribe when connected
203
+ self.subscribed_instruments.update(set(instruments))
141
204
 
142
205
  async def unsubscribe(self, instruments: List[str]) -> None:
143
206
  """
144
207
  Unsubscribe from a list of instruments and update the subscription list.
208
+ Splits large unsubscription requests into batches to avoid message size issues.
145
209
 
146
210
  Args:
147
211
  instruments (List[str]): List of instrument identifiers.
148
212
  """
149
213
  if self.ws and self.ws.state == State.OPEN:
150
214
  unsub_set = set(instruments)
151
- if unsub_set & self.subscribed_instruments:
152
- self.subscribed_instruments.difference_update(unsub_set)
153
- message = {"action": "unsubscribe", "instruments": list(unsub_set)}
154
- await self.ws.send(json.dumps(message))
155
- logger.info("Sent unsubscription message for instruments: %s", list(unsub_set))
215
+ to_unsubscribe = unsub_set & self.subscribed_instruments
216
+
217
+ if to_unsubscribe:
218
+ self.subscribed_instruments.difference_update(to_unsubscribe)
219
+
220
+ # Split into batches to avoid message size issues
221
+ unsub_list = list(to_unsubscribe)
222
+ for batch in self._chunk_list(unsub_list, self.batch_size):
223
+ logger.info("Unsubscribing from batch of %d instruments", len(batch))
224
+ message = {"action": "unsubscribe", "instruments": batch}
225
+ await self.ws.send(json.dumps(message))
226
+ # Small delay between batches to avoid overwhelming the server
227
+ await asyncio.sleep(0.1)
228
+
229
+ logger.info("Completed unsubscription for %d instruments", len(to_unsubscribe))
156
230
  else:
157
231
  logger.info("No matching instruments found in current subscription.")
158
232
  else:
159
233
  logger.info("Cannot unsubscribe: WebSocket is not connected.")
234
+ # Still update the subscription list
235
+ self.subscribed_instruments.difference_update(set(instruments))
160
236
 
161
237
  async def close(self) -> None:
162
238
  """
@@ -164,4 +240,4 @@ class QuotesClient:
164
240
  """
165
241
  if self.ws:
166
242
  await self.ws.close()
167
- logger.info("WebSocket connection closed.")
243
+ logger.info("WebSocket connection closed.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wiz_trader
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A Python SDK for connecting to the Wizzer.
5
5
  Home-page: https://bitbucket.org/wizzer-tech/quotes_sdk.git
6
6
  Author: Pawan Wagh
@@ -0,0 +1,9 @@
1
+ wiz_trader/__init__.py,sha256=IOxpQuLhYFFBe75RCqoa81faDkt84n1pMIuiTND5_Kc,181
2
+ wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
3
+ wiz_trader/apis/client.py,sha256=rq6FEA5DDCGXc4axSSzOdVvet1NWlW8L843GMp0qXQA,20562
4
+ wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
5
+ wiz_trader/quotes/client.py,sha256=gYiX65AjzCEHIwCROjcu7d6uGirpGj4bnkNQ2UOQEv8,9790
6
+ wiz_trader-0.9.0.dist-info/METADATA,sha256=sfnuzoMTsXBeUYdnJ7-Bns7116N5VErpbiIsFfkkigU,4281
7
+ wiz_trader-0.9.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
8
+ wiz_trader-0.9.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
9
+ wiz_trader-0.9.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- wiz_trader/__init__.py,sha256=V4DgwVGNtcnqYZhVe7pww5-IHtYqge5CQXqkaS81mfo,181
2
- wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
3
- wiz_trader/apis/client.py,sha256=rq6FEA5DDCGXc4axSSzOdVvet1NWlW8L843GMp0qXQA,20562
4
- wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
5
- wiz_trader/quotes/client.py,sha256=fUHTMGDauGF9cjsFsVAzoOwqSgD555_TLoNqmnFhLdQ,6203
6
- wiz_trader-0.7.0.dist-info/METADATA,sha256=kqDpwsUfiECcbxhVKPUEuSedAu50L-OOPX2M4JAW12o,4281
7
- wiz_trader-0.7.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
8
- wiz_trader-0.7.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
9
- wiz_trader-0.7.0.dist-info/RECORD,,