itchfeed 1.0.4__py3-none-any.whl → 1.0.6__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.
- itch/__init__.py +8 -2
- itch/messages.py +51 -34
- itch/parser.py +161 -106
- {itchfeed-1.0.4.dist-info → itchfeed-1.0.6.dist-info}/METADATA +28 -50
- itchfeed-1.0.6.dist-info/RECORD +9 -0
- itchfeed-1.0.4.dist-info/RECORD +0 -9
- {itchfeed-1.0.4.dist-info → itchfeed-1.0.6.dist-info}/WHEEL +0 -0
- {itchfeed-1.0.4.dist-info → itchfeed-1.0.6.dist-info}/licenses/LICENSE +0 -0
- {itchfeed-1.0.4.dist-info → itchfeed-1.0.6.dist-info}/top_level.txt +0 -0
itch/__init__.py
CHANGED
|
@@ -4,8 +4,14 @@ Nasdaq TotalView-ITCH 5.0 Parser
|
|
|
4
4
|
|
|
5
5
|
__author__ = "Bertin Balouki SIMYELI"
|
|
6
6
|
__copyright__ = "2025 Bertin Balouki SIMYELI"
|
|
7
|
-
__email__ = "bertin@
|
|
7
|
+
__email__ = "bertin@bbs-trading.com"
|
|
8
8
|
__license__ = "MIT"
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("itchfeed")
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
__version__ = "unknown"
|
|
10
16
|
|
|
11
17
|
|
itch/messages.py
CHANGED
|
@@ -13,13 +13,21 @@ class MarketMessage(object):
|
|
|
13
13
|
|
|
14
14
|
All Message have the following attributes:
|
|
15
15
|
- message_type: A single letter that identify the message
|
|
16
|
+
- timestamp: Time at which the message was generated (Nanoseconds past midnight)
|
|
17
|
+
- stock_locate: Locate code identifying the security
|
|
18
|
+
- tracking_number: Nasdaq internal tracking number
|
|
19
|
+
|
|
20
|
+
The following attributes are not part of the message, but are used to describe the message:
|
|
16
21
|
- description: Describe the message
|
|
17
22
|
- message_format: string format using to unpack the message
|
|
18
23
|
- message_pack_format: string format using to pack the message
|
|
19
24
|
- message_size: The size in bytes of the message
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
|
|
26
|
+
# NOTE:
|
|
27
|
+
Prices are integers fields supplied with an associated precision. When converted to a decimal format, prices are in
|
|
28
|
+
fixed point format, where the precision defines the number of decimal places. For example, a field flagged as Price
|
|
29
|
+
(4) has an implied 4 decimal places. The maximum value of price (4) in TotalView ITCH is 200,000.0000 (decimal,
|
|
30
|
+
77359400 hex). ``price_precision`` is 4 for all messages except MWCBDeclineLeveMessage where ``price_precision`` is 8.
|
|
23
31
|
"""
|
|
24
32
|
|
|
25
33
|
message_type: bytes
|
|
@@ -32,13 +40,24 @@ class MarketMessage(object):
|
|
|
32
40
|
tracking_number: int
|
|
33
41
|
price_precision: int = 4
|
|
34
42
|
|
|
35
|
-
def __repr__(self):
|
|
43
|
+
def __repr__(self) -> str:
|
|
36
44
|
return repr(self.decode())
|
|
37
45
|
|
|
38
|
-
def
|
|
46
|
+
def __bytes__(self) -> bytes:
|
|
47
|
+
return self.to_bytes()
|
|
48
|
+
|
|
49
|
+
def to_bytes(self) -> bytes: # type: ignore
|
|
39
50
|
"""
|
|
40
51
|
Packs the message into bytes using the defined message_pack_format.
|
|
41
52
|
This method should be overridden by subclasses to include specific fields.
|
|
53
|
+
|
|
54
|
+
Note:
|
|
55
|
+
All packed messages do not include
|
|
56
|
+
- ``description``,
|
|
57
|
+
- ``message_format``,
|
|
58
|
+
- ``message_pack_format``,
|
|
59
|
+
- ``message_size``
|
|
60
|
+
- ``price_precision``
|
|
42
61
|
"""
|
|
43
62
|
pass
|
|
44
63
|
|
|
@@ -90,9 +109,7 @@ class MarketMessage(object):
|
|
|
90
109
|
ts1 = self.timestamp >> 32
|
|
91
110
|
ts2 = self.timestamp - (ts1 << 32)
|
|
92
111
|
return (ts1, ts2)
|
|
93
|
-
|
|
94
|
-
ts2 = self.timestamp - (ts1 << 32)
|
|
95
|
-
return (ts1, ts2)
|
|
112
|
+
|
|
96
113
|
|
|
97
114
|
def decode_price(self, price_attr: str) -> float:
|
|
98
115
|
precision = getattr(self, "price_precision")
|
|
@@ -166,7 +183,7 @@ class SystemEventMessage(MarketMessage):
|
|
|
166
183
|
) = struct.unpack(self.message_format, message[1:])
|
|
167
184
|
self.set_timestamp(timestamp1, timestamp2)
|
|
168
185
|
|
|
169
|
-
def
|
|
186
|
+
def to_bytes(self):
|
|
170
187
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
171
188
|
message = struct.pack(
|
|
172
189
|
self.message_pack_format,
|
|
@@ -288,7 +305,7 @@ class StockDirectoryMessage(MarketMessage):
|
|
|
288
305
|
) = struct.unpack(self.message_format, message[1:])
|
|
289
306
|
self.set_timestamp(timestamp1, timestamp2)
|
|
290
307
|
|
|
291
|
-
def
|
|
308
|
+
def to_bytes(self):
|
|
292
309
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
293
310
|
message = struct.pack(
|
|
294
311
|
self.message_pack_format,
|
|
@@ -368,7 +385,7 @@ class StockTradingActionMessage(MarketMessage):
|
|
|
368
385
|
) = struct.unpack(self.message_format, message[1:])
|
|
369
386
|
self.set_timestamp(timestamp1, timestamp2)
|
|
370
387
|
|
|
371
|
-
def
|
|
388
|
+
def to_bytes(self):
|
|
372
389
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
373
390
|
message = struct.pack(
|
|
374
391
|
self.message_pack_format,
|
|
@@ -429,7 +446,7 @@ class RegSHOMessage(MarketMessage):
|
|
|
429
446
|
) = struct.unpack(self.message_format, message[1:])
|
|
430
447
|
self.set_timestamp(timestamp1, timestamp2)
|
|
431
448
|
|
|
432
|
-
def
|
|
449
|
+
def to_bytes(self):
|
|
433
450
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
434
451
|
message = struct.pack(
|
|
435
452
|
self.message_pack_format,
|
|
@@ -491,7 +508,7 @@ class MarketParticipantPositionMessage(MarketMessage):
|
|
|
491
508
|
) = struct.unpack(self.message_format, message[1:])
|
|
492
509
|
self.set_timestamp(timestamp1, timestamp2)
|
|
493
510
|
|
|
494
|
-
def
|
|
511
|
+
def to_bytes(self):
|
|
495
512
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
496
513
|
message = struct.pack(
|
|
497
514
|
self.message_pack_format,
|
|
@@ -542,7 +559,7 @@ class MWCBDeclineLeveMessage(MarketMessage):
|
|
|
542
559
|
) = struct.unpack(self.message_format, message[1:])
|
|
543
560
|
self.set_timestamp(timestamp1, timestamp2)
|
|
544
561
|
|
|
545
|
-
def
|
|
562
|
+
def to_bytes(self):
|
|
546
563
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
547
564
|
message = struct.pack(
|
|
548
565
|
self.message_pack_format,
|
|
@@ -587,7 +604,7 @@ class MWCBStatusMessage(MarketMessage):
|
|
|
587
604
|
) = struct.unpack(self.message_format, message[1:])
|
|
588
605
|
self.set_timestamp(timestamp1, timestamp2)
|
|
589
606
|
|
|
590
|
-
def
|
|
607
|
+
def to_bytes(self):
|
|
591
608
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
592
609
|
message = struct.pack(
|
|
593
610
|
self.message_pack_format,
|
|
@@ -648,7 +665,7 @@ class IPOQuotingPeriodUpdateMessage(MarketMessage):
|
|
|
648
665
|
) = struct.unpack(self.message_format, message[1:])
|
|
649
666
|
self.set_timestamp(timestamp1, timestamp2)
|
|
650
667
|
|
|
651
|
-
def
|
|
668
|
+
def to_bytes(self):
|
|
652
669
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
653
670
|
message = struct.pack(
|
|
654
671
|
self.message_pack_format,
|
|
@@ -704,7 +721,7 @@ class LULDAuctionCollarMessage(MarketMessage):
|
|
|
704
721
|
) = struct.unpack(self.message_format, message[1:])
|
|
705
722
|
self.set_timestamp(timestamp1, timestamp2)
|
|
706
723
|
|
|
707
|
-
def
|
|
724
|
+
def to_bytes(self):
|
|
708
725
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
709
726
|
message = struct.pack(
|
|
710
727
|
self.message_pack_format,
|
|
@@ -769,7 +786,7 @@ class OperationalHaltMessage(MarketMessage):
|
|
|
769
786
|
) = struct.unpack(self.message_format, message[1:])
|
|
770
787
|
self.set_timestamp(timestamp1, timestamp2)
|
|
771
788
|
|
|
772
|
-
def
|
|
789
|
+
def to_bytes(self):
|
|
773
790
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
774
791
|
message = struct.pack(
|
|
775
792
|
self.message_pack_format,
|
|
@@ -832,7 +849,7 @@ class AddOrderNoMPIAttributionMessage(AddOrderMessage):
|
|
|
832
849
|
) = struct.unpack(self.message_format, message[1:])
|
|
833
850
|
self.set_timestamp(timestamp1, timestamp2)
|
|
834
851
|
|
|
835
|
-
def
|
|
852
|
+
def to_bytes(self):
|
|
836
853
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
837
854
|
message = struct.pack(
|
|
838
855
|
self.message_pack_format,
|
|
@@ -886,7 +903,7 @@ class AddOrderMPIDAttribution(AddOrderMessage):
|
|
|
886
903
|
) = struct.unpack(self.message_format, message[1:])
|
|
887
904
|
self.set_timestamp(timestamp1, timestamp2)
|
|
888
905
|
|
|
889
|
-
def
|
|
906
|
+
def to_bytes(self):
|
|
890
907
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
891
908
|
message = struct.pack(
|
|
892
909
|
self.message_pack_format,
|
|
@@ -957,7 +974,7 @@ class OrderExecutedMessage(ModifyOrderMessage):
|
|
|
957
974
|
) = struct.unpack(self.message_format, message[1:])
|
|
958
975
|
self.set_timestamp(timestamp1, timestamp2)
|
|
959
976
|
|
|
960
|
-
def
|
|
977
|
+
def to_bytes(self):
|
|
961
978
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
962
979
|
message = struct.pack(
|
|
963
980
|
self.message_pack_format,
|
|
@@ -1025,7 +1042,7 @@ class OrderExecutedWithPriceMessage(ModifyOrderMessage):
|
|
|
1025
1042
|
) = struct.unpack(self.message_format, message[1:])
|
|
1026
1043
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1027
1044
|
|
|
1028
|
-
def
|
|
1045
|
+
def to_bytes(self):
|
|
1029
1046
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1030
1047
|
message = struct.pack(
|
|
1031
1048
|
self.message_pack_format,
|
|
@@ -1071,7 +1088,7 @@ class OrderCancelMessage(ModifyOrderMessage):
|
|
|
1071
1088
|
) = struct.unpack(self.message_format, message[1:])
|
|
1072
1089
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1073
1090
|
|
|
1074
|
-
def
|
|
1091
|
+
def to_bytes(self):
|
|
1075
1092
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1076
1093
|
message = struct.pack(
|
|
1077
1094
|
self.message_pack_format,
|
|
@@ -1111,7 +1128,7 @@ class OrderDeleteMessage(ModifyOrderMessage):
|
|
|
1111
1128
|
) = struct.unpack(self.message_format, message[1:])
|
|
1112
1129
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1113
1130
|
|
|
1114
|
-
def
|
|
1131
|
+
def to_bytes(self):
|
|
1115
1132
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1116
1133
|
message = struct.pack(
|
|
1117
1134
|
self.message_pack_format,
|
|
@@ -1164,7 +1181,7 @@ class OrderReplaceMessage(ModifyOrderMessage):
|
|
|
1164
1181
|
) = struct.unpack(self.message_format, message[1:])
|
|
1165
1182
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1166
1183
|
|
|
1167
|
-
def
|
|
1184
|
+
def to_bytes(self):
|
|
1168
1185
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1169
1186
|
message = struct.pack(
|
|
1170
1187
|
self.message_pack_format,
|
|
@@ -1238,7 +1255,7 @@ class NonCrossTradeMessage(TradeMessage):
|
|
|
1238
1255
|
) = struct.unpack(self.message_format, message[1:])
|
|
1239
1256
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1240
1257
|
|
|
1241
|
-
def
|
|
1258
|
+
def to_bytes(self):
|
|
1242
1259
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1243
1260
|
message = struct.pack(
|
|
1244
1261
|
self.message_pack_format,
|
|
@@ -1309,7 +1326,7 @@ class CrossTradeMessage(TradeMessage):
|
|
|
1309
1326
|
) = struct.unpack(self.message_format, message[1:])
|
|
1310
1327
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1311
1328
|
|
|
1312
|
-
def
|
|
1329
|
+
def to_bytes(self):
|
|
1313
1330
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1314
1331
|
message = struct.pack(
|
|
1315
1332
|
self.message_pack_format,
|
|
@@ -1359,7 +1376,7 @@ class BrokenTradeMessage(TradeMessage):
|
|
|
1359
1376
|
) = struct.unpack(self.message_format, message[1:])
|
|
1360
1377
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1361
1378
|
|
|
1362
|
-
def
|
|
1379
|
+
def to_bytes(self):
|
|
1363
1380
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1364
1381
|
message = struct.pack(
|
|
1365
1382
|
self.message_pack_format,
|
|
@@ -1441,7 +1458,7 @@ class NOIIMessage(MarketMessage):
|
|
|
1441
1458
|
) = struct.unpack(self.message_format, message[1:])
|
|
1442
1459
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1443
1460
|
|
|
1444
|
-
def
|
|
1461
|
+
def to_bytes(self):
|
|
1445
1462
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1446
1463
|
message = struct.pack(
|
|
1447
1464
|
self.message_pack_format,
|
|
@@ -1496,7 +1513,7 @@ class RetailPriceImprovementIndicator(MarketMessage):
|
|
|
1496
1513
|
) = struct.unpack(self.message_format, message[1:])
|
|
1497
1514
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1498
1515
|
|
|
1499
|
-
def
|
|
1516
|
+
def to_bytes(self):
|
|
1500
1517
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1501
1518
|
message = struct.pack(
|
|
1502
1519
|
self.message_pack_format,
|
|
@@ -1559,7 +1576,7 @@ class DLCRMessage(MarketMessage):
|
|
|
1559
1576
|
) = struct.unpack(self.message_format, message[1:])
|
|
1560
1577
|
self.set_timestamp(timestamp1, timestamp2)
|
|
1561
1578
|
|
|
1562
|
-
def
|
|
1579
|
+
def to_bytes(self):
|
|
1563
1580
|
(timestamp1, timestamp2) = self.split_timestamp()
|
|
1564
1581
|
message = struct.pack(
|
|
1565
1582
|
self.message_pack_format,
|
|
@@ -1579,8 +1596,8 @@ class DLCRMessage(MarketMessage):
|
|
|
1579
1596
|
)
|
|
1580
1597
|
return message
|
|
1581
1598
|
|
|
1582
|
-
|
|
1583
|
-
messages
|
|
1599
|
+
messages: Dict[bytes, Type[MarketMessage]]
|
|
1600
|
+
messages = {
|
|
1584
1601
|
b"S": SystemEventMessage,
|
|
1585
1602
|
b"R": StockDirectoryMessage,
|
|
1586
1603
|
b"H": StockTradingActionMessage,
|
|
@@ -1607,7 +1624,7 @@ messages: Dict[bytes, Type[MarketMessage]] = {
|
|
|
1607
1624
|
}
|
|
1608
1625
|
|
|
1609
1626
|
|
|
1610
|
-
def create_message(message_type: bytes, **kwargs) ->
|
|
1627
|
+
def create_message(message_type: bytes, **kwargs) -> MarketMessage:
|
|
1611
1628
|
"""
|
|
1612
1629
|
Creates a new message of a given type with specified attributes.
|
|
1613
1630
|
|
itch/parser.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import BinaryIO, List, Type
|
|
1
|
+
from typing import IO, BinaryIO, Callable, Iterator, Optional, Tuple
|
|
3
2
|
|
|
4
3
|
from itch.messages import MESSAGES, MarketMessage
|
|
5
4
|
from itch.messages import messages as msgs
|
|
@@ -14,160 +13,216 @@ class MessageParser(object):
|
|
|
14
13
|
def __init__(self, message_type: bytes = MESSAGES):
|
|
15
14
|
self.message_type = message_type
|
|
16
15
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
def get_message_type(self, message: bytes) -> MarketMessage:
|
|
17
|
+
"""
|
|
18
|
+
Take an entire bytearray and return the appropriate ITCH message
|
|
19
|
+
instance based on the message type indicator (first byte of the message).
|
|
20
|
+
|
|
21
|
+
All message type indicators are single ASCII characters.
|
|
22
|
+
"""
|
|
23
|
+
message_type = message[0:1]
|
|
24
|
+
try:
|
|
25
|
+
return msgs[message_type](message) # type: ignore
|
|
26
|
+
except Exception:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"Unknown message type: {message_type.decode(encoding='ascii')}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _parse_message_from_buffer(
|
|
32
|
+
self, buffer: memoryview, offset: int
|
|
33
|
+
) -> Optional[Tuple[MarketMessage, int]]:
|
|
34
|
+
"""
|
|
35
|
+
Parses a single ITCH message from a memory buffer.
|
|
36
|
+
|
|
37
|
+
This method checks for a 2-byte header (a null byte and a length byte),
|
|
38
|
+
determines the full message size, and extracts the message if the
|
|
39
|
+
complete message is present in the buffer.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
buffer (memoryview):
|
|
43
|
+
The buffer containing the binary data.
|
|
44
|
+
offset (int):
|
|
45
|
+
The starting position in the buffer to begin parsing.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Optional[Tuple[MarketMessage, int]]:
|
|
49
|
+
A tuple containing the parsed MarketMessage and the total length
|
|
50
|
+
of the message including the header. Returns None if a complete
|
|
51
|
+
message could not be parsed.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError:
|
|
55
|
+
If the data at the current offset does not start with the
|
|
56
|
+
expected 0x00 byte.
|
|
57
|
+
"""
|
|
58
|
+
buffer_len = len(buffer)
|
|
59
|
+
if offset + 2 > buffer_len:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if buffer[offset : offset + 1] != b"\x00":
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Unexpected start byte at offset {offset}: "
|
|
65
|
+
f"{buffer[offset : offset + 1].tobytes()}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
msg_len = buffer[offset + 1]
|
|
69
|
+
total_len = 2 + msg_len
|
|
70
|
+
|
|
71
|
+
if offset + total_len > buffer_len:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
raw_msg = buffer[offset + 2 : offset + total_len]
|
|
75
|
+
message = self.get_message_type(raw_msg.tobytes())
|
|
76
|
+
return message, total_len
|
|
77
|
+
|
|
78
|
+
def parse_file(
|
|
79
|
+
self, file: BinaryIO, cachesize: int = 65_536, save_file: Optional[IO] = None
|
|
80
|
+
) -> Iterator[MarketMessage]:
|
|
22
81
|
"""
|
|
23
82
|
Reads and parses market messages from a binary file-like object.
|
|
24
83
|
|
|
25
84
|
This method processes binary data in chunks, extracts individual messages
|
|
26
|
-
according to a specific format, and returns a list of successfully decoded
|
|
27
|
-
MarketMessage objects. Parsing stops either when the end of the file is
|
|
85
|
+
according to a specific format, and returns a list of successfully decoded
|
|
86
|
+
MarketMessage objects. Parsing stops either when the end of the file is
|
|
28
87
|
reached or when a system message with an end-of-messages event code is encountered.
|
|
29
88
|
|
|
30
89
|
Args:
|
|
31
|
-
file (BinaryIO):
|
|
90
|
+
file (BinaryIO):
|
|
32
91
|
A binary file-like object (opened in binary mode) from which market messages are read.
|
|
33
|
-
cachesize (int, optional):
|
|
34
|
-
The size
|
|
92
|
+
cachesize (int, optional):
|
|
93
|
+
The size of each data chunk to read. Defaults to 65536 bytes (64KB).
|
|
94
|
+
save_file (IO, optional):
|
|
95
|
+
A binary file-like object (opened in binary write mode) where filtered messages are saved.
|
|
35
96
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
defined in self.message_type.
|
|
97
|
+
Yields:
|
|
98
|
+
MarketMessage:
|
|
99
|
+
The next parsed MarketMessage object from the file.
|
|
40
100
|
|
|
41
101
|
Raises:
|
|
42
|
-
ValueError:
|
|
43
|
-
If a message does not start with the expected 0x00 byte, indicating
|
|
102
|
+
ValueError:
|
|
103
|
+
If a message does not start with the expected 0x00 byte, indicating
|
|
44
104
|
an unexpected file format or possible corruption.
|
|
45
105
|
|
|
46
|
-
|
|
106
|
+
Notes:
|
|
47
107
|
- Each message starts with a 0x00 byte.
|
|
48
108
|
- The following byte specifies the message length.
|
|
49
109
|
- The complete message consists of the first 2 bytes and 'message length' bytes of body.
|
|
50
|
-
- If a system message (message_type == b'S') with event_code == b'C' is encountered,
|
|
51
|
-
|
|
110
|
+
- If a system message (message_type == b'S') with event_code == b'C' is encountered,
|
|
111
|
+
parsing stops immediately.
|
|
52
112
|
|
|
53
113
|
Example:
|
|
54
|
-
>>>
|
|
55
|
-
>>>
|
|
56
|
-
>>>
|
|
114
|
+
>>> data_file = "01302020.NASDAQ_ITCH50.gz"
|
|
115
|
+
>>> message_type = b"AFE" # Example message type to filter
|
|
116
|
+
>>> parser = MessageParser(message_type=message_type)
|
|
117
|
+
>>> with gzip.open(data_file, "rb") as itch_file:
|
|
118
|
+
>>> message_count = 0
|
|
119
|
+
>>> start_time = time.time()
|
|
120
|
+
>>> for message in parser.parse_file(itch_file):
|
|
121
|
+
>>> message_count += 1
|
|
122
|
+
>>> if message_count <= 5:
|
|
57
123
|
>>> print(message)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
messages: List[MarketMessage] = []
|
|
65
|
-
|
|
66
|
-
while not file_end_reached:
|
|
67
|
-
if buffer_len < 2:
|
|
68
|
-
new_data = file.read(cachesize)
|
|
69
|
-
if not new_data:
|
|
70
|
-
break
|
|
71
|
-
data_buffer += new_data
|
|
72
|
-
buffer_len = len(data_buffer)
|
|
73
|
-
continue
|
|
124
|
+
>>> end_time = time.time()
|
|
125
|
+
>>> print(f"Processed {message_count} messages in {end_time - start_time:.2f} seconds")
|
|
126
|
+
>>> print(f"Average time per message: {(end_time - start_time) / message_count:.6f} seconds")
|
|
127
|
+
"""
|
|
128
|
+
if not file.readable():
|
|
129
|
+
raise ValueError("file must be opened in binary read mode")
|
|
74
130
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"Unexpected byte: " + str(data_buffer[0:1], encoding="ascii")
|
|
78
|
-
)
|
|
131
|
+
if save_file is not None and not save_file.writable():
|
|
132
|
+
raise ValueError("save_file must be opened in binary write mode")
|
|
79
133
|
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
data_buffer = b""
|
|
135
|
+
offset = 0
|
|
82
136
|
|
|
83
|
-
|
|
84
|
-
|
|
137
|
+
while True:
|
|
138
|
+
parsed = self._parse_message_from_buffer(memoryview(data_buffer), offset)
|
|
139
|
+
if parsed is None:
|
|
140
|
+
data_buffer = data_buffer[offset:]
|
|
141
|
+
offset = 0
|
|
85
142
|
new_data = file.read(cachesize)
|
|
86
143
|
if not new_data:
|
|
87
144
|
break
|
|
88
145
|
data_buffer += new_data
|
|
89
|
-
buffer_len = len(data_buffer)
|
|
90
146
|
continue
|
|
91
|
-
message_data = data_buffer[2:total_len]
|
|
92
|
-
message = self.get_message_type(message_data)
|
|
93
|
-
|
|
94
|
-
if message.message_type in self.message_type:
|
|
95
|
-
messages.append(message)
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
if message.event_code == b"C": # End of messages
|
|
99
|
-
break
|
|
100
|
-
|
|
101
|
-
# Update buffer
|
|
102
|
-
data_buffer = data_buffer[total_len:]
|
|
103
|
-
buffer_len = len(data_buffer)
|
|
148
|
+
message, total_len = parsed
|
|
104
149
|
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
150
|
+
if message.message_type in self.message_type:
|
|
151
|
+
if save_file is not None:
|
|
152
|
+
msg_len_to_bytes = message.message_size.to_bytes()
|
|
153
|
+
save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
|
|
154
|
+
yield message
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
message.message_type == b"S"
|
|
158
|
+
and getattr(message, "event_code", b"") == b"C"
|
|
159
|
+
):
|
|
160
|
+
break
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
offset += total_len
|
|
114
163
|
|
|
115
|
-
def
|
|
164
|
+
def parse_stream(
|
|
165
|
+
self, data: bytes, save_file: Optional[IO] = None
|
|
166
|
+
) -> Iterator[MarketMessage]:
|
|
116
167
|
"""
|
|
117
168
|
Process one or multiple ITCH binary messages from a raw bytes input.
|
|
118
169
|
|
|
119
170
|
Args:
|
|
120
171
|
data (bytes): Binary blob containing one or more ITCH messages.
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
save_file (IO, optional):
|
|
174
|
+
A binary file-like object (opened in binary write mode) where filtered messages are saved.
|
|
175
|
+
|
|
176
|
+
Yields:
|
|
177
|
+
MarketMessage:
|
|
178
|
+
The next parsed MarketMessage object from the bytes input.
|
|
124
179
|
|
|
125
180
|
Notes:
|
|
126
181
|
- Each message must be prefixed with a 0x00 header and a length byte.
|
|
127
182
|
- No buffering is done here — this is meant for real-time decoding.
|
|
128
183
|
"""
|
|
184
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
185
|
+
raise TypeError("data must be bytes or bytearray, not " + str(type(data)))
|
|
186
|
+
|
|
187
|
+
if save_file is not None and not save_file.writable():
|
|
188
|
+
raise ValueError("save_file must be opened in binary write mode")
|
|
129
189
|
|
|
130
190
|
offset = 0
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
f"Unexpected start byte at offset {offset}: "
|
|
137
|
-
f"{str(data[offset : offset + 1], encoding='ascii')}"
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
msg_len = data[offset + 1]
|
|
141
|
-
total_len = 2 + msg_len
|
|
142
|
-
|
|
143
|
-
if offset + total_len > len(data):
|
|
191
|
+
data_view = memoryview(data)
|
|
192
|
+
|
|
193
|
+
while True:
|
|
194
|
+
parsed = self._parse_message_from_buffer(data_view, offset)
|
|
195
|
+
if parsed is None:
|
|
144
196
|
break
|
|
145
197
|
|
|
146
|
-
|
|
147
|
-
message = self.get_message_type(raw_msg)
|
|
198
|
+
message, total_len = parsed
|
|
148
199
|
|
|
149
200
|
if message.message_type in self.message_type:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
201
|
+
if save_file is not None:
|
|
202
|
+
msg_len_to_bytes = message.message_size.to_bytes()
|
|
203
|
+
save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
|
|
204
|
+
yield message
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
message.message_type == b"S"
|
|
208
|
+
and getattr(message, "event_code", b"") == b"C"
|
|
209
|
+
):
|
|
210
|
+
break
|
|
155
211
|
|
|
156
212
|
offset += total_len
|
|
157
213
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
214
|
+
def parse_messages(
|
|
215
|
+
self,
|
|
216
|
+
data: BinaryIO | bytes | bytearray,
|
|
217
|
+
callback: Callable[[MarketMessage], None],
|
|
218
|
+
) -> None:
|
|
161
219
|
"""
|
|
162
|
-
|
|
163
|
-
instance based on the message type indicator (first byte of the message).
|
|
164
|
-
|
|
165
|
-
All message type indicators are single ASCII characters.
|
|
220
|
+
Parses messages from data and invokes a callback for each message.
|
|
166
221
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
222
|
+
parser_func = (
|
|
223
|
+
self.parse_stream
|
|
224
|
+
if isinstance(data, (bytes, bytearray))
|
|
225
|
+
else self.parse_file
|
|
226
|
+
)
|
|
227
|
+
for message in parser_func(data):
|
|
228
|
+
callback(message)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: itchfeed
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Simple parser for ITCH messages
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
License: The MIT License (MIT)
|
|
5
|
+
Author-email: Bertin Balouki SIMYELI <bertin@bbs-trading.com>
|
|
6
|
+
Maintainer-email: Bertin Balouki SIMYELI <bertin@bbs-trading.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bbalouki/itch
|
|
9
|
+
Project-URL: Download, https://pypi.org/project/itchfeed/
|
|
11
10
|
Project-URL: Source Code, https://github.com/bbalouki/itch
|
|
12
11
|
Keywords: Finance,Financial,Quantitative,Equities,Totalview-ITCH,Totalview,Nasdaq-ITCH,Nasdaq,ITCH,Data,Feed,ETFs,Funds,Trading,Investing
|
|
13
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -19,20 +18,7 @@ Classifier: Operating System :: OS Independent
|
|
|
19
18
|
Description-Content-Type: text/markdown
|
|
20
19
|
License-File: LICENSE
|
|
21
20
|
Requires-Dist: pytest
|
|
22
|
-
Dynamic: author
|
|
23
|
-
Dynamic: author-email
|
|
24
|
-
Dynamic: classifier
|
|
25
|
-
Dynamic: description
|
|
26
|
-
Dynamic: description-content-type
|
|
27
|
-
Dynamic: download-url
|
|
28
|
-
Dynamic: home-page
|
|
29
|
-
Dynamic: keywords
|
|
30
|
-
Dynamic: license
|
|
31
21
|
Dynamic: license-file
|
|
32
|
-
Dynamic: maintainer
|
|
33
|
-
Dynamic: project-url
|
|
34
|
-
Dynamic: requires-dist
|
|
35
|
-
Dynamic: summary
|
|
36
22
|
|
|
37
23
|
# Nasdaq TotalView-ITCH 5.0 Parser
|
|
38
24
|
[](https://pypi.org/project/itchfeed/)
|
|
@@ -59,7 +45,7 @@ Dynamic: summary
|
|
|
59
45
|
* [Data Representation](#data-representation)
|
|
60
46
|
* [Common Attributes of `MarketMessage`](#common-attributes-of-marketmessage)
|
|
61
47
|
* [Common Methods of `MarketMessage`](#common-methods-of-marketmessage)
|
|
62
|
-
* [Serializing Messages with `
|
|
48
|
+
* [Serializing Messages with `to_bytes()`](#serializing-messages-with-to_bytes)
|
|
63
49
|
* [Data Types in Parsed Messages](#data-types-in-parsed-messages)
|
|
64
50
|
* [Error Handling](#error-handling)
|
|
65
51
|
* [Handling Strategies](#handling-strategies)
|
|
@@ -111,6 +97,8 @@ After installation (typically via pip), import the necessary modules directly in
|
|
|
111
97
|
|
|
112
98
|
## Usage
|
|
113
99
|
|
|
100
|
+
Download some sample data [here](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/)
|
|
101
|
+
|
|
114
102
|
### Parsing from a Binary File
|
|
115
103
|
|
|
116
104
|
This is useful for processing historical ITCH data stored in files. The `MessageParser` handles buffering efficiently.
|
|
@@ -131,9 +119,8 @@ parser = MessageParser() # Parses all messages by default
|
|
|
131
119
|
|
|
132
120
|
# Path to your ITCH 5.0 data file
|
|
133
121
|
itch_file_path = 'path/to/your/data'
|
|
134
|
-
# you can find sample data [here](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/)
|
|
135
122
|
|
|
136
|
-
# The `
|
|
123
|
+
# The `parse_file()` method reads the ITCH data in chunks.
|
|
137
124
|
# - `cachesize` (optional, default: 65536 bytes): This parameter determines the size of data chunks
|
|
138
125
|
# read from the file at a time. Adjusting this might impact performance for very large files
|
|
139
126
|
# or memory usage, but the default is generally suitable.
|
|
@@ -144,13 +131,9 @@ itch_file_path = 'path/to/your/data'
|
|
|
144
131
|
|
|
145
132
|
try:
|
|
146
133
|
with open(itch_file_path, 'rb') as itch_file:
|
|
147
|
-
#
|
|
148
|
-
parsed_messages = parser.read_message_from_file(itch_file) # You can also pass cachesize here, e.g., parser.read_message_from_file(itch_file, cachesize=131072)
|
|
149
|
-
|
|
150
|
-
print(f"Parsed {len(parsed_messages)} messages.")
|
|
151
|
-
|
|
134
|
+
# parse_file returns an Iterator of parsed message objects
|
|
152
135
|
# Process the messages
|
|
153
|
-
for message in
|
|
136
|
+
for message in parser.parse_file(itch_file):
|
|
154
137
|
# Access attributes directly
|
|
155
138
|
print(f"Type: {message.message_type.decode()}, Timestamp: {message.timestamp}")
|
|
156
139
|
|
|
@@ -170,8 +153,8 @@ try:
|
|
|
170
153
|
|
|
171
154
|
except FileNotFoundError:
|
|
172
155
|
print(f"Error: File not found at {itch_file_path}")
|
|
173
|
-
except
|
|
174
|
-
print(f"An error occurred: {e}")
|
|
156
|
+
except ValueError as e:
|
|
157
|
+
print(f"An error occurred during parsing: {e}")
|
|
175
158
|
|
|
176
159
|
```
|
|
177
160
|
|
|
@@ -192,14 +175,8 @@ parser = MessageParser()
|
|
|
192
175
|
# Example: \x00\x0bS...\x00\x25R...\x00\x27F...
|
|
193
176
|
raw_binary_data: bytes = b"..." # Your raw ITCH 5.0 data chunk
|
|
194
177
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
print(f"Parsed {message_queue.qsize()} messages from the byte chunk.")
|
|
199
|
-
|
|
200
|
-
# Process messages from the queue
|
|
201
|
-
while not message_queue.empty():
|
|
202
|
-
message = message_queue.get()
|
|
178
|
+
# parse_stream returns an Iterator of parsed message objects
|
|
179
|
+
for message in parser.parse_stream(raw_binary_data)
|
|
203
180
|
|
|
204
181
|
print(f"Type: {message.message_type.decode()}, Timestamp: {message.timestamp}")
|
|
205
182
|
|
|
@@ -276,10 +253,10 @@ for message_type, sample_data in TEST_DATA.items():
|
|
|
276
253
|
print(f"Creating message of type {message_type}")
|
|
277
254
|
message = create_message(message_type, **sample_data)
|
|
278
255
|
print(f"Created message: {message}")
|
|
279
|
-
print(f"Packed message: {message.
|
|
256
|
+
print(f"Packed message: {message.to_bytes()}")
|
|
280
257
|
print(f"Message size: {message.message_size}")
|
|
281
258
|
print(f"Message Attributes: {message.get_attributes()}")
|
|
282
|
-
assert len(message.
|
|
259
|
+
assert len(message.to_bytes()) == message.message_size
|
|
283
260
|
print()
|
|
284
261
|
```
|
|
285
262
|
|
|
@@ -388,14 +365,14 @@ The `MarketMessage` base class, and therefore all specific message classes, prov
|
|
|
388
365
|
* Returns a dictionary of all attributes (fields) of the message instance, along with their current values.
|
|
389
366
|
* This can be useful for generic inspection or logging of message contents without needing to know the specific type of the message beforehand.
|
|
390
367
|
|
|
391
|
-
### Serializing Messages with `
|
|
368
|
+
### Serializing Messages with `to_bytes()`
|
|
392
369
|
|
|
393
|
-
Each specific message class (e.g., `SystemEventMessage`, `AddOrderNoMPIAttributionMessage`) also provides a `
|
|
370
|
+
Each specific message class (e.g., `SystemEventMessage`, `AddOrderNoMPIAttributionMessage`) also provides a `to_bytes()` method. This method is the inverse of the parsing process.
|
|
394
371
|
|
|
395
372
|
* **Purpose:** It serializes the message object, with its current attribute values, back into its raw ITCH 5.0 binary format. The output is a `bytes` object representing the exact byte sequence that would appear in an ITCH data feed for that message.
|
|
396
373
|
* **Usefulness:**
|
|
397
374
|
* **Generating Test Data:** Create custom ITCH messages for testing your own ITCH processing applications.
|
|
398
|
-
* **Modifying Messages:** Parse an existing message, modify some of its attributes, and then `
|
|
375
|
+
* **Modifying Messages:** Parse an existing message, modify some of its attributes, and then `to_bytes()` it back into binary form.
|
|
399
376
|
* **Creating Custom ITCH Feeds:** While more involved, you could use this to construct sequences of ITCH messages for specialized scenarios.
|
|
400
377
|
|
|
401
378
|
**Example:**
|
|
@@ -415,21 +392,21 @@ import time
|
|
|
415
392
|
# for packing requires setting attributes manually if not using raw bytes for construction)
|
|
416
393
|
|
|
417
394
|
event_msg = SystemEventMessage.__new__(SystemEventMessage) # Create instance without calling __init__
|
|
418
|
-
event_msg.message_type = b'S' # Must be set for
|
|
395
|
+
event_msg.message_type = b'S' # Must be set for to_bytes() to know its type
|
|
419
396
|
event_msg.stock_locate = 0 # Placeholder or actual value
|
|
420
397
|
event_msg.tracking_number = 0 # Placeholder or actual value
|
|
421
398
|
event_msg.event_code = b'O' # Example: Start of Messages
|
|
422
399
|
|
|
423
400
|
# 2. Set the timestamp.
|
|
424
401
|
# The `timestamp` attribute (nanoseconds since midnight) must be set.
|
|
425
|
-
# The `
|
|
402
|
+
# The `to_bytes()` method will internally use `split_timestamp()` to get the parts.
|
|
426
403
|
current_nanoseconds = int(time.time() * 1e9) % (24 * 60 * 60 * int(1e9))
|
|
427
404
|
event_msg.timestamp = current_nanoseconds # Directly set the nanosecond timestamp
|
|
428
405
|
|
|
429
406
|
# 3. Pack the message into binary format.
|
|
430
|
-
# The
|
|
407
|
+
# The to_bytes() method prepends the message type and then packs stock_locate,
|
|
431
408
|
# tracking_number, the split timestamp, and then the message-specific fields.
|
|
432
|
-
packed_bytes = event_msg.
|
|
409
|
+
packed_bytes = event_msg.to_bytes()
|
|
433
410
|
|
|
434
411
|
# 4. The result is a bytes object
|
|
435
412
|
print(f"Packed {len(packed_bytes)} bytes: {packed_bytes.hex().upper()}")
|
|
@@ -462,11 +439,12 @@ Common scenarios that can lead to a `ValueError` include:
|
|
|
462
439
|
|
|
463
440
|
It's crucial to anticipate these errors in your application:
|
|
464
441
|
|
|
465
|
-
* **Use `try-except` Blocks:** Wrap your parsing calls (especially `
|
|
442
|
+
* **Use `try-except` Blocks:** Wrap your parsing calls (especially `parse_file` or `parse_stream`) in `try-except ValueError as e:` blocks.
|
|
466
443
|
```python
|
|
467
444
|
try:
|
|
468
445
|
# ... parsing operations ...
|
|
469
|
-
|
|
446
|
+
for message in parser.parse_file(itch_file):
|
|
447
|
+
...
|
|
470
448
|
except ValueError as e:
|
|
471
449
|
print(f"An error occurred during parsing: {e}")
|
|
472
450
|
# Log the error, problematic data chunk, or take other actions
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
itch/__init__.py,sha256=ykG85u8WbMZnQt78wg-a4Zqm6dTGNf3lYRsqh5FOuPI,348
|
|
2
|
+
itch/indicators.py,sha256=-Ed2M8I60xGQ1bIPZCGCKGb8ayT87JAnIaosfiBimXI,6542
|
|
3
|
+
itch/messages.py,sha256=bXEcD9h0ZsD9IXVMfQa7FWZTpgiQoyeSdxZ9hrp3ukQ,64847
|
|
4
|
+
itch/parser.py,sha256=1Skl8SBQz_SDqHEDkFHydJw0TEt3M_xNcUMAE19yquo,8442
|
|
5
|
+
itchfeed-1.0.6.dist-info/licenses/LICENSE,sha256=f2u79rUzh-UcYH0RN0Ph0VvVYHBkYlVxtguhKmrHqsw,1089
|
|
6
|
+
itchfeed-1.0.6.dist-info/METADATA,sha256=ZPgCxVsjBIAXxdkllrx57IrP-wE0oxkgafIZaSsxm0Q,33174
|
|
7
|
+
itchfeed-1.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
itchfeed-1.0.6.dist-info/top_level.txt,sha256=xwsOYShvy3gc1rfyitCTgSxBZDGG1y6bfQxkdhIGmEM,5
|
|
9
|
+
itchfeed-1.0.6.dist-info/RECORD,,
|
itchfeed-1.0.4.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
itch/__init__.py,sha256=M9Jirj4-XXdaCoTcU2_g89z7JHK8mDtJflTFf9HnU-k,205
|
|
2
|
-
itch/indicators.py,sha256=-Ed2M8I60xGQ1bIPZCGCKGb8ayT87JAnIaosfiBimXI,6542
|
|
3
|
-
itch/messages.py,sha256=lm5pKQ00ETMbTij9HqaYVrN08JJdQe-mZKB_C7IAetU,63934
|
|
4
|
-
itch/parser.py,sha256=BOrkGsmRkcYnXSf5S9yqrwzP0jqtkH5mGCpWCeiWNTg,6155
|
|
5
|
-
itchfeed-1.0.4.dist-info/licenses/LICENSE,sha256=f2u79rUzh-UcYH0RN0Ph0VvVYHBkYlVxtguhKmrHqsw,1089
|
|
6
|
-
itchfeed-1.0.4.dist-info/METADATA,sha256=HYjteeevesrGUcrjvuNlW2P-0B9k8jtL0qdovoaLZT0,33793
|
|
7
|
-
itchfeed-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
itchfeed-1.0.4.dist-info/top_level.txt,sha256=xwsOYShvy3gc1rfyitCTgSxBZDGG1y6bfQxkdhIGmEM,5
|
|
9
|
-
itchfeed-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|