PyPlumIO 0.5.51__py3-none-any.whl → 0.5.52__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.
- pyplumio/_version.py +2 -2
- pyplumio/frames/__init__.py +8 -4
- pyplumio/stream.py +134 -49
- pyplumio/structures/alerts.py +1 -1
- pyplumio/structures/schedules.py +1 -1
- {pyplumio-0.5.51.dist-info → pyplumio-0.5.52.dist-info}/METADATA +8 -8
- {pyplumio-0.5.51.dist-info → pyplumio-0.5.52.dist-info}/RECORD +10 -10
- {pyplumio-0.5.51.dist-info → pyplumio-0.5.52.dist-info}/WHEEL +1 -1
- {pyplumio-0.5.51.dist-info → pyplumio-0.5.52.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.51.dist-info → pyplumio-0.5.52.dist-info}/top_level.txt +0 -0
pyplumio/_version.py
CHANGED
pyplumio/frames/__init__.py
CHANGED
@@ -15,10 +15,14 @@ from pyplumio.utils import ensure_dict, to_camelcase
|
|
15
15
|
|
16
16
|
FRAME_START: Final = 0x68
|
17
17
|
FRAME_END: Final = 0x16
|
18
|
-
|
18
|
+
|
19
|
+
|
20
|
+
HEADER_INDEX: Final = 0
|
19
21
|
FRAME_TYPE_SIZE: Final = 1
|
20
|
-
|
22
|
+
BCC_SIZE: Final = 1
|
23
|
+
BCC_INDEX: Final = -2
|
21
24
|
DELIMITER_SIZE: Final = 1
|
25
|
+
|
22
26
|
ECONET_TYPE: Final = 48
|
23
27
|
ECONET_VERSION: Final = 5
|
24
28
|
|
@@ -199,7 +203,7 @@ class Frame(ABC):
|
|
199
203
|
struct_header.size
|
200
204
|
+ FRAME_TYPE_SIZE
|
201
205
|
+ len(self.message)
|
202
|
-
+
|
206
|
+
+ BCC_SIZE
|
203
207
|
+ DELIMITER_SIZE
|
204
208
|
)
|
205
209
|
|
@@ -209,7 +213,7 @@ class Frame(ABC):
|
|
209
213
|
buffer = bytearray(struct_header.size)
|
210
214
|
struct_header.pack_into(
|
211
215
|
buffer,
|
212
|
-
|
216
|
+
HEADER_INDEX,
|
213
217
|
FRAME_START,
|
214
218
|
self.length,
|
215
219
|
int(self.recipient),
|
pyplumio/stream.py
CHANGED
@@ -5,14 +5,16 @@ from __future__ import annotations
|
|
5
5
|
import asyncio
|
6
6
|
from asyncio import IncompleteReadError, StreamReader, StreamWriter
|
7
7
|
import logging
|
8
|
-
from typing import Final, NamedTuple
|
8
|
+
from typing import Final, NamedTuple, SupportsIndex
|
9
9
|
|
10
10
|
from pyplumio.const import DeviceType
|
11
11
|
from pyplumio.devices import is_known_device_type
|
12
12
|
from pyplumio.exceptions import ChecksumError, ReadError, UnknownDeviceError
|
13
13
|
from pyplumio.frames import (
|
14
|
+
BCC_INDEX,
|
14
15
|
DELIMITER_SIZE,
|
15
16
|
FRAME_START,
|
17
|
+
FRAME_TYPE_SIZE,
|
16
18
|
HEADER_SIZE,
|
17
19
|
Frame,
|
18
20
|
bcc,
|
@@ -26,6 +28,8 @@ WRITER_TIMEOUT: Final = 10
|
|
26
28
|
MIN_FRAME_LENGTH: Final = 10
|
27
29
|
MAX_FRAME_LENGTH: Final = 1000
|
28
30
|
|
31
|
+
DEFAULT_BUFFER_SIZE: Final = 5000
|
32
|
+
|
29
33
|
_LOGGER = logging.getLogger(__name__)
|
30
34
|
|
31
35
|
|
@@ -63,6 +67,103 @@ class FrameWriter:
|
|
63
67
|
await self._writer.wait_closed()
|
64
68
|
|
65
69
|
|
70
|
+
class BufferManager:
|
71
|
+
"""Represents a buffered reader for reading frames."""
|
72
|
+
|
73
|
+
__slots__ = ("_buffer", "_reader")
|
74
|
+
|
75
|
+
_buffer: bytearray
|
76
|
+
_reader: StreamReader
|
77
|
+
|
78
|
+
def __init__(self, reader: StreamReader) -> None:
|
79
|
+
"""Initialize a new buffered reader."""
|
80
|
+
self._buffer = bytearray()
|
81
|
+
self._reader = reader
|
82
|
+
|
83
|
+
async def ensure_buffer(self, size: int) -> None:
|
84
|
+
"""Ensure the internal buffer size."""
|
85
|
+
bytes_to_read = size - len(self._buffer)
|
86
|
+
if bytes_to_read <= 0:
|
87
|
+
return None
|
88
|
+
|
89
|
+
try:
|
90
|
+
data = await self._reader.readexactly(bytes_to_read)
|
91
|
+
self._buffer.extend(data)
|
92
|
+
self.trim_to(size)
|
93
|
+
except IncompleteReadError as e:
|
94
|
+
raise ReadError(
|
95
|
+
f"Incomplete read. Tried to read {bytes_to_read} additional bytes "
|
96
|
+
f"to reach a total of {size}, but only {len(e.partial)} bytes were "
|
97
|
+
"available from stream."
|
98
|
+
) from e
|
99
|
+
except asyncio.CancelledError:
|
100
|
+
_LOGGER.debug("Read operation cancelled while ensuring buffer")
|
101
|
+
raise
|
102
|
+
except Exception as e:
|
103
|
+
raise OSError(
|
104
|
+
f"Serial connection broken while trying to ensure {size} bytes: {e}"
|
105
|
+
) from e
|
106
|
+
|
107
|
+
async def consume(self, size: int) -> None:
|
108
|
+
"""Consume the specified number of bytes from the buffer."""
|
109
|
+
await self.ensure_buffer(size)
|
110
|
+
self._buffer = self._buffer[size:]
|
111
|
+
|
112
|
+
async def peek(self, size: int) -> bytearray:
|
113
|
+
"""Read the specified number of bytes without consuming them."""
|
114
|
+
await self.ensure_buffer(size)
|
115
|
+
return self._buffer[:size]
|
116
|
+
|
117
|
+
async def read(self, size: int) -> bytearray:
|
118
|
+
"""Read the bytes from buffer or stream and consume them."""
|
119
|
+
try:
|
120
|
+
return await self.peek(size)
|
121
|
+
finally:
|
122
|
+
await self.consume(size)
|
123
|
+
|
124
|
+
def seek_to(self, delimiter: SupportsIndex) -> bool:
|
125
|
+
"""Trim the buffer to the first occurrence of the delimiter.
|
126
|
+
|
127
|
+
Returns True if the delimiter was found and trimmed, False otherwise.
|
128
|
+
"""
|
129
|
+
if not self._buffer or (index := self._buffer.find(delimiter)) == -1:
|
130
|
+
return False
|
131
|
+
|
132
|
+
self._buffer = self._buffer[index:]
|
133
|
+
return True
|
134
|
+
|
135
|
+
def trim_to(self, size: int) -> None:
|
136
|
+
"""Trim buffer to size."""
|
137
|
+
if len(self._buffer) > size:
|
138
|
+
self._buffer = self._buffer[-size:]
|
139
|
+
|
140
|
+
async def fill(self) -> None:
|
141
|
+
"""Fill the buffer with data from the stream."""
|
142
|
+
try:
|
143
|
+
chunk = await self._reader.read(MAX_FRAME_LENGTH)
|
144
|
+
except asyncio.CancelledError:
|
145
|
+
_LOGGER.debug("Read operation cancelled while filling read buffer.")
|
146
|
+
raise
|
147
|
+
except Exception as e:
|
148
|
+
raise OSError(
|
149
|
+
f"Serial connection broken while filling read buffer: {e}"
|
150
|
+
) from e
|
151
|
+
|
152
|
+
if not chunk:
|
153
|
+
_LOGGER.debug("Stream ended while filling read buffer.")
|
154
|
+
raise OSError(
|
155
|
+
"Serial connection broken: stream ended while filling read buffer"
|
156
|
+
)
|
157
|
+
|
158
|
+
self._buffer.extend(chunk)
|
159
|
+
self.trim_to(DEFAULT_BUFFER_SIZE)
|
160
|
+
|
161
|
+
@property
|
162
|
+
def buffer(self) -> bytearray:
|
163
|
+
"""Return the internal buffer."""
|
164
|
+
return self._buffer
|
165
|
+
|
166
|
+
|
66
167
|
class Header(NamedTuple):
|
67
168
|
"""Represents a frame header."""
|
68
169
|
|
@@ -76,81 +177,65 @@ class Header(NamedTuple):
|
|
76
177
|
class FrameReader:
|
77
178
|
"""Represents a frame reader."""
|
78
179
|
|
79
|
-
__slots__ = ("
|
180
|
+
__slots__ = ("_buffer",)
|
80
181
|
|
81
|
-
|
182
|
+
_buffer: BufferManager
|
82
183
|
|
83
184
|
def __init__(self, reader: StreamReader) -> None:
|
84
185
|
"""Initialize a new frame reader."""
|
85
|
-
self.
|
86
|
-
|
87
|
-
async def _read_header(self) -> tuple[Header, bytes]:
|
88
|
-
"""Locate and read a frame header.
|
89
|
-
|
90
|
-
Raise pyplumio.ReadError if header size is too small and
|
91
|
-
OSError if serial connection is broken.
|
92
|
-
"""
|
93
|
-
while buffer := await self._reader.read(DELIMITER_SIZE):
|
94
|
-
if FRAME_START not in buffer:
|
95
|
-
continue
|
96
|
-
|
97
|
-
try:
|
98
|
-
buffer += await self._reader.readexactly(HEADER_SIZE - DELIMITER_SIZE)
|
99
|
-
except IncompleteReadError as e:
|
100
|
-
raise ReadError(
|
101
|
-
f"Incomplete header, expected {e.expected} bytes"
|
102
|
-
) from e
|
186
|
+
self._buffer = BufferManager(reader)
|
103
187
|
|
104
|
-
|
188
|
+
async def _read_header(self) -> Header:
|
189
|
+
"""Locate and read a frame header."""
|
190
|
+
while True:
|
191
|
+
if self._buffer.seek_to(FRAME_START):
|
192
|
+
header_bytes = await self._buffer.peek(HEADER_SIZE)
|
193
|
+
return Header(*struct_header.unpack_from(header_bytes)[DELIMITER_SIZE:])
|
105
194
|
|
106
|
-
|
195
|
+
await self._buffer.fill()
|
107
196
|
|
108
197
|
@timeout(READER_TIMEOUT)
|
109
198
|
async def read(self) -> Frame | None:
|
110
|
-
"""Read the frame and return corresponding handler object.
|
111
|
-
|
112
|
-
Raise pyplumio.UnknownDeviceError when sender device has an
|
113
|
-
unknown address, raise pyplumio.ReadError on unexpected frame
|
114
|
-
length or incomplete frame, raise pyplumio.ChecksumError on
|
115
|
-
incorrect frame checksum.
|
116
|
-
"""
|
117
|
-
header, buffer = await self._read_header()
|
199
|
+
"""Read the frame and return corresponding handler object."""
|
200
|
+
header = await self._read_header()
|
118
201
|
frame_length, recipient, sender, econet_type, econet_version = header
|
119
202
|
|
120
|
-
if recipient not in (DeviceType.ECONET, DeviceType.ALL):
|
121
|
-
# Not an intended recipient, ignore the frame.
|
122
|
-
return None
|
123
|
-
|
124
|
-
if not is_known_device_type(sender):
|
125
|
-
raise UnknownDeviceError(f"Unknown sender type ({sender})")
|
126
|
-
|
127
203
|
if frame_length > MAX_FRAME_LENGTH or frame_length < MIN_FRAME_LENGTH:
|
204
|
+
await self._buffer.consume(HEADER_SIZE)
|
128
205
|
raise ReadError(
|
129
206
|
f"Unexpected frame length ({frame_length}), expected between "
|
130
207
|
f"{MIN_FRAME_LENGTH} and {MAX_FRAME_LENGTH}"
|
131
208
|
)
|
132
209
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
if (checksum := bcc(buffer[:-2])) and checksum != buffer[-2]:
|
210
|
+
frame_bytes = await self._buffer.peek(frame_length)
|
211
|
+
checksum = bcc(frame_bytes[:BCC_INDEX])
|
212
|
+
if checksum != frame_bytes[BCC_INDEX]:
|
213
|
+
await self._buffer.consume(HEADER_SIZE)
|
139
214
|
raise ChecksumError(
|
140
215
|
f"Incorrect frame checksum: calculated {checksum}, "
|
141
|
-
f"expected {
|
142
|
-
|
216
|
+
f"expected {frame_bytes[BCC_INDEX]}. Frame data: {frame_bytes.hex()}"
|
217
|
+
)
|
218
|
+
|
219
|
+
await self._buffer.consume(frame_length)
|
220
|
+
if recipient not in (DeviceType.ECONET, DeviceType.ALL):
|
221
|
+
_LOGGER.debug(
|
222
|
+
"Skipping frame intended for different recipient (%s)", recipient
|
143
223
|
)
|
224
|
+
return None
|
225
|
+
|
226
|
+
if not is_known_device_type(sender):
|
227
|
+
raise UnknownDeviceError(f"Unknown sender type ({sender})")
|
144
228
|
|
229
|
+
payload_bytes = frame_bytes[HEADER_SIZE:BCC_INDEX]
|
145
230
|
frame = await Frame.create(
|
146
|
-
frame_type=
|
231
|
+
frame_type=payload_bytes[0],
|
147
232
|
recipient=DeviceType(recipient),
|
148
233
|
sender=DeviceType(sender),
|
149
234
|
econet_type=econet_type,
|
150
235
|
econet_version=econet_version,
|
151
|
-
message=
|
236
|
+
message=payload_bytes[FRAME_TYPE_SIZE:],
|
152
237
|
)
|
153
|
-
_LOGGER.debug("Received frame: %s, bytes: %s", frame,
|
238
|
+
_LOGGER.debug("Received frame: %s, bytes: %s", frame, frame_bytes.hex())
|
154
239
|
|
155
240
|
return frame
|
156
241
|
|
pyplumio/structures/alerts.py
CHANGED
pyplumio/structures/schedules.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.52
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -25,17 +25,17 @@ Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
26
26
|
Requires-Dist: dataslots==1.2.0
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
|
-
Requires-Dist: typing-extensions==4.
|
28
|
+
Requires-Dist: typing-extensions==4.14.0
|
29
29
|
Provides-Extra: test
|
30
30
|
Requires-Dist: codespell==2.4.1; extra == "test"
|
31
|
-
Requires-Dist: coverage==7.
|
32
|
-
Requires-Dist: freezegun==1.5.
|
33
|
-
Requires-Dist: mypy==1.
|
31
|
+
Requires-Dist: coverage==7.9.1; extra == "test"
|
32
|
+
Requires-Dist: freezegun==1.5.2; extra == "test"
|
33
|
+
Requires-Dist: mypy==1.16.1; extra == "test"
|
34
34
|
Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
|
35
35
|
Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
|
36
|
-
Requires-Dist: pytest==8.
|
37
|
-
Requires-Dist: pytest-asyncio==0.
|
38
|
-
Requires-Dist: ruff==0.11.
|
36
|
+
Requires-Dist: pytest==8.4.1; extra == "test"
|
37
|
+
Requires-Dist: pytest-asyncio==1.0.0; extra == "test"
|
38
|
+
Requires-Dist: ruff==0.11.13; extra == "test"
|
39
39
|
Requires-Dist: tox==4.26.0; extra == "test"
|
40
40
|
Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
|
41
41
|
Provides-Extra: docs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
|
2
2
|
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=
|
3
|
+
pyplumio/_version.py,sha256=yGNU5eJ0S_ns8vdsPIX0Yq-JAj22V0bb0R9aJSWHpgE,513
|
4
4
|
pyplumio/connection.py,sha256=9MCPb8W62uqCrzd1YCROcn9cCjRY8E65934FnJDF5Js,5902
|
5
5
|
pyplumio/const.py,sha256=eoq-WNJ8TO3YlP7dC7KkVQRKGjt9FbRZ6M__s29vb1U,5659
|
6
6
|
pyplumio/data_types.py,sha256=r-QOIZiIpBFo4kRongyu8n0BHTaEU6wWMTmNkWBNjq8,9223
|
@@ -8,14 +8,14 @@ pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
|
8
8
|
pyplumio/filters.py,sha256=8IPDa8GQLKf4OdoLwlTxFyffvZXt-VrE6nKpttMVTLg,15400
|
9
9
|
pyplumio/protocol.py,sha256=DWM-yJnm2EQPLvGzXNlkQ0IpKQn44e-WkNB_DqZAag8,8313
|
10
10
|
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
pyplumio/stream.py,sha256=
|
11
|
+
pyplumio/stream.py,sha256=XzDZ1mkwUwy8wHtVII2W9t_8NpCQEwFmnlMeHwh8cd0,7832
|
12
12
|
pyplumio/utils.py,sha256=D6_SJzYkFjXoUrlNPt_mIQAP8hjMU05RsTqlAFphj3Y,1205
|
13
13
|
pyplumio/devices/__init__.py,sha256=OLPY_Kk5E2SiJ4FLN2g6zmKQdQfutePV5jRH9kRHAMA,8260
|
14
14
|
pyplumio/devices/ecomax.py,sha256=3_Hk6RaQ2e9WqIJ2NdPhofgVFjLbWIyR3TsRmMG35WY,16043
|
15
15
|
pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
|
16
16
|
pyplumio/devices/mixer.py,sha256=7WdUVgwO4VXmaPNzh3ZWpKr2ooRXWemz2KFHAw35_Rk,2731
|
17
17
|
pyplumio/devices/thermostat.py,sha256=MHMKe45fQ7jKlhBVObJ7McbYQKuF6-LOKSHy-9VNsCU,2253
|
18
|
-
pyplumio/frames/__init__.py,sha256=
|
18
|
+
pyplumio/frames/__init__.py,sha256=iHydFDClh3EDjElBis6nicNmF0QnahguBEtY_HOHsck,7885
|
19
19
|
pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
|
20
20
|
pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
|
21
21
|
pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
|
@@ -32,7 +32,7 @@ pyplumio/parameters/thermostat.py,sha256=-DK2Mb78CGrKmdhwAD0M3GiGJatczPnl1e2gVeT
|
|
32
32
|
pyplumio/parameters/custom/__init__.py,sha256=o1khThLf4FMrjErFIcikAc6jI9gn5IyZlo7LNKKqJG4,3194
|
33
33
|
pyplumio/parameters/custom/ecomax_860d3_hb.py,sha256=IsNgDXmV90QpBilDV4fGSBtIUEQJJbR9rjnfCr3-pHE,2840
|
34
34
|
pyplumio/structures/__init__.py,sha256=emZVH5OFgdTUPbEJoznMKitmK0nlPm0I4SmF86It1Do,1345
|
35
|
-
pyplumio/structures/alerts.py,sha256=
|
35
|
+
pyplumio/structures/alerts.py,sha256=Whl_WyHV9sXr321SuJAYBc1wUawNzi7xMZc41M8qToY,3724
|
36
36
|
pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
|
37
37
|
pyplumio/structures/boiler_power.py,sha256=7CdOk-pYLEpy06oRBAeichvq8o-a2RcesB0tzo9ccBs,951
|
38
38
|
pyplumio/structures/ecomax_parameters.py,sha256=E_s5bO0RqX8p1rM5DtYAsEXcHqS8P6Tg4AGm21cxsnM,1663
|
@@ -52,13 +52,13 @@ pyplumio/structures/product_info.py,sha256=Y5Q5UzKcxrixkB3Fd_BZaj1DdUNvUw1XASqR1
|
|
52
52
|
pyplumio/structures/program_version.py,sha256=qHmmPComCOa-dgq7cFAucEGuRS-jWYwWi40VCiPS7cc,2621
|
53
53
|
pyplumio/structures/regulator_data.py,sha256=SYKI1YPC3mDAth-SpYejttbD0IzBfobjgC-uy_uUKnw,2333
|
54
54
|
pyplumio/structures/regulator_data_schema.py,sha256=0SapbZCGzqAHmHC7dwhufszJ9FNo_ZO_XMrFGNiUe-w,1547
|
55
|
-
pyplumio/structures/schedules.py,sha256=
|
55
|
+
pyplumio/structures/schedules.py,sha256=SGD9p12G_BVU2PSR1k5AS1cgx_bujFw8rqKSFohtEbc,12052
|
56
56
|
pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8pLI,1325
|
57
57
|
pyplumio/structures/temperatures.py,sha256=2VD3P_vwp9PEBkOn2-WhifOR8w-UYNq35aAxle0z2Vg,2831
|
58
58
|
pyplumio/structures/thermostat_parameters.py,sha256=st3x3HkjQm3hqBrn_fpvPDQu8fuc-Sx33ONB19ViQak,3007
|
59
59
|
pyplumio/structures/thermostat_sensors.py,sha256=rO9jTZWGQpThtJqVdbbv8sYMYHxJi4MfwZQza69L2zw,3399
|
60
|
-
pyplumio-0.5.
|
61
|
-
pyplumio-0.5.
|
62
|
-
pyplumio-0.5.
|
63
|
-
pyplumio-0.5.
|
64
|
-
pyplumio-0.5.
|
60
|
+
pyplumio-0.5.52.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
61
|
+
pyplumio-0.5.52.dist-info/METADATA,sha256=FMrf9h1q6n2yj7eafOP4MxGkRwTPEw3a0rMDi_xvyh4,5611
|
62
|
+
pyplumio-0.5.52.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
63
|
+
pyplumio-0.5.52.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
64
|
+
pyplumio-0.5.52.dist-info/RECORD,,
|
File without changes
|
File without changes
|