structwire 0.1.0__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.
structwire/loaders.py ADDED
@@ -0,0 +1,234 @@
1
+ """Load layouts and catalogs from Python mappings or YAML files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .catalog import Catalog
10
+ from .errors import SchemaError
11
+ from .model import Field, Layout
12
+
13
+ _LAYOUT_KEYS = {"name", "description", "size", "fields"}
14
+ _FIELD_KEYS = {
15
+ "name",
16
+ "kind",
17
+ "type",
18
+ "length",
19
+ "offset",
20
+ "mask",
21
+ "bitmask",
22
+ "shift",
23
+ "bitshift",
24
+ "scale",
25
+ "bias",
26
+ "physical_offset",
27
+ "values",
28
+ "bits",
29
+ "byteorder",
30
+ "description",
31
+ "unit",
32
+ }
33
+ _CATALOG_KEYS = {"version", "codecs"}
34
+
35
+
36
+ def _intish(value: Any) -> int:
37
+ if isinstance(value, bool):
38
+ raise ValueError("boolean is not an integer value here")
39
+ if isinstance(value, int):
40
+ return value
41
+ return int(str(value), 0)
42
+
43
+
44
+ def _expect_keys(data: Mapping[str, Any], allowed: set[str], where: str) -> None:
45
+ if not isinstance(data, Mapping):
46
+ raise SchemaError(f"{where} must be a mapping")
47
+ unknown = sorted(str(key) for key in data if key not in allowed)
48
+ if unknown:
49
+ raise SchemaError(f"{where}: unknown key(s): {', '.join(unknown)}")
50
+
51
+
52
+ def _enum_values(data: Mapping[Any, Any]) -> dict[int, str]:
53
+ values: dict[int, str] = {}
54
+ for key, label in data.items():
55
+ raw = _intish(key)
56
+ if raw in values:
57
+ raise SchemaError(f"Duplicate enum value after normalization: {raw}")
58
+ values[raw] = label
59
+ return values
60
+
61
+
62
+ def _floatish(value: Any) -> float:
63
+ if isinstance(value, bool):
64
+ raise ValueError("boolean is not a scale or bias")
65
+ return float(value)
66
+
67
+
68
+ def _alias_value(
69
+ data: Mapping[str, Any],
70
+ primary: str,
71
+ alias: str,
72
+ *,
73
+ default: Any = None,
74
+ where: str,
75
+ ) -> Any:
76
+ has_primary = primary in data
77
+ has_alias = alias in data
78
+ if has_primary and has_alias and data[primary] != data[alias]:
79
+ raise SchemaError(f"{where}: {primary!r} and compatibility alias {alias!r} disagree")
80
+ if has_primary:
81
+ return data[primary]
82
+ if has_alias:
83
+ return data[alias]
84
+ return default
85
+
86
+
87
+ def layout_from_mapping(data: Mapping[str, Any], *, default_name: str = "") -> Layout:
88
+ """Build a :class:`Layout` from a plain mapping.
89
+
90
+ Accepts the aliases ``type``, ``bitmask``, ``bitshift`` and ``physical_offset``.
91
+ Unknown keys and conflicting aliases raise ``SchemaError``. Compile the
92
+ returned layout to validate overlaps and total size.
93
+ """
94
+ _expect_keys(data, _LAYOUT_KEYS, "layout")
95
+ raw_fields = data.get("fields")
96
+ if not isinstance(raw_fields, list):
97
+ raise SchemaError("Layout mapping must contain a 'fields' list")
98
+
99
+ fields: list[Field] = []
100
+ for index, raw in enumerate(raw_fields):
101
+ where = f"fields[{index}]"
102
+ if not isinstance(raw, Mapping):
103
+ raise SchemaError(f"{where} must be a mapping")
104
+ _expect_keys(raw, _FIELD_KEYS, where)
105
+
106
+ name = raw.get("name")
107
+ kind = _alias_value(raw, "kind", "type", where=where)
108
+ if not isinstance(name, str) or not name.strip():
109
+ raise SchemaError(f"{where}.name must be a non-empty string")
110
+ if not isinstance(kind, str) or not kind.strip():
111
+ raise SchemaError(f"{where}.kind/type must be a non-empty string")
112
+
113
+ mask_raw = _alias_value(raw, "mask", "bitmask", where=where)
114
+ shift_raw = _alias_value(raw, "shift", "bitshift", default=0, where=where)
115
+ bias_raw = _alias_value(raw, "bias", "physical_offset", default=0.0, where=where)
116
+ values_raw = raw.get("values", {})
117
+ bits_raw = raw.get("bits", {})
118
+ if not isinstance(values_raw, Mapping) or not isinstance(bits_raw, Mapping):
119
+ raise SchemaError(f"{where}: values/bits must be mappings")
120
+
121
+ try:
122
+ field = Field(
123
+ name=name,
124
+ kind=kind,
125
+ length=_intish(raw["length"]) if raw.get("length") is not None else None,
126
+ offset=_intish(raw["offset"]) if raw.get("offset") is not None else None,
127
+ mask=_intish(mask_raw) if mask_raw is not None else None,
128
+ shift=_intish(shift_raw),
129
+ scale=_floatish(raw.get("scale", 1.0)),
130
+ bias=_floatish(bias_raw),
131
+ values=_enum_values(values_raw),
132
+ bits={k: _intish(v) for k, v in bits_raw.items()},
133
+ byteorder=raw.get("byteorder", "big"),
134
+ description=raw.get("description", ""),
135
+ unit=raw.get("unit", ""),
136
+ )
137
+ except SchemaError:
138
+ raise
139
+ except (TypeError, ValueError, OverflowError) as exc:
140
+ raise SchemaError(f"{where}: invalid field value: {exc}") from exc
141
+ fields.append(field)
142
+
143
+ name = data.get("name", default_name)
144
+ if not isinstance(name, str):
145
+ raise SchemaError("Layout name must be a string")
146
+ name = name.strip()
147
+ if default_name and name != default_name:
148
+ raise SchemaError(f"Catalog key {default_name!r} conflicts with layout name {name!r}")
149
+ size_raw = data.get("size")
150
+ try:
151
+ size = _intish(size_raw) if size_raw is not None else None
152
+ except (TypeError, ValueError) as exc:
153
+ raise SchemaError(f"layout.size: invalid integer value {size_raw!r}") from exc
154
+ return Layout(
155
+ *fields,
156
+ name=name,
157
+ description=data.get("description", ""),
158
+ size=size,
159
+ )
160
+
161
+
162
+ def catalog_from_mapping(data: Mapping[str, Any]) -> Catalog:
163
+ """Load and compile a catalog from a mapping.
164
+
165
+ ``codecs`` maps names to layout definitions. ``version`` defaults to 1,
166
+ the only supported schema version.
167
+ """
168
+ _expect_keys(data, _CATALOG_KEYS, "catalog")
169
+ version = data.get("version", 1)
170
+ try:
171
+ parsed_version = _intish(version)
172
+ except (TypeError, ValueError) as exc:
173
+ raise SchemaError("Catalog version must be an integer") from exc
174
+ if parsed_version != 1:
175
+ raise SchemaError(f"Unsupported catalog version: {parsed_version}")
176
+
177
+ raw_codecs = data.get("codecs")
178
+ if not isinstance(raw_codecs, Mapping) or not raw_codecs:
179
+ raise SchemaError("Catalog mapping must contain a non-empty 'codecs' mapping")
180
+
181
+ layouts: dict[str, Layout] = {}
182
+ for raw_name, raw_layout in raw_codecs.items():
183
+ if not isinstance(raw_name, str) or not raw_name.strip():
184
+ raise SchemaError("Catalog codec names must be non-empty strings")
185
+ name = raw_name.strip()
186
+ if name in layouts:
187
+ raise SchemaError(f"Duplicate codec name: {name!r}")
188
+ if not isinstance(raw_layout, Mapping):
189
+ raise SchemaError(f"codecs.{name} must be a mapping")
190
+ layouts[name] = layout_from_mapping(raw_layout, default_name=name)
191
+ return Catalog(layouts)
192
+
193
+
194
+ def _yaml_load(path: str | Path) -> Mapping[str, Any]:
195
+ import yaml
196
+
197
+ class UniqueSafeLoader(yaml.SafeLoader):
198
+ def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]:
199
+ if not isinstance(node, yaml.MappingNode):
200
+ raise SchemaError("YAML mapping expected")
201
+ self.flatten_mapping(node)
202
+ result: dict[Any, Any] = {}
203
+ for key_node, value_node in node.value:
204
+ key = self.construct_object(key_node, deep=deep)
205
+ try:
206
+ if key in result:
207
+ raise SchemaError(
208
+ f"Duplicate YAML key {key!r} at line {key_node.start_mark.line + 1}"
209
+ )
210
+ result[key] = self.construct_object(value_node, deep=deep)
211
+ except TypeError as exc:
212
+ raise SchemaError("YAML mapping keys must be hashable") from exc
213
+ return result
214
+
215
+ with Path(path).open("r", encoding="utf-8") as handle:
216
+ try:
217
+ data = yaml.load(handle, Loader=UniqueSafeLoader)
218
+ except SchemaError:
219
+ raise
220
+ except (yaml.YAMLError, ValueError, KeyError) as exc:
221
+ raise SchemaError(f"Invalid YAML schema: {exc}") from exc
222
+ if not isinstance(data, Mapping):
223
+ raise SchemaError("YAML root must be a mapping")
224
+ return data
225
+
226
+
227
+ def layout_from_yaml(path: str | Path) -> Layout:
228
+ """Load one layout from YAML."""
229
+ return layout_from_mapping(_yaml_load(path))
230
+
231
+
232
+ def catalog_from_yaml(path: str | Path) -> Catalog:
233
+ """Load a named codec catalog from YAML."""
234
+ return catalog_from_mapping(_yaml_load(path))
structwire/model.py ADDED
@@ -0,0 +1,330 @@
1
+ """Immutable public schema model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from math import isfinite
8
+ from types import MappingProxyType
9
+ from typing import TYPE_CHECKING, Any, Literal, cast
10
+
11
+ from .errors import SchemaError
12
+
13
+ if TYPE_CHECKING:
14
+ from .codec import Codec
15
+
16
+ _INTEGER_KINDS: dict[str, tuple[int, bool, Literal["big", "little"]]] = {
17
+ "u8": (1, False, "big"),
18
+ "i8": (1, True, "big"),
19
+ "u16be": (2, False, "big"),
20
+ "u16le": (2, False, "little"),
21
+ "i16be": (2, True, "big"),
22
+ "i16le": (2, True, "little"),
23
+ "u24be": (3, False, "big"),
24
+ "u24le": (3, False, "little"),
25
+ "i24be": (3, True, "big"),
26
+ "i24le": (3, True, "little"),
27
+ "u32be": (4, False, "big"),
28
+ "u32le": (4, False, "little"),
29
+ "i32be": (4, True, "big"),
30
+ "i32le": (4, True, "little"),
31
+ "u64be": (8, False, "big"),
32
+ "u64le": (8, False, "little"),
33
+ "i64be": (8, True, "big"),
34
+ "i64le": (8, True, "little"),
35
+ }
36
+
37
+ _FLOAT_KINDS: dict[str, tuple[int, Literal["big", "little"]]] = {
38
+ "f32be": (4, "big"),
39
+ "f32le": (4, "little"),
40
+ "f64be": (8, "big"),
41
+ "f64le": (8, "little"),
42
+ }
43
+
44
+ _ALIASES = {
45
+ "uint8": "u8",
46
+ "int8": "i8",
47
+ "uint16_be": "u16be",
48
+ "uint16_le": "u16le",
49
+ "int16_be": "i16be",
50
+ "int16_le": "i16le",
51
+ "uint24_be": "u24be",
52
+ "uint24_le": "u24le",
53
+ "int24_be": "i24be",
54
+ "int24_le": "i24le",
55
+ "uint32_be": "u32be",
56
+ "uint32_le": "u32le",
57
+ "int32_be": "i32be",
58
+ "int32_le": "i32le",
59
+ "uint64_be": "u64be",
60
+ "uint64_le": "u64le",
61
+ "int64_be": "i64be",
62
+ "int64_le": "i64le",
63
+ "float32_be": "f32be",
64
+ "float32_le": "f32le",
65
+ "float64_be": "f64be",
66
+ "float64_le": "f64le",
67
+ "raw": "bytes",
68
+ "mac_address": "mac",
69
+ }
70
+
71
+ _SPECIAL_KINDS = {"bcd", "ascii", "bytes", "mac", "enum", "flags", "bool"}
72
+
73
+
74
+ def _schema_integer(value: Any, where: str) -> int:
75
+ if not isinstance(value, int) or isinstance(value, bool):
76
+ raise SchemaError(f"{where} must be an integer")
77
+ return value
78
+
79
+
80
+ def canonical_kind(kind: str) -> str:
81
+ """Return a stable kind name, accepting compatibility aliases."""
82
+ if not isinstance(kind, str):
83
+ raise SchemaError("Field kind must be a string")
84
+ key = kind.strip().lower().replace("-", "_")
85
+ canonical = _ALIASES.get(key, key)
86
+ if (
87
+ canonical not in _INTEGER_KINDS
88
+ and canonical not in _FLOAT_KINDS
89
+ and canonical not in _SPECIAL_KINDS
90
+ ):
91
+ raise SchemaError(f"Unsupported field kind: {kind!r}")
92
+ return canonical
93
+
94
+
95
+ def integer_spec(kind: str) -> tuple[int, bool, Literal["big", "little"]] | None:
96
+ """Return ``(width, signed, byteorder)`` for an integer kind."""
97
+ return _INTEGER_KINDS.get(canonical_kind(kind))
98
+
99
+
100
+ def float_spec(kind: str) -> tuple[int, Literal["big", "little"]] | None:
101
+ """Return ``(width, byteorder)`` for a floating-point kind."""
102
+ return _FLOAT_KINDS.get(canonical_kind(kind))
103
+
104
+
105
+ def _logical_bits(width: int, mask: int | None, shift: int) -> int:
106
+ if mask is None:
107
+ return width * 8
108
+ return (mask >> shift).bit_length()
109
+
110
+
111
+ @dataclass(frozen=True, slots=True)
112
+ class Field:
113
+ """One field in a binary layout.
114
+
115
+ ``offset`` is a byte offset. When omitted, the field is placed after the
116
+ furthest byte occupied so far. ``mask`` and ``shift`` address sub-fields
117
+ within an integer-sized byte span.
118
+ """
119
+
120
+ name: str
121
+ kind: str
122
+ length: int | None = None
123
+ offset: int | None = None
124
+ mask: int | None = None
125
+ shift: int = 0
126
+ scale: float = 1.0
127
+ bias: float = 0.0
128
+ values: Mapping[int, str] = field(default_factory=dict)
129
+ bits: Mapping[str, int] = field(default_factory=dict)
130
+ byteorder: str = "big"
131
+ description: str = ""
132
+ unit: str = ""
133
+
134
+ def __post_init__(self) -> None:
135
+ if not isinstance(self.name, str) or not self.name.strip():
136
+ raise SchemaError("Field name must be non-empty")
137
+ kind = canonical_kind(self.kind)
138
+ object.__setattr__(self, "kind", kind)
139
+ for attribute in ("offset", "length", "mask"):
140
+ value = getattr(self, attribute)
141
+ if value is not None:
142
+ _schema_integer(value, f"Field {self.name!r}: {attribute}")
143
+ _schema_integer(self.shift, f"Field {self.name!r}: shift")
144
+ if self.offset is not None and self.offset < 0:
145
+ raise SchemaError(f"Field {self.name!r}: offset must be non-negative")
146
+ if self.length is not None and self.length < 1:
147
+ raise SchemaError(f"Field {self.name!r}: length must be >= 1")
148
+ if self.shift < 0:
149
+ raise SchemaError(f"Field {self.name!r}: shift must be non-negative")
150
+ if self.shift and self.mask is None:
151
+ raise SchemaError(f"Field {self.name!r}: shift requires a mask")
152
+ for attribute in ("scale", "bias"):
153
+ value = getattr(self, attribute)
154
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
155
+ raise SchemaError(f"Field {self.name!r}: {attribute} must be a finite number")
156
+ try:
157
+ finite = isfinite(value)
158
+ except OverflowError:
159
+ finite = False
160
+ if not finite:
161
+ raise SchemaError(f"Field {self.name!r}: scale and bias must be finite")
162
+ if self.scale == 0:
163
+ raise SchemaError(f"Field {self.name!r}: scale cannot be zero")
164
+ if self.byteorder not in ("big", "little"):
165
+ raise SchemaError(f"Field {self.name!r}: byteorder must be 'big' or 'little'")
166
+ for attribute in ("description", "unit"):
167
+ if not isinstance(getattr(self, attribute), str):
168
+ raise SchemaError(f"Field {self.name!r}: {attribute} must be a string")
169
+ if not isinstance(self.values, Mapping) or not isinstance(self.bits, Mapping):
170
+ raise SchemaError(f"Field {self.name!r}: values/bits must be mappings")
171
+ if self.values and kind != "enum":
172
+ raise SchemaError(f"Field {self.name!r}: values is only valid for enum fields")
173
+ if self.bits and kind != "flags":
174
+ raise SchemaError(f"Field {self.name!r}: bits is only valid for flags fields")
175
+ if any(not isinstance(label, str) for label in self.values.values()):
176
+ raise SchemaError(f"Field {self.name!r}: enum labels must be strings")
177
+ if any(not isinstance(name, str) or not name.strip() for name in self.bits):
178
+ raise SchemaError(f"Field {self.name!r}: flag names must be non-empty strings")
179
+
180
+ values = {
181
+ _schema_integer(k, f"Field {self.name!r}: enum key"): v for k, v in self.values.items()
182
+ }
183
+ bits = {
184
+ k: _schema_integer(v, f"Field {self.name!r}: flag mask") for k, v in self.bits.items()
185
+ }
186
+ if len(set(values.values())) != len(values):
187
+ raise SchemaError(f"Field {self.name!r}: enum labels must be unique for encoding")
188
+ object.__setattr__(self, "values", MappingProxyType(values))
189
+ object.__setattr__(self, "bits", MappingProxyType(bits))
190
+
191
+ if kind in {"ascii", "bytes"} and self.length is None:
192
+ raise SchemaError(f"Field {self.name!r}: {kind} requires length")
193
+ width = self.width
194
+ if (kind in _INTEGER_KINDS or kind in _FLOAT_KINDS) and self.length not in (None, width):
195
+ raise SchemaError(f"Field {self.name!r}: {kind} is exactly {width} byte(s)")
196
+ if self.mask is not None:
197
+ if self.mask <= 0 or self.mask.bit_length() > width * 8:
198
+ raise SchemaError(
199
+ f"Field {self.name!r}: mask 0x{self.mask:X} does not fit {width} byte(s)"
200
+ )
201
+ shifted = self.mask >> self.shift
202
+ if shifted == 0 or shifted & (shifted + 1):
203
+ raise SchemaError(
204
+ f"Field {self.name!r}: mask must become contiguous low bits after shift"
205
+ )
206
+ if shifted << self.shift != self.mask:
207
+ raise SchemaError(f"Field {self.name!r}: shift does not match mask")
208
+
209
+ if kind == "mac" and self.length not in {None, 6}:
210
+ raise SchemaError(f"Field {self.name!r}: mac fields are exactly 6 bytes")
211
+ if self.mask is not None and (
212
+ kind in _FLOAT_KINDS or kind in {"bcd", "ascii", "bytes", "mac"}
213
+ ):
214
+ raise SchemaError(f"Field {self.name!r}: mask is not valid for {kind}")
215
+ if kind in {"enum", "flags", "bool"} and not 1 <= width <= 8:
216
+ raise SchemaError(f"Field {self.name!r}: {kind} width must be between 1 and 8 bytes")
217
+ if kind in {"enum", "flags"}:
218
+ logical_bits = _logical_bits(width, self.mask, self.shift)
219
+ logical_max = (1 << logical_bits) - 1
220
+ if kind == "enum":
221
+ if not values:
222
+ raise SchemaError(f"Field {self.name!r}: enum requires values")
223
+ invalid = [raw for raw in values if raw < 0 or raw > logical_max]
224
+ if invalid:
225
+ raise SchemaError(
226
+ f"Field {self.name!r}: enum value does not fit {logical_bits} bit(s)"
227
+ )
228
+ if kind == "flags":
229
+ if not bits:
230
+ raise SchemaError(f"Field {self.name!r}: flags requires bits")
231
+ invalid = [mask for mask in bits.values() if mask <= 0 or mask > logical_max]
232
+ if invalid:
233
+ raise SchemaError(
234
+ f"Field {self.name!r}: flag mask 0x{invalid[0]:X} does not fit "
235
+ f"{logical_bits} bit(s)"
236
+ )
237
+ if kind in {"ascii", "bytes", "mac", "flags", "enum", "bool"} and (
238
+ self.scale != 1.0 or self.bias != 0.0
239
+ ):
240
+ raise SchemaError(f"Field {self.name!r}: scale/bias is not valid for {kind}")
241
+
242
+ @property
243
+ def width(self) -> int:
244
+ """Field width in bytes."""
245
+ int_spec = _INTEGER_KINDS.get(self.kind)
246
+ if int_spec is not None:
247
+ return int_spec[0]
248
+ flt_spec = _FLOAT_KINDS.get(self.kind)
249
+ if flt_spec is not None:
250
+ return flt_spec[0]
251
+ if self.kind == "mac":
252
+ return 6
253
+ if self.kind == "bcd":
254
+ return self.length or 1
255
+ if self.kind in {"enum", "flags", "bool"}:
256
+ return self.length or 1
257
+ assert self.length is not None
258
+ return self.length
259
+
260
+ @property
261
+ def effective_byteorder(self) -> Literal["big", "little"]:
262
+ """Kind-defined numeric order, otherwise the supplied byteorder.
263
+
264
+ BCD, ASCII and bytes retain wire order regardless of this parameter.
265
+ """
266
+ spec = _INTEGER_KINDS.get(self.kind)
267
+ if spec is not None:
268
+ return spec[2]
269
+ flt = _FLOAT_KINDS.get(self.kind)
270
+ if flt is not None:
271
+ return flt[1]
272
+ return cast(Literal["big", "little"], self.byteorder)
273
+
274
+ @property
275
+ def signed(self) -> bool:
276
+ spec = _INTEGER_KINDS.get(self.kind)
277
+ return bool(spec and spec[1])
278
+
279
+
280
+ @dataclass(frozen=True, slots=True)
281
+ class Layout:
282
+ """Immutable declarative binary layout."""
283
+
284
+ fields: tuple[Field, ...]
285
+ name: str = ""
286
+ description: str = ""
287
+ size: int | None = None
288
+
289
+ def __init__(
290
+ self,
291
+ *fields: Field,
292
+ name: str = "",
293
+ description: str = "",
294
+ size: int | None = None,
295
+ ) -> None:
296
+ if size is not None:
297
+ _schema_integer(size, "Layout size")
298
+ if not isinstance(name, str):
299
+ raise SchemaError("Layout name must be a string")
300
+ if not isinstance(description, str):
301
+ raise SchemaError("Layout description must be a string")
302
+ if any(not isinstance(item, Field) for item in fields):
303
+ raise SchemaError("Layout fields must be Field objects")
304
+ object.__setattr__(self, "fields", tuple(fields))
305
+ object.__setattr__(self, "name", name)
306
+ object.__setattr__(self, "description", description)
307
+ object.__setattr__(self, "size", size)
308
+ if size is not None and size < 0:
309
+ raise SchemaError("Layout size must be non-negative")
310
+ if len({item.name for item in fields}) != len(fields):
311
+ raise SchemaError("Field names must be unique")
312
+
313
+ def compile(self) -> Codec:
314
+ """Compile this layout into a reusable codec."""
315
+ from .codec import Codec
316
+
317
+ return Codec(self)
318
+
319
+ def encode(self, values: Mapping[str, Any], *, require_all: bool = True) -> bytes:
320
+ """Compile and encode in one call."""
321
+ return self.compile().encode(values, require_all=require_all)
322
+
323
+ def decode(
324
+ self,
325
+ payload: bytes | bytearray | memoryview,
326
+ *,
327
+ strict: bool = True,
328
+ ) -> dict[str, Any]:
329
+ """Compile and decode in one call."""
330
+ return self.compile().decode(payload, strict=strict)
structwire/py.typed ADDED
File without changes