vsslctrl 0.1.12.dev1__py3-none-any.whl → 0.1.13.dev1__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.
vsslctrl/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- VSSL_VERSION = "0.1.12.dev1"
1
+ VSSL_VERSION = "0.1.13.dev1"
2
2
  VSSL_NAME = "VSSL"
3
3
 
4
4
  from .core import Vssl
vsslctrl/api_alpha.py CHANGED
@@ -39,6 +39,12 @@ class APIAlpha(APIBase):
39
39
  self.vssl = vssl_host
40
40
  self.zone = zone
41
41
 
42
+ #
43
+ # Send event on event bus
44
+ #
45
+ def _event_publish(self, event_type, data=None):
46
+ self.zone._event_publish(event_type, (self.zone.host, self.TCP_PORT))
47
+
42
48
  #
43
49
  # Send keep alive
44
50
  #
vsslctrl/api_base.py CHANGED
@@ -45,6 +45,17 @@ class APIBase(ABC):
45
45
 
46
46
  FRIST_BYTE = 1
47
47
 
48
+ #
49
+ # API Events
50
+ #
51
+ class Events:
52
+ PREFIX = "zone.api."
53
+ CONNECTING = PREFIX + "connecting"
54
+ CONNECTED = PREFIX + "connected"
55
+ DISCONNECTING = PREFIX + "disconnecting"
56
+ DISCONNECTED = PREFIX + "disconnected"
57
+ RECONNECTING = PREFIX + "reconnecting"
58
+
48
59
  def __init__(self, host, port):
49
60
  self.host = host
50
61
  self.port = port
@@ -89,6 +100,7 @@ class APIBase(ABC):
89
100
  return self.connected
90
101
 
91
102
  self._connecting = True
103
+ self._event_publish(self.Events.CONNECTING)
92
104
 
93
105
  try:
94
106
  self._log_debug(f"Attemping connection to {self.host}:{self.port}")
@@ -98,6 +110,7 @@ class APIBase(ABC):
98
110
 
99
111
  # Connected
100
112
  self.connection_event.set()
113
+ self._event_publish(self.Events.CONNECTED)
101
114
 
102
115
  # cancel any reconnecting loops
103
116
  self._cancel_keep_connected()
@@ -140,6 +153,7 @@ class APIBase(ABC):
140
153
  @final
141
154
  async def disconnect(self):
142
155
  self._disconnecting = True
156
+ self._event_publish(self.Events.DISCONNECTING)
143
157
 
144
158
  # cancel any reconnecting loops
145
159
  self._cancel_keep_connected()
@@ -176,6 +190,8 @@ class APIBase(ABC):
176
190
 
177
191
  self._disconnecting = False
178
192
 
193
+ self._event_publish(self.Events.DISCONNECTED)
194
+
179
195
  return not self.connected
180
196
 
181
197
  #
@@ -184,7 +200,9 @@ class APIBase(ABC):
184
200
  @final
185
201
  async def reconnect(self):
186
202
  if not self._reconnecting and not self._is_keep_connected_running():
203
+ self._event_publish(self.Events.RECONNECTING)
187
204
  await self.disconnect()
205
+ await asyncio.sleep(1)
188
206
  self._keep_connected()
189
207
 
190
208
  #
@@ -306,6 +324,13 @@ class APIBase(ABC):
306
324
  async def _read_byte_stream(self):
307
325
  pass
308
326
 
327
+ #
328
+ # Send event on event bus
329
+ #
330
+ @abstractmethod
331
+ def _event_publish(self, event_type, data=None):
332
+ pass
333
+
309
334
  #
310
335
  # Send a keep alive
311
336
  #
vsslctrl/api_bravo.py CHANGED
@@ -23,6 +23,12 @@ class APIBravo(APIBase):
23
23
  self.vssl = vssl_host
24
24
  self.zone = zone
25
25
 
