localstack-core 4.8.2.dev2__py3-none-any.whl → 4.8.2.dev4__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.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

@@ -14,27 +14,34 @@ The different protocols have many similarities. The class hierarchy is
14
14
  designed such that the serializers share as much logic as possible.
15
15
  The class hierarchy looks as follows:
16
16
  ::
17
- ┌───────────────────┐
18
- │ResponseSerializer │
19
- └───────────────────┘
20
- ▲ ▲
21
- ┌──────────────────────┘ └──────────────────┐
22
- ┌────────────┴────────────┐ ┌────────────┴─────────────┐ ┌─────────┴────────────┐
23
- │BaseXMLResponseSerializer│ BaseRestResponseSerializerJSONResponseSerializer│
24
- └─────────────────────────┘ └──────────────────────────┘ └──────────────────────┘
25
- ▲ ▲
26
- ┌──────────────────────┴─┐ ┌┴─────────────┴──────────┐ ┌┴──────────────┴──────────┐
27
- QueryResponseSerializer RestXMLResponseSerializer RestJSONResponseSerializer│
28
- └────────────────────────┘ └─────────────────────────┘ └──────────────────────────┘
29
-
30
- ┌──────────┴──────────┐
17
+ ┌────────────────────┐
18
+ ResponseSerializer │
19
+ └────────────────────┘
20
+ ▲ ▲
21
+ ┌─────────────────┬───────┘ └──────────────┬──────────────────────┐
22
+ ┌────────────┴────────────┐ │ ┌───────┴──────────────┐ │ ┌────────────┴─────────────┐
23
+ │BaseXMLResponseSerializer│ │JSONResponseSerializer│ │ │BaseCBORResponseSerializer│
24
+ └─────────────────────────┘ │ └──────────────────────┘ │ └──────────────────────────┘
25
+ ▲ ▲ ┌─────────────┴────────────┐ ┌─────┴─────────────────────┐
26
+ │ │ │BaseRestResponseSerializer│ │ │BaseRpcV2ResponseSerializer│ │ │
27
+ └──────────────────────────┘ └───────────────────────────┘
28
+ │ │ ▲ ▲ │ ▲ │ │
29
+ │ │ │ │ │ │ │ │
30
+ │ ┌─┴──────────────┴────────┐ ┌──┴───────────┴───────────┐ ┌──────────┴───────────┴────┐ │
31
+ │ │RestXMLResponseSerializer│ │RestJSONResponseSerializer│ │RpcV2CBORResponseSerializer│ │
32
+ │ └─────────────────────────┘ └──────────────────────────┘ └───────────────────────────┘ │
33
+ ┌─────┴──────────────────┐ ┌──────────┴─────────────┐
34
+ │QueryResponseSerializer │ │ CBORResponseSerializer │
35
+ └────────────────────────┘ └────────────────────────┘
36
+
37
+ ┌─────────┴───────────┐
31
38
  │EC2ResponseSerializer│
32
39
  └─────────────────────┘
33
40
  ::
34
41
 
35
42
  The ``ResponseSerializer`` contains the logic that is used among all the
36
- different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``, and
37
- ``ec2``).
43
+ different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``, ``cbor``
44
+ and ``ec2``).
38
45
  The protocols relate to each other in the following ways:
39
46
 
40
47
  * The ``query`` and the ``rest-xml`` protocols both have XML bodies in their
@@ -42,10 +49,14 @@ The protocols relate to each other in the following ways:
42
49
  type).
43
50
  * The ``json`` and the ``rest-json`` protocols both have JSON bodies in their
44
51
  responses which are serialized the same way.
52
+ * The ``cbor`` protocol is not properly defined in the spec, but mirrors the
53
+ ``json`` protocol.
45
54
  * The ``rest-json`` and ``rest-xml`` protocols serialize some metadata in
46
- the HTTP response's header fields
55
+ the HTTP response's header fields.
47
56
  * The ``ec2`` protocol is basically similar to the ``query`` protocol with a
48
57
  specific error response formatting.
58
+ * The ``smithy-rpc-v2-cbor`` protocol defines a specific way to route request
59
+ to services via the RPC v2 trait, and encodes its body with the CBOR format.
49
60
 
