zerodds-wire 1.0.0rc7__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.
@@ -0,0 +1,107 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 ZeroDDS Contributors
3
+ """`zerodds_wire` — the Python wire runtime.
4
+
5
+ The runtime a generated Python encoder calls: `Writer` and `Reader`, the
6
+ alignment rules, and the framing forms. It is the counterpart of
7
+ `crates/wire`, not a binding to it — a Python probe that reached the Rust
8
+ core through ctypes would be byte-identical by construction and would
9
+ prove nothing about whether the two read the specification the same way.
10
+
11
+ This package makes no decision about a type. Which members are written,
12
+ in what order, with which header and which length code is the layout
13
+ plan's answer and arrives here as arguments. A runtime that decided any
14
+ of it would be a second place where the wire format lives.
15
+
16
+ Spec: DDS-XTypes 1.3 §7.4, DDSI-RTPS 2.5 §9.6.3.8 and §10.5.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from .encapsulation import Encoding, encapsulation_header, encapsulation_padding
22
+ from .endianness import Endianness
23
+ from .error import ImpossibleLength, NotUtf8, OutOfRange, Truncated, WireError
24
+ from .float128 import Float128, Inexact
25
+ from .reader import EmHeaderRead, PidRead, Reader, ReaderMark
26
+ # `utf8_octets` and `utf16_units` are exported because a generated codec
27
+ # names them: the shared walk emits both through
28
+ # `CodecTarget::runtime_static` where a bounded `string` or `wstring` is
29
+ # measured against its bound (§7.3.1.2.1.12.3). Without them here a
30
+ # generated module needed a second import line reaching into a submodule,
31
+ # which is a path the package's shape does not otherwise ask anyone to
32
+ # know.
33
+ from .typesupport import Discarded, Status, TryConstruct, utf8_octets, utf16_units
34
+ from .values import Mismatch, Missing, SampleError, Unknown
35
+ from .writer import (
36
+ KEY_HASH_LEN,
37
+ PID_EXTENDED,
38
+ PID_IGNORE,
39
+ PID_IMPL_EXTENSION,
40
+ PID_LIST_END,
41
+ PID_MUST_UNDERSTAND,
42
+ PID_SHORT_MAX_ID,
43
+ PID_SHORT_MAX_LEN,
44
+ DHeader,
45
+ EmHeader,
46
+ ParameterRole,
47
+ Pid,
48
+ PlCdr1Framing,
49
+ pid_flags_at,
50
+ bit_pattern_bool,
51
+ bit_pattern_f32,
52
+ bit_pattern_f64,
53
+ bit_pattern_int,
54
+ no_dheader,
55
+ Writer,
56
+ wstring_unit_width,
57
+ )
58
+
59
+ __all__ = [
60
+ "DHeader",
61
+ "Float128",
62
+ "Inexact",
63
+ "EmHeader",
64
+ "EmHeaderRead",
65
+ "Encoding",
66
+ "Endianness",
67
+ "ImpossibleLength",
68
+ "KEY_HASH_LEN",
69
+ "Discarded",
70
+ "Mismatch",
71
+ "Missing",
72
+ "NotUtf8",
73
+ "OutOfRange",
74
+ "PID_EXTENDED",
75
+ "PID_IGNORE",
76
+ "PID_IMPL_EXTENSION",
77
+ "PID_LIST_END",
78
+ "PID_MUST_UNDERSTAND",
79
+ "PID_SHORT_MAX_ID",
80
+ "PID_SHORT_MAX_LEN",
81
+ "ParameterRole",
82
+ "Pid",
83
+ "PlCdr1Framing",
84
+ "pid_flags_at",
85
+ "PidRead",
86
+ "Reader",
87
+ "ReaderMark",
88
+ "SampleError",
89
+ "Status",
90
+ "Truncated",
91
+ "Unknown",
92
+ "TryConstruct",
93
+ "WireError",
94
+ "Writer",
95
+ "encapsulation_header",
96
+ "encapsulation_padding",
97
+ "bit_pattern_bool",
98
+ "bit_pattern_f32",
99
+ "bit_pattern_f64",
100
+ "bit_pattern_int",
101
+ "no_dheader",
102
+ "utf8_octets",
103
+ "utf16_units",
104
+ "wstring_unit_width",
105
+ ]
106
+
107
+ __version__ = "1.0.0-rc.7"
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 ZeroDDS Contributors
3
+ """Encodings, and the four bytes that announce one.
4
+
5
+ DDSI-RTPS 2.5 §10.5 and XTypes 1.3 §7.4.1.1. Kept out of `writer.py`
6
+ because it frames a sample rather than being part of one: a KeyHash
7
+ holder is written by the same writer and carries no header at all.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import enum
13
+
14
+ from .endianness import Endianness
15
+
16
+ __all__ = ["Encoding", "encapsulation_header", "encapsulation_padding"]
17
+
18
+
19
+ class Encoding(enum.Enum):
20
+ """Which encoding a plan and a stream are in.
21
+
22
+ The five of `zerodds_ast_layout::Encoding`. XCDR1 and XCDR2 differ in
23
+ their framing: XCDR1 has no delimiter header, and `@mutable` uses PID
24
+ headers there and EMHEADERs here.
25
+ """
26
+
27
+ PLAIN_CDR1 = "plain-cdr1"
28
+ PL_CDR1 = "pl-cdr1"
29
+ PLAIN_CDR2 = "plain-cdr2"
30
+ DELIMITED_CDR = "delimited-cdr"
31
+ PL_CDR2 = "pl-cdr2"
32
+
33
+ def is_v1(self) -> bool:
34
+ """Whether this is an XCDR version 1 encoding."""
35
+ return self in (Encoding.PLAIN_CDR1, Encoding.PL_CDR1)
36
+
37
+ def is_v2(self) -> bool:
38
+ """Whether this is an XCDR version 2 encoding."""
39
+ return not self.is_v1()
40
+
41
+ @property
42
+ def max_alignment(self) -> int:
43
+ """The largest alignment any primitive takes.
44
+
45
+ XTypes 1.3 §7.4.1: `MAXALIGN(VERSION2) = 4`,
46
+ `MAXALIGN(VERSION1) = 8`, and `ALIGN(N)` aligns to
47
+ `MIN(N, XCDR.maxalign)`. §7.6.8 repeats it for the key holder:
48
+ "the maximum alignment in PLAIN_CDR2 (i.e., 4)".
49
+ """
50
+ return 8 if self.is_v1() else 4
51
+
52
+ @staticmethod
53
+ def parse(name: str) -> Encoding | None:
54
+ """The command line's spelling, including the two shorthands.
55
+
56
+ `xcdr1` and `xcdr2` name the plain form, because that is what a
57
+ caller who has not thought about extensibility means. An unknown
58
+ name is `None` and never a default.
59
+ """
60
+ if name in ("xcdr1", "plain-cdr1"):
61
+ return Encoding.PLAIN_CDR1
62
+ if name in ("xcdr2", "plain-cdr2"):
63
+ return Encoding.PLAIN_CDR2
64
+ for encoding in Encoding:
65
+ if encoding.value == name:
66
+ return encoding
67
+ return None
68
+
69
+
70
+ _IDENTIFIERS: dict[tuple[Encoding, Endianness], int] = {
71
+ (Encoding.PLAIN_CDR1, Endianness.BIG): 0x0000,
72
+ (Encoding.PLAIN_CDR1, Endianness.LITTLE): 0x0001,
73
+ (Encoding.PL_CDR1, Endianness.BIG): 0x0002,
74
+ (Encoding.PL_CDR1, Endianness.LITTLE): 0x0003,
75
+ (Encoding.PLAIN_CDR2, Endianness.BIG): 0x0006,
76
+ (Encoding.PLAIN_CDR2, Endianness.LITTLE): 0x0007,
77
+ (Encoding.DELIMITED_CDR, Endianness.BIG): 0x0008,
78
+ (Encoding.DELIMITED_CDR, Endianness.LITTLE): 0x0009,
79
+ (Encoding.PL_CDR2, Endianness.BIG): 0x000A,
80
+ (Encoding.PL_CDR2, Endianness.LITTLE): 0x000B,
81
+ }
82
+
83
+
84
+ def encapsulation_padding(length: int) -> int:
85
+ """How many zero bytes bring `length` up to the next multiple of four.
86
+
87
+ XTypes 1.3 §7.4.1.1 pads the serialized data to a four-byte
88
+ boundary; §7.6.3.1.2 states the count in the encapsulation options.
89
+ """
90
+ return (4 - (length & 3)) & 3
91
+
92
+
93
+ def encapsulation_header(
94
+ encoding: Encoding, endianness: Endianness, padding: int = 0
95
+ ) -> bytes:
96
+ """The four-byte encapsulation header.
97
+
98
+ The identifier itself is always big-endian on the wire, whatever the
99
+ payload behind it is — it is what tells a reader which order to
100
+ switch to.
101
+
102
+ The two option bytes carry the payload's trailing padding count in
103
+ the low two bits of the second one. XTypes 1.3 §7.6.3.1.2: those
104
+ bits "shall be interpreted by the receiver to determine where the
105
+ serialized data exactly ended". Measured on the loopback wire on
106
+ 2026-08-09: Cyclone, RTI and Fast DDS all pad the payload to four
107
+ and all three state the count — `@final struct One { octet a; }`
108
+ goes out as `0001 0003 ab000000` from every one of them.
109
+ """
110
+ identifier = _IDENTIFIERS[(encoding, endianness)]
111
+ return bytes([identifier >> 8, identifier & 0xFF, 0, padding & 0x3])
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 ZeroDDS Contributors
3
+ """Byte order.
4
+
5
+ The counterpart of `crates/wire/src/endianness.rs`. A separate module
6
+ because the writer and the reader both need it and neither should own it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import enum
12
+
13
+ __all__ = ["Endianness"]
14
+
15
+
16
+ class Endianness(enum.Enum):
17
+ """Which end of a multi-byte value goes out first."""
18
+
19
+ LITTLE = "le"
20
+ BIG = "be"
21
+
22
+ def is_little(self) -> bool:
23
+ """Whether the low byte goes out first."""
24
+ return self is Endianness.LITTLE
25
+
26
+ @property
27
+ def struct_prefix(self) -> str:
28
+ """The `struct` module's byte-order character.
29
+
30
+ Explicit rather than `=` or `@`: the native order of the host is
31
+ not the order the encapsulation identifier announced, and a probe
32
+ that agreed with itself on one machine and not another would be
33
+ useless as a second opinion.
34
+ """
35
+ return "<" if self is Endianness.LITTLE else ">"
36
+
37
+ @staticmethod
38
+ def parse(name: str) -> Endianness | None:
39
+ """The command line's spelling, or `None` for anything else.
40
+
41
+ `None` and not a default: a typo in a wire-affecting flag has to
42
+ be loud, because the bytes it produces look plausible.
43
+ """
44
+ if name in ("le", "little"):
45
+ return Endianness.LITTLE
46
+ if name in ("be", "big"):
47
+ return Endianness.BIG
48
+ return None
zerodds_wire/error.py ADDED
@@ -0,0 +1,72 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 ZeroDDS Contributors
3
+ """What can go wrong on the wire.
4
+
5
+ The variants of `crates/wire/src/error.rs`, as exceptions. Every one
6
+ names what was expected and where: a reader that returned a zero for a
7
+ truncated stream would hand the caller a value that was never sent, and
8
+ a writer that clamped an over-wide integer would put a different number
9
+ on the wire than the caller asked for.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = [
15
+ "WireError",
16
+ "Truncated",
17
+ "ImpossibleLength",
18
+ "NotUtf8",
19
+ "OutOfRange",
20
+ ]
21
+
22
+
23
+ class WireError(Exception):
24
+ """Base of every wire failure."""
25
+
26
+
27
+ class Truncated(WireError):
28
+ """The stream ended before the value did."""
29
+
30
+ def __init__(self, at: int, needed: int, available: int) -> None:
31
+ super().__init__(f"at {at}: {needed} bytes needed, {available} available")
32
+ self.at = at
33
+ self.needed = needed
34
+ self.available = available
35
+
36
+
37
+ class ImpossibleLength(WireError):
38
+ """A length field claims more than the stream holds.
39
+
40
+ Separate from `Truncated`: a length that cannot be true is a
41
+ malformed stream, not a short one, and allocating for it is how a
42
+ reader turns a bad message into an out-of-memory.
43
+ """
44
+
45
+ def __init__(self, at: int, claimed: int, available: int) -> None:
46
+ super().__init__(f"at {at}: a length of {claimed} with {available} bytes left")
47
+ self.at = at
48
+ self.claimed = claimed
49
+ self.available = available
50
+
51
+
52
+ class NotUtf8(WireError):
53
+ """A string held bytes that are not UTF-8."""
54
+
55
+ def __init__(self, at: int) -> None:
56
+ super().__init__(f"at {at}: the string is not UTF-8")
57
+ self.at = at
58
+
59
+
60
+ class OutOfRange(WireError):
61
+ """A value outside what its type admits.
62
+
63
+ Raised by the writer as well as the reader, which is where Python
64
+ differs from the Rust runtime: an `int` there is `i32` or `u64` and
65
+ the compiler has already refused the value, while here it is
66
+ unbounded and the check has to happen at the point of the write.
67
+ """
68
+
69
+ def __init__(self, at: int, expected: str) -> None:
70
+ super().__init__(f"at {at}: expected {expected}")
71
+ self.at = at
72
+ self.expected = expected