26
+ #
27
+ # Send event on event bus
28
+ #
29
+ def _event_publish(self, event_type, data=None):
30
+ self.zone._event_publish(event_type, (self.zone.host, self.TCP_PORT))
31
+
26
32
  #
27
33
  # Send keep alive
28
34
  #
vsslctrl/core.py CHANGED
@@ -28,7 +28,6 @@ class Vssl:
28
28
  MODEL_CHANGE = PREFIX + "model_changed"
29
29
  SW_VERSION_CHANGE = PREFIX + "sw_version_changed"
30
30
  SERIAL_CHANGE = PREFIX + "serial_changed"
31
- ALL = EventBus.WILDCARD
32
31
 
33
32
  def __init__(self, model: Models):
34
33
  self.event_bus = EventBus()
@@ -50,7 +49,7 @@ class Vssl:
50
49
  #
51
50
  async def initialise(self, init_timeout: int = 10):
52
51
  if len(self.zones) < 1:
53
- raise VsslCtrlException("Add atleast one zone before initializing")
52
+ raise VsslCtrlException("Add minimum one zone before initializing")
54
53
 
55
54
  zones_to_init = self.zones.copy()
56
55
 
@@ -64,9 +63,9 @@ class Vssl:
64
63
  await first_zone.initialise()
65
64
 
66
65
  # Wait until we have some basic infomation
67
- await self.event_bus.wait_future(future_serial)
68
- await self.event_bus.wait_future(future_sw_version)
69
- await self.event_bus.wait_future(future_name)
66
+ await self.event_bus.wait_future(future_serial, init_timeout)
67
+ await self.event_bus.wait_future(future_sw_version, init_timeout)
68
+ await self.event_bus.wait_future(future_name, init_timeout)
70
69
 
71
70
  # Check we haven't added too many zones
72
71
  if len(self.zones) > self.model.zone_count:
@@ -92,7 +91,7 @@ class Vssl:
92
91
  raise
93
92
 
94
93
  except asyncio.TimeoutError:
95
- message = f"Timeout during VSSL initialization. Are any zones avaiable?"
94
+ message = f"Timeout during VSSL initialization. Are any zones available?"
96
95
  self._log_critical(message)
97
96
  await first_zone.disconnect()
98
97
  raise VsslCtrlException(message)
@@ -114,17 +113,14 @@ class Vssl:
114
113
  #
115
114
  # Discover host on the network using zero_conf package
116
115
  #
117
- async def discover(self, *args):
118
- try:
119
- check_zeroconf_availability()
116
+ @staticmethod
117
+ async def discover(*args):
118
+ check_zeroconf_availability()
120
119
 
121
- from .discovery import VsslDiscovery
120
+ from .discovery import VsslDiscovery
122
121
 
123
- service = VsslDiscovery(*args)
124
- return await service.discover()
125
- except ZeroConfNotInstalled as e:
126
- self._log_error(e)
127
- raise
122
+ service = VsslDiscovery(*args)
123
+ return await service.discover()
128
124
 
129
125
  #
130
126
  # Update a property and fire an event
@@ -257,7 +253,7 @@ class Vssl:
257
253
  return None
258
254
 
259
255
  #
260
- # Get a Zone that is connected
256
+ # Get a zone that is connected
261
257
  #
262
258
  def get_connected_zone(self):
263
259
  if self.zones:
@@ -265,6 +261,14 @@ class Vssl:
265
261
  zone = self.zones[zone_id]
266
262
  if zone.connected:
267
263
  return zone
264
+ self._log_error("There are no connected zones.")
265
+
266
+ #
267
+ # Has a connected zone
268
+ #
269
+ @property
270
+ def connected(self):
271
+ return True if self.get_connected_zone() else False
268
272
 
269
273
  #
270
274
  # Get the device name
vsslctrl/event_bus.py CHANGED
@@ -1,14 +1,11 @@
1
1
  import asyncio
2
2
  import traceback
3
+ import fnmatch
3
4
  from enum import IntEnum