50
61
  The serializer classes in this module correspond directly to the different
51
62
  protocols. ``#create_serializer`` shows the explicit mapping between the
@@ -54,13 +65,23 @@ The classes are structured as follows:
54
65
 
55
66
  * The ``ResponseSerializer`` contains all the basic logic for the
56
67
  serialization which is shared among all different protocols.
57
- * The ``BaseXMLResponseSerializer`` and the ``JSONResponseSerializer``
58
- contain the logic for the XML and the JSON serialization respectively.
68
+ * The ``BaseXMLResponseSerializer``, ``JSONResponseSerializer`` and
69
+ ``BaseCBORResponseSerializer`` contain the logic for the XML, JSON
70
+ and the CBOR serialization respectively.
59
71
  * The ``BaseRestResponseSerializer`` contains the logic for the REST
60
72
  protocol specifics (i.e. specific HTTP header serializations).
73
+ * The ``BaseRpcV2ResponseSerializer`` contains the logic for the RPC v2
74
+ protocol specifics (i.e. pretty bare, does not has any specific
75
+ about body serialization).
61
76
  * The ``RestXMLResponseSerializer`` and the ``RestJSONResponseSerializer``
62
77
  inherit the ReST specific logic from the ``BaseRestResponseSerializer``
63
78
  and the XML / JSON body serialization from their second super class.
79
+ * The ``RpcV2CBORResponseSerializer`` inherits the RPC v2 specific logic
80
+ from the ``BaseRpcV2ResponseSerializer`` and the CBOR body serialization
81
+ from its second super class.
82
+ * The ``CBORResponseSerializer`` contains the logic specific to the
83
+ non-official ``cbor`` protocol, mirroring the ``json`` protocol but
84
+ with CBOR encoded body
64
85
 
65
86
  The services and their protocols are defined by using AWS's Smithy
