pycyphal2 2.0.0.dev0__py3-none-any.whl → 2.0.0.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.
pycyphal2/__init__.py CHANGED
@@ -1,9 +1,20 @@
1
1
  """
2
- `Cyphal <https://opencyphal.org>`_ in Python —
2
+ [Cyphal](https://opencyphal.org) in Python —
3
3
  decentralized real-time pub/sub with tunable reliability, service discovery, and zero configuration.
4
- Works anywhere, `even baremetal MCUs <https://github.com/OpenCyphal-Garage/cy>`_.
4
+ Works anywhere, [including baremetal MCUs](https://github.com/OpenCyphal-Garage/cy).
5
5
 
6
6
  Supports various transports such as Ethernet (UDP) and CAN FD with optional redundancy.
7
+
8
+ ## Installation
9
+
10
+ Optional features inside the brackets can be removed if not needed; see `pyproject.toml` for the full list:
11
+
12
+ ```
13
+ pip install pycyphal2[udp,pythoncan]
14
+ ```
15
+
16
+ ## Usage
17
+
7
18
  Set up a transport, make a node, publish and subscribe:
8
19
 
9
20
  ```python
@@ -25,22 +36,46 @@ All public symbols live at the top level — just `import pycyphal2`.
25
36
  Transport modules (`pycyphal2.udp`, `pycyphal2.can`) are imported separately
26
37
  so that only the needed dependencies are pulled in.
27
38
 
28
- The source repository contains a collection of runnable examples.
39
+ ### Name resolution
29
40
 
30
- Environment variables control name remapping similar to ROS:
41
+ The topic naming system shares many similarities with ROS.
42
+ A valid name contains printable ASCII characters except space (ASCII codes [33, 126]).
43
+ Normalized names do not have leading or trailing segment separators `/` and do not have consecutive separators.
44
+ Every node should have a unique name, which is called its *home*; home substitution is done via `~/`.
45
+
46
+ | Input name | Namespace | Home | Remap | Resolved name | Note |
47
+ | ----------------- | --------- | ---- | ------------------ | --------------------- | -------------------------------- |
48
+ | `foo/bar` | `ns` | `me` | | `ns/foo/bar` | Relative name |
49
+ | `/foo//bar/` | `ns` | `me` | | `foo/bar` | Absolute name; namespace ignored |
50
+ | `~/foo/bar` | `ns` | `me` | | `me/foo/bar` | Homeful name |
51
+ | `sensor/*/temp` | `diag` | `me` | | `diag/sensor/*/temp` | Pattern with `*` |
52
+ | `/sensor/>` | `diag` | `me` | | `sensor/>` | Pattern with trailing `>` |
53
+ | `foo/bar` | `ns` | `me` | `foo/bar=~/zoo` | `me/zoo` | Remap first, then resolve |
54
+
55
+ Only exact `~` or `~/...` is homeful; `~ns` is literal. A matching remap overrides pinning.
56
+ Pins are allowed only on verbatim names, not on patterns.
57
+
58
+ Environment variables that control name remapping:
31
59
 
32
60
  - `CYPHAL_NAMESPACE` — default namespace prepended to relative topic names.
33
61
  - `CYPHAL_REMAP` — topic name remappings (`from=to` pairs, whitespace-separated).
34
62
 
35
- Publication is best-effort by default. Pass ``reliable=True`` when publishing to retry delivery until
63
+ See also :meth:`Node.remap`.
64
+
65
+ ### Publish
66
+
67
+ Publication is best-effort by default. Pass `reliable=True` when publishing to retry delivery until
36
68
  acknowledged by every known subscriber or until the deadline; if the remote side does not acknowledge in time,
37
69
  :class:`DeliveryError` is raised.
38
70
 
39
71
  ```python
72
+ pub = node.advertise("sensor/temperature")
40
73
  await pub(Instant.now() + 1.0, b"payload", reliable=True)
41
74
  ```
42
75
 
43
- Subscriptions normally yield messages as soon as they arrive. Set ``reordering_window`` [seconds] on
76
+ ### Subscribe
77
+
78
+ Subscriptions normally yield messages as soon as they arrive. Set `reordering_window` [seconds] on
44
79
  :meth:`Node.subscribe` to allow delaying out-of-order messages to reconstruct the original publication order.
45
80
  This is useful for sensor feeds and state estimators.
46
81
 
@@ -48,6 +83,20 @@ This is useful for sensor feeds and state estimators.
48
83
  sub = node.subscribe("sensor/temperature", reordering_window=0.1)
49
84
  ```
50
85
 
86
+ Pattern matching is supported: use `*` to match one name segment (e.g., `sensor/*/temperature`)
87
+ and a trailing `>` to match zero or more trailing segments (e.g., `sensor/>`).
88
+ Pattern subscribers automatically join matching topics as they appear, and unsubscribe as they disappear.
89
+
90
+ ```python
91
+ sub = node.subscribe("sensor/*/temperature")
92
+ async for arrival in sub:
93
+ topic = arrival.breadcrumb.topic
94
+ captures = sub.substitutions(topic)
95
+ print(topic.name, captures) # [('engine', 1)], where 1 is the pattern segment index
96
+ ```
97
+
98
+ ### RPC & streaming
99
+
51
100
  RPC is layered directly on top of pub/sub. Use :meth:`Publisher.request` to publish a message that expects
52
101
  responses, and use :attr:`Arrival.breadcrumb` on the subscriber side to send a unicast reply back to the requester.
53
102
  One request may yield responses from multiple subscribers.
@@ -66,8 +115,33 @@ await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-1", reliable=True)
66
115
  await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-2", reliable=True)
67
116
  ```
68
117
 
118
+ ### Topic pinning
119
+
120
+ Topics may be pinned to a specific subject-ID using `name#1234` to bypass automatic assignment.
121
+ This is useful for applications where a high degree of determinism is required and for Cyphal/CAN v1.0 interoperability.
122
+ Pattern names (e.g., `sensor/*/temperature/>`) cannot be pinned.
123
+
124
+ To join a Cyphal/CAN v1.0 subject, use topic name of the form `subject_id#subject_id`; e.g., `7509#7509`.
125
+
126
+ ```python
127
+ pub = node.advertise("motor/status#1234")
128
+ sub = node.subscribe("1234#1234")
129
+ ```
130
+
131
+ Old Cyphal/CAN v1.0 nodes do not participate in the topic discovery protocol,
132
+ so topics joined only by such nodes are not discoverable by pattern subscribers.
133
+
134
+ ## Remarks
135
+
69
136
  Cyphal does not define a serialization format. Previous versions used to define the DSDL format but it has been
70
137
  extracted into an independent project, and Cyphal was made serialization-agnostic in v1.1+.
138
+
139
+ PyCyphal v2 is published on PyPI as [`pycyphal2`](https://pypi.org/project/pycyphal2/)
140
+ to enable coexistence with the original [`pycyphal` v1](https://pypi.org/project/pycyphal/)
141
+ in the same Python environment.
142
+ The two packages have radically different APIs but are wire-compatible on Cyphal/CAN.
143
+ The maintenance of the original `pycyphal` package will eventually cease;
144
+ existing applications leveraging `pycyphal` should upgrade to the new API of `pycyphal2`.
71
145
  """
72
146
 
73
147
  from __future__ import annotations
@@ -77,7 +151,7 @@ from ._transport import Transport as Transport
77
151
  from ._transport import TransportArrival as TransportArrival
78
152
  from ._transport import SubjectWriter as SubjectWriter
79
153
 
80
- __version__ = "2.0.0.dev0"
154
+ __version__ = "2.0.0.dev1"
81
155
 
82
156
  # pdoc needs __all__ to display re-exported members.
83
157
  __all__ = [
pycyphal2/_api.py CHANGED
@@ -131,7 +131,6 @@ class Closable(ABC):
131
131
  class Topic(ABC):
132
132
  """
133
133
  Topics are managed automatically by the library, created and destroyed as necessary.
134
- This is just a compact view to expose some auxiliary information.
135
134
  """
136
135
 
137
136
  @property
@@ -144,6 +143,16 @@ class Topic(ABC):
144
143
  def name(self) -> str:
145
144
  raise NotImplementedError
146
145
 
146
+ @property
147
+ @abstractmethod
148
+ def evictions(self) -> int:
149
+ raise NotImplementedError
150
+
151
+ @abstractmethod
152
+ def subject_id(self, modulus: int) -> int:
153
+ """The modulus can be obtained from :attr:`Transport.subject_id_modulus`."""
154
+ raise NotImplementedError
155
+
147
156
  @abstractmethod
148
157
  def match(self, pattern: str) -> list[tuple[str, int]] | None:
149
158
  """
@@ -475,6 +484,11 @@ class Node(Closable, ABC):
475
484
  def namespace(self) -> str:
476
485
  raise NotImplementedError
477
486
 
487
+ @property
488
+ @abstractmethod
489
+ def transport(self) -> Transport:
490
+ raise NotImplementedError
491
+
478
492
  @abstractmethod
479
493
  def remap(self, spec: str | dict[str, str]) -> None:
480
494
  """
pycyphal2/_node.py CHANGED
@@ -306,6 +306,7 @@ class _TopicFlyweight(Topic):
306
306
 
307
307
  _topic_hash: int
308
308
  _name: str
309
+ _evictions: int
309
310
 
310
311
  @property
311
312
  def hash(self) -> int:
@@ -315,6 +316,13 @@ class _TopicFlyweight(Topic):
315
316
  def name(self) -> str:
316
317
  return self._name
317
318
 
319
+ @property
320
+ def evictions(self) -> int:
321
+ return self._evictions
322
+
323
+ def subject_id(self, modulus: int) -> int:
324
+ return compute_subject_id(self._topic_hash, self._evictions, modulus)
325
+
318
326
  def match(self, pattern: str) -> list[tuple[str, int]] | None:
319
327
  return match_pattern(pattern, self._name)
320
328
 
@@ -364,7 +372,7 @@ class TopicImpl(Topic):
364
372
  self._node = node
365
373
  self._name = name
366
374
  self._topic_hash = rapidhash(name)
367
- self.evictions = evictions
375
+ self._evictions = evictions
368
376
  self.ts_origin = now
369
377
  self.ts_animated = now
370
378
  self._pub_tag_baseline = int.from_bytes(os.urandom(8), "little")
@@ -392,14 +400,20 @@ class TopicImpl(Topic):
392
400
  def name(self) -> str:
393
401
  return self._name
394
402
 
403
+ @property
404
+ def evictions(self) -> int:
405
+ return self._evictions
406
+
407
+ def set_evictions(self, evictions: int) -> None:
408
+ self._evictions = evictions
409
+
410
+ def subject_id(self, modulus: int) -> int:
411
+ return compute_subject_id(self._topic_hash, self._evictions, modulus)
412
+
395
413
  def match(self, pattern: str) -> list[tuple[str, int]] | None:
396
414
  return match_pattern(pattern, self._name)
397
415
 
398
416
  # -- Internal --
399
- @property
400
- def subject_id(self) -> int:
401
- return compute_subject_id(self._topic_hash, self.evictions, self._node.transport.subject_id_modulus)
402
-
403
417
  def lage(self, now: float) -> int:
404
418
  return log_age(self.ts_origin, now)
405
419
 
@@ -426,14 +440,14 @@ class TopicImpl(Topic):
426
440
 
427
441
  def ensure_writer(self) -> SubjectWriter:
428
442
  if self.pub_writer is None:
429
- sid = self.subject_id
443
+ sid = self.subject_id(self._node.transport.subject_id_modulus)
430
444
  self.pub_writer = self._node.acquire_subject_writer(self, sid)
431
445
  _logger.info("Writer acquired for '%s' sid=%d", self._name, sid)
432
446
  return self.pub_writer
433
447
 
434
448
  def ensure_listener(self) -> None:
435
449
  if self.sub_listener is None and self.couplings:
436
- sid = self.subject_id
450
+ sid = self.subject_id(self._node.transport.subject_id_modulus)
437
451
  self.sub_listener = self._node.acquire_subject_listener(self, sid)
438
452
  _logger.info("Listener acquired for '%s' sid=%d", self._name, sid)
439
453
 
@@ -441,12 +455,12 @@ class TopicImpl(Topic):
441
455
  if self.couplings:
442
456
  self.ensure_listener()
443
457
  elif self.sub_listener is not None:
444
- self._node.release_subject_listener(self, self.subject_id)
458
+ self._node.release_subject_listener(self, self.subject_id(self._node.transport.subject_id_modulus))
445
459
  self.sub_listener = None
446
460
  _logger.info("Listener released for '%s'", self._name)
447
461
 
448
462
  def release_transport_handles(self) -> None:
449
- sid = self.subject_id
463
+ sid = self.subject_id(self._node.transport.subject_id_modulus)
450
464
  if self.pub_writer is not None:
451
465
  self._node.release_subject_writer(self, sid)
452
466
  self.pub_writer = None
@@ -487,7 +501,7 @@ def left_wins(l_lage: int, l_hash: int, r_lage: int, r_hash: int) -> bool:
487
501
 
488
502
  class NodeImpl(Node):
489
503
  def __init__(self, transport: Transport, *, home: str, namespace: str) -> None:
490
- self.transport = transport
504
+ self._transport = transport
491
505
  self._home = home
492
506
  self._namespace = namespace
493
507
  self._remaps: dict[str, str] = {}
@@ -555,6 +569,10 @@ class NodeImpl(Node):
555
569
  def namespace(self) -> str:
556
570
  return self._namespace
557
571
 
572
+ @property
573
+ def transport(self) -> Transport:
574
+ return self._transport
575
+
558
576
  def remap(self, spec: str | dict[str, str]) -> None:
559
577
  if isinstance(spec, str):
560
578
  spec = dict(x.split("=", 1) for x in spec.split() if "=" in x)
@@ -573,7 +591,12 @@ class NodeImpl(Node):
573
591
  topic.pub_count += 1
574
592
  topic.sync_implicit()
575
593
  topic.ensure_writer()
576
- _logger.info("Advertise '%s' -> '%s' sid=%d", name, resolved, topic.subject_id)
594
+ _logger.info(
595
+ "Advertise '%s' -> '%s' sid=%d",
596
+ name,
597
+ resolved,
598
+ topic.subject_id(self.transport.subject_id_modulus),
599
+ )
577
600
  return PublisherImpl(self, topic)
578
601
 
579
602
  def subscribe(self, name: str, *, reordering_window: float | None = None) -> Subscriber:
@@ -662,29 +685,34 @@ class NodeImpl(Node):
662
685
  self.couple_topic_root(topic, root)
663
686
  topic.sync_listener()
664
687
  self.notify_implicit_gc()
665
- _logger.info("Topic created '%s' hash=%016x sid=%d", name, topic.hash, topic.subject_id)
688
+ _logger.info(
689
+ "Topic created '%s' hash=%016x sid=%d",
690
+ name,
691
+ topic.hash,
692
+ topic.subject_id(self.transport.subject_id_modulus),
693
+ )
666
694
  return topic
667
695
 
668
696
  def topic_allocate(self, topic: TopicImpl, new_evictions: int, now: float) -> None:
669
697
  """Iterative subject-ID allocation with collision resolution. Mirrors topic_allocate() in cy.c."""
670
698
  # Work queue: list of (topic, new_evictions) pairs to process.
699
+ modulus = self.transport.subject_id_modulus
671
700
  work: list[tuple[TopicImpl, int]] = [(topic, new_evictions)]
672
701
  while work:
673
702
  t, ev = work.pop(0)
674
703
  # Remove from subject-ID index first.
675
- old_sid = t.subject_id
704
+ old_sid = t.subject_id(modulus)
676
705
  if old_sid in self.topics_by_subject_id and self.topics_by_subject_id[old_sid] is t:
677
706
  del self.topics_by_subject_id[old_sid]
678
707
 
679
708
  if ev >= EVICTIONS_PINNED_MIN:
680
709
  # Pinned topic: no collision detection, shared subject-IDs are fine.
681
710
  t.release_transport_handles()
682
- t.evictions = ev
711
+ t.set_evictions(ev)
683
712
  t.sync_listener()
684
713
  self.schedule_gossip_urgent(t)
685
714
  continue
686
715
 
687
- modulus = self.transport.subject_id_modulus
688
716
  new_sid = compute_subject_id(t.hash, ev, modulus)
689
717
  collider = self.topics_by_subject_id.get(new_sid)
690
718
 
@@ -694,14 +722,14 @@ class NodeImpl(Node):
694
722
  if collider is None:
695
723
  # No collision, install.
696
724
  t.release_transport_handles()
697
- t.evictions = ev
725
+ t.set_evictions(ev)
698
726
  self.topics_by_subject_id[new_sid] = t
699
727
  t.sync_listener()
700
728
  self.schedule_gossip_urgent(t)
701
729
  elif left_wins(t.lage(now), t.hash, collider.lage(now), collider.hash):
702
730
  # Our topic wins: take the slot, evict the collider.
703
731
  t.release_transport_handles()
704
- t.evictions = ev
732
+ t.set_evictions(ev)
705
733
  del self.topics_by_subject_id[new_sid]
706
734
  self.topics_by_subject_id[new_sid] = t
707
735
  if collider.pub_writer is not None:
@@ -1293,7 +1321,7 @@ class NodeImpl(Node):
1293
1321
  self._notify_monitors(topic)
1294
1322
  else:
1295
1323
  self.on_gossip_unknown(hdr.topic_hash, hdr.topic_evictions, hdr.topic_log_age, ts)
1296
- self._notify_monitors(_TopicFlyweight(hdr.topic_hash, name))
1324
+ self._notify_monitors(_TopicFlyweight(hdr.topic_hash, name, hdr.topic_evictions))
1297
1325
 
1298
1326
  def on_gossip_known(
1299
1327
  self,
@@ -1430,7 +1458,7 @@ class NodeImpl(Node):
1430
1458
  self.decouple_topic_root(topic, topic.couplings[0].root, sync_lifecycle=False)
1431
1459
  self.topics_by_name.pop(name, None)
1432
1460
  self.topics_by_hash.pop(topic.hash, None)
1433
- sid = topic.subject_id
1461
+ sid = topic.subject_id(self.transport.subject_id_modulus)
1434
1462
  if self.topics_by_subject_id.get(sid) is topic:
1435
1463
  del self.topics_by_subject_id[sid]
1436
1464
  topic.associations.clear()
pycyphal2/can/__init__.py CHANGED
@@ -12,7 +12,7 @@ transport = CANTransport.new(SocketCANInterface("can0"))
12
12
  ```
13
13
 
14
14
  Python-CAN is useful when the application runs not on GNU/Linux or already uses `python-can` or needs
15
- `one of its *many* hardware backends <https://python-can.readthedocs.io/en/stable/interfaces.html>`_
15
+ [one of its *many* hardware backends](https://python-can.readthedocs.io/en/stable/interfaces.html)
16
16
  -- GS-USB, SLCAN, PCAN, etc:
17
17
 
18
18
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycyphal2
3
- Version: 2.0.0.dev0
3
+ Version: 2.0.0.dev1
4
4
  Summary: Pure-Python implementation of Cyphal -- a simple and robust real-time publish/subscribe stack that runs anywhere.
5
5
  Author-email: Pavel Kirienko and OpenCyphal team <pavel@opencyphal.org>
6
6
  License: MIT
@@ -38,6 +38,7 @@ _pub/sub without steroids_
38
38
 
39
39
  [![Website](https://img.shields.io/badge/website-opencyphal.org-black?color=1700b3)](https://opencyphal.org/)
40
40
  [![Forum](https://img.shields.io/discourse/https/forum.opencyphal.org/users.svg?logo=discourse&color=1700b3)](https://forum.opencyphal.org)
41
+ [![PyPI](https://img.shields.io/pypi/v/pycyphal2.svg)](https://pypi.org/project/pycyphal2/)
41
42
  [![Docs](https://img.shields.io/badge/Docs-rtfm-black?color=ff00aa&logo=readthedocs)](https://opencyphal.github.io/pycyphal)
42
43
 
43
44
  </div>
@@ -46,12 +47,10 @@ _pub/sub without steroids_
46
47
 
47
48
  Python implementation of the [Cyphal](https://opencyphal.org) stack that runs on GNU/Linux, Windows, and macOS.
48
49
 
49
- Install as follows.
50
- Optional features inside the brackets can be removed if not needed; see `pyproject.toml` for the full list:
51
-
52
- ```
53
- pip install pycyphal2[udp,pythoncan]
54
- ```
50
+ PyCyphal v2 is published on PyPI as `pycyphal2` to enable coexistence with v1 `pycyphal` in the same Python environment.
51
+ The two packages have radically different APIs but are wire-compatible on Cyphal/CAN.
52
+ The maintenance of the original `pycyphal` package will eventually cease;
53
+ existing applications leveraging `pycyphal` should upgrade to the new API of `pycyphal2`.
55
54
 
56
55
  📚 **Read the docs** at <https://opencyphal.github.io/pycyphal>.
57
56
 
@@ -1,22 +1,22 @@
1
- pycyphal2/__init__.py,sha256=iFtaeiu8ETt9j2SISzkABbSIO_nu0VfMOn0YFVyU0wg,3385
2
- pycyphal2/_api.py,sha256=z4vbensO4RJbtCt0og1pd29dQNrFeE5NHku0832lRnU,22363
1
+ pycyphal2/__init__.py,sha256=nxnNW6FRdNG9SkboIR3MvzZMmWi4IWw_iOq_0dSuvuA,6780
2
+ pycyphal2/_api.py,sha256=fYtM9J0RmaEp30iSzebi6a0kLeINPRM0UvvAjevEV6E,22688
3
3
  pycyphal2/_hash.py,sha256=qdQ3A35oFy5mxPNO873OlLntVsCuOak6ckJf8r5j254,10621
4
4
  pycyphal2/_header.py,sha256=12r_jQQ1t3rrNG6si5C3-vNcUSkvYQCBo8YhG16g19Q,10448
5
- pycyphal2/_node.py,sha256=lzfUP0AAEsRyQDz8EgG1rmnzP72i4op2SjMuhtBHOvc,56717
5
+ pycyphal2/_node.py,sha256=8MOV6GoROC55bczN8kcYi19IrdD4UF-U7mUaTAaPCJQ,57586
6
6
  pycyphal2/_publisher.py,sha256=e8ydodDeLTqrgeNnBj4hjz2gWqkGgODmUGER9YgyY10,15868
7
7
  pycyphal2/_subscriber.py,sha256=_BOeyjzSJel58GYRlfud0qpilFuaZEgBzBCGRFf4NtI,15721
8
8
  pycyphal2/_transport.py,sha256=C0-hMukqJ_wUbNkXMBpMSp78Rf9FE7rcBfZpLDMkNF4,3492
9
9
  pycyphal2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  pycyphal2/udp.py,sha256=xGrXQZQf-Tqo6em2rBxUTn373HYhaqb0B5nsf0b6SLQ,42389
11
- pycyphal2/can/__init__.py,sha256=n3zuGYPexGQSZdlNulNQyYk8WphmoQhmlIeEBn5QMeQ,1555
11
+ pycyphal2/can/__init__.py,sha256=db9Miv0bKV591WiajAVijef2x7J5Y9-gu0G2mm7hHU4,1553
12
12
  pycyphal2/can/_interface.py,sha256=u2YmS-fUkThE75IRKmF_VoA_YCTMM4ADkfMAi25cOQY,4369
13
13
  pycyphal2/can/_reassembly.py,sha256=tDh0LubxANkOVIRCHG6zbABJEQD7WegxQS-UPqg__6E,5682
14
14
  pycyphal2/can/_transport.py,sha256=sWNoyUnVJnUuhQ8pEtzAkfJSuqvI0RzMVVDKA5j1NOk,21036
15
15
  pycyphal2/can/_wire.py,sha256=OSStXci7qzwnbz0YZ0H07rNU5bdi5fCLRNKVZmDOtdw,13696
16
16
  pycyphal2/can/pythoncan.py,sha256=-zgFhoKDvihlRpVZg3OV12ClBTsf5MNLf1eD9_HxDus,10845
17
17
  pycyphal2/can/socketcan.py,sha256=nBYzbAcoVXeGoRrovpJnCbTqs4MBabpt27QJw4JqUYc,8529
18
- pycyphal2-2.0.0.dev0.dist-info/licenses/LICENSE,sha256=ILoAsB6eavnHqYkHMZt5JIp4-2oIMglw7vQr2aizB8w,1101
19
- pycyphal2-2.0.0.dev0.dist-info/METADATA,sha256=e9IVFZLWa--8ZuOCsGNTaTtC1r4Y3-ONUeIGPe563V0,2313
20
- pycyphal2-2.0.0.dev0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
- pycyphal2-2.0.0.dev0.dist-info/top_level.txt,sha256=m3-ZKFH4OwPKxa94jzEP4OCKx7B7qnONx3GYXgaw8BU,10
22
- pycyphal2-2.0.0.dev0.dist-info/RECORD,,
18
+ pycyphal2-2.0.0.dev1.dist-info/licenses/LICENSE,sha256=ILoAsB6eavnHqYkHMZt5JIp4-2oIMglw7vQr2aizB8w,1101
19
+ pycyphal2-2.0.0.dev1.dist-info/METADATA,sha256=QoXOiDmVMmg9p4swuXqlFSpeBSAApWNC_HmenAI7xok,2602
20
+ pycyphal2-2.0.0.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ pycyphal2-2.0.0.dev1.dist-info/top_level.txt,sha256=m3-ZKFH4OwPKxa94jzEP4OCKx7B7qnONx3GYXgaw8BU,10
22
+ pycyphal2-2.0.0.dev1.dist-info/RECORD,,