4
5
  from typing import Callable
5
6
  from .exceptions import VsslCtrlException
6
7
  from .decorators import logging_helpers
7
8
 
8
- #
9
- # Event Bus
10
- #
11
-
12
9
 
13
10
  @logging_helpers("EventBus:")
14
11
  class EventBus:
@@ -23,6 +20,10 @@ class EventBus:
23
20
 
24
21
  self.process = asyncio.create_task(self.process_events())
25
22
 
23
+ # Helper for wildcard matching, so we can use partial wildcards. e.g zone.api.connected
24
+ def _matches_pattern(self, event_type, pattern):
25
+ return fnmatch.fnmatch(event_type, pattern)
26
+
26
27
  #
27
28
  # Stop
28
29
  #
@@ -67,7 +68,8 @@ class EventBus:
67
68
 
68
69
  async def future_callback(data, *args):
69
70
  nonlocal future
70
- future.set_result(data)
71
+ if not future.done(): # Ensure the future is not already resolved
72
+ future.set_result(data)
71
73
 
72
74
  self.subscribe(event_type, future_callback, entity, once=True)
73
75
 
@@ -115,12 +117,8 @@ class EventBus:
115
117
  event_type = event_type.lower()
116
118
  await self.event_queue.put((event_type, entity, data))
117
119
 
118
- #
119
- # Process Events
120
- #
121
120
  async def process_events(self):
122
121
  self._log_debug(f"starting event processing")
123
-
124
122
  self.running = True
125
123
  while self.running:
126
124
  try:
@@ -136,23 +134,22 @@ class EventBus:
136
134
  message += str(data)
137
135
  self._log_debug(message)
138
136
 
139
- for event in [event_type, self.WILDCARD]:
140
- if event in self.subscribers:
141
- for callback, subscribed_entity, once in self.subscribers[
142
- event
143
- ]:
144
- if entity is None or subscribed_entity in {
145
- entity,
146
- self.WILDCARD,
147
- }:
148
- await callback(data, entity, event_type)
149
- if once:
150
- self.unsubscribe(event, callback)
137
+ # Check for matching subscribers, including wildcards.
138
+ # e.g "zone.*", "zone.api.*"
139
+ matched_subscribers = []
140
+ for pattern in self.subscribers:
141
+ if self._matches_pattern(event_type, pattern):
142
+ matched_subscribers.extend(self.subscribers[pattern])
143
+
144
+ for callback, subscribed_entity, once in matched_subscribers:
145
+ if entity is None or subscribed_entity in {entity, self.WILDCARD}:
146
+ await callback(data, entity, event_type)
147
+ if once:
148
+ self.unsubscribe(pattern, callback)
151
149
 
152
150
  except asyncio.CancelledError:
153
151
  break
154
152
  except Exception as e:
155
- # Capture the traceback as a string
156
153
  traceback_str = traceback.format_exc()
157
154
  self._log_error(
158
155
  f"exception occurred processing event: {e}\n{traceback_str}"
vsslctrl/zone.py CHANGED
@@ -29,7 +29,6 @@ class Zone:
29
29
  # Zone Events
30
30
  #
31
31
  class Events:
32
- ALL = "*"
33
32
  PREFIX = "zone."
34
33
  INITIALISED = PREFIX + "initialised"
35
34
  ID_RECEIVED = PREFIX + "id_received"
@@ -79,7 +78,7 @@ class Zone:
79
78
  )
80
79
 
81
80
  # Initialise
82
- async def initialise(self):
81
+ async def initialise(self, init_timeout: int = 10):
83
82
  # Data we require from the device
84
83
  future_id = self.vssl.event_bus.future(self.Events.ID_RECEIVED, self.id)
85
84
  future_serial = self.vssl.event_bus.future(self.Events.SERIAL_RECEIVED, self.id)
@@ -106,13 +105,15 @@ class Zone:
106
105
 
107
106
  try:
108
107
  # Wait for the ID, serial and name to be returned from the device
109
- received_id = await self.vssl.event_bus.wait_future(future_id)
110
- received_serial = await self.vssl.event_bus.wait_future(future_serial)
111
- await self.vssl.event_bus.wait_future(future_name)
108
+ received_id = await self.vssl.event_bus.wait_future(future_id, init_timeout)
109
+ received_serial = await self.vssl.event_bus.wait_future(
110
+ future_serial, init_timeout
111
+ )
112
+ await self.vssl.event_bus.wait_future(future_name, init_timeout)
112
113
  except asyncio.TimeoutError:
113
- message = f"Zone {self.id}: initialization timeout. Is the zone avaiable?"
114
+ message = f"Zone {self.id}: initialization timeout. Is the zone available?"
114
115
  self._log_critical(message)
115
- await first_zone.disconnect()
116
+ await self.disconnect()
116
117
  raise ZoneError(message)
117
118
 
118
119
  # Confirm the zone id is matches the returned zone ID
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: vsslctrl
3
- Version: 0.1.12.dev1
3
+ Version: 0.1.13.dev1
4
4
  Summary: Package for controlling VSSL's range of streaming amplifiers
5
5
  Author-email: vsslctrl <vsslcontrolled@proton.me>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -76,7 +76,7 @@ async def main():
76
76
  vssl = Vssl(DeviceModels.A1)
77
77
  a1 = vssl.add_zone('192.168.1.10')
78
78
 
79
- # Connect and initiate zones.
79
+ # Connect and initialise zone.
80
80
  await vssl.initialise()
81
81
 
82
82
  """Control Examples"""
@@ -112,7 +112,7 @@ async def main():
112
112
  zone3 = vssl.add_zone('192.168.1.12', ZoneIDs.ZONE_3)
113
113
  #... up to 6 zones for A.6(x)
114
114
 
115
- # Connect and initiate zones.
115
+ # Connect and initialise zones.
116
116
  await vssl.initialise()
117
117
 
118
118
  """Control Examples"""
@@ -134,6 +134,77 @@ async def main():
134
134
  asyncio.run(main())
