wiz-trader 0.21.0__py3-none-any.whl → 0.23.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.21.0"
6
+ __version__ = "0.23.0"
7
7
 
8
8
  __all__ = ["QuotesClient", "WizzerClient"]
@@ -3,11 +3,16 @@ import json
3
3
  import os
4
4
  import logging
5
5
  import random
6
- from typing import Callable, List, Optional, Any, Iterator
6
+ from typing import Callable, List, Optional, Any, Iterator, Dict, Set, TypeVar, Protocol, runtime_checkable
7
7
 
8
- import websockets
9
- from websockets.exceptions import ConnectionClosed
10
- from websockets.protocol import State
8
+ T = TypeVar('T')
9
+
10
+ @runtime_checkable
11
+ class WebSocketClientProtocol(Protocol):
12
+ async def send(self, message: str) -> None: ...
13
+ async def close(self) -> None: ...
14
+ @property
15
+ def open(self) -> bool: ...
11
16
 
12
17
  # Setup module‐level logger with a default handler if none exists.
13
18
  logger = logging.getLogger(__name__)
@@ -39,6 +44,9 @@ class QuotesClient:
39
44
  max_message_size: int = 10 * 1024 * 1024,
40
45
  batch_size: int = 20
41
46
  ):
47
+ logger.debug("Initializing QuotesClient with params: base_url=%s, log_level=%s, max_message_size=%d, batch_size=%d",
48
+ base_url, log_level, max_message_size, batch_size)
49
+
42
50
  valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
43
51
  if log_level not in valid_levels:
44
52
  raise ValueError(f"log_level must be one of {list(valid_levels.keys())}")
@@ -56,65 +64,60 @@ class QuotesClient:
56
64
  raise ValueError("Base URL must be provided as an argument or in .env (WZ__QUOTES_BASE_URL)")
57
65
 
58
66
  self.url = f"{self.base_url}?token={self.token}"
59
- self.ws: Optional[websockets.WebSocketClientProtocol] = None
60
- self.subscribed_instruments: set = set()
67
+ self.ws: Optional[WebSocketClientProtocol] = None
68
+ self.subscribed_instruments: Set[str] = set()
61
69
  self._running = False
62
- self._background_task = None
70
+ self._background_task: Optional[asyncio.Task[None]] = None
63
71
 
64
72
  self._backoff_base = 1
65
73
  self._backoff_factor = 2
66
74
  self._backoff_max = 60
67
75
 
68
76
  # Callbacks are plain synchronous functions
69
- self.on_tick: Optional[Callable[[Any, dict], None]] = None
77
+ self.on_tick: Optional[Callable[[Any, Dict[str, Any]], None]] = None
70
78
  self.on_connect: Optional[Callable[[Any], None]] = None
71
79
  self.on_close: Optional[Callable[[Any, Optional[int], Optional[str]], None]] = None
72
80
  self.on_error: Optional[Callable[[Any, Exception], None]] = None
73
81
 
74
- logger.debug("Initialized QuotesClient with URL: %s", self.url)
82
+ logger.debug("QuotesClient initialized successfully")
75
83
 
76
84
  def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
