smithy-http 0.0.1__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.
- smithy_http/__init__.py +233 -0
- smithy_http/aio/__init__.py +53 -0
- smithy_http/aio/aiohttp.py +140 -0
- smithy_http/aio/auth/__init__.py +2 -0
- smithy_http/aio/auth/apikey.py +117 -0
- smithy_http/aio/crt.py +493 -0
- smithy_http/aio/identity/__init__.py +2 -0
- smithy_http/aio/identity/apikey.py +38 -0
- smithy_http/aio/interfaces/__init__.py +85 -0
- smithy_http/aio/interfaces/auth.py +107 -0
- smithy_http/aio/protocols.py +118 -0
- smithy_http/aio/restjson.py +60 -0
- smithy_http/deserializers.py +259 -0
- smithy_http/endpoints.py +30 -0
- smithy_http/exceptions.py +7 -0
- smithy_http/interceptors/__init__.py +2 -0
- smithy_http/interceptors/user_agent.py +153 -0
- smithy_http/interfaces/__init__.py +141 -0
- smithy_http/plugins/__init__.py +15 -0
- smithy_http/py.typed +0 -0
- smithy_http/restjson.py +29 -0
- smithy_http/serializers.py +631 -0
- smithy_http/user_agent.py +97 -0
- smithy_http/utils.py +125 -0
- smithy_http-0.0.1.dist-info/METADATA +12 -0
- smithy_http-0.0.1.dist-info/RECORD +28 -0
- smithy_http-0.0.1.dist-info/WHEEL +4 -0
- smithy_http-0.0.1.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
|
5
|
+
# the License is located at
|
|
6
|
+
#
|
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
|
8
|
+
#
|
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
|
12
|
+
# language governing permissions and limitations under the License.
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Protocol, TypedDict, TypeVar
|
|
16
|
+
|
|
17
|
+
from smithy_core.aio.interfaces.identity import IdentityResolver
|
|
18
|
+
from smithy_core.interfaces.identity import Identity, IdentityProperties
|
|
19
|
+
|
|
20
|
+
from . import HTTPRequest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SigningProperties(TypedDict):
|
|
24
|
+
"""Additional properties loaded to modify the signing process."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SigningPropertiesType = TypeVar("SigningPropertiesType", bound=SigningProperties)
|
|
28
|
+
SigningPropertiesType_contra = TypeVar(
|
|
29
|
+
"SigningPropertiesType_contra", bound=SigningProperties, contravariant=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HTTPSigner[I: Identity, SP: SigningProperties](Protocol):
|
|
34
|
+
"""An interface for generating a signed HTTP request."""
|
|
35
|
+
|
|
36
|
+
async def sign(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
http_request: HTTPRequest,
|
|
40
|
+
identity: I,
|
|
41
|
+
signing_properties: SP,
|
|
42
|
+
) -> HTTPRequest:
|
|
43
|
+
"""Generate a new signed HTTPRequest based on the one provided.
|
|
44
|
+
|
|
45
|
+
:param http_request: The HTTP request to sign.
|
|
46
|
+
:param identity: The signing identity.
|
|
47
|
+
:param signing_properties: Additional properties loaded to modify the signing
|
|
48
|
+
process.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HTTPAuthScheme[I: Identity, C, IP: IdentityProperties, SP: SigningProperties](
|
|
54
|
+
Protocol
|
|
55
|
+
):
|
|
56
|
+
"""Represents a way a service will authenticate the user's identity."""
|
|
57
|
+
|
|
58
|
+
# A unique identifier for the authentication scheme.
|
|
59
|
+
scheme_id: str
|
|
60
|
+
|
|
61
|
+
# An API that can be used to sign HTTP requests.
|
|
62
|
+
signer: HTTPSigner[I, SP]
|
|
63
|
+
|
|
64
|
+
def identity_resolver(self, *, config: C) -> IdentityResolver[I, IP]:
|
|
65
|
+
"""An API that can be queried to resolve identity."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(kw_only=True)
|
|
70
|
+
class HTTPAuthOption:
|
|
71
|
+
"""Auth scheme used for signing and identity resolution."""
|
|
72
|
+
|
|
73
|
+
# The ID of the scheme to use. This string matches the one returned by
|
|
74
|
+
# HttpAuthScheme.scheme_id
|
|
75
|
+
scheme_id: str
|
|
76
|
+
|
|
77
|
+
# Parameters to pass to IdentityResolver.get_identity.
|
|
78
|
+
identity_properties: dict[str, Any]
|
|
79
|
+
|
|
80
|
+
# Parameters to pass to HttpSigner.sign.
|
|
81
|
+
signer_properties: dict[str, Any]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(kw_only=True)
|
|
85
|
+
class AuthSchemeParameters:
|
|
86
|
+
"""The input to the auth scheme resolver.
|
|
87
|
+
|
|
88
|
+
A code-generated interface for passing in the data required for determining the
|
|
89
|
+
authentication scheme. By default, this only includes the operation name.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
# The service operation being invoked by the client.
|
|
93
|
+
operation: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AuthSchemeResolver(Protocol):
|
|
97
|
+
"""Determines which authentication scheme to use for a given service."""
|
|
98
|
+
|
|
99
|
+
def resolve_auth_scheme(
|
|
100
|
+
self, *, auth_parameters: AuthSchemeParameters
|
|
101
|
+
) -> list[HTTPAuthOption]:
|
|
102
|
+
"""Resolve an ordered list of applicable auth schemes.
|
|
103
|
+
|
|
104
|
+
:param auth_parameters: The parameters required for determining which
|
|
105
|
+
authentication schemes to potentially use.
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from inspect import iscoroutinefunction
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
from smithy_core.aio.interfaces import ClientProtocol
|
|
6
|
+
from smithy_core.codecs import Codec
|
|
7
|
+
from smithy_core.deserializers import DeserializeableShape
|
|
8
|
+
from smithy_core.documents import TypeRegistry
|
|
9
|
+
from smithy_core.exceptions import ExpectationNotMetException
|
|
10
|
+
from smithy_core.interfaces import Endpoint, TypedProperties, URI
|
|
11
|
+
from smithy_core.schemas import APIOperation
|
|
12
|
+
from smithy_core.serializers import SerializeableShape
|
|
13
|
+
from smithy_core.traits import EndpointTrait, HTTPTrait
|
|
14
|
+
|
|
15
|
+
from smithy_http.aio.interfaces import HTTPRequest, HTTPResponse
|
|
16
|
+
from smithy_http.deserializers import HTTPResponseDeserializer
|
|
17
|
+
from smithy_http.serializers import HTTPRequestSerializer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpClientProtocol(ClientProtocol[HTTPRequest, HTTPResponse]):
|
|
21
|
+
"""An HTTP-based protocol."""
|
|
22
|
+
|
|
23
|
+
def set_service_endpoint(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
request: HTTPRequest,
|
|
27
|
+
endpoint: Endpoint,
|
|
28
|
+
) -> HTTPRequest:
|
|
29
|
+
uri = endpoint.uri
|
|
30
|
+
uri_builder = request.destination
|
|
31
|
+
|
|
32
|
+
if uri.scheme:
|
|
33
|
+
uri_builder.scheme = uri.scheme
|
|
34
|
+
if uri.host:
|
|
35
|
+
uri_builder.host = uri.host
|
|
36
|
+
if uri.port and uri.port > -1:
|
|
37
|
+
uri_builder.port = uri.port
|
|
38
|
+
if uri.path:
|
|
39
|
+
uri_builder.path = os.path.join(uri.path, uri_builder.path or "")
|
|
40
|
+
# TODO: merge headers from the endpoint properties bag
|
|
41
|
+
return request
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HttpBindingClientProtocol(HttpClientProtocol):
|
|
45
|
+
"""An HTTP-based protocol that uses HTTP binding traits."""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def payload_codec(self) -> Codec:
|
|
49
|
+
"""The codec used for the serde of input and output payloads."""
|
|
50
|
+
raise NotImplementedError()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def content_type(self) -> str:
|
|
54
|
+
"""The media type of the http payload."""
|
|
55
|
+
raise NotImplementedError()
|
|
56
|
+
|
|
57
|
+
def serialize_request[
|
|
58
|
+
OperationInput: "SerializeableShape",
|
|
59
|
+
OperationOutput: "DeserializeableShape",
|
|
60
|
+
](
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
operation: APIOperation[OperationInput, OperationOutput],
|
|
64
|
+
input: OperationInput,
|
|
65
|
+
endpoint: URI,
|
|
66
|
+
context: TypedProperties,
|
|
67
|
+
) -> HTTPRequest:
|
|
68
|
+
# TODO(optimization): request binding cache like done in SJ
|
|
69
|
+
serializer = HTTPRequestSerializer(
|
|
70
|
+
payload_codec=self.payload_codec,
|
|
71
|
+
http_trait=operation.schema.expect_trait(HTTPTrait),
|
|
72
|
+
endpoint_trait=operation.schema.get_trait(EndpointTrait),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
input.serialize(serializer=serializer)
|
|
76
|
+
request = serializer.result
|
|
77
|
+
|
|
78
|
+
if request is None:
|
|
79
|
+
raise ExpectationNotMetException(
|
|
80
|
+
"Expected request to be serialized, but was None"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return request
|
|
84
|
+
|
|
85
|
+
async def deserialize_response[
|
|
86
|
+
OperationInput: "SerializeableShape",
|
|
87
|
+
OperationOutput: "DeserializeableShape",
|
|
88
|
+
](
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
operation: APIOperation[OperationInput, OperationOutput],
|
|
92
|
+
request: HTTPRequest,
|
|
93
|
+
response: HTTPResponse,
|
|
94
|
+
error_registry: TypeRegistry,
|
|
95
|
+
context: TypedProperties,
|
|
96
|
+
) -> OperationOutput:
|
|
97
|
+
if not (200 <= response.status <= 299):
|
|
98
|
+
# TODO: implement error serde from type registry
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
body = response.body
|
|
102
|
+
|
|
103
|
+
# if body is not streaming and is async, we have to buffer it
|
|
104
|
+
if not operation.output_stream_member:
|
|
105
|
+
if (
|
|
106
|
+
read := getattr(body, "read", None)
|
|
107
|
+
) is not None and iscoroutinefunction(read):
|
|
108
|
+
body = BytesIO(await read())
|
|
109
|
+
|
|
110
|
+
# TODO(optimization): response binding cache like done in SJ
|
|
111
|
+
deserializer = HTTPResponseDeserializer(
|
|
112
|
+
payload_codec=self.payload_codec,
|
|
113
|
+
http_trait=operation.schema.expect_trait(HTTPTrait),
|
|
114
|
+
response=response,
|
|
115
|
+
body=body, # type: ignore
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return operation.output.deserialize(deserializer)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from smithy_core.documents import DocumentValue
|
|
6
|
+
from smithy_core.utils import expect_type
|
|
7
|
+
|
|
8
|
+
from ..restjson import (
|
|
9
|
+
_REST_JSON_CODE_HEADER, # pyright: ignore[reportPrivateUsage]
|
|
10
|
+
_REST_JSON_CODE_KEYS, # pyright: ignore[reportPrivateUsage]
|
|
11
|
+
_REST_JSON_MESSAGE_KEYS, # pyright: ignore[reportPrivateUsage]
|
|
12
|
+
RestJsonErrorInfo,
|
|
13
|
+
)
|
|
14
|
+
from .interfaces import HTTPResponse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def parse_rest_json_error_info(
|
|
18
|
+
http_response: HTTPResponse, check_body: bool = True
|
|
19
|
+
) -> RestJsonErrorInfo:
|
|
20
|
+
"""Parses generic RestJson error info from an HTTP response.
|
|
21
|
+
|
|
22
|
+
:param http_response: The HTTP response to parse.
|
|
23
|
+
:param check_body: Whether to check the body for the code / message.
|
|
24
|
+
:returns: The parsed error information.
|
|
25
|
+
"""
|
|
26
|
+
code: str | None = None
|
|
27
|
+
message: str | None = None
|
|
28
|
+
json_body: dict[str, DocumentValue] | None = None
|
|
29
|
+
|
|
30
|
+
for field in http_response.fields:
|
|
31
|
+
if field.name.lower() == _REST_JSON_CODE_HEADER:
|
|
32
|
+
code = field.values[0]
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
if check_body:
|
|
36
|
+
if body := await http_response.consume_body_async():
|
|
37
|
+
try:
|
|
38
|
+
json_body = json.loads(body)
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
# In some cases the body might end up being HTML depending on the
|
|
41
|
+
# configuration of the server. In those cases we can simply ignore
|
|
42
|
+
# the body because we can't find the information we need there.
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
if json_body:
|
|
46
|
+
for key, value in json_body.items():
|
|
47
|
+
key_lower = key.lower()
|
|
48
|
+
if not code and key_lower in _REST_JSON_CODE_KEYS:
|
|
49
|
+
code = expect_type(str, value)
|
|
50
|
+
if not message and key_lower in _REST_JSON_MESSAGE_KEYS:
|
|
51
|
+
message = expect_type(str, value)
|
|
52
|
+
|
|
53
|
+
# Normalize the error code. Some services may try to send a fully-qualified shape
|
|
54
|
+
# ID or a URI, but we don't want to include those.
|
|
55
|
+
if code:
|
|
56
|
+
if "#" in code:
|
|
57
|
+
code = code.split("#")[1]
|
|
58
|
+
code = code.split(":")[0]
|
|
59
|
+
|
|
60
|
+
return RestJsonErrorInfo(code or "Unknown", message or "Unknown", json_body)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from smithy_core.codecs import Codec
|
|
9
|
+
from smithy_core.deserializers import ShapeDeserializer, SpecificShapeDeserializer
|
|
10
|
+
from smithy_core.exceptions import UnsupportedStreamException
|
|
11
|
+
from smithy_core.interfaces import is_bytes_reader, is_streaming_blob
|
|
12
|
+
from smithy_core.schemas import Schema
|
|
13
|
+
from smithy_core.shapes import ShapeType
|
|
14
|
+
from smithy_core.traits import (
|
|
15
|
+
HTTPHeaderTrait,
|
|
16
|
+
HTTPPayloadTrait,
|
|
17
|
+
HTTPPrefixHeadersTrait,
|
|
18
|
+
HTTPResponseCodeTrait,
|
|
19
|
+
HTTPTrait,
|
|
20
|
+
TimestampFormatTrait,
|
|
21
|
+
)
|
|
22
|
+
from smithy_core.types import TimestampFormat
|
|
23
|
+
from smithy_core.utils import ensure_utc, strict_parse_bool, strict_parse_float
|
|
24
|
+
|
|
25
|
+
from .aio.interfaces import HTTPResponse
|
|
26
|
+
from .interfaces import Field, Fields
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from smithy_core.aio.interfaces import StreamingBlob as AsyncStreamingBlob
|
|
30
|
+
from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["HTTPResponseDeserializer"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HTTPResponseDeserializer(SpecificShapeDeserializer):
|
|
37
|
+
"""Binds :py:class:`HTTPResponse` properties to a DeserializableShape."""
|
|
38
|
+
|
|
39
|
+
# Note: caller will have to read the body if it's async and not streaming
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
payload_codec: Codec,
|
|
43
|
+
http_trait: HTTPTrait,
|
|
44
|
+
response: HTTPResponse,
|
|
45
|
+
body: "SyncStreamingBlob | None" = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize an HTTPResponseDeserializer.
|
|
48
|
+
|
|
49
|
+
:param payload_codec: The Codec to use to deserialize the payload, if present.
|
|
50
|
+
:param http_trait: The HTTP trait of the operation being handled.
|
|
51
|
+
:param response: The HTTP response to read from.
|
|
52
|
+
:param body: The HTTP response body in a synchronously readable form. This is
|
|
53
|
+
necessary for async response bodies when there is no streaming member.
|
|
54
|
+
"""
|
|
55
|
+
self._payload_codec = payload_codec
|
|
56
|
+
self._response = response
|
|
57
|
+
self._http_trait = http_trait
|
|
58
|
+
self._body = body
|
|
59
|
+
|
|
60
|
+
def read_struct(
|
|
61
|
+
self, schema: Schema, consumer: Callable[[Schema, ShapeDeserializer], None]
|
|
62
|
+
) -> None:
|
|
63
|
+
has_body = False
|
|
64
|
+
payload_member: Schema | None = None
|
|
65
|
+
|
|
66
|
+
for member in schema.members.values():
|
|
67
|
+
if (trait := member.get_trait(HTTPHeaderTrait)) is not None:
|
|
68
|
+
header = self._response.fields.entries.get(trait.key.lower())
|
|
69
|
+
if header is not None:
|
|
70
|
+
if member.shape_type is ShapeType.LIST:
|
|
71
|
+
consumer(member, HTTPHeaderListDeserializer(header))
|
|
72
|
+
else:
|
|
73
|
+
consumer(member, HTTPHeaderDeserializer(header.as_string()))
|
|
74
|
+
elif (trait := member.get_trait(HTTPPrefixHeadersTrait)) is not None:
|
|
75
|
+
consumer(
|
|
76
|
+
member,
|
|
77
|
+
HTTPHeaderMapDeserializer(self._response.fields, trait.prefix),
|
|
78
|
+
)
|
|
79
|
+
elif HTTPPayloadTrait in member:
|
|
80
|
+
has_body = True
|
|
81
|
+
payload_member = member
|
|
82
|
+
elif HTTPResponseCodeTrait in member:
|
|
83
|
+
consumer(member, HTTPResponseCodeDeserializer(self._response.status))
|
|
84
|
+
else:
|
|
85
|
+
has_body = True
|
|
86
|
+
|
|
87
|
+
if has_body:
|
|
88
|
+
deserializer = self._create_payload_deserializer(payload_member)
|
|
89
|
+
if payload_member is not None:
|
|
90
|
+
consumer(payload_member, deserializer)
|
|
91
|
+
else:
|
|
92
|
+
deserializer.read_struct(schema, consumer)
|
|
93
|
+
|
|
94
|
+
def _create_payload_deserializer(
|
|
95
|
+
self, payload_member: Schema | None
|
|
96
|
+
) -> ShapeDeserializer:
|
|
97
|
+
body = self._body if self._body is not None else self._response.body
|
|
98
|
+
if payload_member is not None and payload_member.shape_type in (
|
|
99
|
+
ShapeType.BLOB,
|
|
100
|
+
ShapeType.STRING,
|
|
101
|
+
):
|
|
102
|
+
return RawPayloadDeserializer(body)
|
|
103
|
+
|
|
104
|
+
if not is_streaming_blob(body):
|
|
105
|
+
raise UnsupportedStreamException(
|
|
106
|
+
"Unable to read async stream. This stream must be buffered prior "
|
|
107
|
+
"to creating the deserializer."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if isinstance(body, bytearray):
|
|
111
|
+
body = bytes(body)
|
|
112
|
+
|
|
113
|
+
return self._payload_codec.create_deserializer(body)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class HTTPHeaderDeserializer(SpecificShapeDeserializer):
|
|
117
|
+
"""Binds HTTP header values to a deserializable shape.
|
|
118
|
+
|
|
119
|
+
For headers with list values, see :py:class:`HTTPHeaderListDeserializer`.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, value: str) -> None:
|
|
123
|
+
"""Initialize an HTTPHeaderDeserializer.
|
|
124
|
+
|
|
125
|
+
:param value: The string value of the header.
|
|
126
|
+
"""
|
|
127
|
+
self._value = value
|
|
128
|
+
|
|
129
|
+
def read_boolean(self, schema: Schema) -> bool:
|
|
130
|
+
return strict_parse_bool(self._value)
|
|
131
|
+
|
|
132
|
+
def read_byte(self, schema: Schema) -> int:
|
|
133
|
+
return self.read_integer(schema)
|
|
134
|
+
|
|
135
|
+
def read_short(self, schema: Schema) -> int:
|
|
136
|
+
return self.read_integer(schema)
|
|
137
|
+
|
|
138
|
+
def read_integer(self, schema: Schema) -> int:
|
|
139
|
+
return int(self._value)
|
|
140
|
+
|
|
141
|
+
def read_long(self, schema: Schema) -> int:
|
|
142
|
+
return self.read_integer(schema)
|
|
143
|
+
|
|
144
|
+
def read_big_integer(self, schema: Schema) -> int:
|
|
145
|
+
return self.read_integer(schema)
|
|
146
|
+
|
|
147
|
+
def read_float(self, schema: Schema) -> float:
|
|
148
|
+
return strict_parse_float(self._value)
|
|
149
|
+
|
|
150
|
+
def read_double(self, schema: Schema) -> float:
|
|
151
|
+
return self.read_float(schema)
|
|
152
|
+
|
|
153
|
+
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
154
|
+
return Decimal(self._value).canonical()
|
|
155
|
+
|
|
156
|
+
def read_string(self, schema: Schema) -> str:
|
|
157
|
+
return self._value
|
|
158
|
+
|
|
159
|
+
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
160
|
+
format = TimestampFormat.HTTP_DATE
|
|
161
|
+
if (trait := schema.get_trait(TimestampFormatTrait)) is not None:
|
|
162
|
+
format = trait.format
|
|
163
|
+
return ensure_utc(format.deserialize(self._value))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class HTTPHeaderListDeserializer(SpecificShapeDeserializer):
|
|
167
|
+
"""Binds HTTP header lists to a deserializable shape."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, field: Field) -> None:
|
|
170
|
+
"""Initialize an HTTPHeaderListDeserializer.
|
|
171
|
+
|
|
172
|
+
:param field: The field to deserialize.
|
|
173
|
+
"""
|
|
174
|
+
self._field = field
|
|
175
|
+
|
|
176
|
+
def read_list(
|
|
177
|
+
self, schema: Schema, consumer: Callable[["ShapeDeserializer"], None]
|
|
178
|
+
) -> None:
|
|
179
|
+
for value in self._field.values:
|
|
180
|
+
consumer(HTTPHeaderDeserializer(value))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class HTTPHeaderMapDeserializer(SpecificShapeDeserializer):
|
|
184
|
+
"""Binds HTTP header maps to a deserializable shape."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, fields: Fields, prefix: str = "") -> None:
|
|
187
|
+
"""Initialize an HTTPHeaderMapDeserializer.
|
|
188
|
+
|
|
189
|
+
:param fields: The collection of headers to search for map values.
|
|
190
|
+
:param prefix: An optional prefix to limit which headers are pulled in to the
|
|
191
|
+
map. By default, all headers are pulled in, including headers that are bound
|
|
192
|
+
to other properties on the shape.
|
|
193
|
+
"""
|
|
194
|
+
self._prefix = prefix.lower()
|
|
195
|
+
self._fields = fields
|
|
196
|
+
|
|
197
|
+
def read_map(
|
|
198
|
+
self,
|
|
199
|
+
schema: Schema,
|
|
200
|
+
consumer: Callable[[str, "ShapeDeserializer"], None],
|
|
201
|
+
) -> None:
|
|
202
|
+
trim = len(self._prefix)
|
|
203
|
+
for field in self._fields:
|
|
204
|
+
if field.name.lower().startswith(self._prefix):
|
|
205
|
+
consumer(field.name[trim:], HTTPHeaderDeserializer(field.as_string()))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HTTPResponseCodeDeserializer(SpecificShapeDeserializer):
|
|
209
|
+
"""Binds HTTP response codes to a deserializeable shape."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, response_code: int) -> None:
|
|
212
|
+
"""Initialize an HTTPResponseCodeDeserializer.
|
|
213
|
+
|
|
214
|
+
:param response_code: The response code to bind.
|
|
215
|
+
"""
|
|
216
|
+
self._response_code = response_code
|
|
217
|
+
|
|
218
|
+
def read_byte(self, schema: Schema) -> int:
|
|
219
|
+
return self._response_code
|
|
220
|
+
|
|
221
|
+
def read_short(self, schema: Schema) -> int:
|
|
222
|
+
return self._response_code
|
|
223
|
+
|
|
224
|
+
def read_integer(self, schema: Schema) -> int:
|
|
225
|
+
return self._response_code
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class RawPayloadDeserializer(SpecificShapeDeserializer):
|
|
229
|
+
"""Binds an HTTP payload to a deserializeable shape."""
|
|
230
|
+
|
|
231
|
+
def __init__(self, payload: "AsyncStreamingBlob") -> None:
|
|
232
|
+
"""Initialize a RawPayloadDeserializer.
|
|
233
|
+
|
|
234
|
+
:param payload: The payload to bind. If the member that is bound to the payload
|
|
235
|
+
is a string or blob, it MUST NOT be an async stream. Async streams MUST be
|
|
236
|
+
buffered into a synchronous stream ahead of time.
|
|
237
|
+
"""
|
|
238
|
+
self._payload = payload
|
|
239
|
+
|
|
240
|
+
def read_string(self, schema: Schema) -> str:
|
|
241
|
+
return self._consume_payload().decode("utf-8")
|
|
242
|
+
|
|
243
|
+
def read_blob(self, schema: Schema) -> bytes:
|
|
244
|
+
return self._consume_payload()
|
|
245
|
+
|
|
246
|
+
def read_data_stream(self, schema: Schema) -> "AsyncStreamingBlob":
|
|
247
|
+
return self._payload
|
|
248
|
+
|
|
249
|
+
def _consume_payload(self) -> bytes:
|
|
250
|
+
if isinstance(self._payload, bytes):
|
|
251
|
+
return self._payload
|
|
252
|
+
if isinstance(self._payload, bytearray):
|
|
253
|
+
return bytes(self._payload)
|
|
254
|
+
if is_bytes_reader(self._payload):
|
|
255
|
+
return self._payload.read()
|
|
256
|
+
raise UnsupportedStreamException(
|
|
257
|
+
"Unable to read async stream. This stream must be buffered prior "
|
|
258
|
+
"to creating the deserializer."
|
|
259
|
+
)
|
smithy_http/endpoints.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from smithy_core.endpoints import Endpoint
|
|
6
|
+
from smithy_core.interfaces import TypedProperties as _TypedProperties
|
|
7
|
+
from smithy_core.interfaces import URI
|
|
8
|
+
from smithy_core.types import PropertyKey, TypedProperties
|
|
9
|
+
|
|
10
|
+
from . import Fields, interfaces
|
|
11
|
+
|
|
12
|
+
HEADERS = PropertyKey(key="headers", value_type=interfaces.Fields)
|
|
13
|
+
"""An Endpoint property indicating the given fields MUST be added to the request."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(init=False, kw_only=True)
|
|
17
|
+
class HTTPEndpoint(Endpoint):
|
|
18
|
+
"""A resolved endpoint with optional HTTP headers."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
uri: URI,
|
|
24
|
+
properties: _TypedProperties | None = None,
|
|
25
|
+
headers: interfaces.Fields | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.uri = uri
|
|
28
|
+
self.properties = properties if properties is not None else TypedProperties()
|
|
29
|
+
headers = headers if headers is not None else Fields()
|
|
30
|
+
self.properties[HEADERS] = headers
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from smithy_core.exceptions import SmithyException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SmithyHTTPException(SmithyException):
|
|
7
|
+
"""Base exception type for all exceptions raised in HTTP clients."""
|