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.

@@ -19,22 +19,23 @@ The class hierarchy looks as follows:
19
19
  │RequestParser│
20
20
  └─────────────┘
21
21
  ▲ ▲ ▲
22
- ┌─────────────────┘ │ └────────────────────┐
23
- ┌────────┴─────────┐ ┌─────────┴───────────┐ ┌──────────┴──────────┐
24
- │QueryRequestParser│ │BaseRestRequestParser│ │BaseJSONRequestParser│
25
- └──────────────────┘ └─────────────────────┘ └─────────────────────┘
26
- ▲ ▲ ▲ ▲ ▲
27
- ┌───────┴────────┐ ┌─────────┴──────────┐ │ │
28
- │EC2RequestParser│ │RestXMLRequestParser│ │ │
29
- └────────────────┘ └────────────────────┘ │ │
30
- ┌────────────────┴───┴┐ ┌────────┴────────┐
31
- │RestJSONRequestParser│ │JSONRequestParser│
32
- └─────────────────────┘ └─────────────────┘
22
+ ┌─────────────────┘ │ └────────────────────┬───────────────────────┬───────────────────────┐
23
+ ┌────────┴─────────┐ ┌─────────┴───────────┐ ┌──────────┴──────────┐ ┌──────────┴──────────┐ ┌──────────┴───────────┐
24
+ │QueryRequestParser│ │BaseRestRequestParser│ │BaseJSONRequestParser│ │BaseCBORRequestParser│ │BaseRpcV2RequestParser│
25
+ └──────────────────┘ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ └──────────────────────┘
26
+ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲
27
+ ┌───────┴────────┐ ┌─────────┴──────────┐ │ │ ┌────────┴────────┐ ┌───┴─────────────┴────┐
28
+ │EC2RequestParser│ │RestXMLRequestParser│ │ │ JSONRequestParser│ │ │RpcV2CBORRequestParser│
29
+ └────────────────┘ └────────────────────┘ │ │ └─────────────────┘ └──────────────────────┘
30
+ ┌────────────────┴───┴┐ ▲ │
31
+ │RestJSONRequestParser│ ┌───┴──────┴──────┐
32
+ └─────────────────────┘ │CBORRequestParser│
33
+ └─────────────────┘
33
34
  ::
34
35
 
35
36
  The ``RequestParser`` contains the logic that is used among all the
36
37
  different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``,
37
- and ``ec2``).
38
+ ``cbor`` and ``ec2``).
38
39
  The relation between the different protocols is described in the
39
40
  ``serializer``.
40
41
 
@@ -44,13 +45,21 @@ The classes are structured as follows:
44
45
  which is shared among all different protocols.
45
46
  * The ``BaseRestRequestParser`` contains the logic for the REST
46
47
  protocol specifics (i.e. specific HTTP metadata parsing).
48
+ * The ``BaseRpcV2RequestParser`` contains the logic for the RPC v2
49
+ protocol specifics (special path routing, no logic about body decoding)
47
50
  * The ``BaseJSONRequestParser`` contains the logic for the JSON body
48
51
  parsing.
52
+ * The ``BaseCBORRequestParser`` contains the logic for the CBOR body
53
+ parsing.
49
54
  * The ``RestJSONRequestParser`` inherits the ReST specific logic from
50
55
  the ``BaseRestRequestParser`` and the JSON body parsing from the
51
56
  ``BaseJSONRequestParser``.
52
- * The ``QueryRequestParser``, ``RestXMLRequestParser``, and the
53
- ``JSONRequestParser`` have a conventional inheritance structure.
57
+ * The ``CBORRequestParser`` inherits the ``json``-protocol specific
58
+ logic from the ``JSONRequestParser`` and the CBOR body parsing
59
+ from the ``BaseCBORRequestParser``.
60
+ * The ``QueryRequestParser``, ``RestXMLRequestParser``,
61
+ ``RpcV2CBORRequestParser`` and ``JSONRequestParser`` have a
62
+ conventional inheritance structure.
54
63
 
