smithy-aws-event-stream 0.0.1__tar.gz
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_aws_event_stream-0.0.1/.gitignore +36 -0
- smithy_aws_event_stream-0.0.1/CHANGES.md +11 -0
- smithy_aws_event_stream-0.0.1/NOTICE +1 -0
- smithy_aws_event_stream-0.0.1/PKG-INFO +7 -0
- smithy_aws_event_stream-0.0.1/README.md +0 -0
- smithy_aws_event_stream-0.0.1/pyproject.toml +21 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/__init__.py +6 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/_private/__init__.py +14 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/_private/deserializers.py +166 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/_private/serializers.py +181 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/aio/__init__.py +141 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/events.py +789 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/exceptions.py +104 -0
- smithy_aws_event_stream-0.0.1/src/smithy_aws_event_stream/py.typed +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Eclipse
|
|
2
|
+
.classpath
|
|
3
|
+
.project
|
|
4
|
+
.settings/
|
|
5
|
+
|
|
6
|
+
# Intellij
|
|
7
|
+
.idea/
|
|
8
|
+
*.iml
|
|
9
|
+
*.iws
|
|
10
|
+
|
|
11
|
+
# VS Code
|
|
12
|
+
.vscode/
|
|
13
|
+
|
|
14
|
+
# Mac
|
|
15
|
+
.DS_Store
|
|
16
|
+
|
|
17
|
+
# Maven
|
|
18
|
+
target/
|
|
19
|
+
**/dependency-reduced-pom.xml
|
|
20
|
+
|
|
21
|
+
# Gradle
|
|
22
|
+
**/.gradle
|
|
23
|
+
build/
|
|
24
|
+
*/out/
|
|
25
|
+
**/*/out/
|
|
26
|
+
|
|
27
|
+
# Python
|
|
28
|
+
__pycache__
|
|
29
|
+
*.swp
|
|
30
|
+
.mypy_cache
|
|
31
|
+
.coverage
|
|
32
|
+
coverage.xml
|
|
33
|
+
htmlcov
|
|
34
|
+
*.egg-info
|
|
35
|
+
dist
|
|
36
|
+
.venv
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
* <Add new items here>
|
|
6
|
+
|
|
7
|
+
## v0.0.1
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
* Added basic support for the [Amazon Event Stream](https://smithy.io/2.0/aws/amazon-eventstream.html) specification.
|
|
11
|
+
* Added support for unidirectdional and bidirectional event streams.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smithy-aws-event-stream"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Smithy components for Amazon Event Streams."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"smithy-core",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build]
|
|
16
|
+
exclude = [
|
|
17
|
+
"tests",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.ruff]
|
|
21
|
+
src = ["src"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from smithy_core.schemas import Schema
|
|
4
|
+
from smithy_core.traits import EventPayloadTrait
|
|
5
|
+
|
|
6
|
+
INITIAL_REQUEST_EVENT_TYPE = "initial-request"
|
|
7
|
+
INITIAL_RESPONSE_EVENT_TYPE = "initial-response"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_payload_member(schema: Schema) -> Schema | None:
|
|
11
|
+
for member in schema.members.values():
|
|
12
|
+
if EventPayloadTrait in member:
|
|
13
|
+
return member
|
|
14
|
+
return None
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from smithy_core.codecs import Codec
|
|
8
|
+
from smithy_core.deserializers import (
|
|
9
|
+
ShapeDeserializer,
|
|
10
|
+
SpecificShapeDeserializer,
|
|
11
|
+
)
|
|
12
|
+
from smithy_core.schemas import Schema
|
|
13
|
+
from smithy_core.shapes import ShapeType
|
|
14
|
+
from smithy_core.traits import EventHeaderTrait
|
|
15
|
+
from smithy_core.utils import expect_type
|
|
16
|
+
|
|
17
|
+
from ..events import HEADERS_DICT, Event
|
|
18
|
+
from ..exceptions import EventError, UnmodeledEventError
|
|
19
|
+
from . import (
|
|
20
|
+
INITIAL_REQUEST_EVENT_TYPE,
|
|
21
|
+
INITIAL_RESPONSE_EVENT_TYPE,
|
|
22
|
+
get_payload_member,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
INITIAL_MESSAGE_TYPES = (INITIAL_REQUEST_EVENT_TYPE, INITIAL_RESPONSE_EVENT_TYPE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EventDeserializer(SpecificShapeDeserializer):
|
|
31
|
+
def __init__(
|
|
32
|
+
self, event: Event, payload_codec: Codec, is_client_mode: bool = True
|
|
33
|
+
) -> None:
|
|
34
|
+
self._event = event
|
|
35
|
+
self._payload_codec = payload_codec
|
|
36
|
+
self._is_client_mode = is_client_mode
|
|
37
|
+
|
|
38
|
+
def read_struct(
|
|
39
|
+
self,
|
|
40
|
+
schema: Schema,
|
|
41
|
+
consumer: Callable[[Schema, ShapeDeserializer], None],
|
|
42
|
+
) -> None:
|
|
43
|
+
headers = self._event.message.headers
|
|
44
|
+
|
|
45
|
+
match headers.get(":message-type"):
|
|
46
|
+
case "event":
|
|
47
|
+
member_name = expect_type(str, headers[":event-type"])
|
|
48
|
+
if member_name in INITIAL_MESSAGE_TYPES:
|
|
49
|
+
# If it's an initial message, skip straight to deserialization.
|
|
50
|
+
message_deserializer = self._create_deserializer(schema, headers)
|
|
51
|
+
message_deserializer.read_struct(schema, consumer)
|
|
52
|
+
else:
|
|
53
|
+
member_schema = schema.members[member_name]
|
|
54
|
+
message_deserializer = self._create_deserializer(
|
|
55
|
+
member_schema, headers
|
|
56
|
+
)
|
|
57
|
+
consumer(member_schema, message_deserializer)
|
|
58
|
+
case "exception":
|
|
59
|
+
member_name = expect_type(str, headers[":exception-type"])
|
|
60
|
+
member_schema = schema.members[member_name]
|
|
61
|
+
message_deserializer = self._create_deserializer(member_schema, headers)
|
|
62
|
+
consumer(member_schema, message_deserializer)
|
|
63
|
+
case "error":
|
|
64
|
+
# The `application/vnd.amazon.eventstream` format allows for explicitly
|
|
65
|
+
# unmodeled exceptions. These exceptions MUST have the `:error-code`
|
|
66
|
+
# and `:error-message` headers set, and they MUST be strings.
|
|
67
|
+
raise UnmodeledEventError(
|
|
68
|
+
expect_type(str, headers[":error-code"]),
|
|
69
|
+
expect_type(str, headers[":error-message"]),
|
|
70
|
+
)
|
|
71
|
+
case _:
|
|
72
|
+
raise EventError(f"Unknown event structure: {self._event}")
|
|
73
|
+
|
|
74
|
+
def _create_deserializer(
|
|
75
|
+
self, schema: Schema, headers: HEADERS_DICT
|
|
76
|
+
) -> ShapeDeserializer:
|
|
77
|
+
payload_member = get_payload_member(schema)
|
|
78
|
+
payload_deserializer = self._create_payload_deserializer(payload_member)
|
|
79
|
+
return EventMessageDeserializer(headers, payload_deserializer, payload_member)
|
|
80
|
+
|
|
81
|
+
def _create_payload_deserializer(
|
|
82
|
+
self, payload_member: Schema | None
|
|
83
|
+
) -> ShapeDeserializer | None:
|
|
84
|
+
if not self._event.message.payload:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if payload_member is not None and payload_member.shape_type in (
|
|
88
|
+
ShapeType.BLOB,
|
|
89
|
+
ShapeType.STRING,
|
|
90
|
+
):
|
|
91
|
+
return RawPayloadDeserializer(self._event.message.payload)
|
|
92
|
+
|
|
93
|
+
return self._payload_codec.create_deserializer(self._event.message.payload)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RawPayloadDeserializer(SpecificShapeDeserializer):
|
|
97
|
+
def __init__(self, payload: bytes) -> None:
|
|
98
|
+
self._payload = payload
|
|
99
|
+
|
|
100
|
+
def read_string(self, schema: Schema) -> str:
|
|
101
|
+
return self._payload.decode("utf-8")
|
|
102
|
+
|
|
103
|
+
def read_blob(self, schema: Schema) -> bytes:
|
|
104
|
+
return self._payload
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EventMessageDeserializer(SpecificShapeDeserializer):
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
headers: HEADERS_DICT,
|
|
111
|
+
payload_deserializer: ShapeDeserializer | None,
|
|
112
|
+
payload_member: Schema | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
self._headers = headers
|
|
115
|
+
self._payload_deserializer = payload_deserializer
|
|
116
|
+
self._payload_member = payload_member
|
|
117
|
+
|
|
118
|
+
def read_struct(
|
|
119
|
+
self,
|
|
120
|
+
schema: Schema,
|
|
121
|
+
consumer: Callable[[Schema, ShapeDeserializer], None],
|
|
122
|
+
) -> None:
|
|
123
|
+
headers_deserializer = EventHeaderDeserializer(self._headers)
|
|
124
|
+
for key in self._headers.keys():
|
|
125
|
+
member_schema = schema.members.get(key)
|
|
126
|
+
if member_schema is not None and EventHeaderTrait in member_schema:
|
|
127
|
+
consumer(member_schema, headers_deserializer)
|
|
128
|
+
|
|
129
|
+
if self._payload_deserializer:
|
|
130
|
+
if self._payload_member is not None:
|
|
131
|
+
consumer(self._payload_member, self._payload_deserializer)
|
|
132
|
+
else:
|
|
133
|
+
self._payload_deserializer.read_struct(schema, consumer)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EventHeaderDeserializer(SpecificShapeDeserializer):
|
|
137
|
+
def __init__(self, headers: HEADERS_DICT) -> None:
|
|
138
|
+
self._headers = headers
|
|
139
|
+
|
|
140
|
+
def read_boolean(self, schema: "Schema") -> bool:
|
|
141
|
+
return expect_type(bool, self._headers[schema.expect_member_name()])
|
|
142
|
+
|
|
143
|
+
def read_blob(self, schema: "Schema") -> bytes:
|
|
144
|
+
return expect_type(bytes, self._headers[schema.expect_member_name()])
|
|
145
|
+
|
|
146
|
+
def read_byte(self, schema: "Schema") -> int:
|
|
147
|
+
return expect_type(int, self._headers[schema.expect_member_name()])
|
|
148
|
+
|
|
149
|
+
def read_short(self, schema: "Schema") -> int:
|
|
150
|
+
return expect_type(int, self._headers[schema.expect_member_name()])
|
|
151
|
+
|
|
152
|
+
def read_integer(self, schema: "Schema") -> int:
|
|
153
|
+
return expect_type(int, self._headers[schema.expect_member_name()])
|
|
154
|
+
|
|
155
|
+
def read_long(self, schema: "Schema") -> int:
|
|
156
|
+
return expect_type(int, self._headers[schema.expect_member_name()])
|
|
157
|
+
|
|
158
|
+
def read_string(self, schema: "Schema") -> str:
|
|
159
|
+
return expect_type(str, self._headers[schema.expect_member_name()])
|
|
160
|
+
|
|
161
|
+
def read_timestamp(self, schema: "Schema") -> datetime.datetime:
|
|
162
|
+
# TODO: do we support timestamp format here? One would assume not since the
|
|
163
|
+
# format has a specific timestamp type.
|
|
164
|
+
return expect_type(
|
|
165
|
+
datetime.datetime, self._headers[schema.expect_member_name()]
|
|
166
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from typing import Never
|
|
9
|
+
|
|
10
|
+
from smithy_core.codecs import Codec
|
|
11
|
+
from smithy_core.schemas import Schema
|
|
12
|
+
from smithy_core.serializers import (
|
|
13
|
+
InterceptingSerializer,
|
|
14
|
+
ShapeSerializer,
|
|
15
|
+
SpecificShapeSerializer,
|
|
16
|
+
)
|
|
17
|
+
from smithy_core.shapes import ShapeType
|
|
18
|
+
from smithy_core.traits import ErrorTrait, EventHeaderTrait, MediaTypeTrait
|
|
19
|
+
|
|
20
|
+
from ..events import HEADER_VALUE, Byte, EventHeaderEncoder, EventMessage, Long, Short
|
|
21
|
+
from ..exceptions import InvalidHeaderValue
|
|
22
|
+
from . import (
|
|
23
|
+
INITIAL_REQUEST_EVENT_TYPE,
|
|
24
|
+
INITIAL_RESPONSE_EVENT_TYPE,
|
|
25
|
+
get_payload_member,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_DEFAULT_STRING_CONTENT_TYPE = "text/plain"
|
|
31
|
+
_DEFAULT_BLOB_CONTENT_TYPE = "application/octet-stream"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventSerializer(SpecificShapeSerializer):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
payload_codec: Codec,
|
|
38
|
+
is_client_mode: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._payload_codec = payload_codec
|
|
41
|
+
self._result: EventMessage | None = None
|
|
42
|
+
if is_client_mode:
|
|
43
|
+
self._initial_message_event_type = INITIAL_REQUEST_EVENT_TYPE
|
|
44
|
+
else:
|
|
45
|
+
self._initial_message_event_type = INITIAL_RESPONSE_EVENT_TYPE
|
|
46
|
+
self.event_header_encoder_cls = EventHeaderEncoder
|
|
47
|
+
|
|
48
|
+
def get_result(self) -> EventMessage | None:
|
|
49
|
+
return self._result
|
|
50
|
+
|
|
51
|
+
@contextmanager
|
|
52
|
+
def begin_struct(self, schema: "Schema") -> Iterator[ShapeSerializer]:
|
|
53
|
+
# Event stream definitions are unions. Nothing about the union shape actually
|
|
54
|
+
# matters for the purposes of event stream serialization though, we just care
|
|
55
|
+
# about the specific member we're serializing. So here we yield immediately,
|
|
56
|
+
# and the next time this method is called it'll be by the member that we
|
|
57
|
+
# actually care about.
|
|
58
|
+
#
|
|
59
|
+
# Note that if we're serializing an operation input or output, it won't be a
|
|
60
|
+
# union at all, so this won't get triggered. Thankfully, that's what we want.
|
|
61
|
+
if schema.shape_type is ShapeType.UNION:
|
|
62
|
+
try:
|
|
63
|
+
yield self
|
|
64
|
+
finally:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
headers: dict[str, HEADER_VALUE] = {}
|
|
68
|
+
|
|
69
|
+
if ErrorTrait in schema:
|
|
70
|
+
headers[":message-type"] = "exception"
|
|
71
|
+
headers[":exception-type"] = schema.expect_member_name()
|
|
72
|
+
else:
|
|
73
|
+
headers[":message-type"] = "event"
|
|
74
|
+
if schema.member_name is None:
|
|
75
|
+
# If there's no member name, that must mean that the structure is
|
|
76
|
+
# either an input or output structure, and so this represents the
|
|
77
|
+
# initial message.
|
|
78
|
+
headers[":event-type"] = self._initial_message_event_type
|
|
79
|
+
else:
|
|
80
|
+
headers[":event-type"] = schema.member_name
|
|
81
|
+
|
|
82
|
+
payload = BytesIO()
|
|
83
|
+
payload_serializer: ShapeSerializer = self._payload_codec.create_serializer(
|
|
84
|
+
payload
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
header_serializer = EventHeaderSerializer(headers)
|
|
88
|
+
|
|
89
|
+
media_type = self._payload_codec.media_type
|
|
90
|
+
|
|
91
|
+
if (payload_member := get_payload_member(schema)) is not None:
|
|
92
|
+
media_type = self._get_payload_media_type(payload_member, media_type)
|
|
93
|
+
if payload_member.shape_type in (ShapeType.BLOB, ShapeType.STRING):
|
|
94
|
+
payload_serializer = RawPayloadSerializer(payload)
|
|
95
|
+
yield EventStreamBindingSerializer(header_serializer, payload_serializer)
|
|
96
|
+
else:
|
|
97
|
+
with payload_serializer.begin_struct(schema) as body_serializer:
|
|
98
|
+
yield EventStreamBindingSerializer(header_serializer, body_serializer)
|
|
99
|
+
|
|
100
|
+
payload_bytes = payload.getvalue()
|
|
101
|
+
if payload_bytes:
|
|
102
|
+
headers[":content-type"] = media_type
|
|
103
|
+
|
|
104
|
+
self._result = EventMessage(headers=headers, payload=payload_bytes)
|
|
105
|
+
|
|
106
|
+
def _get_payload_media_type(self, schema: Schema, default: str) -> str:
|
|
107
|
+
if (media_type := schema.get_trait(MediaTypeTrait)) is not None:
|
|
108
|
+
return media_type.value
|
|
109
|
+
|
|
110
|
+
match schema.shape_type:
|
|
111
|
+
case ShapeType.STRING:
|
|
112
|
+
return _DEFAULT_STRING_CONTENT_TYPE
|
|
113
|
+
case ShapeType.BLOB:
|
|
114
|
+
return _DEFAULT_BLOB_CONTENT_TYPE
|
|
115
|
+
case _:
|
|
116
|
+
return default
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EventHeaderSerializer(SpecificShapeSerializer):
|
|
120
|
+
def __init__(self, headers: dict[str, HEADER_VALUE]) -> None:
|
|
121
|
+
self._headers = headers
|
|
122
|
+
|
|
123
|
+
def _invalid_state(
|
|
124
|
+
self, schema: "Schema | None" = None, message: str | None = None
|
|
125
|
+
) -> Never:
|
|
126
|
+
if message is None:
|
|
127
|
+
message = f"Invalid header value type: {schema}"
|
|
128
|
+
raise InvalidHeaderValue(message)
|
|
129
|
+
|
|
130
|
+
def write_boolean(self, schema: "Schema", value: bool) -> None:
|
|
131
|
+
self._headers[schema.expect_member_name()] = value
|
|
132
|
+
|
|
133
|
+
def write_byte(self, schema: "Schema", value: int) -> None:
|
|
134
|
+
self._headers[schema.expect_member_name()] = Byte(value)
|
|
135
|
+
|
|
136
|
+
def write_short(self, schema: "Schema", value: int) -> None:
|
|
137
|
+
self._headers[schema.expect_member_name()] = Short(value)
|
|
138
|
+
|
|
139
|
+
def write_integer(self, schema: "Schema", value: int) -> None:
|
|
140
|
+
self._headers[schema.expect_member_name()] = value
|
|
141
|
+
|
|
142
|
+
def write_long(self, schema: "Schema", value: int) -> None:
|
|
143
|
+
self._headers[schema.expect_member_name()] = Long(value)
|
|
144
|
+
|
|
145
|
+
def write_string(self, schema: "Schema", value: str) -> None:
|
|
146
|
+
self._headers[schema.expect_member_name()] = value
|
|
147
|
+
|
|
148
|
+
def write_blob(self, schema: "Schema", value: bytes) -> None:
|
|
149
|
+
self._headers[schema.expect_member_name()] = value
|
|
150
|
+
|
|
151
|
+
def write_timestamp(self, schema: "Schema", value: datetime.datetime) -> None:
|
|
152
|
+
self._headers[schema.expect_member_name()] = value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RawPayloadSerializer(SpecificShapeSerializer):
|
|
156
|
+
def __init__(self, payload: BytesIO) -> None:
|
|
157
|
+
self._payload = payload
|
|
158
|
+
|
|
159
|
+
def write_string(self, schema: "Schema", value: str) -> None:
|
|
160
|
+
self._payload.write(value.encode("utf-8"))
|
|
161
|
+
|
|
162
|
+
def write_blob(self, schema: "Schema", value: bytes) -> None:
|
|
163
|
+
self._payload.write(value)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class EventStreamBindingSerializer(InterceptingSerializer):
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
header_serializer: EventHeaderSerializer,
|
|
170
|
+
payload_struct_serializer: ShapeSerializer,
|
|
171
|
+
) -> None:
|
|
172
|
+
self._header_serializer = header_serializer
|
|
173
|
+
self._payload_struct_serializer = payload_struct_serializer
|
|
174
|
+
|
|
175
|
+
def before(self, schema: "Schema") -> ShapeSerializer:
|
|
176
|
+
if EventHeaderTrait in schema:
|
|
177
|
+
return self._header_serializer
|
|
178
|
+
return self._payload_struct_serializer
|
|
179
|
+
|
|
180
|
+
def after(self, schema: "Schema") -> None:
|
|
181
|
+
pass
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from smithy_core.aio.interfaces import AsyncByteStream, AsyncWriter
|
|
9
|
+
from smithy_core.aio.interfaces.eventstream import EventPublisher, EventReceiver
|
|
10
|
+
from smithy_core.codecs import Codec
|
|
11
|
+
from smithy_core.deserializers import DeserializeableShape, ShapeDeserializer
|
|
12
|
+
from smithy_core.exceptions import ExpectationNotMetException
|
|
13
|
+
from smithy_core.serializers import SerializeableShape
|
|
14
|
+
|
|
15
|
+
from .._private.deserializers import EventDeserializer as _EventDeserializer
|
|
16
|
+
from .._private.serializers import EventSerializer as _EventSerializer
|
|
17
|
+
from ..events import Event, EventHeaderEncoder, EventMessage
|
|
18
|
+
from ..exceptions import EventError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EventSigner(Protocol):
|
|
24
|
+
"""A signer to manage credentials and EventMessages for an Event Stream lifecyle."""
|
|
25
|
+
|
|
26
|
+
async def sign_event(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
event_message: EventMessage,
|
|
30
|
+
event_encoder_cls: type[EventHeaderEncoder],
|
|
31
|
+
) -> EventMessage: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AWSEventPublisher[E: SerializeableShape](EventPublisher[E]):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
payload_codec: Codec,
|
|
38
|
+
async_writer: AsyncWriter,
|
|
39
|
+
signer: EventSigner | None = None,
|
|
40
|
+
is_client_mode: bool = True,
|
|
41
|
+
):
|
|
42
|
+
self._writer = async_writer
|
|
43
|
+
self._signer = signer
|
|
44
|
+
self._serializer = _EventSerializer(
|
|
45
|
+
payload_codec=payload_codec, is_client_mode=is_client_mode
|
|
46
|
+
)
|
|
47
|
+
self._closed = False
|
|
48
|
+
|
|
49
|
+
async def send(self, event: E) -> None:
|
|
50
|
+
if self._closed:
|
|
51
|
+
raise OSError("Attempted to write to closed stream.")
|
|
52
|
+
logger.debug("Preparing to publish event: %s", event)
|
|
53
|
+
event.serialize(self._serializer)
|
|
54
|
+
result = self._serializer.get_result()
|
|
55
|
+
if result is None:
|
|
56
|
+
raise ExpectationNotMetException(
|
|
57
|
+
"Expected an event message to be serialized, but was None."
|
|
58
|
+
)
|
|
59
|
+
if self._signer is not None:
|
|
60
|
+
encoder = self._serializer.event_header_encoder_cls
|
|
61
|
+
result = await self._signer.sign_event(
|
|
62
|
+
event_message=result,
|
|
63
|
+
event_encoder_cls=encoder,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
encoded_result = result.encode()
|
|
67
|
+
try:
|
|
68
|
+
logger.debug("Publishing serialized event: %s", result)
|
|
69
|
+
await self._writer.write(encoded_result)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
await self.close()
|
|
72
|
+
raise OSError("Failed to write to stream.") from e
|
|
73
|
+
|
|
74
|
+
async def close(self) -> None:
|
|
75
|
+
if self._closed:
|
|
76
|
+
return
|
|
77
|
+
self._closed = True
|
|
78
|
+
|
|
79
|
+
if (close := getattr(self._writer, "close", None)) is not None:
|
|
80
|
+
if asyncio.iscoroutine(result := close()):
|
|
81
|
+
await result
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def closed(self) -> bool:
|
|
85
|
+
return self._closed
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AWSEventReceiver[E: DeserializeableShape](EventReceiver[E]):
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
payload_codec: Codec,
|
|
92
|
+
source: AsyncByteStream,
|
|
93
|
+
deserializer: Callable[[ShapeDeserializer], E],
|
|
94
|
+
is_client_mode: bool = True,
|
|
95
|
+
) -> None:
|
|
96
|
+
self._payload_codec = payload_codec
|
|
97
|
+
self._source = source
|
|
98
|
+
self._is_client_mode = is_client_mode
|
|
99
|
+
self._deserializer = deserializer
|
|
100
|
+
self._closed = False
|
|
101
|
+
|
|
102
|
+
async def receive(self) -> E | None:
|
|
103
|
+
if self._closed:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
event = await Event.decode_async(self._source)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
await self.close()
|
|
110
|
+
if not isinstance(e, EventError):
|
|
111
|
+
raise OSError("Failed to read from stream.") from e
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
if event is None:
|
|
115
|
+
logger.debug("No event received from the source.")
|
|
116
|
+
return None
|
|
117
|
+
logger.debug("Received raw event: %s", event)
|
|
118
|
+
|
|
119
|
+
deserializer = _EventDeserializer(
|
|
120
|
+
event=event,
|
|
121
|
+
payload_codec=self._payload_codec,
|
|
122
|
+
is_client_mode=self._is_client_mode,
|
|
123
|
+
)
|
|
124
|
+
result = self._deserializer(deserializer)
|
|
125
|
+
logger.debug("Successfully deserialized event: %s", result)
|
|
126
|
+
if isinstance(getattr(result, "value"), Exception):
|
|
127
|
+
raise result.value # type: ignore
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
if self._closed:
|
|
132
|
+
return
|
|
133
|
+
self._closed = True
|
|
134
|
+
|
|
135
|
+
if (close := getattr(self._source, "close", None)) is not None:
|
|
136
|
+
if asyncio.iscoroutine(result := close()):
|
|
137
|
+
await result
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def closed(self) -> bool:
|
|
141
|
+
return self._closed
|