66
87
  (a language to define services in a - somewhat - protocol-agnostic
@@ -73,21 +94,32 @@ be sent back to the calling client.
73
94
 
74
95
  import abc
75
96
  import base64
97
+ import copy
98
+ import datetime
76
99
  import functools
77
100
  import json
78
101
  import logging
102
+ import math
79
103
  import string
104
+ import struct
80
105
  from abc import ABC
81
106
  from binascii import crc32
82
107
  from collections.abc import Iterable, Iterator
83
- from datetime import datetime
84
108
  from email.utils import formatdate
85
109
  from struct import pack
86
- from typing import Any
110
+ from typing import IO, Any
87
111
  from xml.etree import ElementTree as ETree
88
112
 
89
113
  import xmltodict
90
- from botocore.model import ListShape, MapShape, OperationModel, ServiceModel, Shape, StructureShape
114
+ from botocore.model import (
115
+ ListShape,
116
+ MapShape,
117
+ OperationModel,
118
+ ServiceModel,
119
+ Shape,
120
+ StringShape,
121
+ StructureShape,
122
+ )
91
123
  from botocore.serialize import ISO8601, ISO8601_MICRO
92
124
  from botocore.utils import calculate_md5, is_json_value_header, parse_to_aware_datetime
93
125
 
@@ -506,7 +538,7 @@ class ResponseSerializer(abc.ABC):
506
538
  # Some extra utility methods subclasses can use.
507
539
 
508
540
  @staticmethod
509
- def _timestamp_iso8601(value: datetime) -> str:
541
+ def _timestamp_iso8601(value: datetime.datetime) -> str:
510
542
  if value.microsecond > 0:
511
543
  timestamp_format = ISO8601_MICRO
512
544
  else:
@@ -514,15 +546,17 @@ class ResponseSerializer(abc.ABC):
514
546
  return value.strftime(timestamp_format)
515
547
 
516
548
  @staticmethod
517
- def _timestamp_unixtimestamp(value: datetime) -> float:
549
+ def _timestamp_unixtimestamp(value: datetime.datetime) -> float:
518
550
  return value.timestamp()
519
551
 
520
- def _timestamp_rfc822(self, value: datetime) -> str:
521
- if isinstance(value, datetime):
552
+ def _timestamp_rfc822(self, value: datetime.datetime) -> str:
553
+ if isinstance(value, datetime.datetime):
522
554
  value = self._timestamp_unixtimestamp(value)
523
555
  return formatdate(value, usegmt=True)
524
556
 
525
- def _convert_timestamp_to_str(self, value: int | str | datetime, timestamp_format=None) -> str:
557
+ def _convert_timestamp_to_str(
558
+ self, value: int | str | datetime.datetime, timestamp_format=None
559
+ ) -> str:
526
560
  if timestamp_format is None:
527
561
  timestamp_format = self.TIMESTAMP_FORMAT
528
562
  timestamp_format = timestamp_format.lower()
@@ -1407,6 +1441,459 @@ class RestJSONResponseSerializer(BaseRestResponseSerializer, JSONResponseSeriali
1407
1441
  serialized.headers["Content-Type"] = mime_type
1408
1442
 
1409
1443
 
1444
+ class BaseCBORResponseSerializer(ResponseSerializer):
1445
+ """
1446
+ The ``BaseCBORResponseSerializer`` performs the basic logic for the CBOR response serialization.
1447
+
1448
+ There are two types of map/list in CBOR, indefinite length types and "defined" ones:
1449
+ You can use the `\xbf` byte marker to indicate a map with indefinite length, then `\xff` to indicate the end
1450
+ of the map.
1451
+ You can also use, for example, `\xa4` to indicate a map with exactly 4 things in it, so `\xff` is not
1452
+ required at the end.
1453
+ AWS, for both Kinesis and `smithy-rpc-v2-cbor` services, is using indefinite data structures when returning
1454
+ responses.
1455
+ """
1456
+
1457
+ SUPPORTED_MIME_TYPES = [APPLICATION_CBOR, APPLICATION_AMZ_CBOR_1_1]
1458
+
1459
+ UNSIGNED_INT_MAJOR_TYPE = 0
1460
+ NEGATIVE_INT_MAJOR_TYPE = 1
1461
+ BLOB_MAJOR_TYPE = 2
1462
+ STRING_MAJOR_TYPE = 3
1463
+ LIST_MAJOR_TYPE = 4
1464
+ MAP_MAJOR_TYPE = 5
1465
+ TAG_MAJOR_TYPE = 6
1466
+ FLOAT_AND_SIMPLE_MAJOR_TYPE = 7
1467
+
1468
+ INDEFINITE_ITEM_ADDITIONAL_INFO = 31
1469
+ BREAK_CODE = b"\xff"
1470
+ USE_INDEFINITE_DATA_STRUCTURE = True
1471
+
1472
+ def _serialize_data_item(
1473
+ self, serialized: bytearray, value: Any, shape: Shape | None, name: str | None = None
1474
+ ) -> None:
1475
+ method = getattr(self, f"_serialize_type_{shape.type_name}")
1476
+ if method is None:
1477
+ raise ValueError(
1478
+ f"Unrecognized C2J type: {shape.type_name}, unable to serialize request"
1479
+ )
1480
+ method(serialized, value, shape, name)
1481
+
1482
+ def _serialize_type_integer(
1483
+ self, serialized: bytearray, value: int, shape: Shape | None, name: str | None = None
1484
+ ) -> None:
1485
+ if value >= 0:
1486
+ major_type = self.UNSIGNED_INT_MAJOR_TYPE
1487
+ else:
1488
+ major_type = self.NEGATIVE_INT_MAJOR_TYPE
1489
+ # The only differences in serializing negative and positive integers is
1490
+ # that for negative, we set the major type to 1 and set the value to -1
1491
+ # minus the value
1492
+ value = -1 - value
1493
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(value)
1494
+ initial_byte = self._get_initial_byte(major_type, additional_info)
1495
+ if num_bytes == 0:
1496
+ serialized.extend(initial_byte)
1497
+ else:
1498
+ serialized.extend(initial_byte + value.to_bytes(num_bytes, "big"))
1499
+
1500
+ def _serialize_type_long(
1501
+ self, serialized: bytearray, value: int, shape: Shape, name: str | None = None
1502
+ ) -> None:
1503
+ self._serialize_type_integer(serialized, value, shape, name)
1504
+
1505
+ def _serialize_type_blob(
1506
+ self,
1507
+ serialized: bytearray,
1508
+ value: str | bytes | IO[bytes],
1509
+ shape: Shape | None,
1510
+ name: str | None = None,
1511
+ ) -> None:
1512
+ if isinstance(value, str):
1513
+ value = value.encode("utf-8")
1514
+ elif not isinstance(value, (bytes, bytearray)):
1515
+ # We support file-like objects for blobs; these already have been
1516
+ # validated to ensure they have a read method
1517
+ value = value.read()
1518
+ length = len(value)
1519
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1520
+ initial_byte = self._get_initial_byte(self.BLOB_MAJOR_TYPE, additional_info)
1521
+ if num_bytes == 0:
1522
+ serialized.extend(initial_byte)
1523
+ else:
1524
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
1525
+ serialized.extend(value)
1526
+
1527
+ def _serialize_type_string(
1528
+ self, serialized: bytearray, value: str, shape: Shape | None, name: str | None = None
1529
+ ) -> None:
1530
+ encoded = value.encode("utf-8")
1531
+ length = len(encoded)
1532
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1533
+ initial_byte = self._get_initial_byte(self.STRING_MAJOR_TYPE, additional_info)
1534
+ if num_bytes == 0:
1535
+ serialized.extend(initial_byte + encoded)
1536
+ else:
1537
+ serialized.extend(initial_byte + length.to_bytes(num_bytes, "big") + encoded)
1538
+
1539
+ def _serialize_type_list(
1540
+ self, serialized: bytearray, value: list, shape: Shape | None, name: str | None = None
1541
+ ) -> None:
1542
+ initial_bytes, closing_bytes = self._get_bytes_for_data_structure(
1543
+ value, self.LIST_MAJOR_TYPE
1544
+ )
1545
+ serialized.extend(initial_bytes)
1546
+
1547
+ for item in value:
1548
+ self._serialize_data_item(serialized, item, shape.member)
1549
+
1550
+ if closing_bytes is not None:
1551
+ serialized.extend(closing_bytes)
1552
+
1553
+ def _serialize_type_map(
1554
+ self, serialized: bytearray, value: dict, shape: Shape | None, name: str | None = None
1555
+ ) -> None:
1556
+ initial_bytes, closing_bytes = self._get_bytes_for_data_structure(
1557
+ value, self.MAP_MAJOR_TYPE
1558
+ )
1559
+ serialized.extend(initial_bytes)
1560
+
1561
+ for key_item, item in value.items():
1562
+ self._serialize_data_item(serialized, key_item, shape.key)
1563
+ self._serialize_data_item(serialized, item, shape.value)
1564
+
1565
+ if closing_bytes is not None:
1566
+ serialized.extend(closing_bytes)
1567
+
1568
+ def _serialize_type_structure(
1569
+ self, serialized: bytearray, value: dict, shape: Shape | None, name: str | None = None
1570
+ ) -> None:
1571
+ if name is not None:
1572
+ # For nested structures, we need to serialize the key first
1573
+ self._serialize_data_item(serialized, name, shape.key_shape)
1574
+
1575
+ # Remove `None` values from the dictionary
1576
+ value = {k: v for k, v in value.items() if v is not None}
1577
+
1578
+ initial_bytes, closing_bytes = self._get_bytes_for_data_structure(
1579
+ value, self.MAP_MAJOR_TYPE
1580
+ )
1581
+ serialized.extend(initial_bytes)
1582
+
1583
+ members = shape.members
1584
+ for member_key, member_value in value.items():
1585
+ member_shape = members[member_key]
1586
+ if "name" in member_shape.serialization:
1587
+ member_key = member_shape.serialization["name"]
1588
+ if member_value is not None:
1589
+ self._serialize_type_string(serialized, member_key, None, None)
1590
+ self._serialize_data_item(serialized, member_value, member_shape)
1591
+
1592
+ if closing_bytes is not None:
1593
+ serialized.extend(closing_bytes)
1594
+
1595
+ def _serialize_type_timestamp(
1596
+ self,
1597
+ serialized: bytearray,
1598
+ value: int | str | datetime.datetime,
1599
+ shape: Shape | None,
1600
+ name: str | None = None,
1601
+ ) -> None:
1602
+ # https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html#timestamp-type-serialization
1603
+ tag = 1 # Use tag 1 for unix timestamp
1604
+ initial_byte = self._get_initial_byte(self.TAG_MAJOR_TYPE, tag)
1605
+ serialized.extend(initial_byte) # Tagging the timestamp
1606
+
1607
+ # we encode the timestamp as a double, like the Go SDK
1608
+ # https://github.com/aws/aws-sdk-go-v2/blob/5d7c17325a2581afae4455c150549174ebfd9428/internal/protocoltest/smithyrpcv2cbor/serializers.go#L664-L669
1609
+ # Currently, the Botocore serializer using unsigned integers, but it does not conform to the Smithy specs:
1610
+ # > This protocol uses epoch-seconds, also known as Unix timestamps, with millisecond
1611
+ # > (1/1000th of a second) resolution.
1612
+ timestamp = float(self._convert_timestamp_to_str(value))
1613
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 27)
1614
+ serialized.extend(initial_byte + struct.pack(">d", timestamp))
1615
+
1616
+ def _serialize_type_float(
1617
+ self, serialized: bytearray, value: float, shape: Shape | None, name: str | None = None
1618
+ ) -> None:
1619
+ if self._is_special_number(value):
1620
+ serialized.extend(
1621
+ self._get_bytes_for_special_numbers(value)
1622
+ ) # Handle special values like NaN or Infinity
1623
+ else:
1624
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 26)
1625
+ serialized.extend(initial_byte + struct.pack(">f", value))
1626
+
1627
+ def _serialize_type_double(
1628
+ self, serialized: bytearray, value: float, shape: Shape | None, name: str | None = None
1629
+ ) -> None:
1630
+ if self._is_special_number(value):
1631
+ serialized.extend(
1632
+ self._get_bytes_for_special_numbers(value)
1633
+ ) # Handle special values like NaN or Infinity
1634
+ else:
1635
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 27)
1636
+ serialized.extend(initial_byte + struct.pack(">d", value))
1637
+
1638
+ def _serialize_type_boolean(
1639
+ self, serialized: bytearray, value: bool, shape: Shape | None, name: str | None = None
1640
+ ) -> None:
1641
+ additional_info = 21 if value else 20
1642
+ serialized.extend(self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info))
1643
+
1644
+ @staticmethod
1645
+ def _get_additional_info_and_num_bytes(value: int) -> tuple[int, int]:
1646
+ # Values under 24 can be stored in the initial byte and don't need further
1647
+ # encoding
1648
+ if value < 24:
1649
+ return value, 0
1650
+ # Values between 24 and 255 (inclusive) can be stored in 1 byte and
1651
+ # correspond to additional info 24
1652
+ elif value < 256:
1653
+ return 24, 1
1654
+ # Values up to 65535 can be stored in two bytes and correspond to additional
1655
+ # info 25
1656
+ elif value < 65536:
1657
+ return 25, 2
1658
+ # Values up to 4294967296 can be stored in four bytes and correspond to
1659
+ # additional info 26
1660
+ elif value < 4294967296:
1661
+ return 26, 4
1662
+ # The maximum number of bytes in a definite length data items is 8 which
1663
+ # to additional info 27
1664
+ else:
1665
+ return 27, 8
1666
+
1667
+ def _get_initial_byte(self, major_type: int, additional_info: int) -> bytes:
1668
+ # The highest order three bits are the major type, so we need to bitshift the
1669
+ # major type by 5
1670
+ major_type_bytes = major_type << 5
1671
+ return (major_type_bytes | additional_info).to_bytes(1, "big")
1672
+
1673
+ @staticmethod
1674
+ def _is_special_number(value: int | float) -> bool:
1675
+ return any(
1676
+ [
1677
+ value == float("inf"),
1678
+ value == float("-inf"),
1679
+ math.isnan(value),
1680
+ ]
1681
+ )
1682
+
1683
+ def _get_bytes_for_special_numbers(self, value: int | float) -> bytes:
1684
+ additional_info = 25
1685
+ initial_byte = self._get_initial_byte(self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info)
1686
+ if value == float("inf"):
1687
+ return initial_byte + struct.pack(">H", 0x7C00)
1688
+ elif value == float("-inf"):
1689
+ return initial_byte + struct.pack(">H", 0xFC00)
1690
+ elif math.isnan(value):
1691
+ return initial_byte + struct.pack(">H", 0x7E00)
1692
+
1693
+ def _get_bytes_for_data_structure(
1694
+ self, value: list | dict, major_type: int
1695
+ ) -> tuple[bytes, bytes | None]:
1696
+ if self.USE_INDEFINITE_DATA_STRUCTURE:
1697
+ additional_info = self.INDEFINITE_ITEM_ADDITIONAL_INFO
1698
+ return self._get_initial_byte(major_type, additional_info), self.BREAK_CODE
1699
+ else:
1700
+ length = len(value)
1701
+ additional_info, num_bytes = self._get_additional_info_and_num_bytes(length)
1702
+ initial_byte = self._get_initial_byte(major_type, additional_info)
1703
+ if num_bytes != 0:
1704
+ initial_byte = initial_byte + length.to_bytes(num_bytes, "big")
1705
+
1706
+ return initial_byte, None
1707
+
1708
+
1709
+ class CBORResponseSerializer(BaseCBORResponseSerializer):
1710
+ """
1711
+ The ``CBORResponseSerializer`` is responsible for the serialization of responses from services with the ``cbor``
1712
+ protocol. It implements the CBOR response body serialization, which is only currently used by Kinesis and is derived
1713
+ conceptually from the ``JSONResponseSerializer``
1714
+ """
1715
+
1716
+ TIMESTAMP_FORMAT = "unixtimestamp"
1717
+
1718
+ def _serialize_error(
1719
+ self,
1720
+ error: ServiceException,
1721
+ response: Response,
1722
+ shape: StructureShape,
1723
+ operation_model: OperationModel,
1724
+ mime_type: str,
1725
+ request_id: str,
1726
+ ) -> None:
1727
+ body = bytearray()
1728
+ response.content_type = mime_type
1729
+ response.headers["X-Amzn-Errortype"] = error.code
1730
+
1731
+ if shape:
1732
+ # FIXME: we need to manually add the `__type` field to the shape as it is not part of the specs
1733
+ # think about a better way, this is very hacky
1734
+ shape_copy = copy.deepcopy(shape)
1735
+ shape_copy.members["__type"] = StringShape(
1736
+ shape_name="__type", shape_model={"type": "string"}
1737
+ )
1738
+ remaining_params = {"__type": error.code}
1739
+
1740
+ for member_name in shape_copy.members:
1741
+ if hasattr(error, member_name):
1742
+ remaining_params[member_name] = getattr(error, member_name)
1743
+ # Default error message fields can sometimes have different casing in the specs
1744
+ elif member_name.lower() in ["code", "message"] and hasattr(
1745
+ error, member_name.lower()
1746
+ ):
1747
+ remaining_params[member_name] = getattr(error, member_name.lower())
1748
+
1749
+ self._serialize_data_item(body, remaining_params, shape_copy, None)
1750
+
1751
+ response.set_response(bytes(body))
1752
+
1753
+ def _serialize_response(
1754
+ self,
1755
+ parameters: dict,
1756
+ response: Response,
1757
+ shape: Shape | None,
1758
+ shape_members: dict,
1759
+ operation_model: OperationModel,
1760
+ mime_type: str,
1761
+ request_id: str,
1762
+ ) -> None:
1763
+ response.content_type = mime_type
1764
+ response.set_response(
1765
+ self._serialize_body_params(parameters, shape, operation_model, mime_type, request_id)
1766
+ )
1767
+
1768
+ def _serialize_body_params(
1769
+ self,
1770
+ params: dict,
1771
+ shape: Shape,
1772
+ operation_model: OperationModel,
1773
+ mime_type: str,
1774
+ request_id: str,
1775
+ ) -> bytes | None:
1776
+ body = bytearray()
1777
+ self._serialize_data_item(body, params, shape)
1778
+ return bytes(body)
1779
+
1780
+ def _prepare_additional_traits_in_response(
1781
+ self, response: Response, operation_model: OperationModel, request_id: str
1782
+ ) -> Response:
1783
+ response.headers["x-amzn-requestid"] = request_id
1784
+ response = super()._prepare_additional_traits_in_response(
1785
+ response, operation_model, request_id
1786
+ )
1787
+ return response
1788
+
1789
+
1790
+ class BaseRpcV2ResponseSerializer(ResponseSerializer):
1791
+ """
1792
+ The BaseRpcV2ResponseSerializer performs the basic logic for the RPC V2 response serialization.
1793
+ The only variance between the various RPCv2 protocols is the way the body is serialized for regular responses,
1794
+ and the way they will encode exceptions.
1795
+ """
1796
+
1797
+ def _serialize_response(
1798
+ self,
1799
+ parameters: dict,
1800
+ response: Response,
1801
+ shape: Shape | None,
1802
+ shape_members: dict,
1803
+ operation_model: OperationModel,
1804
+ mime_type: str,
1805
+ request_id: str,
1806
+ ) -> None:
1807
+ response.content_type = mime_type
1808
+ response.set_response(
1809
+ self._serialize_body_params(parameters, shape, operation_model, mime_type, request_id)
1810
+ )
1811
+
1812
+ def _serialize_body_params(
1813
+ self,
1814
+ params: dict,
1815
+ shape: Shape,
1816
+ operation_model: OperationModel,
1817
+ mime_type: str,
1818
+ request_id: str,
1819
+ ) -> bytes | None:
1820
+ raise NotImplementedError
1821
+
1822
+
1823
+ class RpcV2CBORResponseSerializer(BaseRpcV2ResponseSerializer, BaseCBORResponseSerializer):
1824
+ """
1825
+ The RpcV2CBORResponseSerializer implements the CBOR body serialization part for the RPC v2 protocol, and implements the
1826
+ specific exception serialization.
1827
+ https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
1828
+ """
1829
+
1830
+ # the Smithy spec defines that only `application/cbor` is supported for RPC v2 CBOR
1831
+ SUPPORTED_MIME_TYPES = [APPLICATION_CBOR]
1832
+ TIMESTAMP_FORMAT = "unixtimestamp"
1833
+
1834
+ def _serialize_body_params(
1835
+ self,
1836
+ params: dict,
1837
+ shape: Shape,
1838
+ operation_model: OperationModel,
1839
+ mime_type: str,
1840
+ request_id: str,
1841
+ ) -> bytes | None:
1842
+ body = bytearray()
1843
+ self._serialize_data_item(body, params, shape)
1844
+ return bytes(body)
1845
+
1846
+ def _serialize_error(
1847
+ self,
1848
+ error: ServiceException,
1849
+ response: Response,
1850
+ shape: StructureShape,
1851
+ operation_model: OperationModel,
1852
+ mime_type: str,
1853
+ request_id: str,
1854
+ ) -> None:
1855
+ body = bytearray()
1856
+ response.content_type = mime_type # can only be 'application/cbor'
1857
+ # TODO: the Botocore parser is able to look at the `x-amzn-query-error` header for the RpcV2 CBOR protocol
1858
+ # we'll need to investigate which services need it
1859
+ # Responses for the rpcv2Cbor protocol SHOULD NOT contain the X-Amzn-ErrorType header.
1860
+ # Type information is always serialized in the payload. This is different than `json` protocol
1861
+
1862
+ if shape:
1863
+ # FIXME: we need to manually add the `__type` field to the shape as it is not part of the specs
1864
+ # think about a better way, this is very hacky
1865
+ # Error responses in the rpcv2Cbor protocol MUST be serialized identically to standard responses with one
1866
+ # additional component to distinguish which error is contained: a body field named __type.
1867
+ shape_copy = copy.deepcopy(shape)
1868
+ shape_copy.members["__type"] = StringShape(
1869
+ shape_name="__type", shape_model={"type": "string"}
1870
+ )
1871
+ remaining_params = {"__type": error.code}
1872
+
1873
+ for member_name in shape_copy.members:
1874
+ if hasattr(error, member_name):
1875
+ remaining_params[member_name] = getattr(error, member_name)
1876
+ # Default error message fields can sometimes have different casing in the specs
1877
+ elif member_name.lower() in ["code", "message"] and hasattr(
1878
+ error, member_name.lower()
1879
+ ):
1880
+ remaining_params[member_name] = getattr(error, member_name.lower())
1881
+
1882
+ self._serialize_data_item(body, remaining_params, shape_copy, None)
1883
+
1884
+ response.set_response(bytes(body))
1885
+
1886
+ def _prepare_additional_traits_in_response(
1887
+ self, response: Response, operation_model: OperationModel, request_id: str
1888
+ ):
1889
+ response.headers["x-amzn-requestid"] = request_id
1890
+ response.headers["Smithy-Protocol"] = "rpc-v2-cbor"
1891
+ response = super()._prepare_additional_traits_in_response(
1892
+ response, operation_model, request_id
1893
+ )
1894
+ return response
1895
+
1896
+
1410
1897
  class S3ResponseSerializer(RestXMLResponseSerializer):