55
64
  The services and their protocols are defined by using AWS's Smithy
56
65
  (a language to define services in a - somewhat - protocol-agnostic
@@ -66,7 +75,10 @@ import abc
66
75
  import base64
67
76
  import datetime
68
77
  import functools
78
+ import io
79
+ import os
69
80
  import re
81
+ import struct
70
82
  from abc import ABC
71
83
  from collections.abc import Mapping
72
84
  from email.utils import parsedate_to_datetime
@@ -89,6 +101,7 @@ from cbor2._decoder import loads as cbor2_loads
89
101
  from werkzeug.exceptions import BadRequest, NotFound
90
102
 
91
103
  from localstack.aws.protocol.op_router import RestServiceOperationRouter
104
+ from localstack.aws.spec import ProtocolName
92
105
  from localstack.http import Request
93
106
 
94
107
 
@@ -332,7 +345,7 @@ class RequestParser(abc.ABC):
332
345
  _parse_double = _parse_float
333
346
  _parse_long = _parse_integer
334
347
 
335
- def _convert_str_to_timestamp(self, value: str, timestamp_format=None):
348
+ def _convert_str_to_timestamp(self, value: str, timestamp_format=None) -> datetime.datetime:
336
349
  if timestamp_format is None:
337
350
  timestamp_format = self.TIMESTAMP_FORMAT
338
351
  timestamp_format = timestamp_format.lower()
@@ -346,11 +359,13 @@ class RequestParser(abc.ABC):
346
359
 
347
360
  @staticmethod
348
361
  def _timestamp_unixtimestamp(timestamp_string: str) -> datetime.datetime:
349
- return datetime.datetime.utcfromtimestamp(int(timestamp_string))
362
+ dt = datetime.datetime.fromtimestamp(int(timestamp_string), tz=datetime.UTC)
363
+ return dt.replace(tzinfo=None)
350
364
 
351
365
  @staticmethod
352
366
  def _timestamp_unixtimestampmillis(timestamp_string: str) -> datetime.datetime:
353
- return datetime.datetime.utcfromtimestamp(float(timestamp_string) / 1000)
367
+ dt = datetime.datetime.fromtimestamp(float(timestamp_string) / 1000, tz=datetime.UTC)
368
+ return dt.replace(tzinfo=None)
354
369
 
355
370
  @staticmethod
356
371
  def _timestamp_rfc822(datetime_string: str) -> datetime.datetime:
@@ -976,6 +991,388 @@ class RestJSONRequestParser(BaseRestRequestParser, BaseJSONRequestParser):
976
991
  raise NotImplementedError
977
992
 
978
993
 
994
+ class BaseCBORRequestParser(RequestParser, ABC):
995
+ """
996
+ The ``BaseCBORRequestParser`` is the base class for all CBOR-based AWS service protocols.
997
+ This base-class handles parsing the payload / body as CBOR.
998
+ """
999
+
1000
+ INDEFINITE_ITEM_ADDITIONAL_INFO = 31
1001
+ BREAK_CODE = 0xFF
1002
+ # timestamp format for requests with CBOR content type
1003
+ TIMESTAMP_FORMAT = "unixtimestamp"
1004
+
1005
+ @functools.cached_property
1006
+ def major_type_to_parsing_method_map(self):
1007
+ return {
1008
+ 0: self._parse_type_unsigned_integer,
1009
+ 1: self._parse_type_negative_integer,
1010
+ 2: self._parse_type_byte_string,
1011
+ 3: self._parse_type_text_string,
1012
+ 4: self._parse_type_array,
1013
+ 5: self._parse_type_map,
1014
+ 6: self._parse_type_tag,
1015
+ 7: self._parse_type_simple_and_float,
1016
+ }
1017
+
1018
+ @staticmethod
1019
+ def get_peekable_stream_from_bytes(_bytes: bytes) -> io.BufferedReader:
1020
+ return io.BufferedReader(io.BytesIO(_bytes))
1021
+
1022
+ def parse_data_item(self, stream: io.BufferedReader) -> Any:
1023
+ # CBOR data is divided into "data items", and each data item starts
1024
+ # with an initial byte that describes how the following bytes should be parsed
1025
+ initial_byte = self._read_bytes_as_int(stream, 1)
1026
+ # The highest order three bits of the initial byte describe the CBOR major type
1027
+ major_type = initial_byte >> 5
1028
+ # The lowest order 5 bits of the initial byte tells us more information about
1029
+ # how the bytes should be parsed that will be used
1030
+ additional_info: int = initial_byte & 0b00011111
1031
+
1032
+ if major_type in self.major_type_to_parsing_method_map:
1033
+ method = self.major_type_to_parsing_method_map[major_type]
1034
+ return method(stream, additional_info)
1035
+ else:
1036
+ raise ProtocolParserError(
1037
+ f"Unsupported inital byte found for data item- "
1038
+ f"Major type:{major_type}, Additional info: "
1039
+ f"{additional_info}"
1040
+ )
1041
+
1042
+ # Major type 0 - unsigned integers
1043
+ def _parse_type_unsigned_integer(self, stream: io.BufferedReader, additional_info: int) -> int:
1044
+ additional_info_to_num_bytes = {
1045
+ 24: 1,
1046
+ 25: 2,
1047
+ 26: 4,
1048
+ 27: 8,
1049
+ }
1050
+ # Values under 24 don't need a full byte to be stored; their values are
1051
+ # instead stored as the "additional info" in the initial byte
1052
+ if additional_info < 24:
1053
+ return additional_info
1054
+ elif additional_info in additional_info_to_num_bytes:
1055
+ num_bytes = additional_info_to_num_bytes[additional_info]
1056
+ return self._read_bytes_as_int(stream, num_bytes)
1057
+ else:
1058
+ raise ProtocolParserError(
1059
+ "Invalid CBOR integer returned from the service; unparsable "
1060
+ f"additional info found for major type 0 or 1: {additional_info}"
1061
+ )
1062
+
1063
+ # Major type 1 - negative integers
1064
+ def _parse_type_negative_integer(self, stream: io.BufferedReader, additional_info: int) -> int:
1065
+ return -1 - self._parse_type_unsigned_integer(stream, additional_info)
1066
+
1067
+ # Major type 2 - byte string
1068
+ def _parse_type_byte_string(self, stream: io.BufferedReader, additional_info: int) -> bytes:
1069
+ if additional_info != self.INDEFINITE_ITEM_ADDITIONAL_INFO:
1070
+ length = self._parse_type_unsigned_integer(stream, additional_info)
1071
+ return self._read_from_stream(stream, length)
1072
+ else:
1073
+ chunks = []
1074
+ while True:
1075
+ if self._handle_break_code(stream):
1076
+ break
1077
+ initial_byte = self._read_bytes_as_int(stream, 1)
1078
+ additional_info = initial_byte & 0b00011111
1079
+ length = self._parse_type_unsigned_integer(stream, additional_info)
1080
+ chunks.append(self._read_from_stream(stream, length))
1081
+ return b"".join(chunks)
1082
+
1083
+ # Major type 3 - text string
1084
+ def _parse_type_text_string(self, stream: io.BufferedReader, additional_info: int) -> str:
1085
+ return self._parse_type_byte_string(stream, additional_info).decode("utf-8")
1086
+
1087
+ # Major type 4 - lists
1088
+ def _parse_type_array(self, stream: io.BufferedReader, additional_info: int) -> list:
1089
+ if additional_info != self.INDEFINITE_ITEM_ADDITIONAL_INFO:
1090
+ length = self._parse_type_unsigned_integer(stream, additional_info)
1091
+ return [self.parse_data_item(stream) for _ in range(length)]
1092
+ else:
1093
+ items = []
1094
+ while not self._handle_break_code(stream):
1095
+ items.append(self.parse_data_item(stream))
1096
+ return items
1097
+
1098
+ # Major type 5 - maps
1099
+ def _parse_type_map(self, stream: io.BufferedReader, additional_info: int) -> dict:
1100
+ items = {}
1101
+ if additional_info != self.INDEFINITE_ITEM_ADDITIONAL_INFO:
1102
+ length = self._parse_type_unsigned_integer(stream, additional_info)
1103
+ for _ in range(length):
1104
+ self._parse_type_key_value_pair(stream, items)
1105
+ return items
1106
+
1107
+ else:
1108
+ while not self._handle_break_code(stream):
1109
+ self._parse_type_key_value_pair(stream, items)
1110
+ return items
1111
+
1112
+ def _parse_type_key_value_pair(self, stream: io.BufferedReader, items: dict) -> None:
1113
+ key = self.parse_data_item(stream)
1114
+ value = self.parse_data_item(stream)
1115
+ if value is not None:
1116
+ items[key] = value
1117
+
1118
+ # Major type 6 is tags. The only tag we currently support is tag 1 for unix
1119
+ # timestamps
1120
+ def _parse_type_tag(self, stream: io.BufferedReader, additional_info: int):
1121
+ tag = self._parse_type_unsigned_integer(stream, additional_info)
1122
+ value = self.parse_data_item(stream)
1123
+ if tag == 1: # Epoch-based date/time in milliseconds
1124
+ return self._parse_type_datetime(value)
1125
+ else:
1126
+ raise ProtocolParserError(f"Found CBOR tag not supported by botocore: {tag}")
1127
+
1128
+ def _parse_type_datetime(self, value: int | float) -> datetime.datetime:
1129
+ if isinstance(value, (int, float)):
1130
+ return self._convert_str_to_timestamp(str(value))
1131
+ else:
1132
+ raise ProtocolParserError(f"Unable to parse datetime value: {value}")
1133
+
1134
+ # Major type 7 includes floats and "simple" types. Supported simple types are
1135
+ # currently boolean values, CBOR's null, and CBOR's undefined type. All other
1136
+ # values are either floats or invalid.
1137
+ def _parse_type_simple_and_float(
1138
+ self, stream: io.BufferedReader, additional_info: int
1139
+ ) -> bool | float | None:
1140
+ # For major type 7, values 20-23 correspond to CBOR "simple" values
1141
+ additional_info_simple_values = {
1142
+ 20: False, # CBOR false
1143
+ 21: True, # CBOR true
1144
+ 22: None, # CBOR null
1145
+ 23: None, # CBOR undefined
1146
+ }
1147
+ # First we check if the additional info corresponds to a supported simple value
1148
+ if additional_info in additional_info_simple_values:
1149
+ return additional_info_simple_values[additional_info]
1150
+
1151
+ # If it's not a simple value, we need to parse it into the correct format and
1152
+ # number fo bytes
1153
+ float_formats = {
1154
+ 25: (">e", 2),
1155
+ 26: (">f", 4),
1156
+ 27: (">d", 8),
1157
+ }
1158
+
1159
+ if additional_info in float_formats:
1160
+ float_format, num_bytes = float_formats[additional_info]
1161
+ return struct.unpack(float_format, self._read_from_stream(stream, num_bytes))[0]
1162
+ raise ProtocolParserError(
1163
+ f"Invalid additional info found for major type 7: {additional_info}. "
1164
+ f"This indicates an unsupported simple type or an indefinite float value"
1165
+ )
1166
+
1167
+ @_text_content
1168
+ def _parse_blob(self, _, __, node: bytes, ___) -> bytes:
1169
+ return node
1170
+
1171
+ # This helper method is intended for use when parsing indefinite length items.
1172
+ # It does nothing if the next byte is not the break code. If the next byte is
1173
+ # the break code, it advances past that byte and returns True so the calling
1174
+ # method knows to stop parsing that data item.
1175
+ def _handle_break_code(self, stream: io.BufferedReader) -> bool | None:
1176
+ if int.from_bytes(stream.peek(1)[:1], "big") == self.BREAK_CODE:
1177
+ stream.seek(1, os.SEEK_CUR)
1178
+ return True
1179
+
1180
+ def _read_bytes_as_int(self, stream: IO[bytes], num_bytes: int) -> int:
1181
+ byte = self._read_from_stream(stream, num_bytes)
1182
+ return int.from_bytes(byte, "big")
1183
+
1184
+ @staticmethod
1185
+ def _read_from_stream(stream: IO[bytes], num_bytes: int) -> bytes:
1186
+ value = stream.read(num_bytes)
1187
+ if len(value) != num_bytes:
1188
+ raise ProtocolParserError(
1189
+ "End of stream reached; this indicates a "
1190
+ "malformed CBOR response from the server or an "
1191
+ "issue in botocore"
1192
+ )
1193
+ return value
1194
+
1195
+
1196
+ class CBORRequestParser(BaseCBORRequestParser, JSONRequestParser):
1197
+ """
1198
+ The ``CBORRequestParser`` is responsible for parsing incoming requests for services which use the ``cbor``
1199
+ protocol.
1200
+ The requests for these services encode the majority of their parameters as CBOR in the request body.
1201
+ The operation is defined in an HTTP header field.
1202
+ This protocol is not properly defined in the specs, but it is derived from the ``json`` protocol. Only Kinesis uses
1203
+ it for now.
1204
+ """
1205
+
1206
+ # timestamp format is different from traditional CBOR, and is encoded as a milliseconds integer
1207
+ TIMESTAMP_FORMAT = "unixtimestampmillis"
1208
+
1209
+ def _do_parse(
1210
+ self, request: Request, shape: Shape, uri_params: Mapping[str, Any] = None
1211
+ ) -> dict:
1212
+ parsed = {}
1213
+ if shape is not None:
1214
+ event_name = shape.event_stream_name
1215
+ if event_name:
1216
+ parsed = self._handle_event_stream(request, shape, event_name)
1217
+ else:
1218
+ self._parse_payload(request, shape, parsed, uri_params)
1219
+ return parsed
1220
+
1221
+ def _handle_event_stream(self, request: Request, shape: Shape, event_name: str):
1222
+ # TODO handle event streams
1223
+ raise NotImplementedError
1224
+
1225
+ def _parse_payload(
1226
+ self,
1227
+ request: Request,
1228
+ shape: Shape,
1229
+ final_parsed: dict,
1230
+ uri_params: Mapping[str, Any] = None,
1231
+ ) -> None:
1232
+ original_parsed = self._initial_body_parse(request)
1233
+ body_parsed = self._parse_shape(request, shape, original_parsed, uri_params)
1234
+ final_parsed.update(body_parsed)
1235
+
1236
+ def _initial_body_parse(self, request: Request) -> Any:
1237
+ body_contents = request.data
1238
+ if body_contents == b"":
1239
+ return body_contents
1240
+ body_contents_stream = self.get_peekable_stream_from_bytes(body_contents)
1241
+ return self.parse_data_item(body_contents_stream)
1242
+
1243
+ def _parse_timestamp(
1244
+ self, request: Request, shape: Shape, node: str, uri_params: Mapping[str, Any] = None
1245
+ ) -> datetime.datetime:
1246
+ # TODO: remove once CBOR support has been removed from `JSONRequestParser`
1247
+ return super()._parse_timestamp(request, shape, node, uri_params)
1248
+
1249
+
1250
+ class BaseRpcV2RequestParser(RequestParser):
1251
+ """
1252
+ The ``BaseRpcV2RequestParser`` is the base class for all RPC V2-based AWS service protocols.
1253
+ This base class handles the routing of the request, which is specific based on the path.
1254
+ The body decoding is done in the respective subclasses.
1255
+ """
1256
+
1257
+ @_handle_exceptions
1258
+ def parse(self, request: Request) -> tuple[OperationModel, Any]:
1259
+ # see https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
1260
+ if request.method != "POST":
1261
+ raise ProtocolParserError("RPC v2 only accepts POST requests.")
1262
+
1263
+ headers = request.headers
1264
+ if "X-Amz-Target" in headers or "X-Amzn-Target" in headers:
1265
+ raise ProtocolParserError(
1266
+ "RPC v2 does not accept 'X-Amz-Target' or 'X-Amzn-Target'. "
1267
+ "Such requests are rejected for security reasons."
1268
+ )
1269
+ # TODO: add this special path handling to the ServiceNameParser to allow RPC v2 service to be properly extracted
1270
+ # path = '/service/{service_name}/operation/{operation_name}'
1271
+ # The Smithy RPCv2 CBOR protocol will only use the last four segments of the URL when routing requests.
1272
+ rpc_v2_params = request.path.lstrip("/").split("/")
1273
+ if len(rpc_v2_params) < 4 or not (
1274
+ operation := self.service.operation_model(rpc_v2_params[-1])
1275
+ ):
1276
+ raise OperationNotFoundParserError(
1277
+ f"Unable to find operation for request to service "
1278
+ f"{self.service.service_name}: {request.method} {request.path}"
1279
+ )
1280
+
1281
+ # there are no URI params in RPC v2
1282
+ uri_params = {}
1283
+ shape: StructureShape = operation.input_shape
1284
+ final_parsed = self._do_parse(request, shape, uri_params)
1285
+ return operation, final_parsed
1286
+
1287
+ @_handle_exceptions
1288
+ def _do_parse(
1289
+ self, request: Request, shape: Shape, uri_params: Mapping[str, Any] = None
1290
+ ) -> dict[str, Any]:
1291
+ parsed = {}
1292
+ if shape is not None:
1293
+ event_stream_name = shape.event_stream_name
1294
+ if event_stream_name:
1295
+ parsed = self._handle_event_stream(request, shape, event_stream_name)
1296
+ else:
1297
+ parsed = {}
1298
+ self._parse_payload(request, shape, parsed, uri_params)
1299
+
1300
+ return parsed
1301
+
1302
+ def _handle_event_stream(self, request: Request, shape: Shape, event_name: str):
1303
+ # TODO handle event streams
1304
+ raise NotImplementedError
1305
+
1306
+ def _parse_structure(
1307
+ self,
1308
+ request: Request,
1309
+ shape: StructureShape,
1310
+ node: dict | None,
1311
+ uri_params: Mapping[str, Any] = None,
1312
+ ):
1313
+ if shape.is_document_type:
1314
+ final_parsed = node
1315
+ else:
1316
+ if node is None:
1317
+ # If the comes across the wire as "null" (None in python),
1318
+ # we should be returning this unchanged, instead of as an
1319
+ # empty dict.
1320
+ return None
1321
+ final_parsed = {}
1322
+ members = shape.members
1323
+ if shape.is_tagged_union:
1324
+ cleaned_value = node.copy()
1325
+ cleaned_value.pop("__type", None)
1326
+ cleaned_value = {k: v for k, v in cleaned_value.items() if v is not None}
1327
+ if len(cleaned_value) != 1:
1328
+ raise ProtocolParserError(
1329
+ f"Invalid service response: {shape.name} must have one and only one member set."
1330
+ )
1331
+
1332
+ for member_name, member_shape in members.items():
1333
+ member_value = node.get(member_name)
1334
+ if member_value is not None:
1335
+ final_parsed[member_name] = self._parse_shape(
1336
+ request, member_shape, member_value, uri_params
1337
+ )
1338
+
1339
+ return final_parsed
1340
+
1341
+ def _parse_payload(
1342
+ self,
1343
+ request: Request,
1344
+ shape: Shape,
1345
+ final_parsed: dict,
1346
+ uri_params: Mapping[str, Any] = None,
1347
+ ) -> None:
1348
+ original_parsed = self._initial_body_parse(request)
1349
+ body_parsed = self._parse_shape(request, shape, original_parsed, uri_params)
1350
+ final_parsed.update(body_parsed)
1351
+
1352
+ def _initial_body_parse(self, request: Request):
1353
+ # This method should do the initial parsing of the
1354
+ # body. We still need to walk the parsed body in order
1355
+ # to convert types, but this method will do the first round
1356
+ # of parsing.
1357
+ raise NotImplementedError("_initial_body_parse")
1358
+
1359
+
1360
+ class RpcV2CBORRequestParser(BaseRpcV2RequestParser, BaseCBORRequestParser):
1361
+ """
1362
+ The ``RpcV2CBORRequestParser`` is responsible for parsing incoming requests for services which use the
1363
+ ``rpc-v2-cbor`` protocol. The requests for these services encode all of their parameters as CBOR in the
1364
+ request body.
1365
+ """
1366
+
1367
+ # TODO: investigate datetime format for RpcV2CBOR protocol, which might be different than Kinesis CBOR
1368
+ def _initial_body_parse(self, request: Request):
1369
+ body_contents = request.data
1370
+ if body_contents == b"":
1371
+ return body_contents
1372
+ body_contents_stream = self.get_peekable_stream_from_bytes(body_contents)
1373
+ return self.parse_data_item(body_contents_stream)
1374
+
1375
+
979
1376
  class EC2RequestParser(QueryRequestParser):
980
1377
  """
981
1378
  The ``EC2RequestParser`` is responsible for parsing incoming requests for services which use the ``ec2``
@@ -1154,11 +1551,12 @@ class SQSQueryRequestParser(QueryRequestParser):
1154
1551
 
1155
1552
 
1156
1553
  @functools.cache
1157
- def create_parser(service: ServiceModel) -> RequestParser:
1554
+ def create_parser(service: ServiceModel, protocol: ProtocolName | None = None) -> RequestParser:
1158
1555
  """
1159
1556
  Creates the right parser for the given service model.
1160
1557
 
1161
1558
  :param service: to create the parser for
1559
+ :param protocol: the protocol for the parser. If not provided, fallback to the service's default protocol
1162
1560
  :return: RequestParser which can handle the protocol of the service
1163
1561
  """
1164
1562
  # Unfortunately, some services show subtle differences in their parsing or operation detection behavior, even though
@@ -1176,14 +1574,22 @@ def create_parser(service: ServiceModel) -> RequestParser:
1176
1574
  "rest-json": RestJSONRequestParser,
1177
1575
  "rest-xml": RestXMLRequestParser,
1178
1576
  "ec2": EC2RequestParser,
1577
+ "smithy-rpc-v2-cbor": RpcV2CBORRequestParser,
1578
+ # TODO: implement multi-protocol support for Kinesis, so that it can uses the `cbor` protocol and remove
1579
+ # CBOR handling from JSONRequestParser
1580
+ # this is not an "official" protocol defined from the spec, but is derived from ``json``
1179
1581
  }
1180
1582
 
1583
+ # TODO: do we want to add a check if the user-defined protocol is part of the available ones in the ServiceModel?
1584
+ # or should it be checked once
1585
+ service_protocol = protocol or service.protocol
1586
+
1181
1587
  # Try to select a service- and protocol-specific parser implementation
1182
1588
  if (
1183
1589
  service.service_name in service_specific_parsers
1184
- and service.protocol in service_specific_parsers[service.service_name]
1590
+ and service_protocol in service_specific_parsers[service.service_name]
1185
1591
  ):
1186
- return service_specific_parsers[service.service_name][service.protocol](service)
1592
+ return service_specific_parsers[service.service_name][service_protocol](service)
1187
1593
  else:
1188
1594
  # Otherwise, pick the protocol-specific parser for the protocol of the service
1189
- return protocol_specific_parsers[service.protocol](service)
1595
+ return protocol_specific_parsers[service_protocol](service)