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.
- localstack/aws/protocol/parser.py +427 -21
- localstack/aws/protocol/serializer.py +530 -32
- localstack/aws/spec.py +1 -1
- localstack/version.py +2 -2
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/METADATA +1 -1
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/RECORD +14 -14
- localstack_core-4.8.2.dev4.dist-info/plux.json +1 -0
- localstack_core-4.8.2.dev2.dist-info/plux.json +0 -1
- {localstack_core-4.8.2.dev2.data → localstack_core-4.8.2.dev4.data}/scripts/localstack +0 -0
- {localstack_core-4.8.2.dev2.data → localstack_core-4.8.2.dev4.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.8.2.dev2.data → localstack_core-4.8.2.dev4.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/WHEEL +0 -0
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.8.2.dev2.dist-info → localstack_core-4.8.2.dev4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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``,
|
|
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
|
|
58
|
-
contain the logic for the XML
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
2274
|
+
and service_protocol in service_specific_serializers[service.service_name]
|
|
1777
2275
|
):
|
|
1778
|
-
return service_specific_serializers[service.service_name][
|
|
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[
|
|
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):
|