• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 16820655284

07 Aug 2025 05:03PM UTC coverage: 86.841% (-0.05%) from 86.892%
16820655284

push

github

web-flow
CFNV2: support CDK bootstrap and deployment (#12967)

32 of 38 new or added lines in 5 files covered. (84.21%)

2013 existing lines in 125 files now uncovered.

66606 of 76699 relevant lines covered (86.84%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.37
/localstack-core/localstack/aws/protocol/parser.py
1
"""
2
Request parsers for the different AWS service protocols.
3

4
The module contains classes that take an HTTP request to a service, and
5
given an operation model, parse the HTTP request according to the
6
specified input shape.
7

8
It can be seen as the counterpart to the ``serialize`` module in
9
``botocore`` (which serializes the request before sending it to this
10
parser). It has a lot of similarities with the ``parse`` module in
11
``botocore``, but serves a different purpose (parsing requests
12
instead of responses).
13

14
The different protocols have many similarities. The class hierarchy is
15
designed such that the parsers share as much logic as possible.
16
The class hierarchy looks as follows:
17
::
18
                          ┌─────────────┐
19
                          │RequestParser│
20
                          └─────────────┘
21
                             ▲   ▲   ▲
22
           ┌─────────────────┘   │   └────────────────────┐
23
  ┌────────┴─────────┐ ┌─────────┴───────────┐ ┌──────────┴──────────┐
24
  │QueryRequestParser│ │BaseRestRequestParser│ │BaseJSONRequestParser│
25
  └──────────────────┘ └─────────────────────┘ └─────────────────────┘
26
          ▲                    ▲            ▲   ▲           ▲
27
  ┌───────┴────────┐ ┌─────────┴──────────┐ │   │           │
28
  │EC2RequestParser│ │RestXMLRequestParser│ │   │           │
29
  └────────────────┘ └────────────────────┘ │   │           │
30
                           ┌────────────────┴───┴┐ ┌────────┴────────┐
31
                           │RestJSONRequestParser│ │JSONRequestParser│
32
                           └─────────────────────┘ └─────────────────┘
33
::
34

35
The ``RequestParser`` contains the logic that is used among all the
36
different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``,
37
and ``ec2``).
38
The relation between the different protocols is described in the
39
``serializer``.
40

41
The classes are structured as follows:
42

43
* The ``RequestParser`` contains all the basic logic for the parsing
44
  which is shared among all different protocols.
45
* The ``BaseRestRequestParser`` contains the logic for the REST
46
  protocol specifics (i.e. specific HTTP metadata parsing).
47
* The ``BaseJSONRequestParser`` contains the logic for the JSON body
48
  parsing.
49
* The ``RestJSONRequestParser`` inherits the ReST specific logic from
50
  the ``BaseRestRequestParser`` and the JSON body parsing from the
51
  ``BaseJSONRequestParser``.
52
* The ``QueryRequestParser``, ``RestXMLRequestParser``, and the
53
  ``JSONRequestParser`` have a conventional inheritance structure.
54

55
The services and their protocols are defined by using AWS's Smithy
56
(a language to define services in a - somewhat - protocol-agnostic
57
way). The "peculiarities" in this parser code usually correspond
58
to certain so-called "traits" in Smithy.
59

60
The result of the parser methods are the operation model of the
61
service's action which the request was aiming for, as well as the
62
parsed parameters for the service's function invocation.
63
"""
64

65
import abc
1✔
66
import base64
1✔
67
import datetime
1✔
68
import functools
1✔
69
import re
1✔
70
from abc import ABC
1✔
71
from collections.abc import Mapping
1✔
72
from email.utils import parsedate_to_datetime
1✔
73
from typing import IO, Any
1✔
74
from xml.etree import ElementTree as ETree
1✔
75

76
import dateutil.parser
1✔
77
from botocore.model import (
1✔
78
    ListShape,
79
    MapShape,
80
    OperationModel,
81
    OperationNotFoundError,
82
    ServiceModel,
83
    Shape,
84
    StructureShape,
85
)
86

87
# cbor2: explicitly load from private _decoder module to avoid using the (non-patched) C-version
88
from cbor2._decoder import loads as cbor2_loads
1✔
89
from werkzeug.exceptions import BadRequest, NotFound
1✔
90

91
from localstack.aws.protocol.op_router import RestServiceOperationRouter
1✔
92
from localstack.http import Request
1✔
93

94

95
def _text_content(func):
1✔
96
    """
97
    This decorator hides the difference between an XML node with text or a plain string.
98
    It's used to ensure that scalar processing operates only on text strings, which
99
    allows the same scalar handlers to be used for XML nodes from the body, HTTP headers,
100
    and across different protocols.
101

102
    :param func: function which should be wrapped
103
    :return: wrapper function which can be called with a node or a string, where the
104
             wrapped function is always called with a string
105
    """
106

107
    def _get_text_content(
1✔
108
        self,
109
        request: Request,
110
        shape: Shape,
111
        node_or_string: ETree.Element | str,
112
        uri_params: Mapping[str, Any] = None,
113
    ):
114
        if hasattr(node_or_string, "text"):
1✔
115
            text = node_or_string.text
1✔
116
            if text is None:
1✔
117
                # If an XML node is empty <foo></foo>, we want to parse that as an empty string,
118
                # not as a null/None value.
119
                text = ""
1✔
120
        else:
121
            text = node_or_string
1✔
122
        return func(self, request, shape, text, uri_params)
1✔
123

124
    return _get_text_content
1✔
125

126

127
class RequestParserError(Exception):
1✔
128
    """
129
    Error which is thrown if the request parsing fails.
130
    Super class of all exceptions raised by the parser.
131
    """
132

133
    pass
1✔
134

135

136
class UnknownParserError(RequestParserError):
1✔
137
    """
138
    Error which indicates that the raised exception of the parser could be caused by invalid data or by any other
139
    (unknown) issue. Errors like this should be reported and indicate an issue in the parser itself.
140
    """
141

142
    pass
1✔
143

144

145
class ProtocolParserError(RequestParserError):
1✔
146
    """
147
    Error which indicates that the given data is not compliant with the service's specification and cannot be parsed.
148
    This usually results in a response with an HTTP 4xx status code (client error).
149
    """
150

151
    pass
1✔
152

153

154
class OperationNotFoundParserError(ProtocolParserError):
1✔
155
    """
156
    Error which indicates that the given data cannot be matched to a specific operation.
157
    The request is likely _not_ meant to be handled by the ASF service provider itself.
158
    """
159

160
    pass
1✔
161

162

163
def _handle_exceptions(func):
1✔
164
    """
165
    Decorator which handles the exceptions raised by the parser. It ensures that all exceptions raised by the public
166
    methods of the parser are instances of RequestParserError.
167
    :param func: to wrap in order to add the exception handling
168
    :return: wrapped function
169
    """
170

171
    @functools.wraps(func)
1✔
172
    def wrapper(*args, **kwargs):
1✔
173
        try:
1✔
174
            return func(*args, **kwargs)
1✔
175
        except RequestParserError:
1✔
176
            raise
1✔
177
        except Exception as e:
1✔
178
            raise UnknownParserError(
1✔
179
                "An unknown error occurred when trying to parse the request."
180
            ) from e
181

182
    return wrapper
1✔
183

184

185
class RequestParser(abc.ABC):
1✔
186
    """
187
    The request parser is responsible for parsing an incoming HTTP request.
188
    It determines which operation the request was aiming for and parses the incoming request such that the resulting
189
    dictionary can be used to invoke the service's function implementation.
190
    It is the base class for all parsers and therefore contains the basic logic which is used among all of them.
191
    """
192

193
    service: ServiceModel
1✔
194
    DEFAULT_ENCODING = "utf-8"
1✔
195
    # The default timestamp format is ISO8601, but this can be overwritten by subclasses.
196
    TIMESTAMP_FORMAT = "iso8601"
1✔
197
    # The default timestamp format for header fields
198
    HEADER_TIMESTAMP_FORMAT = "rfc822"
1✔
199
    # The default timestamp format for query fields
200
    QUERY_TIMESTAMP_FORMAT = "iso8601"
1✔
201

202
    def __init__(self, service: ServiceModel) -> None:
1✔
203
        super().__init__()
1✔
204
        self.service = service
1✔
205

206
    @_handle_exceptions
1✔
207
    def parse(self, request: Request) -> tuple[OperationModel, Any]:
1✔
208
        """
209
        Determines which operation the request was aiming for and parses the incoming request such that the resulting
210
        dictionary can be used to invoke the service's function implementation.
211

212
        :param request: to parse
213
        :return: a tuple with the operation model (defining the action / operation which the request aims for),
214
                 and the parsed service parameters
215
        :raises: RequestParserError (either a ProtocolParserError or an UnknownParserError)
216
        """
217
        raise NotImplementedError
218

219
    def _parse_shape(
1✔
220
        self, request: Request, shape: Shape, node: Any, uri_params: Mapping[str, Any] = None
221
    ) -> Any:
222
        """
223
        Main parsing method which dynamically calls the parsing function for the specific shape.
224

225
        :param request: the complete Request
226
        :param shape: of the node
227
        :param node: the single part of the HTTP request to parse
228
        :param uri_params: the extracted URI path params
229
        :return: result of the parsing operation, the type depends on the shape
230
        """
231
        if shape is None:
1✔
232
            return None
1✔
233
        location = shape.serialization.get("location")
1✔
234
        if location is not None:
1✔
235
            if location == "header":
1✔
236
                header_name = shape.serialization.get("name")
1✔
237
                payload = request.headers.get(header_name)
1✔
238
                if payload and shape.type_name == "list":
1✔
239
                    # headers may contain a comma separated list of values (e.g., the ObjectAttributes member in
240
                    # s3.GetObjectAttributes), so we prepare it here for the handler, which will be `_parse_list`.
241
                    # Header lists can contain optional whitespace, so we strip it
242
                    # https://www.rfc-editor.org/rfc/rfc9110.html#name-lists-rule-abnf-extension
243
                    payload = [value.strip() for value in payload.split(",")]
1✔
244
            elif location == "headers":
1✔
245
                payload = self._parse_header_map(shape, request.headers)
1✔
246
                # shapes with the location trait "headers" only contain strings and are not further processed
247
                return payload
1✔
248
            elif location == "querystring":
1✔
249
                query_name = shape.serialization.get("name")
1✔
250
                parsed_query = request.args
1✔
251
                if shape.type_name == "list":
1✔
252
                    payload = parsed_query.getlist(query_name)
1✔
253
                else:
254
                    payload = parsed_query.get(query_name)
1✔
255
            elif location == "uri":
1✔
256
                uri_param_name = shape.serialization.get("name")
1✔
257
                if uri_param_name in uri_params:
1✔
258
                    payload = uri_params[uri_param_name]
1✔
259
            else:
UNCOV
260
                raise UnknownParserError("Unknown shape location '%s'." % location)
×
261
        else:
262
            # If we don't have to use a specific location, we use the node
263
            payload = node
1✔
264

265
        fn_name = "_parse_%s" % shape.type_name
1✔
266
        handler = getattr(self, fn_name, self._noop_parser)
1✔
267
        try:
1✔
268
            return handler(request, shape, payload, uri_params) if payload is not None else None
1✔
269
        except (TypeError, ValueError, AttributeError) as e:
1✔
270
            raise ProtocolParserError(
1✔
271
                f"Invalid type when parsing {shape.name}: '{payload}' cannot be parsed to {shape.type_name}."
272
            ) from e
273

274
    # The parsing functions for primitive types, lists, and timestamps are shared among subclasses.
275

276
    def _parse_list(
1✔
277
        self,
278
        request: Request,
279
        shape: ListShape,
280
        node: list,
281
        uri_params: Mapping[str, Any] = None,
282
    ):
283
        parsed = []
1✔
284
        member_shape = shape.member
1✔
285
        for item in node:
1✔
286
            parsed.append(self._parse_shape(request, member_shape, item, uri_params))
1✔
287
        return parsed
1✔
288

289
    @_text_content
1✔
290
    def _parse_integer(self, _, __, node: str, ___) -> int:
1✔
291
        return int(node)
1✔
292

293
    @_text_content
1✔
294
    def _parse_float(self, _, __, node: str, ___) -> float:
1✔
295
        return float(node)
1✔
296

297
    @_text_content
1✔
298
    def _parse_blob(self, _, __, node: str, ___) -> bytes:
1✔
299
        return base64.b64decode(node)
1✔
300

301
    @_text_content
1✔
302
    def _parse_timestamp(self, _, shape: Shape, node: str, ___) -> datetime.datetime:
1✔
303
        timestamp_format = shape.serialization.get("timestampFormat")
1✔
304
        if not timestamp_format and shape.serialization.get("location") == "header":
1✔
305
            timestamp_format = self.HEADER_TIMESTAMP_FORMAT
1✔
306
        elif not timestamp_format and shape.serialization.get("location") == "querystring":
1✔
307
            timestamp_format = self.QUERY_TIMESTAMP_FORMAT
1✔
308
        return self._convert_str_to_timestamp(node, timestamp_format)
1✔
309

310
    @_text_content
1✔
311
    def _parse_boolean(self, _, __, node: str, ___) -> bool:
1✔
312
        value = node.lower()
1✔
313
        if value == "true":
1✔
314
            return True
1✔
315
        if value == "false":
1✔
316
            return False
1✔
UNCOV
317
        raise ValueError("cannot parse boolean value %s" % node)
×
318

319
    @_text_content
1✔
320
    def _noop_parser(self, _, __, node: Any, ___):
1✔
321
        return node
1✔
322

323
    _parse_character = _parse_string = _noop_parser
1✔
324
    _parse_double = _parse_float
1✔
325
    _parse_long = _parse_integer
1✔
326

327
    def _convert_str_to_timestamp(self, value: str, timestamp_format=None):
1✔
328
        if timestamp_format is None:
1✔
329
            timestamp_format = self.TIMESTAMP_FORMAT
1✔
330
        timestamp_format = timestamp_format.lower()
1✔
331
        converter = getattr(self, "_timestamp_%s" % timestamp_format)
1✔
332
        final_value = converter(value)
1✔
333
        return final_value
1✔
334

335
    @staticmethod
1✔
336
    def _timestamp_iso8601(date_string: str) -> datetime.datetime:
1✔
337
        return dateutil.parser.isoparse(date_string)
1✔
338

339
    @staticmethod
1✔
340
    def _timestamp_unixtimestamp(timestamp_string: str) -> datetime.datetime:
1✔
341
        return datetime.datetime.utcfromtimestamp(int(timestamp_string))
1✔
342

343
    @staticmethod
1✔
344
    def _timestamp_unixtimestampmillis(timestamp_string: str) -> datetime.datetime:
1✔
345
        return datetime.datetime.utcfromtimestamp(float(timestamp_string) / 1000)
1✔
346

347
    @staticmethod
1✔
348
    def _timestamp_rfc822(datetime_string: str) -> datetime.datetime:
1✔
349
        return parsedate_to_datetime(datetime_string)
1✔
350

351
    @staticmethod
1✔
352
    def _parse_header_map(shape: Shape, headers: dict) -> dict:
1✔
353
        # Note that headers are case insensitive, so we .lower() all header names and header prefixes.
354
        parsed = {}
1✔
355
        prefix = shape.serialization.get("name", "").lower()
1✔
356
        for header_name, header_value in headers.items():
1✔
357
            if header_name.lower().startswith(prefix):
1✔
358
                # The key name inserted into the parsed hash strips off the prefix.
359
                name = header_name[len(prefix) :]
1✔
360
                parsed[name] = header_value
1✔
361
        return parsed
1✔
362

363

364
class QueryRequestParser(RequestParser):
1✔
365
    """
366
    The ``QueryRequestParser`` is responsible for parsing incoming requests for services which use the ``query``
367
    protocol. The requests for these services encode the majority of their parameters in the URL query string.
368
    """
369

370
    @_handle_exceptions
1✔
371
    def parse(self, request: Request) -> tuple[OperationModel, Any]:
1✔
372
        instance = request.values
1✔
373
        if "Action" not in instance:
1✔
UNCOV
374
            raise ProtocolParserError(
×
375
                f"Operation detection failed. "
376
                f"Missing Action in request for query-protocol service {self.service}."
377
            )
378
        action = instance["Action"]
1✔
379
        try:
1✔
380
            operation: OperationModel = self.service.operation_model(action)
1✔
381
        except OperationNotFoundError as e:
1✔
382
            raise OperationNotFoundParserError(
1✔
383
                f"Operation detection failed."
384
                f"Operation {action} could not be found for service {self.service}."
385
            ) from e
386
        # There are no uri params in the query protocol (all ops are POST on "/")
387
        uri_params = {}
1✔
388
        input_shape: StructureShape = operation.input_shape
1✔
389
        parsed = self._parse_shape(request, input_shape, instance, uri_params)
1✔
390
        if parsed is None:
1✔
391
            return operation, {}
1✔
392
        return operation, parsed
1✔
393

394
    def _process_member(
1✔
395
        self,
396
        request: Request,
397
        member_name: str,
398
        member_shape: Shape,
399
        node: dict,
400
        uri_params: Mapping[str, Any] = None,
401
    ):
402
        if isinstance(member_shape, (MapShape, ListShape, StructureShape)):
1✔
403
            # If we have a complex type, we filter the node and change it's keys to craft a new "context" for the
404
            # new hierarchy level
405
            sub_node = self._filter_node(member_name, node)
1✔
406
        else:
407
            # If it is a primitive type we just get the value from the dict
408
            sub_node = node.get(member_name)
1✔
409
        # The filtered node is processed and returned (or None if the sub_node is None)
410
        return (
1✔
411
            self._parse_shape(request, member_shape, sub_node, uri_params)
412
            if sub_node is not None
413
            else None
414
        )
415

416
    def _parse_structure(
1✔
417
        self,
418
        request: Request,
419
        shape: StructureShape,
420
        node: dict,
421
        uri_params: Mapping[str, Any] = None,
422
    ) -> dict:
423
        result = {}
1✔
424

425
        for member, member_shape in shape.members.items():
1✔
426
            # The key in the node is either the serialization config "name" of the shape, or the name of the member
427
            member_name = self._get_serialized_name(member_shape, member, node)
1✔
428
            # BUT, if it's flattened and a list, the name is defined by the list's member's name
429
            if member_shape.serialization.get("flattened"):
1✔
430
                if isinstance(member_shape, ListShape):
1✔
431
                    member_name = self._get_serialized_name(member_shape.member, member, node)
1✔
432
            value = self._process_member(request, member_name, member_shape, node, uri_params)
1✔
433
            if value is not None or member in shape.required_members:
1✔
434
                # If the member is required, but not existing, we explicitly set None
435
                result[member] = value
1✔
436

437
        return result if len(result) > 0 else None
1✔
438

439
    def _parse_map(
1✔
440
        self, request: Request, shape: MapShape, node: dict, uri_params: Mapping[str, Any]
441
    ) -> dict:
442
        """
443
        This is what the node looks like for a flattened map::
444
        ::
445
          {
446
              "Attribute.1.Name": "MyKey",
447
              "Attribute.1.Value": "MyValue",
448
              "Attribute.2.Name": ...,
449
              ...
450
          }
451
        ::
452
        This function expects an already filtered / pre-processed node. The node dict would therefore look like:
453
        ::
454
          {
455
              "1.Name": "MyKey",
456
              "1.Value": "MyValue",
457
              "2.Name": ...
458
          }
459
        ::
460
        """
461
        key_prefix = ""
1✔
462
        # Non-flattened maps have an additional hierarchy level named "entry"
463
        # https://awslabs.github.io/smithy/1.0/spec/core/xml-traits.html#xmlflattened-trait
464
        if not shape.serialization.get("flattened"):
1✔
465
            key_prefix += "entry."
1✔
466
        result = {}
1✔
467

468
        i = 0
1✔
469
        while True:
1✔
470
            i += 1
1✔
471
            # The key and value can be renamed (with their serialization config's "name").
472
            # By default they are called "key" and "value".
473
            key_name = f"{key_prefix}{i}.{self._get_serialized_name(shape.key, 'key', node)}"
1✔
474
            value_name = f"{key_prefix}{i}.{self._get_serialized_name(shape.value, 'value', node)}"
1✔
475

476
            # We process the key and value individually
477
            k = self._process_member(request, key_name, shape.key, node)
1✔
478
            v = self._process_member(request, value_name, shape.value, node)
1✔
479
            if k is None or v is None:
1✔
480
                # technically, if one exists but not the other, then that would be an invalid request
481
                break
1✔
482
            result[k] = v
1✔
483

484
        return result if len(result) > 0 else None
1✔
485

486
    def _parse_list(
1✔
487
        self,
488
        request: Request,
489
        shape: ListShape,
490
        node: dict,
491
        uri_params: Mapping[str, Any] = None,
492
    ) -> list:
493
        """
494
        Some actions take lists of parameters. These lists are specified using the param.[member.]n notation.
495
        The "member" is used if the list is not flattened.
496
        Values of n are integers starting from 1.
497
        For example, a list with two elements looks like this:
498
        - Flattened: &AttributeName.1=first&AttributeName.2=second
499
        - Non-flattened: &AttributeName.member.1=first&AttributeName.member.2=second
500
        This function expects an already filtered / processed node. The node dict would therefore look like:
501
        ::
502
          {
503
              "1": "first",
504
              "2": "second",
505
              "3": ...
506
          }
507
        ::
508
        """
509
        # The keys might be prefixed (f.e. for flattened lists)
510
        key_prefix = self._get_list_key_prefix(shape, node)
1✔
511

512
        # We collect the list value as well as the integer indicating the list position so we can
513
        # later sort the list by the position, in case they attribute values are unordered
514
        result: list[tuple[int, Any]] = []
1✔
515

516
        i = 0
1✔
517
        while True:
1✔
518
            i += 1
1✔
519
            key_name = f"{key_prefix}{i}"
1✔
520
            value = self._process_member(request, key_name, shape.member, node)
1✔
521
            if value is None:
1✔
522
                break
1✔
523
            result.append((i, value))
1✔
524

525
        return [r[1] for r in sorted(result)] if len(result) > 0 else None
1✔
526

527
    @staticmethod
1✔
528
    def _filter_node(name: str, node: dict) -> dict:
1✔
529
        """Filters the node dict for entries where the key starts with the given name."""
530
        filtered = {k[len(name) + 1 :]: v for k, v in node.items() if k.startswith(name)}
1✔
531
        return filtered if len(filtered) > 0 else None
1✔
532

533
    def _get_serialized_name(self, shape: Shape, default_name: str, node: dict) -> str:
1✔
534
        """
535
        Returns the serialized name for the shape if it exists.
536
        Otherwise, it will return the given default_name.
537
        """
538
        return shape.serialization.get("name", default_name)
1✔
539

540
    def _get_list_key_prefix(self, shape: ListShape, node: dict):
1✔
541
        key_prefix = ""
1✔
542
        # Non-flattened lists have an additional hierarchy level:
543
        # https://awslabs.github.io/smithy/1.0/spec/core/xml-traits.html#xmlflattened-trait
544
        # The hierarchy level's name is the serialization name of its member or (by default) "member".
545
        if not shape.serialization.get("flattened"):
1✔
546
            key_prefix += f"{self._get_serialized_name(shape.member, 'member', node)}."
1✔
547
        return key_prefix
1✔
548

549

550
class BaseRestRequestParser(RequestParser):
1✔
551
    """
552
    The ``BaseRestRequestParser`` is the base class for all "resty" AWS service protocols.
553
    The operation which should be invoked is determined based on the HTTP method and the path suffix.
554
    The body encoding is done in the respective subclasses.
555
    """
556

557
    def __init__(self, service: ServiceModel) -> None:
1✔
558
        super().__init__(service)
1✔
559
        self.ignore_get_body_errors = False
1✔
560
        self._operation_router = RestServiceOperationRouter(service)
1✔
561

562
    @_handle_exceptions
1✔
563
    def parse(self, request: Request) -> tuple[OperationModel, Any]:
1✔
564
        try:
1✔
565
            operation, uri_params = self._operation_router.match(request)
1✔
566
        except NotFound as e:
1✔
567
            raise OperationNotFoundParserError(
1✔
568
                f"Unable to find operation for request to service "
569
                f"{self.service.service_name}: {request.method} {request.path}"
570
            ) from e
571

572
        shape: StructureShape = operation.input_shape
1✔
573
        final_parsed = {}
1✔
574
        if shape is not None:
1✔
575
            self._parse_payload(request, shape, shape.members, uri_params, final_parsed)
1✔
576
        return operation, final_parsed
1✔
577

578
    def _parse_payload(
1✔
579
        self,
580
        request: Request,
581
        shape: Shape,
582
        member_shapes: dict[str, Shape],
583
        uri_params: Mapping[str, Any],
584
        final_parsed: dict,
585
    ) -> None:
586
        """Parses all attributes which are located in the payload / body of the incoming request."""
587
        payload_parsed = {}
1✔
588
        non_payload_parsed = {}
1✔
589
        if "payload" in shape.serialization:
1✔
590
            # If a payload is specified in the output shape, then only that shape is used for the body payload.
591
            payload_member_name = shape.serialization["payload"]
1✔
592
            body_shape = member_shapes[payload_member_name]
1✔
593
            if body_shape.serialization.get("eventstream"):
1✔
594
                body = self._create_event_stream(request, body_shape)
×
UNCOV
595
                payload_parsed[payload_member_name] = body
×
596
            elif body_shape.type_name == "string":
1✔
597
                # Only set the value if it's not empty (the request's data is an empty binary by default)
598
                if request.data:
1✔
599
                    body = request.data
1✔
600
                    if isinstance(body, bytes):
1✔
601
                        body = body.decode(self.DEFAULT_ENCODING)
1✔
602
                    payload_parsed[payload_member_name] = body
1✔
603
            elif body_shape.type_name == "blob":
1✔
604
                # This control path is equivalent to operation.has_streaming_input (shape has a payload which is a blob)
605
                # in which case we assume essentially an IO[bytes] to be passed. Since the payload can be optional, we
606
                # only set the parameter if content_length=0, which indicates an empty request. If the content length is
607
                # not set, it could be a streaming response.
608
                if request.content_length != 0:
1✔
609
                    payload_parsed[payload_member_name] = self.create_input_stream(request)
1✔
610
            else:
611
                original_parsed = self._initial_body_parse(request)
1✔
612
                payload_parsed[payload_member_name] = self._parse_shape(
1✔
613
                    request, body_shape, original_parsed, uri_params
614
                )
615
        else:
616
            # The payload covers the whole body. We only parse the body if it hasn't been handled by the payload logic.
617
            try:
1✔
618
                non_payload_parsed = self._initial_body_parse(request)
1✔
619
            except ProtocolParserError:
1✔
620
                # GET requests should ignore the body, so we just let them pass
621
                if not (request.method in ["GET", "HEAD"] and self.ignore_get_body_errors):
1✔
622
                    raise
1✔
623

624
        # even if the payload has been parsed, the rest of the shape needs to be processed as well
625
        # (for members which are located outside of the body, like uri or header)
626
        non_payload_parsed = self._parse_shape(request, shape, non_payload_parsed, uri_params)
1✔
627
        # update the final result with the parsed body and the parsed payload (where the payload has precedence)
628
        final_parsed.update(non_payload_parsed)
1✔
629
        final_parsed.update(payload_parsed)
1✔
630

631
    def _initial_body_parse(self, request: Request) -> Any:
1✔
632
        """
633
        This method executes the initial parsing of the body (XML, JSON, or CBOR).
634
        The parsed body will afterwards still be walked through and the nodes will be converted to the appropriate
635
        types, but this method does the first round of parsing.
636

637
        :param request: of which the body should be parsed
638
        :return: depending on the actual implementation
639
        """
640
        raise NotImplementedError("_initial_body_parse")
641

642
    def _create_event_stream(self, request: Request, shape: Shape) -> Any:
1✔
643
        # TODO handle event streams
644
        raise NotImplementedError("_create_event_stream")
645

646
    def create_input_stream(self, request: Request) -> IO[bytes]:
1✔
647
        """
648
        Returns an IO object that makes the payload of the Request available for streaming.
649

650
        :param request: the http request
651
        :return: the input stream that allows services to consume the request payload
652
        """
653
        # for now _get_stream_for_parsing seems to be a good compromise. it can be used even after `request.data` was
654
        # previously called. however the reverse doesn't work. once the stream has been consumed, `request.data` will
655
        # return b''
656
        return request._get_stream_for_parsing()
1✔
657

658

659
class RestXMLRequestParser(BaseRestRequestParser):
1✔
660
    """
661
    The ``RestXMLRequestParser`` is responsible for parsing incoming requests for services which use the ``rest-xml``
662
    protocol. The requests for these services encode the majority of their parameters as XML in the request body.
663
    """
664

665
    def __init__(self, service_model: ServiceModel):
1✔
666
        super(RestXMLRequestParser, self).__init__(service_model)
1✔
667
        self.ignore_get_body_errors = True
1✔
668
        self._namespace_re = re.compile("{.*}")
1✔
669

670
    def _initial_body_parse(self, request: Request) -> ETree.Element:
1✔
671
        body = request.data
1✔
672
        if not body:
1✔
673
            return ETree.Element("")
1✔
674
        return self._parse_xml_string_to_dom(body)
1✔
675

676
    def _parse_structure(
1✔
677
        self,
678
        request: Request,
679
        shape: StructureShape,
680
        node: ETree.Element,
681
        uri_params: Mapping[str, Any] = None,
682
    ) -> dict:
683
        parsed = {}
1✔
684
        xml_dict = self._build_name_to_xml_node(node)
1✔
685
        for member_name, member_shape in shape.members.items():
1✔
686
            xml_name = self._member_key_name(member_shape, member_name)
1✔
687
            member_node = xml_dict.get(xml_name)
1✔
688
            # If a shape defines a location trait, the node might be None (since these are extracted from the request's
689
            # metadata like headers or the URI)
690
            if (
1✔
691
                member_node is not None
692
                or "location" in member_shape.serialization
693
                or member_shape.serialization.get("eventheader")
694
            ):
695
                parsed[member_name] = self._parse_shape(
1✔
696
                    request, member_shape, member_node, uri_params
697
                )
698
            elif member_shape.serialization.get("xmlAttribute"):
1✔
699
                attributes = {}
1✔
700
                location_name = member_shape.serialization["name"]
1✔
701
                for key, value in node.attrib.items():
1✔
702
                    new_key = self._namespace_re.sub(location_name.split(":")[0] + ":", key)
1✔
703
                    attributes[new_key] = value
1✔
704
                if location_name in attributes:
1✔
705
                    parsed[member_name] = attributes[location_name]
1✔
706
            elif member_name in shape.required_members:
1✔
707
                # If the member is required, but not existing, we explicitly set None
708
                parsed[member_name] = None
1✔
709
        return parsed
1✔
710

711
    def _parse_map(
1✔
712
        self,
713
        request: Request,
714
        shape: MapShape,
715
        node: dict,
716
        uri_params: Mapping[str, Any] = None,
717
    ) -> dict:
718
        parsed = {}
×
719
        key_shape = shape.key
×
720
        value_shape = shape.value
×
721
        key_location_name = key_shape.serialization.get("name", "key")
×
722
        value_location_name = value_shape.serialization.get("name", "value")
×
723
        if shape.serialization.get("flattened") and not isinstance(node, list):
×
724
            node = [node]
×
725
        for keyval_node in node:
×
726
            key_name = val_name = None
×
UNCOV
727
            for single_pair in keyval_node:
×
728
                # Within each <entry> there's a <key> and a <value>
729
                tag_name = self._node_tag(single_pair)
×
730
                if tag_name == key_location_name:
×
731
                    key_name = self._parse_shape(request, key_shape, single_pair, uri_params)
×
732
                elif tag_name == value_location_name:
×
UNCOV
733
                    val_name = self._parse_shape(request, value_shape, single_pair, uri_params)
×
734
                else:
735
                    raise ProtocolParserError("Unknown tag: %s" % tag_name)
×
736
            parsed[key_name] = val_name
×
UNCOV
737
        return parsed
×
738

739
    def _parse_list(
1✔
740
        self,
741
        request: Request,
742
        shape: ListShape,
743
        node: dict,
744
        uri_params: Mapping[str, Any] = None,
745
    ) -> list:
746
        # When we use _build_name_to_xml_node, repeated elements are aggregated
747
        # into a list. However, we can't tell the difference between a scalar
748
        # value and a single element flattened list. So before calling the
749
        # real _handle_list, we know that "node" should actually be a list if
750
        # it's flattened, and if it's not, then we make it a one element list.
751
        if shape.serialization.get("flattened") and not isinstance(node, list):
1✔
752
            node = [node]
1✔
753
        return super(RestXMLRequestParser, self)._parse_list(request, shape, node, uri_params)
1✔
754

755
    def _node_tag(self, node: ETree.Element) -> str:
1✔
756
        return self._namespace_re.sub("", node.tag)
1✔
757

758
    @staticmethod
1✔
759
    def _member_key_name(shape: Shape, member_name: str) -> str:
1✔
760
        # This method is needed because we have to special case flattened list
761
        # with a serialization name.  If this is the case we use the
762
        # locationName from the list's member shape as the key name for the
763
        # surrounding structure.
764
        if isinstance(shape, ListShape) and shape.serialization.get("flattened"):
1✔
765
            list_member_serialized_name = shape.member.serialization.get("name")
1✔
766
            if list_member_serialized_name is not None:
1✔
767
                return list_member_serialized_name
1✔
768
        serialized_name = shape.serialization.get("name")
1✔
769
        if serialized_name is not None:
1✔
770
            return serialized_name
1✔
771
        return member_name
1✔
772

773
    @staticmethod
1✔
774
    def _parse_xml_string_to_dom(xml_string: str) -> ETree.Element:
1✔
775
        try:
1✔
776
            parser = ETree.XMLParser(target=ETree.TreeBuilder())
1✔
777
            parser.feed(xml_string)
1✔
778
            root = parser.close()
1✔
779
        except ETree.ParseError as e:
1✔
780
            raise ProtocolParserError(
1✔
781
                "Unable to parse request (%s), invalid XML received:\n%s" % (e, xml_string)
782
            ) from e
783
        return root
1✔
784

785
    def _build_name_to_xml_node(self, parent_node: list | ETree.Element) -> dict:
1✔
786
        # If the parent node is actually a list. We should not be trying
787
        # to serialize it to a dictionary. Instead, return the first element
788
        # in the list.
789
        if isinstance(parent_node, list):
1✔
UNCOV
790
            return self._build_name_to_xml_node(parent_node[0])
×
791
        xml_dict = {}
1✔
792
        for item in parent_node:
1✔
793
            key = self._node_tag(item)
1✔
794
            if key in xml_dict:
1✔
795
                # If the key already exists, the most natural
796
                # way to handle this is to aggregate repeated
797
                # keys into a single list.
798
                # <foo>1</foo><foo>2</foo> -> {'foo': [Node(1), Node(2)]}
799
                if isinstance(xml_dict[key], list):
1✔
800
                    xml_dict[key].append(item)
1✔
801
                else:
802
                    # Convert from a scalar to a list.
803
                    xml_dict[key] = [xml_dict[key], item]
1✔
804
            else:
805
                xml_dict[key] = item
1✔
806
        return xml_dict
1✔
807

808
    def _create_event_stream(self, request: Request, shape: Shape) -> Any:
1✔
809
        # TODO handle event streams
810
        raise NotImplementedError("_create_event_stream")
811

812

813
class BaseJSONRequestParser(RequestParser, ABC):
1✔
814
    """
815
    The ``BaseJSONRequestParser`` is the base class for all JSON-based AWS service protocols.
816
    This base-class handles parsing the payload / body as JSON.
817
    """
818

819
    # default timestamp format for JSON requests
820
    TIMESTAMP_FORMAT = "unixtimestamp"
1✔
821
    # timestamp format for requests with CBOR content type
822
    CBOR_TIMESTAMP_FORMAT = "unixtimestampmillis"
1✔
823

824
    def _parse_structure(
1✔
825
        self,
826
        request: Request,
827
        shape: StructureShape,
828
        value: dict | None,
829
        uri_params: Mapping[str, Any] = None,
830
    ) -> dict | None:
831
        if shape.is_document_type:
1✔
UNCOV
832
            final_parsed = value
×
833
        else:
834
            if value is None:
1✔
835
                # If the comes across the wire as "null" (None in python),
836
                # we should be returning this unchanged, instead of as an
837
                # empty dict.
UNCOV
838
                return None
×
839
            final_parsed = {}
1✔
840
            for member_name, member_shape in shape.members.items():
1✔
841
                json_name = member_shape.serialization.get("name", member_name)
1✔
842
                raw_value = value.get(json_name)
1✔
843
                parsed = self._parse_shape(request, member_shape, raw_value, uri_params)
1✔
844
                if parsed is not None or member_name in shape.required_members:
1✔
845
                    # If the member is required, but not existing, we set it to None anyways
846
                    final_parsed[member_name] = parsed
1✔
847
        return final_parsed
1✔
848

849
    def _parse_map(
1✔
850
        self,
851
        request: Request,
852
        shape: MapShape,
853
        value: dict | None,
854
        uri_params: Mapping[str, Any] = None,
855
    ) -> dict | None:
856
        if value is None:
1✔
UNCOV
857
            return None
×
858
        parsed = {}
1✔
859
        key_shape = shape.key
1✔
860
        value_shape = shape.value
1✔
861
        for key, val in value.items():
1✔
862
            actual_key = self._parse_shape(request, key_shape, key, uri_params)
1✔
863
            actual_value = self._parse_shape(request, value_shape, val, uri_params)
1✔
864
            parsed[actual_key] = actual_value
1✔
865
        return parsed
1✔
866

867
    def _parse_body_as_json(self, request: Request) -> dict:
1✔
868
        body_contents = request.data
1✔
869
        if not body_contents:
1✔
870
            return {}
1✔
871
        if request.mimetype.startswith("application/x-amz-cbor"):
1✔
872
            try:
1✔
873
                return cbor2_loads(body_contents)
1✔
874
            except ValueError as e:
×
UNCOV
875
                raise ProtocolParserError("HTTP body could not be parsed as CBOR.") from e
×
876
        else:
877
            try:
1✔
878
                return request.get_json(force=True)
1✔
879
            except BadRequest as e:
1✔
880
                raise ProtocolParserError("HTTP body could not be parsed as JSON.") from e
1✔
881

882
    def _parse_boolean(
1✔
883
        self, request: Request, shape: Shape, node: bool, uri_params: Mapping[str, Any] = None
884
    ) -> bool:
885
        return super()._noop_parser(request, shape, node, uri_params)
1✔
886

887
    def _parse_timestamp(
1✔
888
        self, request: Request, shape: Shape, node: str, uri_params: Mapping[str, Any] = None
889
    ) -> datetime.datetime:
890
        if not shape.serialization.get("timestampFormat") and request.mimetype.startswith(
1✔
891
            "application/x-amz-cbor"
892
        ):
893
            # cbor2 has native support for timestamp decoding, so this node could already have the right type
894
            if isinstance(node, datetime.datetime):
1✔
895
                return node
1✔
896
            # otherwise parse the timestamp using the AWS CBOR timestamp format
897
            # (non-CBOR-standard conform, uses millis instead of floating-point-millis)
898
            return self._convert_str_to_timestamp(node, self.CBOR_TIMESTAMP_FORMAT)
1✔
899
        return super()._parse_timestamp(request, shape, node, uri_params)
1✔
900

901
    def _parse_blob(
1✔
902
        self, request: Request, shape: Shape, node: bool, uri_params: Mapping[str, Any] = None
903
    ) -> bytes:
904
        if isinstance(node, bytes) and request.mimetype.startswith("application/x-amz-cbor"):
1✔
905
            # CBOR does not base64 encode binary data
906
            return bytes(node)
1✔
907
        else:
908
            return super()._parse_blob(request, shape, node, uri_params)
1✔
909

910

911
class JSONRequestParser(BaseJSONRequestParser):
1✔
912
    """
913
    The ``JSONRequestParser`` is responsible for parsing incoming requests for services which use the ``json``
914
    protocol.
915
    The requests for these services encode the majority of their parameters as JSON in the request body.
916
    The operation is defined in an HTTP header field.
917
    """
918

919
    @_handle_exceptions
1✔
920
    def parse(self, request: Request) -> tuple[OperationModel, Any]:
1✔
921
        target = request.headers["X-Amz-Target"]
1✔
922
        # assuming that the last part of the target string (e.g., "x.y.z.MyAction") contains the operation name
923
        operation_name = target.rpartition(".")[2]
1✔
924
        operation = self.service.operation_model(operation_name)
1✔
925
        shape = operation.input_shape
1✔
926
        # There are no uri params in the query protocol
927
        uri_params = {}
1✔
928
        final_parsed = self._do_parse(request, shape, uri_params)
1✔
929
        return operation, final_parsed
1✔
930

931
    def _do_parse(
1✔
932
        self, request: Request, shape: Shape, uri_params: Mapping[str, Any] = None
933
    ) -> dict:
934
        parsed = {}
1✔
935
        if shape is not None:
1✔
936
            event_name = shape.event_stream_name
1✔
937
            if event_name:
1✔
UNCOV
938
                parsed = self._handle_event_stream(request, shape, event_name)
×
939
            else:
940
                parsed = self._handle_json_body(request, shape, uri_params)
1✔
941
        return parsed
1✔
942

943
    def _handle_event_stream(self, request: Request, shape: Shape, event_name: str):
1✔
944
        # TODO handle event streams
945
        raise NotImplementedError
946

947
    def _handle_json_body(
1✔
948
        self, request: Request, shape: Shape, uri_params: Mapping[str, Any] = None
949
    ) -> Any:
950
        # The json.loads() gives us the primitive JSON types, but we need to traverse the parsed JSON data to convert
951
        # to richer types (blobs, timestamps, etc.)
952
        parsed_json = self._parse_body_as_json(request)
1✔
953
        return self._parse_shape(request, shape, parsed_json, uri_params)
1✔
954

955

956
class RestJSONRequestParser(BaseRestRequestParser, BaseJSONRequestParser):
1✔
957
    """
958
    The ``RestJSONRequestParser`` is responsible for parsing incoming requests for services which use the ``rest-json``
959
    protocol.
960
    The requests for these services encode the majority of their parameters as JSON in the request body.
961
    The operation is defined by the HTTP method and the path suffix.
962
    """
963

964
    def _initial_body_parse(self, request: Request) -> dict:
1✔
965
        return self._parse_body_as_json(request)
1✔
966

967
    def _create_event_stream(self, request: Request, shape: Shape) -> Any:
1✔
968
        raise NotImplementedError
969

970

971
class EC2RequestParser(QueryRequestParser):
1✔
972
    """
973
    The ``EC2RequestParser`` is responsible for parsing incoming requests for services which use the ``ec2``
974
    protocol (which only is EC2). Protocol is quite similar to the ``query`` protocol with some small differences.
975
    """
976

977
    def _get_serialized_name(self, shape: Shape, default_name: str, node: dict) -> str:
1✔
978
        # Returns the serialized name for the shape if it exists.
979
        # Otherwise it will return the passed in default_name.
980
        if "queryName" in shape.serialization:
1✔
UNCOV
981
            return shape.serialization["queryName"]
×
982
        elif "name" in shape.serialization:
1✔
983
            # A locationName is always capitalized on input for the ec2 protocol.
984
            name = shape.serialization["name"]
1✔
985
            return name[0].upper() + name[1:]
1✔
986
        else:
987
            return default_name
1✔
988

989
    def _get_list_key_prefix(self, shape: ListShape, node: dict):
1✔
990
        # The EC2 protocol does not use a prefix notation for flattened lists
991
        return ""
1✔
992

993

994
class S3RequestParser(RestXMLRequestParser):
1✔
995
    class VirtualHostRewriter:
1✔
996
        """
997
        Context Manager which rewrites the request object parameters such that - within the context - it looks like a
998
        normal S3 request.
999
        FIXME: this is not optimal because it mutates the Request object. Once we have better utility to create/copy
1000
        a request instead of EnvironBuilder, we should copy it before parsing (except the stream).
1001
        """
1002

1003
        def __init__(self, request: Request):
1✔
1004
            self.request = request
1✔
1005
            self.old_host = None
1✔
1006
            self.old_path = None
1✔
1007

1008
        def __enter__(self):
1✔
1009
            # only modify the request if it uses the virtual host addressing
1010
            if bucket_name := self._is_vhost_address_get_bucket(self.request):
1✔
1011
                # save the original path and host for restoring on context exit
1012
                self.old_path = self.request.path
1✔
1013
                self.old_host = self.request.host
1✔
1014
                self.old_raw_uri = self.request.environ.get("RAW_URI")
1✔
1015

1016
                # remove the bucket name from the host part of the request
1017
                new_host = self.old_host.removeprefix(f"{bucket_name}.")
1✔
1018

1019
                # put the bucket name at the front
1020
                new_path = "/" + bucket_name + self.old_path or "/"
1✔
1021

1022
                # create a new RAW_URI for the WSGI environment, this is necessary because of our `get_raw_path` utility
1023
                if self.old_raw_uri:
1✔
1024
                    new_raw_uri = "/" + bucket_name + self.old_raw_uri or "/"
1✔
1025
                    if qs := self.request.query_string:
1✔
1026
                        new_raw_uri += "?" + qs.decode("utf-8")
1✔
1027
                else:
1028
                    new_raw_uri = None
1✔
1029

1030
                # set the new path and host
1031
                self._set_request_props(self.request, new_path, new_host, new_raw_uri)
1✔
1032
            return self.request
1✔
1033

1034
        def __exit__(self, exc_type, exc_value, exc_traceback):
1✔
1035
            # reset the original request properties on exit of the context
1036
            if self.old_host or self.old_path:
1✔
1037
                self._set_request_props(
1✔
1038
                    self.request, self.old_path, self.old_host, self.old_raw_uri
1039
                )
1040

1041
        @staticmethod
1✔
1042
        def _set_request_props(request: Request, path: str, host: str, raw_uri: str | None = None):
1✔
1043
            """Sets the HTTP request's path and host and clears the cache in the request object."""
1044
            request.path = path
1✔
1045
            request.headers["Host"] = host
1✔
1046
            if raw_uri:
1✔
1047
                request.environ["RAW_URI"] = raw_uri
1✔
1048

1049
            try:
1✔
1050
                # delete the werkzeug request property cache that depends on path, but make sure all of them are
1051
                # initialized first, otherwise `del` will raise a key error
1052
                request.host = None  # noqa
1✔
1053
                request.url = None  # noqa
1✔
1054
                request.base_url = None  # noqa
1✔
1055
                request.full_path = None  # noqa
1✔
1056
                request.host_url = None  # noqa
1✔
1057
                request.root_url = None  # noqa
1✔
1058
                del request.host  # noqa
1✔
1059
                del request.url  # noqa
1✔
1060
                del request.base_url  # noqa
1✔
1061
                del request.full_path  # noqa
1✔
1062
                del request.host_url  # noqa
1✔
1063
                del request.root_url  # noqa
1✔
UNCOV
1064
            except AttributeError:
×
1065
                pass
×
1066

1067
        @staticmethod
1✔
1068
        def _is_vhost_address_get_bucket(request: Request) -> str | None:
1✔
1069
            from localstack.services.s3.utils import uses_host_addressing
1✔
1070

1071
            return uses_host_addressing(request.headers)
1✔
1072

1073
    @_handle_exceptions
1✔
1074
    def parse(self, request: Request) -> tuple[OperationModel, Any]:
1✔
1075
        """Handle virtual-host-addressing for S3."""
1076
        with self.VirtualHostRewriter(request):
1✔
1077
            return super().parse(request)
1✔
1078

1079
    def _parse_shape(
1✔
1080
        self, request: Request, shape: Shape, node: Any, uri_params: Mapping[str, Any] = None
1081
    ) -> Any:
1082
        """
1083
        Special handling of parsing the shape for s3 object-names (=key):
1084
        Trailing '/' are valid and need to be preserved, however, the url-matcher removes it from the key.
1085
        We need special logic to compare the parsed Key parameter against the path and add back the missing slashes
1086
        """
1087
        if (
1✔
1088
            shape is not None
1089
            and uri_params is not None
1090
            and shape.serialization.get("location") == "uri"
1091
            and shape.serialization.get("name") == "Key"
1092
            and (
1093
                (trailing_slashes := request.path.rpartition(uri_params["Key"])[2])
1094
                and all(char == "/" for char in trailing_slashes)
1095
            )
1096
        ):
1097
            uri_params = dict(uri_params)
1✔
1098
            uri_params["Key"] = uri_params["Key"] + trailing_slashes
1✔
1099
        return super()._parse_shape(request, shape, node, uri_params)
1✔
1100

1101
    @_text_content
1✔
1102
    def _parse_integer(self, _, shape, node: str, ___) -> int | None:
1✔
1103
        # S3 accepts empty query string parameters that should be integer
1104
        # to not break other cases, validate that the shape is in the querystring
1105
        if node == "" and shape.serialization.get("location") == "querystring":
1✔
1106
            return None
1✔
1107
        return int(node)
1✔
1108

1109

1110
class SQSQueryRequestParser(QueryRequestParser):
1✔
1111
    def _get_serialized_name(self, shape: Shape, default_name: str, node: dict) -> str:
1✔
1112
        """
1113
        SQS allows using both - the proper serialized name of a map as well as the member name - as name for maps.
1114
        For example, both works for the TagQueue operation:
1115
        - Using the proper serialized name "Tag": Tag.1.Key=key&Tag.1.Value=value
1116
        - Using the member name "Tag" in the parent structure: Tags.1.Key=key&Tags.1.Value=value
1117
        - Using "Name" to represent the Key for a nested dict: MessageAttributes.1.Name=key&MessageAttributes.1.Value.StringValue=value
1118
            resulting in {MessageAttributes: {key : {StringValue: value}}}
1119
        The Java SDK implements the second variant: https://github.com/aws/aws-sdk-java-v2/issues/2524
1120
        This has been approved to be a bug and against the spec, but since the client has a lot of users, and AWS SQS
1121
        supports both, we need to handle it here.
1122
        """
1123
        # ask the super implementation for the proper serialized name
1124
        primary_name = super()._get_serialized_name(shape, default_name, node)
1✔
1125

1126
        # determine potential suffixes for the name of the member in the node
1127
        suffixes = []
1✔
1128
        if shape.type_name == "map":
1✔
1129
            if not shape.serialization.get("flattened"):
1✔
UNCOV
1130
                suffixes = [".entry.1.Key", ".entry.1.Name"]
×
1131
            else:
1132
                suffixes = [".1.Key", ".1.Name"]
1✔
1133
        if shape.type_name == "list":
1✔
1134
            if not shape.serialization.get("flattened"):
1✔
UNCOV
1135
                suffixes = [".member.1"]
×
1136
            else:
1137
                suffixes = [".1"]
1✔
1138

1139
        # if the primary name is _not_ available in the node, but the default name is, we use the default name
1140
        if not any(f"{primary_name}{suffix}" in node for suffix in suffixes) and any(
1✔
1141
            f"{default_name}{suffix}" in node for suffix in suffixes
1142
        ):
1143
            return default_name
1✔
1144
        # otherwise we use the primary name
1145
        return primary_name
1✔
1146

1147

1148
@functools.cache
1✔
1149
def create_parser(service: ServiceModel) -> RequestParser:
1✔
1150
    """
1151
    Creates the right parser for the given service model.
1152

1153
    :param service: to create the parser for
1154
    :return: RequestParser which can handle the protocol of the service
1155
    """
1156
    # Unfortunately, some services show subtle differences in their parsing or operation detection behavior, even though
1157
    # their specification states they implement the same protocol.
1158
    # In order to avoid bundling the whole complexity in the specific protocols, or even have service-distinctions
1159
    # within the parser implementations, the service-specific parser implementations (basically the implicit /
1160
    # informally more specific protocol implementation) has precedence over the more general protocol-specific parsers.
1161
    service_specific_parsers = {
1✔
1162
        "s3": {"rest-xml": S3RequestParser},
1163
        "sqs": {"query": SQSQueryRequestParser},
1164
    }
1165
    protocol_specific_parsers = {
1✔
1166
        "query": QueryRequestParser,
1167
        "json": JSONRequestParser,
1168
        "rest-json": RestJSONRequestParser,
1169
        "rest-xml": RestXMLRequestParser,
1170
        "ec2": EC2RequestParser,
1171
    }
1172

1173
    # Try to select a service- and protocol-specific parser implementation
1174
    if (
1✔
1175
        service.service_name in service_specific_parsers
1176
        and service.protocol in service_specific_parsers[service.service_name]
1177
    ):
1178
        return service_specific_parsers[service.service_name][service.protocol](service)
1✔
1179
    else:
1180
        # Otherwise, pick the protocol-specific parser for the protocol of the service
1181
        return protocol_specific_parsers[service.protocol](service)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc