pycyphal2 2.0.0.dev5__tar.gz → 2.0.0.dev6__tar.gz

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.
Files changed (45) hide show
  1. {pycyphal2-2.0.0.dev5/src/pycyphal2.egg-info → pycyphal2-2.0.0.dev6}/PKG-INFO +1 -1
  2. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/__init__.py +1 -1
  3. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_api.py +4 -5
  4. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_hash.py +0 -13
  5. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_header.py +0 -34
  6. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_node.py +2 -82
  7. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_publisher.py +0 -6
  8. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_subscriber.py +0 -17
  9. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/_transport.py +5 -0
  10. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/_interface.py +19 -4
  11. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/_media_slcan.py +1 -2
  12. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/_transport.py +16 -7
  13. pycyphal2-2.0.0.dev6/src/pycyphal2/can/_tx.py +125 -0
  14. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/pythoncan.py +34 -33
  15. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/socketcan.py +28 -31
  16. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/webserial.py +36 -27
  17. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/udp.py +2 -68
  18. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6/src/pycyphal2.egg-info}/PKG-INFO +1 -1
  19. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2.egg-info/SOURCES.txt +1 -0
  20. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_gossip.py +84 -50
  21. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_hash.py +0 -2
  22. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_header.py +0 -34
  23. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_integration.py +7 -35
  24. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_monitor.py +3 -5
  25. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_names.py +11 -84
  26. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_parity.py +24 -115
  27. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_pubsub.py +16 -96
  28. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_reliable.py +3 -103
  29. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_reorder.py +1 -4
  30. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_rpc.py +3 -18
  31. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_scout.py +0 -2
  32. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_topic.py +18 -78
  33. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_udp.py +3 -100
  34. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/LICENSE +0 -0
  35. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/README.md +0 -0
  36. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/pyproject.toml +0 -0
  37. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/setup.cfg +0 -0
  38. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/__init__.py +0 -0
  39. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/_reassembly.py +0 -0
  40. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/can/_wire.py +0 -0
  41. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2/py.typed +0 -0
  42. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2.egg-info/dependency_links.txt +0 -0
  43. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2.egg-info/requires.txt +0 -0
  44. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/src/pycyphal2.egg-info/top_level.txt +0 -0
  45. {pycyphal2-2.0.0.dev5 → pycyphal2-2.0.0.dev6}/tests/test_parity_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycyphal2
3
- Version: 2.0.0.dev5
3
+ Version: 2.0.0.dev6
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
@@ -155,7 +155,7 @@ from ._transport import SubjectWriter as SubjectWriter
155
155
  from ._transport import Transport as Transport
156
156
  from ._transport import TransportArrival as TransportArrival
157
157
 
158
- __version__ = "2.0.0.dev5"
158
+ __version__ = "2.0.0.dev6"
159
159
 
160
160
  # pdoc needs __all__ to display re-exported members.