1411
1898
  """
1412
1899
  The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
@@ -1573,7 +2060,7 @@ class S3ResponseSerializer(RestXMLResponseSerializer):
1573
2060
  root.attrib["xmlns"] = self.XML_NAMESPACE
1574
2061
 
1575
2062
  @staticmethod
1576
- def _timestamp_iso8601(value: datetime) -> str:
2063
+ def _timestamp_iso8601(value: datetime.datetime) -> str:
1577
2064
  """
1578
2065
  This is very specific to S3, S3 returns an ISO8601 timestamp but with milliseconds always set to 000
1579
2066
  Some SDKs are very picky about the length
@@ -1744,11 +2231,14 @@ def gen_amzn_requestid():
1744
2231
 
1745
2232
 
1746
2233
  @functools.cache
1747
- def create_serializer(service: ServiceModel) -> ResponseSerializer:
2234
+ def create_serializer(
2235
+ service: ServiceModel, protocol: ProtocolName | None = None
2236
+ ) -> ResponseSerializer:
1748
2237
  """
1749
2238
  Creates the right serializer for the given service model.
1750
2239
 
1751
2240
  :param service: to create the serializer for
2241
+ :param protocol: the protocol for the serializer. If not provided, fallback to the service's default protocol
1752
2242
  :return: ResponseSerializer which can handle the protocol of the service
1753
2243
  """