135
135
  ```
136
136
 
137
+ ### Device Discovery Helper
138
+
139
+ You can discover VSSL devices on the network using [mDNS](https://wikipedia.org/wiki/Multicast_DNS) / Bonjour if you have the [`zeroconf`](https://pypi.org/project/zeroconf/) package installed.
140
+
141
+ This uses airplay service string `_airplay._tcp.local.`, therefore airplay needs to available and will not work across VLANs without other provisions.
142
+
143
+ **Note:** This is designed to be a helper and its not recommended to be used for the initialization of the VSSL class.
144
+
145
+ ```python
146
+ import asyncio
147
+ from vsslctrl import Vssl
148
+
149
+ async def main():
150
+
151
+ print(await Vssl.discover())
152
+
153
+ """
154
+ {
155
+ 'XXXXXXXXXXXX': [
156
+ {
157
+ 'host': '192.168.168.25',
158
+ 'name': 'Living Room',
159
+ 'model': 'A1x',
160
+ 'mac_addr': 'AA:BB:CC:DD:EE:FF',
161
+ 'zone_id': '7',
162
+ 'serial': 'XXXXXXXXXXXX'
163
+ }
164
+ ]
165
+ }
166
+
167
+ """
168
+
169
+ asyncio.run(main())
170
+ ```
171
+
172
+
173
+ # API Functionality
174
+
175
+ Most functionality is achieved via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
176
+
177
+ The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
178
+
179
+ ```python
180
+ # Setting the zone name
181
+ zone1.settings.name = 'Living Room'
182
+ >>> 'Old Zone Name'
183
+
184
+ # Printing zone name
185
+ print(zone1.settings.name)
186
+ >>> 'Living Room'
187
+ ```
188
+
189
+ **Important** in the above example, `zone1.settings.name` won't be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events as below:
190
+
191
+ ```python
192
+ from vsslctrl.settings import ZoneSettings
193
+ # Setting the zone name and wait for feedback
194
+ future_name = vssl.event_bus.future(ZoneSettings.Events.NAME_CHANGE, zone1.id)
195
+ zone1.settings.name = 'Bathroom'
196
+ # Helper to await a future with timeout
197
+ new_name = await vssl.event_bus.wait_future(future_name)
198
+ # Printing zone name
199
+ print(new_name)
200
+ >>> 'Bathroom'
201
+ # or
202
+ print(zone1.settings.name)
203
+ >>> 'Bathroom'
204
+ ```
205
+
206
+ # API Reference
207
+
137
208
  # `DeviceModels`
138
209
 
139
210
  A device model has to be passed to VSSL so it knows internally what features are supported by the device.
@@ -150,35 +221,20 @@ This might be removed in the future if we can differentiate different models fro
150
221
 
151
222
  # `ZoneIDs`
152
223
 
153
- | Property | Description |
154
- | ------------|--------- |
155
- | `A1` | A.1(x) Only |
156
- | `ZONE_1` | Zone 1 of A.3(x) and A.6(x) |
157
- | `ZONE_2` | Zone 2 of A.3(x) and A.6(x) |
158
- | `ZONE_3` | Zone 3 of A.3(x) and A.6(x) |
159
- | `ZONE_4` | Zone 4 of A.6(x) |
160
- | `ZONE_5` | Zone 5 of A.6(x) |
161
- | `ZONE_6` | Zone 6 of A.6(x) |
224
+ A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
162
225
 
226
+ If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
163
227
 
164
- # API
165
-
166
- Most functionality is achieved via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
167
-
168
- The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
169
-
170
- ```python
171
- # Setting the zones name
172
- zone1.settings.name = 'Living Room'
173
- >>> None
174
-
175
- # Printing zone name
176
- zone_name = zone1.settings.name
177
- print(zone_name)
178
- >>> 'Living Room'
179
- ```
228
+ | Property | Description | A.1(x) | A.3(x) | A.6(x)
229
+ | ------------|--------- |-------- | -------- | -------- |
230
+ | `A1` | |✔️ | | |
231
+ | `ZONE_1` | Zone 1 | | ✔️| ✔️|
232
+ | `ZONE_2` | Zone 2 | | ✔️| ✔️|
233
+ | `ZONE_3` | Zone 3 | | ✔️| ✔️|
234
+ | `ZONE_4` | Zone 4 | | | ✔️|
235
+ | `ZONE_5` | Zone 5 | | | ✔️|
236
+ | `ZONE_6` | Zone 6 | | | ✔️|
180
237
 
181
- **Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
182
238
 
183
239
  # `Vssl`
184
240
 
@@ -1,13 +1,13 @@
1
- vsslctrl/__init__.py,sha256=XaTETnpaNSebBchnHG68v7At2KwMDmBUgeXHsXejQb8,174
2
- vsslctrl/api_alpha.py,sha256=N8Byglyn6TYJQneF1OH1nDDRUV3gp-Nt4xBck477ku8,36810
3
- vsslctrl/api_base.py,sha256=a80CRtUkYK2OwXQOL8bP-YOvt4YE-ALd-VONza_gjK8,9623
4
- vsslctrl/api_bravo.py,sha256=pchZN16KIvdSrqkI1WU9PA9IEmxkyyP-XifxpRC0jDE,12283
5
- vsslctrl/core.py,sha256=ZEMpIdK10sDLssEWNWfx781LWWLOWV7FxaDfxAfaMxQ,9474
1
+ vsslctrl/__init__.py,sha256=Gl9Q-q3nJi1hTjsCSXwJVY2r_zDmSvLBZ4yoKQjINTQ,174
2
+ vsslctrl/api_alpha.py,sha256=UQsvFPYCbczrG74fbAWYxVeKLw7R1uT0evxprMyuev8,36984
3
+ vsslctrl/api_base.py,sha256=g5rmjahkjyY_OB-2UjNbSwSUXVcWuL1RZZcElRmciTM,10366
4
+ vsslctrl/api_bravo.py,sha256=xpOCCREHIe7dlLu3-PIaMqV2gqPUnX-gaE0MrZlo_YI,12457
5
+ vsslctrl/core.py,sha256=tmWr4vxeQgrFryB-ELMDFtXs1qlwPzZ3tMVSQhHTKgY,9573
6
6
  vsslctrl/data_structure.py,sha256=3Tu8CIVM6ednmKVJE1fX6Ny-rKmlJLa2iJHsnjy7fvI,10884
7
7
  vsslctrl/decorators.py,sha256=_yQJ0MTXRX6LfvaXVgT2OwbYi9u83w4ciozIsnkxDdI,3406
8
8
  vsslctrl/device.py,sha256=JQK0k3N65uSLlYzfNMhUF5Pd7nfSgZj4WCT7o2-p_gs,7860
9
9
  vsslctrl/discovery.py,sha256=3z7z6YQUtvjFvRlQOZXIu6Wd4M896rMbi8SUkTub3AM,5568
10
- vsslctrl/event_bus.py,sha256=K4t2XQKb4JH0AEl_Kw7HyoczvxTTal-3Hp2zk-722y8,5036
10
+ vsslctrl/event_bus.py,sha256=1MIUh8v_ukgqwbGzYkzqnwYPCtNyDcJwWpeB4oRk_uo,5282
11
11
  vsslctrl/exceptions.py,sha256=EEfNlu--9PMIidj3V3ZB8c7p3My8siqKw7YIOjPFaqE,419
12
12
  vsslctrl/group.py,sha256=hD46J-9oHk9EQhnBBlo4HiAA6XTSZ16bq0QMMR889NU,6227
13
13
  vsslctrl/io.py,sha256=xBOZjFyP7z_oaYRCxO56EwOJ1FesSphSWkHzGzBlgoU,8796
@@ -15,9 +15,9 @@ vsslctrl/settings.py,sha256=aD-XUykRDe_EmTtSm1h8isHPDAs9DKIcLk5sz7g8PsQ,19507
15
15
  vsslctrl/track.py,sha256=tmEADTqQfW6Nk2fvmyNAajTLzuUEn3D71JHJWPYOhxY,10126
16
16
  vsslctrl/transport.py,sha256=hNo_gjVk2mCW_Z5cq-kj5xSMjWkXnGgZYpdwFel7K98,6992
17
17
  vsslctrl/utils.py,sha256=_bvsOIu3G5czdYhrtm_c0fM60t-UXcLxztnCduQoyvs,2340
18
- vsslctrl/zone.py,sha256=RWgRgMyrhIjhSdTBiXEqvqkMxTwhJ8-sHMd6Ccg1ZZU,13623
19
- vsslctrl-0.1.12.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
20
- vsslctrl-0.1.12.dev1.dist-info/METADATA,sha256=im-rpyDc4Ju09YUrOtV0ug5vrEXyaHTihfKx-yV5a2Q,17947
21
- vsslctrl-0.1.12.dev1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
22
- vsslctrl-0.1.12.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
23
- vsslctrl-0.1.12.dev1.dist-info/RECORD,,
18
+ vsslctrl/zone.py,sha256=_uzwC7qJanUQ8GamYFSdJxAEj0qdtwZi9c_m7jnw05U,13696
19
+ vsslctrl-0.1.13.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
20
+ vsslctrl-0.1.13.dev1.dist-info/METADATA,sha256=dW3xfOXA721g4uGZ29gLtVlfeSCD4YV8Y75QLP1ksL4,19617
21
+ vsslctrl-0.1.13.dev1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
+ vsslctrl-0.1.13.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
23
+ vsslctrl-0.1.13.dev1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5