161
161
  __all__ = [
@@ -219,7 +219,7 @@ class Publisher(Closable, ABC):
219
219
  Represents the intent to send messages on a topic.
220
220
 
221
221
  Calling the publisher sends one message.
222
- By default this is best-effort publication: the message is sent once and only immediate send failures are reported.
222
+ By default this is best-effort publication: the message is sent once and delivery is not confirmed.
223
223
  With ``reliable=True``, the library retransmits until the deadline and waits for acknowledgments from remote
224
224
  subscribers.
225
225
 
@@ -262,6 +262,9 @@ class Publisher(Closable, ABC):
262
262
  Blocks at most until ``deadline``.
263
263
  Raises :class:`SendError` if the message could not be sent before the deadline.
264
264
 
265
+ Returning means the message has been handed over to the network, not that anyone received it.
266
+ The call waits for that handover, so a congested network applies backpressure to the publisher.
267
+
265
268
  If ``reliable`` is false, the message is sent once.
266
269
  If ``reliable`` is true, the library retransmits until ``deadline`` leveraging :attr:`ack_timeout`.
267
270
  """
@@ -552,21 +555,17 @@ class Node(Closable, ABC):
552
555
  """
553
556
  from ._node import NodeImpl
554
557
 
555
- # Add random suffix if requested or generate pure random home.
556
558
  # Leading/trailing separators will be normalized away.
557
559
  home = home.strip() or "/"
558
560
  if home.endswith("/"):
559
561
  uid = transport.uid if hasattr(transport, "uid") else eui64()
560
562
  home += f"{uid:016x}"
561
563
 
562
- # Initialize the namespace: if not given explicitly, read it from the standard environment.
563
564
  namespace = namespace.strip() or os.getenv("CYPHAL_NAMESPACE", "").strip()
564
565
 
565
- # Construct the node.
566
566
  node = NodeImpl(transport, home=home, namespace=namespace)
567
567
  _logger.info("Constructed %s", node)
568
568
 
569
- # Set up default name remapping.
570
569
  try:
571
570
  node.remap(os.getenv("CYPHAL_REMAP", ""))
572
571
  except Exception as ex:
@@ -1,11 +1,5 @@
1
- """Hash and CRC utilities"""
2
-
3
1
  from __future__ import annotations
4
2
 
5
- # =====================================================================================================================
6
- # CRC-32C (Castagnoli)
7
- # =====================================================================================================================
8
-
9
3
  CRC32C_INITIAL = 0xFFFFFFFF
10
4
  CRC32C_OUTPUT_XOR = 0xFFFFFFFF
11
5
  CRC32C_RESIDUE = 0x48674BC7
@@ -59,10 +53,6 @@ def crc32c_full(data: bytes | memoryview) -> int:
59
53
  return crc32c_add(CRC32C_INITIAL, data) ^ CRC32C_OUTPUT_XOR
60
54
 
61
55
 
62
- # =====================================================================================================================
63
- # CRC-16/CCITT-FALSE
64
- # =====================================================================================================================
65
-
66
56
  CRC16CCITT_FALSE_INITIAL = 0xFFFF
67
57
  CRC16CCITT_FALSE_RESIDUE = 0x0000
68
58
  # fmt: off
@@ -103,10 +93,7 @@ def crc16ccitt_false_full(data: bytes | memoryview) -> int:
103
93
  return crc16ccitt_false_add(CRC16CCITT_FALSE_INITIAL, data)
104
94
 
105
95
 
106
- # =====================================================================================================================
107
96
  # rapidhash V3
108
- # =====================================================================================================================
109
-
110
97
  _RAPID_MASK = 0xFFFFFFFFFFFFFFFF
111
98
  _RAPID_SECRET = (
112
99
  0x2D358DCCAA6C78A5,
@@ -11,11 +11,6 @@ LAGE_MIN = -1
11
11
  LAGE_MAX = 35
12
12
 
13
13
 
14
- # =====================================================================================================================
15
- # MSG headers
16
- # =====================================================================================================================
17
-
18
-
19
14
  @dataclass(frozen=True)
20
15
  class MsgBeHeader:
21
16
  TYPE = 0
@@ -76,11 +71,6 @@ def _deserialize_msg(buf: bytes | memoryview) -> tuple[int, int, int, int] | Non
76
71
  return (lage, evictions, topic_hash, tag)
77
72
 
78
73
 
79
- # =====================================================================================================================
80
- # MSG ACK/NACK headers
81
- # =====================================================================================================================
82
-
83
-
84
74
  @dataclass(frozen=True)
85
75
  class MsgAckHeader:
86
76
  TYPE = 2
@@ -131,11 +121,6 @@ def _deserialize_msg_ack(buf: bytes | memoryview) -> tuple[int, int] | None:
131
121
  return (topic_hash, tag)
132
122
 
133
123
 
134
- # =====================================================================================================================
135
- # RSP headers (responses)
136
- # =====================================================================================================================
137
-
138
-
139
124
  @dataclass(frozen=True)
140
125
  class RspBeHeader:
141
126
  TYPE = 4
@@ -172,11 +157,6 @@ class RspRelHeader:
172
157
  return RspRelHeader(*r) if r is not None else None
173
158
 
174
159
 
175
- # =====================================================================================================================
176
- # RSP ACK/NACK headers
177
- # =====================================================================================================================
178
-
179
-
180
160
  @dataclass(frozen=True)
181
161
  class RspAckHeader:
182
162
  TYPE = 6
@@ -237,11 +217,6 @@ def _deserialize_rsp(buf: bytes | memoryview) -> tuple[int, int, int, int] | Non
237
217
  return (tag, seqno, topic_hash, message_tag)
238
218
 
239
219
 
240
- # =====================================================================================================================
241
- # GOSSIP header
242
- # =====================================================================================================================
243
-
244
-
245
220
  @dataclass(frozen=True)
246
221
  class GossipHeader:
247
222
  TYPE = 8
@@ -275,11 +250,6 @@ class GossipHeader:
275
250
  return GossipHeader(lage, topic_hash, evictions, name_len)
276
251
 
277
252
 
278
- # =====================================================================================================================
279
- # SCOUT header
280
- # =====================================================================================================================
281
-
282
-
283
253
  @dataclass(frozen=True)
284
254
  class ScoutHeader:
285
255
  TYPE = 9
@@ -303,10 +273,6 @@ class ScoutHeader:
303
273
  return ScoutHeader(buf[23])
304
274
 
305
275
 
306
- # =====================================================================================================================
307
- # Dispatcher
308
- # =====================================================================================================================
309
-
310
276
  HeaderType = (
311
277
  MsgBeHeader
312
278
  | MsgRelHeader
@@ -38,10 +38,6 @@ if TYPE_CHECKING:
38
38
 
39
39
  _logger = logging.getLogger(__name__)
40
40
 
41
- # =====================================================================================================================
42
- # Constants
43
- # =====================================================================================================================
44
-
45
41
  TOPIC_NAME_MAX = 200
46
42
  EVICTIONS_PINNED_MIN = 0xFFFFE000
47
43
  GOSSIP_PERIOD = 5.0
@@ -82,11 +78,6 @@ class GossipScope(Enum):
82
78
  INLINE = auto()
83
79
 
84
80
 
85
- # =====================================================================================================================
86
- # Name Resolution
87
- # =====================================================================================================================
88
-
89
-
90
81
  def _name_normalize(name: str) -> str:
91
82
  """Collapse separators, strip leading/trailing separators."""
92
83
  parts: list[str] = []
@@ -120,7 +111,6 @@ def _name_consume_pin_suffix(name: str) -> tuple[str, int | None]:
120
111
 
121
112
 
122
113
  def _name_join(left: str, right: str) -> str:
123
- """Join two name parts with separator, normalizing the result."""
124
114
  left = _name_normalize(left)
125
115
  right = _name_normalize(right)
126
116
  if left and right:
@@ -163,7 +153,6 @@ def resolve_name(
163
153
  if not name:
164
154
  raise ValueError("Empty name")
165
155
 
166
- # Strip pin suffix first.
167
156
  name, pin = _name_consume_pin_suffix(name)
168
157
 
169
158
  # Apply remapping: lookup on normalized pin-free name; matched rule replaces both name and pin.
@@ -173,7 +162,6 @@ def resolve_name(
173
162
  name = remaps[lookup]
174
163
  name, pin = _name_consume_pin_suffix(name)
175
164
 
176
- # Classify and construct.
177
165
  if name.startswith("/"):
178
166
  resolved = _name_normalize(name)
179
167
  elif _name_is_homeful(name):
@@ -191,7 +179,6 @@ def resolve_name(
191
179
  raise ValueError("Name resolves to empty string")
192
180
  if len(resolved) > TOPIC_NAME_MAX:
193
181
  raise ValueError(f"Resolved name exceeds {TOPIC_NAME_MAX} characters")
194
- # Validate characters: ASCII 33-126 and '/' only.
195
182
  for ch in resolved:
196
183
  o = ord(ch)
197
184
  if o < 33 or o > 126:
@@ -203,11 +190,6 @@ def resolve_name(
203
190
  return resolved, pin, verbatim
204
191
 
205
192
 
206
- # =====================================================================================================================
207
- # Pattern Matching
208
- # =====================================================================================================================
209
-
210
-
211
193
  def match_pattern(pattern: str, name: str) -> list[tuple[str, int]] | None:
212
194
  """
213
195
  Match a pattern against a topic name.
@@ -237,13 +219,7 @@ def match_pattern(pattern: str, name: str) -> list[tuple[str, int]] | None:
237
219
  return subs
238
220
 
239
221
 
240
- # =====================================================================================================================
241
- # Subject-ID Computation
242
- # =====================================================================================================================
243
-
244
-
245
222
  def compute_subject_id(topic_hash: int, evictions: int, modulus: int) -> int:
246
- """Compute the subject-ID for a topic given its hash, evictions, and subject-ID modulus."""
247
223
  if evictions >= EVICTIONS_PINNED_MIN:
248
224
  return 0xFFFFFFFF - evictions
249
225
  h = topic_hash % modulus
@@ -251,11 +227,6 @@ def compute_subject_id(topic_hash: int, evictions: int, modulus: int) -> int:
251
227
  return SUBJECT_ID_PINNED_MAX + 1 + ((h + ((e * e) % modulus)) % modulus)
252
228
 
253
229
 
254
- # =====================================================================================================================
255
- # Internal Data Structures
256
- # =====================================================================================================================
257
-
258
-
259
230
  @dataclass
260
231
  class Association:
261
232
  """Tracks a known remote subscriber for reliable delivery ACK tracking."""
@@ -396,11 +367,6 @@ class PublishTracker:
396
367
  self.ack_event.set()
397
368
 
398
369
 
399
- # =====================================================================================================================
400
- # Topic
401
- # =====================================================================================================================
402
-
403
-
404
370
  class TopicImpl(Topic):
405
371
 
406
372
  def __init__(self, node: NodeImpl, name: str, evictions: int, now: float) -> None:
@@ -426,7 +392,6 @@ class TopicImpl(Topic):
426
392
  self.gossip_task_is_periodic = False
427
393
  self.gossip_counter = 0
428
394
 
429
- # -- Topic ABC --
430
395
  @property
431
396
  def hash(self) -> int:
432
397
  return self._topic_hash
@@ -448,7 +413,6 @@ class TopicImpl(Topic):
448
413
  def match(self, pattern: str) -> list[tuple[str, int]] | None:
449
414
  return match_pattern(pattern, self._name)
450
415
 
451
- # -- Internal --
452
416
  def lage(self, now: float) -> int:
453
417
  return log_age(self.ts_origin, now)
454
418
 
@@ -529,11 +493,6 @@ def left_wins(l_lage: int, l_hash: int, r_lage: int, r_hash: int) -> bool:
529
493
  return l_lage > r_lage if l_lage != r_lage else l_hash < r_hash
530
494
 
531
495
 
532
- # =====================================================================================================================
533
- # Node
534
- # =====================================================================================================================
535
-
536
-
537
496
  class NodeImpl(Node):
538
497
  def __init__(self, transport: Transport, *, home: str, namespace: str) -> None:
539
498
  self._transport = transport
@@ -545,26 +504,21 @@ class NodeImpl(Node):
545
504
  self._monitor_callbacks: dict[int, Callable[[Topic], None]] = {}
546
505
  self._next_monitor_callback_id = 0
547
506
 
548
- # Topic indexes.
549
507
  self.topics_by_name: dict[str, TopicImpl] = {}
550
508
  self.topics_by_hash: dict[int, TopicImpl] = {}
551
509
  self.topics_by_subject_id: dict[int, TopicImpl] = {} # non-pinned only
552
510
 
553
- # Subscriber roots.
554
511
  self.sub_roots_verbatim: dict[str, SubscriberRoot] = {}
555
512
  self.sub_roots_pattern: dict[str, SubscriberRoot] = {}
556
513
 
557
- # Respond futures for reliable responses.
558
514
  self.respond_futures: dict[tuple[int, ...], RespondTracker] = {}
559
515
 
560
- # Compute broadcast and gossip shard subject IDs.
561
516
  modulus = transport.subject_id_modulus
562
517
  sid_max = SUBJECT_ID_PINNED_MAX + modulus
563
518
  self.broadcast_subject_id = (1 << (int(math.log2(sid_max)) + 1)) - 1
564
519
  self.gossip_shard_count = self.broadcast_subject_id - (sid_max + 1)
565
520
  assert self.gossip_shard_count > 0
566
521
 
567
- # Set up broadcast writer and listener.
568
522
  self.broadcast_writer = transport.subject_advertise(self.broadcast_subject_id)
569
523
 
570
524
  def broadcast_handler(arrival: TransportArrival) -> None:
@@ -572,16 +526,13 @@ class NodeImpl(Node):
572
526
 
573
527
  self.broadcast_listener = transport.subject_listen(self.broadcast_subject_id, broadcast_handler)
574
528
 
575
- # Gossip shard state: lazily created per shard.
576
529
  self.gossip_shard_writers: dict[int, SubjectWriter] = {}
577
530
  self.gossip_shard_listeners: dict[int, Closable] = {}
578
531
  self.shared_subject_writers: dict[int, SharedSubjectWriter] = {}
579
532
  self.shared_subject_listeners: dict[int, SharedSubjectListener] = {}
580
533
 
581
- # Register unicast handler.
582
534
  transport.unicast_listen(self.on_unicast_arrival)
583
535
 
584
- # Implicit topic GC task, driven by the earliest implicit-topic expiry.
585
536
  self._implicit_topics: OrderedDict[TopicImpl, None] = OrderedDict()
586
537
  self._implicit_gc_wakeup = asyncio.Event()
587
538
  self._gc_task = self.loop.create_task(self.implicit_gc_loop())
@@ -594,7 +545,6 @@ class NodeImpl(Node):
594
545
  self.gossip_shard_count,
595
546
  )
596
547
 
597
- # -- Node ABC --
598
548
  @property
599
549
  def home(self) -> str:
600
550
  return self._home
@@ -666,7 +616,6 @@ class NodeImpl(Node):
666
616
  if pin is not None and not verbatim:
667
617
  raise ValueError("Pattern names cannot be pinned")
668
618
 
669
- # Ensure subscriber root.
670
619
  if verbatim:
671
620
  root = self.sub_roots_verbatim.get(resolved)
672
621
  if root is None:
@@ -682,12 +631,10 @@ class NodeImpl(Node):
682
631
  root.subscribers.append(subscriber)
683
632
 
684
633
  if verbatim:
685
- # Ensure topic exists and couple.
686
634
  topic = self.topic_ensure(resolved, pin)
687
635
  self.couple_topic_root(topic, root)
688
636
  topic.sync_implicit()
689
637
  else:
690
- # Pattern subscriber: couple with all existing matching topics and scout once per root.
691
638
  for topic in list(self.topics_by_name.values()):
692
639
  self.couple_topic_root(topic, root)
693
640
  topic.sync_implicit()
@@ -725,10 +672,7 @@ class NodeImpl(Node):
725
672
  except Exception as ex:
726
673
  raise SendError(f"Scout send failed for '{resolved}'") from ex
727
674
 
728
- # -- Topic Management --
729
-
730
675
  def topic_ensure(self, name: str, pin: int | None) -> TopicImpl:
731
- """Get or create a topic by resolved name."""
732
676
  topic = self.topics_by_name.get(name)
733
677
  if topic is not None:
734
678
  return topic
@@ -742,7 +686,6 @@ class NodeImpl(Node):
742
686
  self.ensure_gossip_shard(self.gossip_shard_subject_id(topic.hash))
743
687
  self.touch_implicit_topic(topic)
744
688
  self.topic_allocate(topic, evictions, now)
745
- # Couple with existing pattern subscriber roots.
746
689
  for root in self.sub_roots_pattern.values():
747
690
  self.couple_topic_root(topic, root)
748
691
  topic.sync_listener()
@@ -757,12 +700,10 @@ class NodeImpl(Node):
757
700
 
758
701
  def topic_allocate(self, topic: TopicImpl, new_evictions: int, now: float) -> None:
759
702
  """Iterative subject-ID allocation with collision resolution. Mirrors topic_allocate() in cy.c."""
760
- # Work queue: list of (topic, new_evictions) pairs to process.
761
703
  modulus = self.transport.subject_id_modulus
762
704
  work: list[tuple[TopicImpl, int]] = [(topic, new_evictions)]
763
705
  while work:
764
706
  t, ev = work.pop(0)
765
- # Remove from subject-ID index first.
766
707
  old_sid = t.subject_id(modulus)
767
708
  if old_sid in self.topics_by_subject_id and self.topics_by_subject_id[old_sid] is t:
768
709
  del self.topics_by_subject_id[old_sid]
@@ -782,14 +723,12 @@ class NodeImpl(Node):
782
723
  collider = None # same topic, no real collision
783
724
 
784
725
  if collider is None:
785
- # No collision, install.
786
726
  t.release_transport_handles()
787
727
  t.set_evictions(ev)
788
728
  self.topics_by_subject_id[new_sid] = t
789
729
  t.sync_listener()
790
730
  self.schedule_gossip_urgent(t)
791
731
  elif left_wins(t.lage(now), t.hash, collider.lage(now), collider.hash):
792
- # Our topic wins: take the slot, evict the collider.
793
732
  t.release_transport_handles()
794
733
  t.set_evictions(ev)
795
734
  del self.topics_by_subject_id[new_sid]
@@ -798,11 +737,9 @@ class NodeImpl(Node):
798
737
  t.pub_writer = self.acquire_subject_writer(t, new_sid)
799
738
  t.sync_listener()
800
739
  self.schedule_gossip_urgent(t)
801
- # Schedule collider for reallocation.
802
740
  collider.release_transport_handles()
803
741
  work.append((collider, collider.evictions + 1))
804
742
  else:
805
- # Our topic loses: increment evictions and retry.
806
743
  work.append((t, ev + 1))
807
744
 
808
745
  def sync_topic_lifecycle(self, topic: TopicImpl) -> None:
@@ -877,17 +814,14 @@ class NodeImpl(Node):
877
814
 
878
815
  @staticmethod
879
816
  def couple_topic_root(topic: TopicImpl, root: SubscriberRoot) -> None:
880
- """Create a coupling between a topic and a subscriber root if not already coupled."""
881
817
  for c in topic.couplings:
882
818
  if c.root is root:
883
- return # already coupled
819
+ return
884
820
  subs = match_pattern(root.name, topic.name) if root.is_pattern else ([] if root.name == topic.name else None)
885
821
  if subs is not None:
886
822
  topic.couplings.append(Coupling(root=root, substitutions=subs))
887
823
  _logger.debug("Coupled '%s' <-> root '%s'", topic.name, root.name)
888
824
 
889
- # -- Gossip --
890
-
891
825
  def gossip_shard_subject_id(self, topic_hash: int) -> int:
892
826
  modulus = self.transport.subject_id_modulus
893
827
  sid_max = SUBJECT_ID_PINNED_MAX + modulus
@@ -950,9 +884,8 @@ class NodeImpl(Node):
950
884
  _logger.debug("Shared subject listener released sid=%d", subject_id)
951
885
 
952
886
  def schedule_gossip(self, topic: TopicImpl) -> None:
953
- """Start periodic gossip for an explicit topic."""
954
887
  if topic.gossip_task is not None:
955
- return # already scheduled
888
+ return
956
889
  self._reschedule_gossip_periodic(topic, suppressed=False)
957
890
 
958
891
  @staticmethod
@@ -1066,8 +999,6 @@ class NodeImpl(Node):
1066
999
  except (SendError, OSError) as e:
1067
1000
  _logger.warning("Gossip unicast send failed for '%s': %s", topic.name, e)
1068
1001
 
1069
- # -- Scout --
1070
-
1071
1002
  async def _transmit_scout(self, pattern: str) -> None:
1072
1003
  pattern_bytes = pattern.encode("utf-8")
1073
1004
  hdr = ScoutHeader(pattern_len=len(pattern_bytes))
@@ -1096,14 +1027,10 @@ class NodeImpl(Node):
1096
1027
 
1097
1028
  root.scout_task = self.loop.create_task(do_send())
1098
1029
 
1099
- # -- Message Dispatch --
1100
-
1101
1030
  def on_subject_arrival(self, subject_id: int, arrival: TransportArrival) -> None:
1102
- """Handle an arrival on a subject (multicast)."""
1103
1031
  self.dispatch_arrival(arrival, subject_id=subject_id, unicast=False)
1104
1032
 
1105
1033
  def on_unicast_arrival(self, arrival: TransportArrival) -> None:
1106
- """Handle an arrival via unicast."""
1107
1034
  self.dispatch_arrival(arrival, subject_id=None, unicast=True)
1108
1035
 
1109
1036
  def dispatch_arrival(self, arrival: TransportArrival, *, subject_id: int | None, unicast: bool) -> None:
@@ -1293,7 +1220,6 @@ class NodeImpl(Node):
1293
1220
  tracker.on_ack(remote_id, positive)
1294
1221
 
1295
1222
  def on_rsp(self, arrival: TransportArrival, hdr: RspBeHeader | RspRelHeader, payload: bytes) -> None:
1296
- """Handle a response message (for RPC)."""
1297
1223
  ack = False
1298
1224
  topic = self.topics_by_hash.get(hdr.topic_hash)
1299
1225
  if topic is not None:
@@ -1317,7 +1243,6 @@ class NodeImpl(Node):
1317
1243
  )
1318
1244
 
1319
1245
  def on_rsp_ack(self, arrival: TransportArrival, hdr: RspAckHeader | RspNackHeader) -> None:
1320
- """Handle a response ACK/NACK."""
1321
1246
  key = (arrival.remote_id, hdr.message_tag, hdr.topic_hash, hdr.seqno, hdr.tag)
1322
1247
  future = self.respond_futures.get(key)
1323
1248
  if future is not None:
@@ -1366,7 +1291,6 @@ class NodeImpl(Node):
1366
1291
 
1367
1292
  topic = self.topics_by_hash.get(hdr.topic_hash)
1368
1293
 
1369
- # If unknown topic with a name, check for pattern subscriber matches.
1370
1294
  if topic is None and name:
1371
1295
  if scope in {GossipScope.UNICAST, GossipScope.BROADCAST}:
1372
1296
  topic = self.topic_subscribe_if_matching(
@@ -1472,8 +1396,6 @@ class NodeImpl(Node):
1472
1396
  self.send_gossip_unicast(topic, arrival.remote_id, arrival.priority), "gossip unicast"
1473
1397
  )
1474
1398
 
1475
- # -- Implicit Topic GC --
1476
-
1477
1399
  def notify_implicit_gc(self) -> None:
1478
1400
  if not self._closed:
1479
1401
  self._implicit_gc_wakeup.set()
@@ -1534,8 +1456,6 @@ class NodeImpl(Node):
1534
1456
  self.notify_implicit_gc()
1535
1457
  _logger.info("Topic destroyed '%s'", name)
1536
1458
 
1537
- # -- Cleanup --
1538
-
1539
1459
  def close(self) -> None:
1540
1460
  if self._closed:
1541
1461
  return
@@ -297,11 +297,6 @@ class PublisherImpl(Publisher):
297
297
  _logger.info("Publisher closed for '%s'", self._topic.name)
298
298
 
299
299
 
300
- # =====================================================================================================================
301
- # Response Stream
302
- # =====================================================================================================================
303
-
304
-
305
300
  class ResponseStreamImpl(ResponseStream):
306
301
  def __init__(
307
302
  self,
@@ -367,7 +362,6 @@ class ResponseStreamImpl(ResponseStream):
367
362
  hdr: RspBeHeader | RspRelHeader,
368
363
  payload: bytes,
369
364
  ) -> bool:
370
- """Called by the node when a response arrives matching our message_tag."""
371
365
  reliable = isinstance(hdr, RspRelHeader)
372
366
  if self.closed:
373
367
  if not reliable:
@@ -23,11 +23,6 @@ _logger = logging.getLogger(__name__)
23
23
  REORDERING_WINDOW_MAX = SESSION_LIFETIME / 2
24
24
 
25
25
 
26
- # =====================================================================================================================
27
- # Reordering
28
- # =====================================================================================================================
29
-
30
-
31
26
  @dataclass
32
27
  class InternedMsg:
33
28
  arrival: Arrival
@@ -114,13 +109,11 @@ class SubscriberImpl(Subscriber):
114
109
  return item
115
110
 
116
111
  def deliver(self, arrival: Arrival, tag: int, remote_id: int) -> bool:
117
- """Called by the node to deliver a message to this subscriber."""
118
112
  if self.closed:
119
113
  return False
120
114
  if self._reordering_window is None:
121
115
  self.queue.put_nowait(arrival)
122
116
  return True
123
- # Reordering enabled.
124
117
  self._drop_stale_reordering(arrival.timestamp.s)
125
118
  topic_hash = arrival.breadcrumb.topic.hash
126
119
  key = (remote_id, topic_hash)
@@ -148,7 +141,6 @@ class SubscriberImpl(Subscriber):
148
141
 
149
142
  expected = state.last_ejected_lin_tag + 1
150
143
  if lin_tag == expected:
151
- # In-order: eject immediately and scan for consecutive.
152
144
  self.queue.put_nowait(arrival)
153
145
  state.last_ejected_lin_tag = lin_tag
154
146
  self._scan_reordering(state, force_first=False)
@@ -160,7 +152,6 @@ class SubscriberImpl(Subscriber):
160
152
  lin_tag = (tag - state.tag_baseline) & ((1 << 64) - 1)
161
153
  _logger.debug("Reorder resequence tag=%d lin=%d", tag, lin_tag)
162
154
 
163
- # Out-of-order but within capacity: intern.
164
155
  if lin_tag in state.interned:
165
156
  return True
166
157
  state.interned[lin_tag] = InternedMsg(arrival=arrival, tag=tag, remote_id=remote_id, lin_tag=lin_tag)
@@ -187,7 +178,6 @@ class SubscriberImpl(Subscriber):
187
178
  break
188
179
 
189
180
  def _force_eject_all(self, state: ReorderingState, *, silenced: bool = False) -> None:
190
- """Force-eject all interned messages in tag order."""
191
181
  while state.interned:
192
182
  lin_tag = min(state.interned)
193
183
  interned = state.interned.pop(lin_tag)
@@ -199,7 +189,6 @@ class SubscriberImpl(Subscriber):
199
189
  state.timeout_handle = None
200
190
 
201
191
  def _rearm_reorder_timeout(self, state: ReorderingState) -> None:
202
- """Arm or rearm the reordering timeout against the current head-of-line slot."""
203
192
  if self._reordering_window is None:
204
193
  return
205
194
  if not state.interned:
@@ -256,11 +245,6 @@ class SubscriberImpl(Subscriber):
256
245
  _logger.info("Subscriber closed for '%s'", self._pattern)
257
246
 
258
247
 
259
- # =====================================================================================================================
260
- # Breadcrumb
261
- # =====================================================================================================================
262
-
263
-
264
248
  class BreadcrumbImpl(Breadcrumb):
265
249
  def __init__(
266
250
  self,
@@ -322,7 +306,6 @@ class BreadcrumbImpl(Breadcrumb):
322
306
  _logger.debug("Response BE sent seqno=%d to %016x", seqno, self._remote_id)
323
307
  return
324
308
 
325
- # Reliable response with retransmission.
326
309
  tracker = RespondTracker(
327
310
  remote_id=self._remote_id,
328
311
  message_tag=self._message_tag,
@@ -20,6 +20,11 @@ SUBJECT_ID_MODULUS_32bit = 4294954663 # Incompatible with Cyphal/CAN and Cyphal
20
20
  class SubjectWriter(Closable):
21
21
  @abstractmethod
22
22
  async def __call__(self, deadline: Instant, priority: Priority, message: bytes | memoryview) -> None:
23
+ """
24
+ Send one message on the subject. Returning means every frame of the transfer has been handed over to the
25
+ OS or the driver on at least one interface; implementations that queue internally must await that handover.
26
+ Raises :class:`SendError` if the message could not be handed over before the deadline.
27
+ """
23
28
  raise NotImplementedError
24
29
 
25
30