smithy-cbor 0.1.0__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.
@@ -0,0 +1,16 @@
1
+ {
2
+ "changes": [
3
+ {
4
+ "type": "feature",
5
+ "description": "Added the `smithy-cbor` package: an RFC 8949 CBOR codec supporting the `smithy.protocols#rpcv2Cbor` protocol."
6
+ },
7
+ {
8
+ "type": "enhancement",
9
+ "description": "Cache CBOR member encodings and structure member lookup metadata using schema extensions."
10
+ },
11
+ {
12
+ "type": "bugfix",
13
+ "description": "Allow CBOR integer tokens within the signed 64-bit range to deserialize into Smithy float and double shapes."
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,36 @@
1
+ # Eclipse
2
+ .classpath
3
+ .project
4
+ .settings/
5
+
6
+ # Intellij
7
+ .idea/
8
+ *.iml
9
+ *.iws
10
+
11
+ # VS Code
12
+ .vscode/
13
+
14
+ # Mac
15
+ .DS_Store
16
+
17
+ # Maven
18
+ target/
19
+ **/dependency-reduced-pom.xml
20
+
21
+ # Gradle
22
+ **/.gradle
23
+ build/
24
+ */out/
25
+ **/*/out/
26
+
27
+ # Python
28
+ __pycache__
29
+ *.swp
30
+ .mypy_cache
31
+ .coverage
32
+ coverage.xml
33
+ htmlcov
34
+ *.egg-info
35
+ dist
36
+ .venv
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0
4
+
5
+ ### Features
6
+ * Added the `smithy-cbor` package: an RFC 8949 CBOR codec supporting the `smithy.protocols#rpcv2Cbor` protocol.
7
+
8
+ ### Enhancements
9
+ * Cache CBOR member encodings and structure member lookup metadata using schema extensions.
10
+
11
+ ### Bug fixes
12
+ * Allow CBOR integer tokens within the signed 64-bit range to deserialize into Smithy float and double shapes.
@@ -0,0 +1 @@
1
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.5
2
+ Name: smithy-cbor
3
+ Version: 0.1.0
4
+ Summary: CBOR serialization and deserialization support for Smithy tooling.
5
+ Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/CHANGELOG.md
6
+ Project-URL: Code, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/
7
+ Project-URL: Issue tracker, https://github.com/smithy-lang/smithy-python/issues
8
+ Author: Amazon Web Services
9
+ License: Apache License 2.0
10
+ License-File: NOTICE
11
+ Keywords: cbor,sdk,smithy
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
25
+ Classifier: Programming Language :: Python :: Implementation :: CPython
26
+ Classifier: Topic :: Software Development :: Libraries
27
+ Requires-Python: >=3.12
28
+ Requires-Dist: smithy-core
29
+ Description-Content-Type: text/markdown
30
+
31
+ # smithy-cbor
32
+
33
+ This package provides generic CBOR ([RFC 8949](https://www.rfc-editor.org/rfc/rfc8949))
34
+ serialization and deserialization support for Smithy clients and servers, targeting
35
+ the subset of CBOR used by the `smithy.protocols#rpcv2Cbor` protocol.
@@ -0,0 +1,5 @@
1
+ # smithy-cbor
2
+
3
+ This package provides generic CBOR ([RFC 8949](https://www.rfc-editor.org/rfc/rfc8949))
4
+ serialization and deserialization support for Smithy clients and servers, targeting
5
+ the subset of CBOR used by the `smithy.protocols#rpcv2Cbor` protocol.
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "smithy-cbor"
3
+ dynamic = ["version"]
4
+ requires-python = ">=3.12"
5
+ authors = [
6
+ {name = "Amazon Web Services"},
7
+ ]
8
+ description = "CBOR serialization and deserialization support for Smithy tooling."
9
+ readme = "README.md"
10
+ license = {text = "Apache License 2.0"}
11
+ keywords = ["smithy", "sdk", "cbor"]
12
+ classifiers = [
13
+ "Development Status :: 2 - Pre-Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: System Administrators",
16
+ "Natural Language :: English",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Programming Language :: Python :: Implementation :: CPython",
26
+ "Programming Language :: Python :: Free Threading :: 2 - Beta",
27
+ "Topic :: Software Development :: Libraries"
28
+ ]
29
+ dependencies = [
30
+ "smithy-core",
31
+ ]
32
+
33
+ [project.urls]
34
+ "Changelog" = "https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/CHANGELOG.md"
35
+ "Code" = "https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/"
36
+ "Issue tracker" = "https://github.com/smithy-lang/smithy-python/issues"
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/smithy_cbor/__init__.py"
44
+
45
+ [tool.hatch.build]
46
+ exclude = [
47
+ "tests",
48
+ ]
49
+
50
+ [tool.ruff]
51
+ src = ["src"]
@@ -0,0 +1,40 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from io import BytesIO
4
+
5
+ from smithy_core.codecs import Codec
6
+ from smithy_core.deserializers import ShapeDeserializer
7
+ from smithy_core.interfaces import BytesReader, BytesWriter
8
+ from smithy_core.serializers import ShapeSerializer
9
+
10
+ from ._private.deserializers import CBORShapeDeserializer as _CBORShapeDeserializer
11
+ from ._private.generic import loads as loads
12
+ from ._private.serializers import CBORShapeSerializer as _CBORShapeSerializer
13
+ from .settings import CBORSettings
14
+
15
+ __version__ = "0.1.0"
16
+ __all__ = ("CBORCodec", "CBORSettings", "loads")
17
+
18
+
19
+ class CBORCodec(Codec):
20
+ """Targets the subset of CBOR used by the ``smithy.protocols#rpcv2Cbor`` protocol."""
21
+
22
+ def __init__(
23
+ self,
24
+ default_namespace: str | None = None,
25
+ ) -> None:
26
+ self._settings = CBORSettings(
27
+ default_namespace=default_namespace,
28
+ )
29
+
30
+ @property
31
+ def media_type(self) -> str:
32
+ return "application/cbor"
33
+
34
+ def create_serializer(self, sink: BytesWriter) -> "ShapeSerializer":
35
+ return _CBORShapeSerializer(sink, settings=self._settings)
36
+
37
+ def create_deserializer(self, source: bytes | BytesReader) -> "ShapeDeserializer":
38
+ if isinstance(source, bytes):
39
+ source = BytesIO(source)
40
+ return _CBORShapeDeserializer(source, settings=self._settings)
@@ -0,0 +1,2 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,431 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """CBOR shape deserializer."""
4
+
5
+ import datetime
6
+ import struct as _struct
7
+ from collections.abc import Callable
8
+ from decimal import Decimal
9
+ from typing import override
10
+
11
+ from smithy_core.deserializers import ShapeDeserializer
12
+ from smithy_core.documents import Document
13
+ from smithy_core.exceptions import SmithyError
14
+ from smithy_core.interfaces import BytesReader
15
+ from smithy_core.schemas import Schema
16
+
17
+ from ..settings import CBORSettings
18
+
19
+ # Major type occupies the high 3 bits; the low 5 bits are the "additional information".
20
+ _MAJOR_UINT = 0
21
+ _MAJOR_NEGINT = 1
22
+ _MAJOR_BYTES = 2
23
+ _MAJOR_TEXT = 3
24
+ _MAJOR_ARRAY = 4
25
+ _MAJOR_MAP = 5
26
+ _MAJOR_TAG = 6
27
+ _MAJOR_SIMPLE = 7
28
+
29
+ # Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
30
+ _W_1BYTE = 24
31
+ _W_2BYTE = 25
32
+ _W_4BYTE = 26
33
+ _W_8BYTE = 27
34
+ _INDEFINITE = 31
35
+
36
+ _SIMPLE_FALSE = 20
37
+ _SIMPLE_TRUE = 21
38
+ _SIMPLE_NULL = 22
39
+
40
+ _TAG_EPOCH = 1
41
+ _TAG_UNSIGNED_BIGNUM = 2
42
+ _TAG_NEGATIVE_BIGNUM = 3
43
+ _TAG_DECIMAL_FRACTION = 4
44
+
45
+ # ends indefinite item
46
+ _BREAK = 0xFF
47
+
48
+ # Initial byte of a CBOR null (major 7, simple value 22).
49
+ _NULL_BYTE = (_MAJOR_SIMPLE << 5) | _SIMPLE_NULL
50
+
51
+
52
+ class CBORDecodeError(SmithyError):
53
+ pass
54
+
55
+
56
+ class CBORShapeDeserializer(ShapeDeserializer):
57
+ def __init__(self, source: BytesReader, settings: CBORSettings) -> None:
58
+ self._settings = settings
59
+ self._data = source.read()
60
+ self._len: int = len(self._data)
61
+ self._pos: int = 0
62
+
63
+ @override
64
+ def read_struct(
65
+ self,
66
+ schema: Schema,
67
+ consumer: Callable[[Schema, ShapeDeserializer], None],
68
+ ) -> None:
69
+ count = self._expect(_MAJOR_MAP)
70
+ members_get = schema.members.get
71
+ cursor = self
72
+ read_text = self._read_text
73
+ skip_value = self._skip_value
74
+ read_null = self.read_null
75
+
76
+ indefinite = count == -1
77
+ remaining = count
78
+ while True:
79
+ if indefinite:
80
+ if cursor._at_break():
81
+ cursor._consume_break()
82
+ break
83
+ else:
84
+ if remaining <= 0:
85
+ break
86
+ remaining -= 1
87
+ key = read_text()
88
+ member = members_get(key)
89
+ if member is None:
90
+ skip_value()
91
+ elif cursor._at_null():
92
+ read_null()
93
+ else:
94
+ consumer(member, self)
95
+
96
+ @override
97
+ def read_list(
98
+ self, schema: Schema, consumer: Callable[[ShapeDeserializer], None]
99
+ ) -> None:
100
+ count = self._expect(_MAJOR_ARRAY)
101
+ if count == -1:
102
+ while not self._at_break():
103
+ consumer(self)
104
+ self._consume_break()
105
+ else:
106
+ for _ in range(count):
107
+ consumer(self)
108
+
109
+ @override
110
+ def read_map(
111
+ self,
112
+ schema: Schema,
113
+ consumer: Callable[[str, ShapeDeserializer], None],
114
+ ) -> None:
115
+ count = self._expect(_MAJOR_MAP)
116
+ if count == -1:
117
+ while not self._at_break():
118
+ key = self._read_text()
119
+ consumer(key, self)
120
+ self._consume_break()
121
+ else:
122
+ for _ in range(count):
123
+ key = self._read_text()
124
+ consumer(key, self)
125
+
126
+ @override
127
+ def is_null(self) -> bool:
128
+ initial = self._peek_byte()
129
+ return initial == ((_MAJOR_SIMPLE << 5) | _SIMPLE_NULL)
130
+
131
+ @override
132
+ def read_null(self) -> None:
133
+ major, arg = self._read_head()
134
+ if major != _MAJOR_SIMPLE or arg != _SIMPLE_NULL:
135
+ raise CBORDecodeError("Expected CBOR null.")
136
+
137
+ @override
138
+ def read_boolean(self, schema: Schema) -> bool:
139
+ major, arg = self._read_head()
140
+ if major != _MAJOR_SIMPLE or arg not in (_SIMPLE_FALSE, _SIMPLE_TRUE):
141
+ raise CBORDecodeError("Expected CBOR boolean.")
142
+ return arg == _SIMPLE_TRUE
143
+
144
+ @override
145
+ def read_blob(self, schema: Schema) -> bytes:
146
+ return self._read_string_bytes(_MAJOR_BYTES)
147
+
148
+ @override
149
+ def read_integer(self, schema: Schema) -> int:
150
+ major, arg = self._read_head()
151
+ if major == _MAJOR_UINT:
152
+ return arg
153
+ if major == _MAJOR_NEGINT:
154
+ return -1 - arg
155
+ raise CBORDecodeError(f"Expected CBOR integer, found major type {major}.")
156
+
157
+ @override
158
+ def read_float(self, schema: Schema) -> float:
159
+ major, add_info, arg = self._read_head_add_info()
160
+ if major != _MAJOR_SIMPLE:
161
+ raise CBORDecodeError("Expected CBOR float.")
162
+ if add_info == _W_8BYTE: # double
163
+ return _struct.unpack(">d", arg.to_bytes(8, "big"))[0]
164
+ if add_info == _W_4BYTE: # single
165
+ return _struct.unpack(">f", arg.to_bytes(4, "big"))[0]
166
+ if add_info == _W_2BYTE: # half
167
+ return _decode_half(arg.to_bytes(2, "big"))
168
+ raise CBORDecodeError(f"Unexpected float additional-info: {add_info}.")
169
+
170
+ @override
171
+ def read_big_integer(self, schema: Schema) -> int:
172
+ major, tag = self._read_head()
173
+ if major != _MAJOR_TAG or tag not in (
174
+ _TAG_UNSIGNED_BIGNUM,
175
+ _TAG_NEGATIVE_BIGNUM,
176
+ ):
177
+ raise CBORDecodeError("Expected CBOR bignum tag.")
178
+ magnitude = int.from_bytes(self.read_blob(schema), "big")
179
+ return magnitude if tag == _TAG_UNSIGNED_BIGNUM else -1 - magnitude
180
+
181
+ @override
182
+ def read_big_decimal(self, schema: Schema) -> Decimal:
183
+ major, tag = self._read_head()
184
+ if major != _MAJOR_TAG or tag != _TAG_DECIMAL_FRACTION:
185
+ raise CBORDecodeError("Expected CBOR decimal-fraction tag.")
186
+ count = self._expect(_MAJOR_ARRAY)
187
+ if count != 2:
188
+ raise CBORDecodeError("Decimal fraction must be a 2-element array.")
189
+ exponent = self.read_integer(schema)
190
+ mantissa = self.read_integer(schema)
191
+ return Decimal(mantissa).scaleb(exponent)
192
+
193
+ @override
194
+ def read_string(self, schema: Schema) -> str:
195
+ return self._read_text()
196
+
197
+ @override
198
+ def read_document(self, schema: Schema) -> Document:
199
+ raise NotImplementedError(
200
+ "This version of the Smithy RPCv2 CBOR protocol does not support document types."
201
+ )
202
+
203
+ @override
204
+ def read_timestamp(self, schema: Schema) -> datetime.datetime:
205
+ major, tag = self._read_head()
206
+ if major != _MAJOR_TAG or tag != _TAG_EPOCH:
207
+ raise CBORDecodeError("Expected CBOR epoch-timestamp tag.")
208
+ # The tagged value is an int or float number of epoch seconds.
209
+ peek_major = self._peek_byte() >> 5
210
+ if peek_major in (_MAJOR_UINT, _MAJOR_NEGINT):
211
+ seconds: float = self.read_integer(schema)
212
+ else:
213
+ seconds = self.read_float(schema)
214
+ return datetime.datetime.fromtimestamp(seconds, tz=datetime.UTC)
215
+
216
+ def _read_text(self) -> str:
217
+ text = self._try_read_text()
218
+ if text is not None:
219
+ return text
220
+ return self._read_string_bytes(_MAJOR_TEXT).decode("utf-8")
221
+
222
+ def _read_string_bytes(self, major: int) -> bytes:
223
+ """An indefinite-length string (RFC 8949 §3.2.3) is a sequence of definite-length
224
+ chunks of the same major type, terminated by a break.
225
+ """
226
+ found_major, arg = self._read_head()
227
+ while found_major == _MAJOR_TAG:
228
+ found_major, arg = self._read_head()
229
+ if found_major != major:
230
+ raise CBORDecodeError(
231
+ f"Expected CBOR major type {major}, found {found_major}."
232
+ )
233
+ if arg != -1:
234
+ return self._take(arg)
235
+ chunks = bytearray()
236
+ while not self._at_break():
237
+ chunk_major, chunk_len = self._read_head()
238
+ if chunk_major != major:
239
+ raise CBORDecodeError(
240
+ "Indefinite-length string chunk has mismatched major type."
241
+ )
242
+ chunks += self._take(chunk_len)
243
+ self._consume_break()
244
+ return bytes(chunks)
245
+
246
+ def _skip_value(self) -> None:
247
+ """e.g. unrecognized member."""
248
+ major, arg = self._read_head()
249
+ if major in (_MAJOR_UINT, _MAJOR_NEGINT):
250
+ return
251
+ if major in (_MAJOR_BYTES, _MAJOR_TEXT):
252
+ self._take(arg)
253
+ elif major == _MAJOR_ARRAY:
254
+ if arg == -1:
255
+ while not self._at_break():
256
+ self._skip_value()
257
+ self._consume_break()
258
+ else:
259
+ for _ in range(arg):
260
+ self._skip_value()
261
+ elif major == _MAJOR_MAP:
262
+ if arg == -1:
263
+ while not self._at_break():
264
+ self._skip_value()
265
+ self._skip_value()
266
+ self._consume_break()
267
+ else:
268
+ for _ in range(arg):
269
+ self._skip_value()
270
+ self._skip_value()
271
+ elif major == _MAJOR_TAG:
272
+ self._skip_value()
273
+ elif major == _MAJOR_SIMPLE:
274
+ if arg == _W_2BYTE:
275
+ self._take(2)
276
+ elif arg == _W_4BYTE:
277
+ self._take(4)
278
+ elif arg == _W_8BYTE:
279
+ self._take(8)
280
+
281
+ def _expect(self, major: int) -> int:
282
+ found_major, arg = self._read_head()
283
+ while found_major == _MAJOR_TAG:
284
+ found_major, arg = self._read_head()
285
+ if found_major != major:
286
+ raise CBORDecodeError(
287
+ f"Expected CBOR major type {major}, found {found_major}."
288
+ )
289
+ return arg
290
+
291
+ def _take(self, n: int) -> bytes:
292
+ end = self._pos + n
293
+ if end > self._len:
294
+ raise CBORDecodeError("Unexpected end of CBOR input.")
295
+ chunk = self._data[self._pos : end]
296
+ self._pos = end
297
+ return chunk
298
+
299
+ def _peek_byte(self) -> int:
300
+ pos = self._pos
301
+ if pos >= self._len:
302
+ raise CBORDecodeError("Unexpected end of CBOR input.")
303
+ return self._data[pos]
304
+
305
+ def _read_head(self) -> tuple[int, int]:
306
+ """For indefinite-length items the argument is returned as ``-1``."""
307
+ data = self._data
308
+ pos = self._pos
309
+ if pos >= self._len:
310
+ raise CBORDecodeError("Unexpected end of CBOR input.")
311
+ initial = data[pos]
312
+ major = initial >> 5
313
+ add_info = initial & 0x1F
314
+ if add_info < _W_1BYTE:
315
+ self._pos = pos + 1
316
+ return major, add_info
317
+ if add_info == _W_1BYTE:
318
+ end = pos + 2
319
+ if end > self._len:
320
+ raise CBORDecodeError("Unexpected end of CBOR input.")
321
+ self._pos = end
322
+ return major, data[pos + 1]
323
+ if add_info == _W_2BYTE:
324
+ width = 2
325
+ elif add_info == _W_4BYTE:
326
+ width = 4
327
+ elif add_info == _W_8BYTE:
328
+ width = 8
329
+ elif add_info == _INDEFINITE:
330
+ self._pos = pos + 1
331
+ return major, -1
332
+ else:
333
+ raise CBORDecodeError(f"Reserved additional-information value: {add_info}")
334
+ start = pos + 1
335
+ end = start + width
336
+ if end > self._len:
337
+ raise CBORDecodeError("Unexpected end of CBOR input.")
338
+ self._pos = end
339
+ return major, int.from_bytes(data[start:end], "big")
340
+
341
+ def _read_head_add_info(self) -> tuple[int, int, int]:
342
+ """Returns (major, add_info, arg): major type, raw additional-info bits, and the
343
+ decoded argument. add_info is kept to recover a float's width, which arg alone
344
+ loses; arg is -1 for an indefinite-length item.
345
+ """
346
+ data = self._data
347
+ pos = self._pos
348
+ if pos >= self._len:
349
+ raise CBORDecodeError("Unexpected end of CBOR input.")
350
+ initial = data[pos]
351
+ major = initial >> 5
352
+ add_info = initial & 0x1F
353
+ if add_info < _W_1BYTE:
354
+ self._pos = pos + 1
355
+ return major, add_info, add_info
356
+ if add_info == _W_1BYTE:
357
+ end = pos + 2
358
+ if end > self._len:
359
+ raise CBORDecodeError("Unexpected end of CBOR input.")
360
+ self._pos = end
361
+ return major, add_info, data[pos + 1]
362
+ if add_info == _W_2BYTE:
363
+ width = 2
364
+ elif add_info == _W_4BYTE:
365
+ width = 4
366
+ elif add_info == _W_8BYTE:
367
+ width = 8
368
+ elif add_info == _INDEFINITE:
369
+ self._pos = pos + 1
370
+ return major, add_info, -1
371
+ else:
372
+ raise CBORDecodeError(f"Reserved additional-information value: {add_info}")
373
+ start = pos + 1
374
+ end = start + width
375
+ if end > self._len:
376
+ raise CBORDecodeError("Unexpected end of CBOR input.")
377
+ self._pos = end
378
+ return major, add_info, int.from_bytes(data[start:end], "big")
379
+
380
+ def _at_break(self) -> bool:
381
+ pos = self._pos
382
+ return pos < self._len and self._data[pos] == _BREAK
383
+
384
+ def _consume_break(self) -> None:
385
+ self._take(1)
386
+
387
+ def _try_read_text(self) -> str | None:
388
+ """Fast path for a definite-length text string (major 3, no tag prefix), which
389
+ is every struct key and every string value in practice. Returns ``None`` when
390
+ the next item is not such a string, so the caller falls back to the general path.
391
+ """
392
+ data = self._data
393
+ pos = self._pos
394
+ if pos >= self._len:
395
+ return None
396
+ initial = data[pos]
397
+ if (initial >> 5) != _MAJOR_TEXT:
398
+ return None
399
+ add_info = initial & 0x1F
400
+ if add_info < _W_1BYTE:
401
+ start = pos + 1
402
+ end = start + add_info
403
+ elif add_info == _W_1BYTE and pos + 1 < self._len:
404
+ length = data[pos + 1]
405
+ start = pos + 2
406
+ end = start + length
407
+ else:
408
+ return None
409
+ if end > self._len:
410
+ return None
411
+ self._pos = end
412
+ return data[start:end].decode("utf-8")
413
+
414
+ def _at_null(self) -> bool:
415
+ pos = self._pos
416
+ return pos < self._len and self._data[pos] == _NULL_BYTE
417
+
418
+
419
+ def _decode_half(data: bytes) -> float:
420
+ """Decodes an IEEE 754 half-precision float, implementing RFC 8949 Appendix D."""
421
+ (bits,) = _struct.unpack(">H", data)
422
+ sign = (bits >> 15) & 0x1
423
+ exp = (bits >> 10) & 0x1F
424
+ mant = bits & 0x3FF
425
+ if exp == 0:
426
+ value = (mant / 1024.0) * (2.0**-14)
427
+ elif exp == 0x1F:
428
+ value = float("inf") if mant == 0 else float("nan")
429
+ else:
430
+ value = (1.0 + mant / 1024.0) * (2.0 ** (exp - 15))
431
+ return -value if sign else value
@@ -0,0 +1,152 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Generic, schema-free CBOR decoding."""
4
+
5
+ import datetime
6
+ import struct as _struct
7
+ from decimal import Decimal
8
+ from typing import Any
9
+
10
+ _TAG_EPOCH = 1
11
+ _TAG_UNSIGNED_BIGNUM = 2
12
+ _TAG_NEGATIVE_BIGNUM = 3
13
+ _TAG_DECIMAL_FRACTION = 4
14
+
15
+ # Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
16
+ _W_1BYTE = 24
17
+ _W_2BYTE = 25
18
+ _W_4BYTE = 26
19
+ _W_8BYTE = 27
20
+ _INDEFINITE = 31
21
+
22
+ _BREAK = 0xFF
23
+
24
+
25
+ def loads(data: bytes) -> Any:
26
+ return _decode(data, 0)[0]
27
+
28
+
29
+ def _read_head(data: bytes, pos: int) -> tuple[int, int, int, int]:
30
+ """Indefinite arg is -1."""
31
+ initial = data[pos]
32
+ major = initial >> 5
33
+ ai = initial & 0x1F
34
+ pos += 1
35
+ if ai < _W_1BYTE:
36
+ return major, ai, ai, pos
37
+ if ai == _W_1BYTE:
38
+ return major, ai, data[pos], pos + 1
39
+ if ai == _W_2BYTE:
40
+ return major, ai, int.from_bytes(data[pos : pos + 2], "big"), pos + 2
41
+ if ai == _W_4BYTE:
42
+ return major, ai, int.from_bytes(data[pos : pos + 4], "big"), pos + 4
43
+ if ai == _W_8BYTE:
44
+ return major, ai, int.from_bytes(data[pos : pos + 8], "big"), pos + 8
45
+ if ai == _INDEFINITE:
46
+ return major, ai, -1, pos
47
+ raise ValueError(f"Reserved CBOR additional-information value: {ai}")
48
+
49
+
50
+ def _decode(data: bytes, pos: int) -> tuple[Any, int]:
51
+ major, ai, arg, pos = _read_head(data, pos)
52
+ if major == 0:
53
+ return arg, pos
54
+ if major == 1:
55
+ return -1 - arg, pos
56
+ if major == 2:
57
+ return _decode_bytes(data, pos, arg, major)
58
+ if major == 3:
59
+ raw, pos = _decode_bytes(data, pos, arg, major)
60
+ return raw.decode("utf-8"), pos
61
+ if major == 4:
62
+ return _decode_array(data, pos, arg)
63
+ if major == 5:
64
+ return _decode_map(data, pos, arg)
65
+ if major == 6:
66
+ return _decode_tagged(data, pos, arg)
67
+ return _decode_simple(data, ai, arg, pos)
68
+
69
+
70
+ def _decode_bytes(data: bytes, pos: int, arg: int, major: int) -> tuple[bytes, int]:
71
+ if arg != -1:
72
+ return data[pos : pos + arg], pos + arg
73
+ # Indefinite-length string: concatenation of definite-length chunks until break.
74
+ chunks = bytearray()
75
+ while data[pos] != _BREAK:
76
+ _cmajor, _cai, clen, pos = _read_head(data, pos)
77
+ chunks += data[pos : pos + clen]
78
+ pos += clen
79
+ return bytes(chunks), pos + 1
80
+
81
+
82
+ def _decode_array(data: bytes, pos: int, arg: int) -> tuple[list[Any], int]:
83
+ items: list[Any] = []
84
+ if arg == -1:
85
+ while data[pos] != _BREAK:
86
+ value, pos = _decode(data, pos)
87
+ items.append(value)
88
+ return items, pos + 1
89
+ for _ in range(arg):
90
+ value, pos = _decode(data, pos)
91
+ items.append(value)
92
+ return items, pos
93
+
94
+
95
+ def _decode_map(data: bytes, pos: int, arg: int) -> tuple[dict[Any, Any], int]:
96
+ result: dict[Any, Any] = {}
97
+ if arg == -1:
98
+ while data[pos] != _BREAK:
99
+ key, pos = _decode(data, pos)
100
+ value, pos = _decode(data, pos)
101
+ result[key] = value
102
+ return result, pos + 1
103
+ for _ in range(arg):
104
+ key, pos = _decode(data, pos)
105
+ value, pos = _decode(data, pos)
106
+ result[key] = value
107
+ return result, pos
108
+
109
+
110
+ def _decode_tagged(data: bytes, pos: int, tag: int) -> tuple[Any, int]:
111
+ if tag in (_TAG_UNSIGNED_BIGNUM, _TAG_NEGATIVE_BIGNUM):
112
+ magnitude_bytes, pos = _decode(data, pos)
113
+ magnitude = int.from_bytes(magnitude_bytes, "big")
114
+ return (magnitude if tag == _TAG_UNSIGNED_BIGNUM else -1 - magnitude), pos
115
+ if tag == _TAG_DECIMAL_FRACTION:
116
+ array, pos = _decode(data, pos)
117
+ exponent, mantissa = array
118
+ return Decimal(mantissa).scaleb(exponent), pos
119
+ if tag == _TAG_EPOCH:
120
+ seconds, pos = _decode(data, pos)
121
+ return datetime.datetime.fromtimestamp(seconds, tz=datetime.UTC), pos
122
+ # Unknown tag: return the tagged content unchanged.
123
+ return _decode(data, pos)
124
+
125
+
126
+ def _decode_simple(data: bytes, ai: int, arg: int, pos: int) -> tuple[Any, int]:
127
+ if ai == 20:
128
+ return False, pos
129
+ if ai == 21:
130
+ return True, pos
131
+ if ai in (22, 23):
132
+ return None, pos
133
+ if ai == 25:
134
+ return _decode_half(arg), pos
135
+ if ai == 26:
136
+ return _struct.unpack(">f", arg.to_bytes(4, "big"))[0], pos
137
+ if ai == 27:
138
+ return _struct.unpack(">d", arg.to_bytes(8, "big"))[0], pos
139
+ return arg, pos
140
+
141
+
142
+ def _decode_half(bits: int) -> float:
143
+ sign = (bits >> 15) & 0x1
144
+ exp = (bits >> 10) & 0x1F
145
+ mant = bits & 0x3FF
146
+ if exp == 0:
147
+ value = (mant / 1024.0) * (2.0**-14)
148
+ elif exp == 0x1F:
149
+ value = float("inf") if mant == 0 else float("nan")
150
+ else:
151
+ value = (1.0 + mant / 1024.0) * (2.0 ** (exp - 15))
152
+ return -value if sign else value
@@ -0,0 +1,115 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Cached schema metadata used by the CBOR codec."""
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from smithy_core.schemas import Schema, SchemaExtension
8
+ from smithy_core.shapes import ShapeType
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class CBORMemberLookup:
13
+ """Matches raw UTF-8 CBOR field names to member schemas."""
14
+
15
+ ordered_schemas: tuple[Schema, ...]
16
+ ordered_name_bytes: tuple[bytes, ...]
17
+ entries_by_name: dict[bytes, Schema]
18
+
19
+ def lookup(
20
+ self,
21
+ data: bytes,
22
+ start: int,
23
+ end: int,
24
+ expected_next: int,
25
+ ) -> Schema | None:
26
+ """Look up a member without decoding its field name to a string."""
27
+ name_length = end - start
28
+ if 0 <= expected_next < len(self.ordered_name_bytes):
29
+ expected = self.ordered_name_bytes[expected_next]
30
+ if len(expected) == name_length and data.startswith(expected, start, end):
31
+ return self.ordered_schemas[expected_next]
32
+
33
+ return self.entries_by_name.get(data[start:end])
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class CBORSchemaMetadata:
38
+ """Precomputed CBOR data stored on a schema."""
39
+
40
+ member_name_bytes: bytes | None = None
41
+ member_lookup: CBORMemberLookup | None = None
42
+ field_name_table: tuple[bytes | None, ...] = ()
43
+ member_count: int = 0
44
+ struct_header_width: int = 1
45
+
46
+
47
+ def _encode_member_name(name: str) -> bytes:
48
+ encoded = name.encode("utf-8")
49
+ length = len(encoded)
50
+ if length < 24:
51
+ return bytes((0x60 | length,)) + encoded
52
+ if length < 0x100:
53
+ return b"\x78" + bytes((length,)) + encoded
54
+ if length < 0x10000:
55
+ return b"\x79" + length.to_bytes(2, "big") + encoded
56
+ if length < 0x100000000:
57
+ return b"\x7a" + length.to_bytes(4, "big") + encoded
58
+ return b"\x7b" + length.to_bytes(8, "big") + encoded
59
+
60
+
61
+ def _head_width(arg: int) -> int:
62
+ if arg < 24:
63
+ return 1
64
+ if arg < 0x100:
65
+ return 2
66
+ if arg < 0x10000:
67
+ return 3
68
+ if arg < 0x100000000:
69
+ return 5
70
+ return 9
71
+
72
+
73
+ def _build_cbor_schema_metadata(schema: Schema) -> CBORSchemaMetadata | None:
74
+ if schema.member_target is not None:
75
+ return CBORSchemaMetadata(
76
+ member_name_bytes=_encode_member_name(schema.expect_member_name())
77
+ )
78
+
79
+ if schema.shape_type not in (ShapeType.STRUCTURE, ShapeType.UNION):
80
+ return None
81
+
82
+ members = tuple(
83
+ sorted(schema.members.values(), key=lambda member: member.expect_member_index())
84
+ )
85
+ member_count = len(members)
86
+ if not members:
87
+ return CBORSchemaMetadata()
88
+
89
+ max_index = max(member.expect_member_index() for member in members)
90
+ field_name_table: list[bytes | None] = [None] * (max_index + 1)
91
+ ordered_name_bytes: list[bytes] = []
92
+ entries_by_name: dict[bytes, Schema] = {}
93
+
94
+ for member in members:
95
+ raw_name = member.expect_member_name().encode("utf-8")
96
+ ordered_name_bytes.append(raw_name)
97
+ entries_by_name[raw_name] = member
98
+ field_name_table[member.expect_member_index()] = _encode_member_name(
99
+ member.expect_member_name()
100
+ )
101
+
102
+ return CBORSchemaMetadata(
103
+ member_lookup=CBORMemberLookup(
104
+ ordered_schemas=members,
105
+ ordered_name_bytes=tuple(ordered_name_bytes),
106
+ entries_by_name=entries_by_name,
107
+ ),
108
+ field_name_table=tuple(field_name_table),
109
+ member_count=member_count,
110
+ struct_header_width=_head_width(member_count),
111
+ )
112
+
113
+
114
+ CBOR_SCHEMA_EXTENSION = SchemaExtension(_build_cbor_schema_metadata)
115
+ """Shared descriptor for lazily cached CBOR schema metadata."""
@@ -0,0 +1,357 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """CBOR shape serializer.
4
+
5
+ Aggregates are definite-length: lists and maps take their element count up front, and
6
+ structs reserve a header slot sized for the schema's max member count, then patch the
7
+ actual present-member count into it on close.
8
+ """
9
+
10
+ import datetime
11
+ import struct as _struct
12
+ from collections.abc import Callable
13
+ from contextlib import AbstractContextManager, nullcontext
14
+ from decimal import Decimal
15
+ from io import BytesIO
16
+ from types import TracebackType
17
+ from typing import Self, override
18
+
19
+ from smithy_core.documents import Document
20
+ from smithy_core.interfaces import BytesWriter
21
+ from smithy_core.schemas import Schema
22
+ from smithy_core.serializers import MapSerializer, ShapeSerializer
23
+ from smithy_core.shapes import ShapeType
24
+ from smithy_core.utils import ensure_utc
25
+
26
+ from ..settings import CBORSettings
27
+
28
+ # CBOR major types, shifted into the high 3 bits of the initial byte.
29
+ _MAJOR_UINT = 0 << 5
30
+ _MAJOR_NEGINT = 1 << 5
31
+ _MAJOR_BYTES = 2 << 5
32
+ _MAJOR_TEXT = 3 << 5
33
+ _MAJOR_ARRAY = 4 << 5
34
+ _MAJOR_MAP = 5 << 5
35
+ _MAJOR_TAG = 6 << 5
36
+ _MAJOR_SIMPLE = 7 << 5
37
+
38
+ # Tags used by rpcv2Cbor.
39
+ _TAG_EPOCH = 1
40
+ _TAG_UNSIGNED_BIGNUM = 2
41
+ _TAG_NEGATIVE_BIGNUM = 3
42
+ _TAG_DECIMAL_FRACTION = 4
43
+
44
+ # Simple values (major type 7).
45
+ _SIMPLE_FALSE = 20
46
+ _SIMPLE_TRUE = 21
47
+ _SIMPLE_NULL = 22
48
+
49
+ # Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
50
+ _W_1BYTE = 24
51
+ _W_2BYTE = 25
52
+ _W_4BYTE = 26
53
+ _W_8BYTE = 27
54
+
55
+
56
+ class CBORShapeSerializer(ShapeSerializer):
57
+ def __init__(self, sink: BytesWriter, settings: CBORSettings) -> None:
58
+ self._sink = sink
59
+ self.settings = settings
60
+
61
+ def write(self, data: bytes) -> None:
62
+ self._sink.write(data)
63
+
64
+ @override
65
+ def begin_struct(self, schema: Schema) -> AbstractContextManager[ShapeSerializer]:
66
+ return _CBORStructSerializer(self, self._sink, schema)
67
+
68
+ @override
69
+ def begin_list(
70
+ self, schema: Schema, size: int
71
+ ) -> AbstractContextManager[ShapeSerializer]:
72
+ # A definite-length list needs no per-element framing or close step, and a value
73
+ # encodes identically inside a collection or not, so elements reuse this same
74
+ # serializer; only the header is context-specific.
75
+ self.write(_encode_head(_MAJOR_ARRAY, size))
76
+ return nullcontext(self)
77
+
78
+ @override
79
+ def begin_map(
80
+ self, schema: Schema, size: int
81
+ ) -> AbstractContextManager[MapSerializer]:
82
+ self.write(_encode_head(_MAJOR_MAP, size))
83
+ return nullcontext(_CBORMapSerializer(self))
84
+
85
+ @override
86
+ def write_null(self, schema: Schema) -> None:
87
+ self.write(bytes((_MAJOR_SIMPLE | _SIMPLE_NULL,)))
88
+
89
+ @override
90
+ def write_boolean(self, schema: Schema, value: bool) -> None:
91
+ self.write(bytes((_MAJOR_SIMPLE | (_SIMPLE_TRUE if value else _SIMPLE_FALSE),)))
92
+
93
+ @override
94
+ def write_integer(self, schema: Schema, value: int) -> None:
95
+ self.write(_encode_int(value))
96
+
97
+ @override
98
+ def write_float(self, schema: Schema, value: float) -> None:
99
+ # A `float` shape encodes as IEEE single (major 7, add_info 26); `double` as
100
+ # IEEE double (add_info 27). Half-precision is not emitted. NaN/Infinity
101
+ # round-trip through struct.pack unchanged.
102
+ if schema.shape_type is ShapeType.FLOAT:
103
+ self.write(bytes((_MAJOR_SIMPLE | 26,)) + _struct.pack(">f", value))
104
+ else:
105
+ self.write(bytes((_MAJOR_SIMPLE | 27,)) + _struct.pack(">d", value))
106
+
107
+ @override
108
+ def write_big_integer(self, schema: Schema, value: int) -> None:
109
+ if value >= 0:
110
+ tag, magnitude = _TAG_UNSIGNED_BIGNUM, value
111
+ else:
112
+ tag, magnitude = _TAG_NEGATIVE_BIGNUM, -1 - value
113
+ length = (magnitude.bit_length() + 7) // 8
114
+ payload = magnitude.to_bytes(length, "big")
115
+ self.write(_encode_head(_MAJOR_TAG, tag))
116
+ self.write(_encode_head(_MAJOR_BYTES, len(payload)))
117
+ self.write(payload)
118
+
119
+ @override
120
+ def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
121
+ sign, digits, exponent = value.as_tuple()
122
+ if not isinstance(exponent, int): # "n"/"N"/"F" for special values
123
+ raise ValueError(f"Cannot CBOR-encode non-finite Decimal: {value!r}")
124
+ mantissa = int("".join(map(str, digits)) or "0")
125
+ if sign:
126
+ mantissa = -mantissa
127
+ self.write(_encode_head(_MAJOR_TAG, _TAG_DECIMAL_FRACTION))
128
+ self.write(_encode_head(_MAJOR_ARRAY, 2))
129
+ self.write(_encode_int(exponent))
130
+ self.write(_encode_int(mantissa))
131
+
132
+ @override
133
+ def write_string(self, schema: Schema, value: str) -> None:
134
+ encoded = value.encode("utf-8")
135
+ self.write(_encode_head(_MAJOR_TEXT, len(encoded)))
136
+ self.write(encoded)
137
+
138
+ @override
139
+ def write_blob(self, schema: Schema, value: bytes) -> None:
140
+ self.write(_encode_head(_MAJOR_BYTES, len(value)))
141
+ self.write(value)
142
+
143
+ @override
144
+ def write_timestamp(self, schema: Schema, value: datetime.datetime) -> None:
145
+ # timestampFormat MUST NOT be respected.
146
+ self.write(_encode_head(_MAJOR_TAG, _TAG_EPOCH))
147
+ self.write_float(schema, ensure_utc(value).timestamp())
148
+
149
+ @override
150
+ def write_document(self, schema: Schema, value: Document) -> None:
151
+ raise NotImplementedError(
152
+ "The rpcv2Cbor protocol does not support document types."
153
+ )
154
+
155
+ @override
156
+ def flush(self) -> None:
157
+ # BytesWriter has no flush; the sink is written to directly.
158
+ pass
159
+
160
+
161
+ def _encode_head(major: int, arg: int) -> bytes:
162
+ """Uses the smallest of the 1/2/4/8-byte length forms per RFC 8949 §3."""
163
+ if arg < _W_1BYTE:
164
+ return bytes((major | arg,))
165
+ if arg < 0x100:
166
+ return bytes((major | _W_1BYTE, arg))
167
+ if arg < 0x10000:
168
+ return bytes((major | _W_2BYTE,)) + arg.to_bytes(2, "big")
169
+ if arg < 0x100000000:
170
+ return bytes((major | _W_4BYTE,)) + arg.to_bytes(4, "big")
171
+ return bytes((major | _W_8BYTE,)) + arg.to_bytes(8, "big")
172
+
173
+
174
+ def _head_width(arg: int) -> int:
175
+ """Bytes _encode_head needs for arg. A struct reserves this for its MAX member count
176
+ up front, then patches the actual (<=) count into that width on close.
177
+ """
178
+ if arg < _W_1BYTE:
179
+ return 1
180
+ if arg < 0x100:
181
+ return 2
182
+ if arg < 0x10000:
183
+ return 3
184
+ if arg < 0x100000000:
185
+ return 5
186
+ return 9
187
+
188
+
189
+ def _write_head_fixed(
190
+ buf: memoryview, offset: int, major: int, arg: int, width: int
191
+ ) -> None:
192
+ """Patches a definite header of exactly `width` bytes into `buf` at `offset`.
193
+
194
+ CBOR allows a non-minimal length form, so an over-reserved width just holds the same
195
+ count in a wider encoding (e.g. 5 as 0xb8 0x05, not the minimal 0xa5) -- no gap.
196
+ """
197
+ if width == 1:
198
+ buf[offset] = major | arg
199
+ return
200
+ add_info = {2: _W_1BYTE, 3: _W_2BYTE, 5: _W_4BYTE, 9: _W_8BYTE}[width]
201
+ buf[offset] = major | add_info
202
+ buf[offset + 1 : offset + width] = arg.to_bytes(width - 1, "big")
203
+
204
+
205
+ def _encode_int(value: int) -> bytes:
206
+ if value >= 0:
207
+ return _encode_head(_MAJOR_UINT, value)
208
+ # Negative integers encode -1 - n as an unsigned argument.
209
+ return _encode_head(_MAJOR_NEGINT, -1 - value)
210
+
211
+
212
+ # Attribute name under which a member schema's pre-encoded CBOR key (text header +
213
+ # UTF-8 name) is memoized. Member schemas are long-lived and shared, so encoding the
214
+ # key once per schema removes a per-serialization encode + head allocation.
215
+ _CBOR_KEY_ATTR = "_cbor_encoded_key"
216
+
217
+
218
+ def _encoded_member_key(schema: Schema) -> bytes:
219
+ key = getattr(schema, _CBOR_KEY_ATTR, None)
220
+ if key is None:
221
+ encoded = schema.expect_member_name().encode("utf-8")
222
+ key = _encode_head(_MAJOR_TEXT, len(encoded)) + encoded
223
+ # Schema is a frozen dataclass; object.__setattr__ is its own escape hatch.
224
+ object.__setattr__(schema, _CBOR_KEY_ATTR, key)
225
+ return key
226
+
227
+
228
+ class _CBORStructSerializer(ShapeSerializer):
229
+ """Emits definite-length map. Reserves the full memberCount worth of byte-width
230
+ and writes the actual member count back to that position when completing the struct.
231
+ This may waste up to 1 byte in edge-case conditions, but minimizes overall work.
232
+ """
233
+
234
+ def __init__(
235
+ self, parent: CBORShapeSerializer, sink: BytesWriter, schema: Schema
236
+ ) -> None:
237
+ self._parent = parent
238
+ # Reserve-and-patch needs a seekable buffer; Codec.serialize always supplies a
239
+ # BytesIO, and rpcv2Cbor has no streaming-serialize path that would not.
240
+ if not isinstance(sink, BytesIO):
241
+ raise TypeError(
242
+ "CBOR struct serialization requires a seekable BytesIO sink."
243
+ )
244
+ self._sink: BytesIO = sink
245
+ self._width = _head_width(len(schema.members))
246
+ self._offset = sink.tell()
247
+ sink.write(bytes(self._width)) # reserved header slot, patched on close
248
+ self._count = 0
249
+
250
+ def __enter__(self) -> Self:
251
+ return self
252
+
253
+ def __exit__(
254
+ self,
255
+ exc_type: type[BaseException] | None,
256
+ exc_value: BaseException | None,
257
+ traceback: TracebackType | None,
258
+ ) -> None:
259
+ if exc_value is not None:
260
+ return
261
+ buf = self._sink.getbuffer()
262
+ _write_head_fixed(buf, self._offset, _MAJOR_MAP, self._count, self._width)
263
+ buf.release()
264
+
265
+ @override
266
+ def begin_struct(self, schema: Schema) -> AbstractContextManager[ShapeSerializer]:
267
+ self._write_key(schema)
268
+ return self._parent.begin_struct(schema)
269
+
270
+ @override
271
+ def begin_list(
272
+ self, schema: Schema, size: int
273
+ ) -> AbstractContextManager[ShapeSerializer]:
274
+ self._write_key(schema)
275
+ return self._parent.begin_list(schema, size)
276
+
277
+ @override
278
+ def begin_map(
279
+ self, schema: Schema, size: int
280
+ ) -> AbstractContextManager[MapSerializer]:
281
+ self._write_key(schema)
282
+ return self._parent.begin_map(schema, size)
283
+
284
+ @override
285
+ def write_null(self, schema: Schema) -> None:
286
+ self._write_key(schema)
287
+ self._parent.write_null(schema)
288
+
289
+ @override
290
+ def write_boolean(self, schema: Schema, value: bool) -> None:
291
+ self._write_key(schema)
292
+ self._parent.write_boolean(schema, value)
293
+
294
+ @override
295
+ def write_integer(self, schema: Schema, value: int) -> None:
296
+ self._write_key(schema)
297
+ self._parent.write_integer(schema, value)
298
+
299
+ @override
300
+ def write_float(self, schema: Schema, value: float) -> None:
301
+ self._write_key(schema)
302
+ self._parent.write_float(schema, value)
303
+
304
+ @override
305
+ def write_big_integer(self, schema: Schema, value: int) -> None:
306
+ self._write_key(schema)
307
+ self._parent.write_big_integer(schema, value)
308
+
309
+ @override
310
+ def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
311
+ self._write_key(schema)
312
+ self._parent.write_big_decimal(schema, value)
313
+
314
+ @override
315
+ def write_string(self, schema: Schema, value: str) -> None:
316
+ self._write_key(schema)
317
+ self._parent.write_string(schema, value)
318
+
319
+ @override
320
+ def write_blob(self, schema: Schema, value: bytes) -> None:
321
+ self._write_key(schema)
322
+ self._parent.write_blob(schema, value)
323
+
324
+ @override
325
+ def write_timestamp(self, schema: Schema, value: datetime.datetime) -> None:
326
+ self._write_key(schema)
327
+ self._parent.write_timestamp(schema, value)
328
+
329
+ @override
330
+ def write_document(self, schema: Schema, value: Document) -> None:
331
+ raise NotImplementedError(
332
+ "The rpcv2Cbor protocol does not support document types."
333
+ )
334
+
335
+ @override
336
+ def flush(self) -> None:
337
+ pass
338
+
339
+ def _write_key(self, schema: Schema) -> None:
340
+ self._count += 1
341
+ self._sink.write(_encoded_member_key(schema))
342
+
343
+
344
+ class _CBORMapSerializer(MapSerializer):
345
+ """The definite-length map header (from `size`) is written by `begin_map` before
346
+ this is constructed; entries stream straight to the sink.
347
+ """
348
+
349
+ def __init__(self, parent: CBORShapeSerializer) -> None:
350
+ self._parent = parent
351
+
352
+ @override
353
+ def entry(self, key: str, value_writer: Callable[[ShapeSerializer], None]) -> None:
354
+ encoded = key.encode("utf-8")
355
+ self._parent.write(_encode_head(_MAJOR_TEXT, len(encoded)))
356
+ self._parent.write(encoded)
357
+ value_writer(self._parent)
@@ -0,0 +1 @@
1
+ Marker
@@ -0,0 +1,12 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class CBORSettings:
8
+ """Configuration for the CBOR codec."""
9
+
10
+ default_namespace: str | None = None
11
+ """Default namespace for resolving a document's discriminator. Not yet consumed:
12
+ the codec does not implement document types."""