77
- for i in range(0, len(data), chunk_size):
78
- yield data[i:i + chunk_size]
85
+ logger.debug("Chunking list of size %d with chunk_size %d", len(data), chunk_size)
86
+ return (data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
79
87
 
80
88
  async def _connect_with_backoff(self) -> None:
81
89
  backoff = self._backoff_base
90
+ logger.debug("Starting connection with initial backoff: %d", backoff)
82
91
 
83
92
  while self._running:
84
93
  try:
85
- logger.info("Connecting to %s ...", self.url)
86
- async with websockets.connect(self.url, max_size=self.max_message_size) as websocket:
87
- self.ws = websocket
88
- logger.info("Connected to the quotes server.")
89
-
90
- # plain sync on_connect
91
- if self.on_connect:
92
- try:
93
- self.on_connect(self)
94
- except Exception as e:
95
- logger.error("Error in on_connect callback: %s", e, exc_info=True)
96
-
97
- # re-subscribe on reconnect
98
- if self.subscribed_instruments:
99
- for batch in self._chunk_list(list(self.subscribed_instruments), self.batch_size):
100
- msg = {"action": self.ACTION_SUBSCRIBE, "instruments": batch}
94
+ logger.debug("Attempting WebSocket connection to %s", self.url)
95
+ # Using string literal for import to avoid type checking issues
96
+ ws = await __import__('websockets').connect(self.url, max_size=self.max_message_size)
97
+ self.ws = ws
98
+ logger.debug("WebSocket connection established successfully")
99
+
100
+ if self.on_connect:
101
+ logger.debug("Executing on_connect callback")
102
+ try:
103
+ self.on_connect(self)
104
+ except Exception as e:
105
+ logger.error("Error in on_connect callback: %s", e, exc_info=True)
106
+
107
+ if self.subscribed_instruments:
108
+ logger.debug("Re-subscribing to %d instruments", len(self.subscribed_instruments))
109
+ for batch in self._chunk_list(list(self.subscribed_instruments), self.batch_size):
110
+ msg = {"action": self.ACTION_SUBSCRIBE, "instruments": batch}
111
+ if self.ws: # Check if ws is not None
101
112
  await self.ws.send(json.dumps(msg))
102
113
  logger.info("Re-subscribed to %d instruments", len(batch))
103
114
  await asyncio.sleep(0.1)
104
115
 
105
- backoff = self._backoff_base
106
- await self._handle_messages()
107
-
108
- except ConnectionClosed as e:
109
- logger.info("Disconnected: %s", e)
110
- if self.on_close:
111
- try:
112
- self.on_close(self, getattr(e, 'code', None), str(e))
113
- except Exception as ex:
114
- logger.error("Error in on_close callback: %s", ex, exc_info=True)
116
+ backoff = self._backoff_base
117
+ await self._handle_messages()
115
118
 
116
119
  except Exception as e:
117
- logger.error("Connection error: %s", e, exc_info=True)
120
+ logger.debug("Connection error occurred: %s", str(e), exc_info=True)
118
121
  if self.on_error:
119
122
  try:
120
123
  self.on_error(self, e)
@@ -122,70 +125,90 @@ class QuotesClient:
122
125
  logger.error("Error in on_error callback: %s", ex, exc_info=True)
123
126
 
124
127
  if not self._running:
128
+ logger.debug("Client stopped, breaking reconnection loop")
125
129
  break
126
130
 
127
131
  sleep_time = min(backoff, self._backoff_max)
128
- logger.info("Reconnecting in %s seconds...", sleep_time)
132
+ logger.debug("Calculated reconnection backoff: %s seconds", sleep_time)
129
133
  await asyncio.sleep(sleep_time)
130
134
  backoff = backoff * self._backoff_factor + random.uniform(0, 1)
131
135
 
132
136
  async def _handle_messages(self) -> None:
133
137
  try:
138
+ logger.debug("Starting message handling loop")
139
+ if self.ws is None:
140
+ logger.error("WebSocket connection is None")
141
+ return
142
+
143
+ # Using string literal for import to avoid type checking issues
134
144
  async for message in self.ws: # type: ignore
135
- if self.log_level == "debug" and isinstance(message, str):
136
- size = len(message.encode("utf-8"))
137
- if size > 1024 * 1024:
138
- logger.debug("Large message: %d bytes", size)
139
-
140
145
  if isinstance(message, str):
146
+ msg_size = len(message.encode("utf-8"))
147
+ logger.debug("Received message of size: %d bytes", msg_size)
148
+
141
149
  for chunk in message.strip().split("\n"):
142
150
  if not chunk:
143
151
  continue
144
152
  try:
145
153
  tick = json.loads(chunk)
154
+ logger.debug("Successfully parsed JSON message for instrument: %s",
155
+ tick.get('instrument', 'unknown'))
146
156
  if self.on_tick:
157
+ logger.debug("Executing on_tick callback")
147
158
  self.on_tick(self, tick)
148
159
  except json.JSONDecodeError as e:
160
+ logger.debug("Failed to parse JSON message: %s. Content: %s", str(e), chunk[:100])
149
161
  logger.error("JSON parse error: %s", e)
150
162
  else:
151
- logger.warning("Non-string message: %s", type(message))
152
- except ConnectionClosed:
153
- logger.info("Connection closed during message handling")
163
+ logger.debug("Received non-string message of type: %s", type(message).__name__)
164
+ logger.warning("Non-string message: %s", type(message).__name__)
154
165
  except Exception as e:
155
- logger.error("Error processing message: %s", e, exc_info=True)
166
+ logger.debug("Error in message handling: %s", str(e), exc_info=True)
156
167
  if self.on_error:
157
168
  try:
158
169
  self.on_error(self, e)
159
170
  except Exception:
160
171
  pass
161
172
 
162
- # -- Async core methods (for internal use) --
163
-
164
173
  async def _subscribe_async(self, instruments: List[str]) -> None:
165
- """
166
- Internal async subscription. Use `subscribe()` wrapper to schedule this.
167
- """
168
- if self.ws and self.ws.state == State.OPEN:
174
+ logger.debug("Processing async subscription request for %d instruments", len(instruments))
175
+ if self.ws and self.ws.open:
169
176
  new = set(instruments) - self.subscribed_instruments
170
177
  if new:
178
+ logger.debug("Found %d new instruments to subscribe", len(new))
171
179
  self.subscribed_instruments |= new
172
180
  for batch in self._chunk_list(list(new), self.batch_size):
173
- logger.info("Subscribing to %d instruments", len(batch))
181
+ logger.debug("Sending subscription request for batch of %d instruments", len(batch))
174
182
  await self.ws.send(json.dumps({
175
183
  "action": self.ACTION_SUBSCRIBE,
176
184
  "instruments": batch
177
185
  }))
178
186
  await asyncio.sleep(0.1)
179
187
  else:
180
- # queue it for next connect
188
+ logger.debug("WebSocket not ready, queueing %d instruments for later subscription", len(instruments))
181
189
  self.subscribed_instruments |= set(instruments)
182
190
 
183
- # -- Public wrappers for plain callback users --
191
+ async def _unsubscribe_async(self, instruments: List[str]) -> None:
192
+ logger.debug("Processing async unsubscription request for %d instruments", len(instruments))
193
+ if self.ws and self.ws.open:
194
+ to_remove = set(instruments) & self.subscribed_instruments
195
+ if to_remove:
196
+ logger.debug("Found %d instruments to unsubscribe", len(to_remove))
197
+ for batch in self._chunk_list(list(to_remove), self.batch_size):
198
+ logger.debug("Sending unsubscription request for batch of %d instruments", len(batch))
199
+ await self.ws.send(json.dumps({
200
+ "action": self.ACTION_UNSUBSCRIBE,
201
+ "instruments": batch
202
+ }))
203
+ await asyncio.sleep(0.1)
204
+ logger.debug("Removed %d instruments from subscription set", len(to_remove))
205
+ self.subscribed_instruments -= to_remove
206
+ else:
207
+ logger.debug("WebSocket not ready, removing %d instruments from queue", len(instruments))
208
+ self.subscribed_instruments -= set(instruments)
184
209
 
185
210
  def subscribe(self, instruments: List[str]) -> None:
186
- """
187
- Schedule an async subscribe so users can call this without 'await'.
188
- """
211
+ logger.debug("Scheduling subscription for %d instruments", len(instruments))
189
212
  try:
190
213
  loop = asyncio.get_event_loop()
191
214
  except RuntimeError:
@@ -193,17 +216,24 @@ class QuotesClient:
193
216
  asyncio.set_event_loop(loop)
194
217
  loop.create_task(self._subscribe_async(instruments))
195
218
 
196
- # You could add a similar unsubscribe wrapper if needed
219
+ def unsubscribe(self, instruments: List[str]) -> None:
220
+ logger.debug("Scheduling unsubscription for %d instruments", len(instruments))
221
+ try:
222
+ loop = asyncio.get_event_loop()
223
+ except RuntimeError:
224
+ loop = asyncio.new_event_loop()
225
+ asyncio.set_event_loop(loop)
226
+ loop.create_task(self._unsubscribe_async(instruments))
197
227
 
198
228
  async def close(self) -> None:
199
- """
200
- Close the WebSocket connection.
201
- """
229
+ logger.debug("Initiating WebSocket connection closure")
202
230
  self._running = False
203
231
  if self.ws:
232
+ logger.debug("Closing active WebSocket connection")
204
233
  await self.ws.close()
205
234
  logger.info("WebSocket closed.")
206
235
  if self._background_task and not self._background_task.done():
236
+ logger.debug("Cancelling background connection task")
207
237
  self._background_task.cancel()
208
238
  try:
209
239
  await self._background_task
@@ -211,9 +241,7 @@ class QuotesClient:
211
241
  pass
212
242
 
213
243
  def connect(self) -> None:
214
- """
215
- Blocking connect (runs the internal asyncio loop until stop()).
216
- """
244
+ logger.debug("Starting blocking connect operation")
217
245
  self._running = True
218
246
  try:
219
247
  loop = asyncio.get_event_loop()
@@ -234,9 +262,7 @@ class QuotesClient:
234
262
  pass
235
263
 
236
264
  def connect_async(self) -> None:
237
- """
238
- Non-blocking connect: starts the background task.
239
- """
265
+ logger.debug("Starting non-blocking async connect operation")
240
266
  if self._running:
241
267
  logger.warning("Client already running.")
242
268
  return
@@ -245,8 +271,6 @@ class QuotesClient:
245
271
  self._background_task = loop.create_task(self._connect_with_backoff())
246
272
 
247
273
  def stop(self) -> None:
248
- """
249
- Signal the client to stop and close.
250
- """
274
+ logger.debug("Stop signal received")
251
275
  self._running = False
252
276
  logger.info("Client stopping; will close soon.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wiz_trader
3
- Version: 0.21.0
3
+ Version: 0.23.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=6c10AeBkvODzgW6IRSOimyIKsojSQTsjDk05-tZwcrs,182
2
+ wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
3
+ wiz_trader/apis/client.py,sha256=GY1aAaV4ia1tnFnB2qaNqnv-qeUvkVlvw9xOKN54qIs,59786
4
+ wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
5
+ wiz_trader/quotes/client.py,sha256=UTkBGwrrlNO6i5r8oH63B9vXTvkvb-_bFlqHGiHufik,12289
6
+ wiz_trader-0.23.0.dist-info/METADATA,sha256=O_3IvT5NG0r-YMthKIZZSq_refcgZRdZfT1LykSFwl0,87046
7
+ wiz_trader-0.23.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ wiz_trader-0.23.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
9
+ wiz_trader-0.23.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- wiz_trader/__init__.py,sha256=34Jeuq5zT3Iu6yzfCqm63Zjemvdt3p4Knbk642fZPyI,182
2
- wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
3
- wiz_trader/apis/client.py,sha256=GY1aAaV4ia1tnFnB2qaNqnv-qeUvkVlvw9xOKN54qIs,59786
4
- wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
5
- wiz_trader/quotes/client.py,sha256=LJeMcQPjJIRxrTIGalWsLYh_XfinDXBP5-4cNS7qCxc,9709
6
- wiz_trader-0.21.0.dist-info/METADATA,sha256=QAED1SHUWcA_U1IzP0_ugjC1Ne1xkrPsTPKJJIKyZkw,87046
7
- wiz_trader-0.21.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- wiz_trader-0.21.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
9
- wiz_trader-0.21.0.dist-info/RECORD,,