Qubx 0.6.58__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.59__cp312-cp312-manylinux_2_39_x86_64.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.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- qubx/connectors/ccxt/data.py +119 -11
- qubx/core/mixins/processing.py +3 -3
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- {qubx-0.6.58.dist-info → qubx-0.6.59.dist-info}/METADATA +1 -1
- {qubx-0.6.58.dist-info → qubx-0.6.59.dist-info}/RECORD +10 -10
- {qubx-0.6.58.dist-info → qubx-0.6.59.dist-info}/LICENSE +0 -0
- {qubx-0.6.58.dist-info → qubx-0.6.59.dist-info}/WHEEL +0 -0
- {qubx-0.6.58.dist-info → qubx-0.6.59.dist-info}/entry_points.txt +0 -0
qubx/connectors/ccxt/data.py
CHANGED
|
@@ -44,10 +44,12 @@ class CcxtDataProvider(IDataProvider):
|
|
|
44
44
|
|
|
45
45
|
# - subscriptions
|
|
46
46
|
_subscriptions: Dict[str, Set[Instrument]]
|
|
47
|
+
_pending_subscriptions: Dict[str, Set[Instrument]] # Track subscriptions being established
|
|
47
48
|
_sub_to_coro: Dict[str, concurrent.futures.Future]
|
|
48
49
|
_sub_to_name: Dict[str, str]
|
|
49
50
|
_sub_to_unsubscribe: Dict[str, Callable[[], Awaitable[None]]]
|
|
50
51
|
_is_sub_name_enabled: Dict[str, bool]
|
|
52
|
+
_sub_connection_ready: Dict[str, bool] # Track if connection is actually ready
|
|
51
53
|
|
|
52
54
|
_sub_instr_to_time: Dict[Tuple[str, Instrument], dt_64]
|
|
53
55
|
_last_quotes: Dict[Instrument, Optional[Quote]]
|
|
@@ -80,10 +82,12 @@ class CcxtDataProvider(IDataProvider):
|
|
|
80
82
|
|
|
81
83
|
self._last_quotes = defaultdict(lambda: None)
|
|
82
84
|
self._subscriptions = defaultdict(set)
|
|
85
|
+
self._pending_subscriptions = defaultdict(set)
|
|
83
86
|
self._sub_to_coro = {}
|
|
84
87
|
self._sub_to_name = {}
|
|
85
88
|
self._sub_to_unsubscribe = {}
|
|
86
89
|
self._is_sub_name_enabled = defaultdict(lambda: False)
|
|
90
|
+
self._sub_connection_ready = defaultdict(lambda: False)
|
|
87
91
|
self._symbol_to_instrument = {}
|
|
88
92
|
self._subscribers = {
|
|
89
93
|
n.split("_subscribe_")[1]: f
|
|
@@ -135,11 +139,33 @@ class CcxtDataProvider(IDataProvider):
|
|
|
135
139
|
def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
|
|
136
140
|
if not subscription_type:
|
|
137
141
|
return list(self.subscribed_instruments)
|
|
138
|
-
|
|
142
|
+
|
|
143
|
+
# Return active subscriptions, fallback to pending if no active ones
|
|
144
|
+
_sub_type, _ = DataType.from_str(subscription_type)
|
|
145
|
+
if _sub_type in self._subscriptions:
|
|
146
|
+
return list(self._subscriptions[_sub_type])
|
|
147
|
+
elif _sub_type in self._pending_subscriptions:
|
|
148
|
+
return list(self._pending_subscriptions[_sub_type])
|
|
149
|
+
else:
|
|
150
|
+
return []
|
|
139
151
|
|
|
140
152
|
def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
141
153
|
sub_type, _ = DataType.from_str(subscription_type)
|
|
142
|
-
return
|
|
154
|
+
# Only return True if subscription is actually active (not just pending)
|
|
155
|
+
return (
|
|
156
|
+
sub_type in self._subscriptions
|
|
157
|
+
and instrument in self._subscriptions[sub_type]
|
|
158
|
+
and self._sub_connection_ready.get(sub_type, False)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def has_pending_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
162
|
+
"""Check if a subscription is pending (connection being established)."""
|
|
163
|
+
sub_type, _ = DataType.from_str(subscription_type)
|
|
164
|
+
return (
|
|
165
|
+
sub_type in self._pending_subscriptions
|
|
166
|
+
and instrument in self._pending_subscriptions[sub_type]
|
|
167
|
+
and not self._sub_connection_ready.get(subscription_type, False)
|
|
168
|
+
)
|
|
143
169
|
|
|
144
170
|
def warmup(self, warmups: Dict[Tuple[str, Instrument], str]) -> None:
|
|
145
171
|
_coros = []
|
|
@@ -207,9 +233,9 @@ class CcxtDataProvider(IDataProvider):
|
|
|
207
233
|
|
|
208
234
|
@property
|
|
209
235
|
def subscribed_instruments(self) -> Set[Instrument]:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return
|
|
236
|
+
active = set.union(*self._subscriptions.values()) if self._subscriptions else set()
|
|
237
|
+
pending = set.union(*self._pending_subscriptions.values()) if self._pending_subscriptions else set()
|
|
238
|
+
return active.union(pending)
|
|
213
239
|
|
|
214
240
|
@property
|
|
215
241
|
def is_read_only(self) -> bool:
|
|
@@ -226,19 +252,29 @@ class CcxtDataProvider(IDataProvider):
|
|
|
226
252
|
if _subscriber is None:
|
|
227
253
|
raise ValueError(f"{self._exchange_id}: Subscription type {sub_type} is not supported")
|
|
228
254
|
|
|
255
|
+
# Save old subscription state before starting cleanup
|
|
256
|
+
old_sub_info = None
|
|
229
257
|
if sub_type in self._sub_to_coro:
|
|
258
|
+
old_sub_info = {"name": self._sub_to_name[sub_type], "coro": self._sub_to_coro[sub_type]}
|
|
230
259
|
logger.debug(
|
|
231
|
-
f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions
|
|
260
|
+
f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions.get(_sub_type, set())}"
|
|
232
261
|
)
|
|
233
|
-
|
|
234
|
-
|
|
262
|
+
|
|
263
|
+
# Clear state immediately to prevent interference with new subscription
|
|
235
264
|
del self._sub_to_coro[sub_type]
|
|
236
265
|
del self._sub_to_name[sub_type]
|
|
237
|
-
|
|
266
|
+
# Clean up both active and pending subscriptions
|
|
267
|
+
self._subscriptions.pop(_sub_type, None)
|
|
268
|
+
self._pending_subscriptions.pop(_sub_type, None)
|
|
269
|
+
self._sub_connection_ready.pop(sub_type, None)
|
|
238
270
|
|
|
239
271
|
if instruments is not None and len(instruments) == 0:
|
|
240
272
|
return
|
|
241
273
|
|
|
274
|
+
# Mark subscription as pending (not active yet)
|
|
275
|
+
self._pending_subscriptions[_sub_type] = instruments
|
|
276
|
+
self._sub_connection_ready[sub_type] = False
|
|
277
|
+
|
|
242
278
|
kwargs = {"instruments": instruments, **_params}
|
|
243
279
|
_subscriber = self._subscribers[_sub_type]
|
|
244
280
|
_subscriber_params = set(_subscriber.__code__.co_varnames[: _subscriber.__code__.co_argcount])
|
|
@@ -247,7 +283,12 @@ class CcxtDataProvider(IDataProvider):
|
|
|
247
283
|
self._sub_to_name[sub_type] = (name := self._get_subscription_name(_sub_type, **kwargs))
|
|
248
284
|
self._sub_to_coro[sub_type] = self._loop.submit(_subscriber(self, name, _sub_type, self.channel, **kwargs))
|
|
249
285
|
|
|
250
|
-
|
|
286
|
+
# Now stop the old subscriber after new one is started (to avoid interference)
|
|
287
|
+
if old_sub_info is not None:
|
|
288
|
+
# Stop old subscriber in background to avoid blocking
|
|
289
|
+
self._loop.submit(self._stop_old_subscriber(old_sub_info["name"], old_sub_info["coro"]))
|
|
290
|
+
|
|
291
|
+
# Don't set _subscriptions here - it will be set when connection is established
|
|
251
292
|
|
|
252
293
|
def _time_msec_nbars_back(self, timeframe: str, nbarsback: int = 1) -> int:
|
|
253
294
|
return (self.time_provider.time() - nbarsback * pd.Timedelta(timeframe)).asm8.item() // 1000000
|
|
@@ -278,6 +319,14 @@ class CcxtDataProvider(IDataProvider):
|
|
|
278
319
|
_name += f" ({kwargs_str})"
|
|
279
320
|
return _name
|
|
280
321
|
|
|
322
|
+
def _mark_subscription_active(self, sub_type: str) -> None:
|
|
323
|
+
"""Mark a subscription as active once the WebSocket connection is established."""
|
|
324
|
+
_sub_type, _ = DataType.from_str(sub_type)
|
|
325
|
+
if _sub_type in self._pending_subscriptions:
|
|
326
|
+
self._subscriptions[_sub_type] = self._pending_subscriptions[_sub_type]
|
|
327
|
+
self._sub_connection_ready[sub_type] = True
|
|
328
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Subscription {sub_type} is now active")
|
|
329
|
+
|
|
281
330
|
async def _stop_subscriber(self, sub_type: str, sub_name: str) -> None:
|
|
282
331
|
try:
|
|
283
332
|
self._is_sub_name_enabled[sub_name] = False # stop the subscriber
|
|
@@ -303,11 +352,57 @@ class CcxtDataProvider(IDataProvider):
|
|
|
303
352
|
del self._sub_to_unsubscribe[sub_name]
|
|
304
353
|
|
|
305
354
|
del self._is_sub_name_enabled[sub_name]
|
|
355
|
+
|
|
356
|
+
# Clean up connection state for this subscription
|
|
357
|
+
for sub_type, stream_name in list(self._sub_to_name.items()):
|
|
358
|
+
if stream_name == sub_name:
|
|
359
|
+
self._sub_connection_ready.pop(sub_type, None)
|
|
360
|
+
break
|
|
361
|
+
|
|
306
362
|
logger.debug(f"<yellow>{self._exchange_id}</yellow> Unsubscribed from {sub_name}")
|
|
307
363
|
except Exception as e:
|
|
308
364
|
logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping {sub_name}")
|
|
309
365
|
logger.exception(e)
|
|
310
366
|
|
|
367
|
+
async def _stop_old_subscriber(self, old_name: str, old_coro: concurrent.futures.Future) -> None:
|
|
368
|
+
"""Stop an old subscriber safely without interfering with new subscriptions."""
|
|
369
|
+
try:
|
|
370
|
+
# Disable the old stream by name
|
|
371
|
+
self._is_sub_name_enabled[old_name] = False
|
|
372
|
+
|
|
373
|
+
# Wait for the old coroutine to finish
|
|
374
|
+
total_sleep_time = 0.0
|
|
375
|
+
while old_coro.running():
|
|
376
|
+
await asyncio.sleep(1.0)
|
|
377
|
+
total_sleep_time += 1.0
|
|
378
|
+
if total_sleep_time >= 20.0:
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
if old_coro.running():
|
|
382
|
+
logger.warning(
|
|
383
|
+
f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} is still running. Cancelling it."
|
|
384
|
+
)
|
|
385
|
+
old_coro.cancel()
|
|
386
|
+
else:
|
|
387
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} has been stopped")
|
|
388
|
+
|
|
389
|
+
# Clean up old unsubscriber if it exists
|
|
390
|
+
if old_name in self._sub_to_unsubscribe:
|
|
391
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Calling old unsubscriber for {old_name}")
|
|
392
|
+
await self._sub_to_unsubscribe[old_name]()
|
|
393
|
+
# Use pop to safely remove, in case it was already removed
|
|
394
|
+
self._sub_to_unsubscribe.pop(old_name, None)
|
|
395
|
+
|
|
396
|
+
# Clean up old stream state
|
|
397
|
+
if old_name in self._is_sub_name_enabled:
|
|
398
|
+
del self._is_sub_name_enabled[old_name]
|
|
399
|
+
|
|
400
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscription {old_name} cleaned up")
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping old subscriber {old_name}")
|
|
404
|
+
logger.exception(e)
|
|
405
|
+
|
|
311
406
|
async def _listen_to_stream(
|
|
312
407
|
self,
|
|
313
408
|
subscriber: Callable[[], Awaitable[None]],
|
|
@@ -322,10 +417,22 @@ class CcxtDataProvider(IDataProvider):
|
|
|
322
417
|
|
|
323
418
|
self._is_sub_name_enabled[name] = True
|
|
324
419
|
n_retry = 0
|
|
420
|
+
connection_established = False
|
|
421
|
+
|
|
325
422
|
while channel.control.is_set() and self._is_sub_name_enabled[name]:
|
|
326
423
|
try:
|
|
327
424
|
await subscriber()
|
|
328
425
|
n_retry = 0
|
|
426
|
+
|
|
427
|
+
# Mark subscription as active on first successful data reception
|
|
428
|
+
if not connection_established:
|
|
429
|
+
# Find the subscription type for this stream name
|
|
430
|
+
for sub_type, stream_name in self._sub_to_name.items():
|
|
431
|
+
if stream_name == name:
|
|
432
|
+
self._mark_subscription_active(sub_type)
|
|
433
|
+
connection_established = True
|
|
434
|
+
break
|
|
435
|
+
|
|
329
436
|
if not self._is_sub_name_enabled[name]:
|
|
330
437
|
break
|
|
331
438
|
except CcxtSymbolNotRecognized:
|
|
@@ -616,7 +723,8 @@ class CcxtDataProvider(IDataProvider):
|
|
|
616
723
|
for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
|
|
617
724
|
instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
|
|
618
725
|
quote = ccxt_convert_ticker(ccxt_ticker)
|
|
619
|
-
|
|
726
|
+
last_quote = self._last_quotes[instrument]
|
|
727
|
+
if last_quote is None or quote.time > last_quote.time:
|
|
620
728
|
self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
|
|
621
729
|
self._last_quotes[instrument] = quote
|
|
622
730
|
channel.send((instrument, sub_type, quote, False))
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -191,9 +191,9 @@ class ProcessingManager(IProcessingManager):
|
|
|
191
191
|
|
|
192
192
|
# - if strategy still fitting - skip on_event call
|
|
193
193
|
if self._fit_is_running:
|
|
194
|
-
logger.debug(
|
|
195
|
-
|
|
196
|
-
)
|
|
194
|
+
# logger.debug(
|
|
195
|
+
# f"Skipping {self._strategy_name}::on_event({instrument}, {d_type}, [...], {is_historical}) fitting in progress (orders and deals processed)!"
|
|
196
|
+
# )
|
|
197
197
|
return False
|
|
198
198
|
|
|
199
199
|
signals: list[Signal] | Signal = []
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -21,7 +21,7 @@ qubx/cli/tui.py,sha256=N15UiNEdnWOWYh8E9DNlQCDWdoyP6rMGMhEItogPW88,16491
|
|
|
21
21
|
qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
|
|
22
22
|
qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
|
|
23
23
|
qubx/connectors/ccxt/broker.py,sha256=Hg2tC3qKPYAURGro9ONzzR7QaiI41oytyKqQlpcM5Zw,16175
|
|
24
|
-
qubx/connectors/ccxt/data.py,sha256=
|
|
24
|
+
qubx/connectors/ccxt/data.py,sha256=gH1ug0vFvbEyUnHvgwlujsJdWbg9RXopIX3_fQhzApk,35456
|
|
25
25
|
qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
|
|
26
26
|
qubx/connectors/ccxt/exchanges/__init__.py,sha256=dEBkyeiGEQgfyuGVhhx4ZTRIlU9e_H1m6K2ROXRpIi8,1884
|
|
27
27
|
qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
|
|
@@ -49,15 +49,15 @@ qubx/core/lookups.py,sha256=2UmODxDeDQqi-xHOvjm2_GckC4piKI3x4ZEf5vny8YI,18275
|
|
|
49
49
|
qubx/core/metrics.py,sha256=74xIecCvlxVXl0gy0JvgjJ2X5gg-RMmVZw9hQikkHE0,60269
|
|
50
50
|
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
51
51
|
qubx/core/mixins/market.py,sha256=f0PJTK5Y9nQkV2XWxytixFG1Oc6ihYVwlxIMraTnmms,4566
|
|
52
|
-
qubx/core/mixins/processing.py,sha256=
|
|
52
|
+
qubx/core/mixins/processing.py,sha256=OBsr-2c-W_Iy68tL80wFqTPN3HnW1stDjpm77vFaSB0,30661
|
|
53
53
|
qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
|
|
54
54
|
qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
|
|
55
55
|
qubx/core/mixins/universe.py,sha256=tsMpBriLHwK9lAVYvIrO94EIx8_ETSXUlzxN_sDOsL8,9838
|
|
56
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
56
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=V-Kj9bhekUUV4wX6Ao1JzXG5tzHeaqruPLjG7Lo1i_A,978280
|
|
57
57
|
qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
|
|
58
58
|
qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
|
|
59
59
|
qubx/core/series.pyx,sha256=ZQPp1-Kp2lX22n04gaBWak9mW139e-uc8bhsbZMohgs,46507
|
|
60
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
60
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=KpHSsIjpXpVCQ8BO8gqFT64DRPezuaYxvOJ8T4kGeVY,86568
|
|
61
61
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
62
62
|
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
63
63
|
qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
|
|
@@ -129,7 +129,7 @@ qubx/restorers/signal.py,sha256=0QFoy7OzDkK6AAmJEbbmSsHwmAhjMJYYggVFuLraKjk,1089
|
|
|
129
129
|
qubx/restorers/state.py,sha256=dLaVnUwRCNRkUqbYyi0RfZs3Q3AdglkI_qTtQ8GDD5Y,7289
|
|
130
130
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
131
131
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
132
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
132
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=OV84KH_YT8D_SIQjCZWERFdmBMB4o38Q8tqdDBjSpb8,654440
|
|
133
133
|
qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
|
|
134
134
|
qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
|
|
135
135
|
qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
|
|
@@ -166,8 +166,8 @@ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,
|
|
|
166
166
|
qubx/utils/runner/runner.py,sha256=PnSQ_soWXzjcIYxG5HHUtbZnWSaoeSXr7VxbQWtOsII,31444
|
|
167
167
|
qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
|
|
168
168
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
169
|
-
qubx-0.6.
|
|
170
|
-
qubx-0.6.
|
|
171
|
-
qubx-0.6.
|
|
172
|
-
qubx-0.6.
|
|
173
|
-
qubx-0.6.
|
|
169
|
+
qubx-0.6.59.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
170
|
+
qubx-0.6.59.dist-info/METADATA,sha256=kXg2STWUJpn3dNfxzEngSiV7OMDN1unJIbcrjKzTTEE,4612
|
|
171
|
+
qubx-0.6.59.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
|
|
172
|
+
qubx-0.6.59.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
173
|
+
qubx-0.6.59.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|