1754
2244
 
@@ -1768,17 +2258,25 @@ def create_serializer(service: ServiceModel) -> ResponseSerializer:
1768
2258
  "rest-json": RestJSONResponseSerializer,
1769
2259
  "rest-xml": RestXMLResponseSerializer,
1770
2260
  "ec2": EC2ResponseSerializer,
2261
+ "smithy-rpc-v2-cbor": RpcV2CBORResponseSerializer,
2262
+ # TODO: implement multi-protocol support for Kinesis, so that it can uses the `cbor` protocol and remove
2263
+ # CBOR handling from JSONResponseParser
2264
+ # this is not an "official" protocol defined from the spec, but is derived from ``json``
1771
2265
  }
2266
+ # TODO: even though our Service Name Parser will only use a protocol that is available for the service, we might
2267
+ # want to verify if the given protocol here is available for that service, in case we are manually calling
2268
+ # this factory. Revisit once we implement multi-protocol support
2269
+ service_protocol = protocol or service.protocol
1772
2270
 
1773
2271
  # Try to select a service- and protocol-specific serializer implementation
1774
2272
  if (
1775
2273
  service.service_name in service_specific_serializers
1776
- and service.protocol in service_specific_serializers[service.service_name]
2274
+ and service_protocol in service_specific_serializers[service.service_name]
1777
2275
  ):
1778
- return service_specific_serializers[service.service_name][service.protocol]()
2276
+ return service_specific_serializers[service.service_name][service_protocol]()
1779
2277
  else:
1780
2278
  # Otherwise, pick the protocol-specific serializer for the protocol of the service
1781
- return protocol_specific_serializers[service.protocol]()
2279
+ return protocol_specific_serializers[service_protocol]()
1782
2280
 
1783
2281
 
1784
2282
  def aws_response_serializer(
localstack/aws/spec.py CHANGED
@@ -21,7 +21,7 @@ from localstack.utils.objects import singleton_factory
21
21
  LOG = logging.getLogger(__name__)
22
22
 
23
23
  ServiceName = str
24
- ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2"]
24
+ ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2", "smithy-rpc-v2-cbor"]
25
25
 
26
26
 
27
27
  class ServiceModelIdentifier(NamedTuple):