smithy-core 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.
@@ -0,0 +1,141 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import importlib.metadata
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from functools import cached_property
7
+ from urllib.parse import urlunparse
8
+
9
+ from . import interfaces, rfc3986
10
+ from .exceptions import SmithyException
11
+
12
+ __version__: str = importlib.metadata.version("smithy-core")
13
+
14
+
15
+ class HostType(Enum):
16
+ """Enumeration of possible host types."""
17
+
18
+ IPv6 = "IPv6"
19
+ """Host is an IPv6 address."""
20
+
21
+ IPv4 = "IPv4"
22
+ """Host is an IPv4 address."""
23
+
24
+ DOMAIN = "DOMAIN"
25
+ """Host type is a domain name."""
26
+
27
+ UNKNOWN = "UNKNOWN"
28
+ """Host type is unknown."""
29
+
30
+
31
+ @dataclass(kw_only=True, frozen=True)
32
+ class URI(interfaces.URI):
33
+ """Universal Resource Identifier, target location for a :py:class:`HTTPRequest`."""
34
+
35
+ scheme: str = "https"
36
+ """For example ``http`` or ``https``."""
37
+
38
+ username: str | None = None
39
+ """Username part of the userinfo URI component."""
40
+
41
+ password: str | None = None
42
+ """Password part of the userinfo URI component."""
43
+
44
+ host: str
45
+ """The hostname, for example ``amazonaws.com``."""
46
+
47
+ port: int | None = None
48
+ """An explicit port number."""
49
+
50
+ path: str | None = None
51
+ """Path component of the URI."""
52
+
53
+ query: str | None = None
54
+ """Query component of the URI as string."""
55
+
56
+ fragment: str | None = None
57
+ """Part of the URI specification, but may not be transmitted by a client."""
58
+
59
+ def __post_init__(self) -> None:
60
+ """Validate host component."""
61
+ if not rfc3986.HOST_MATCHER.match(self.host) and not rfc3986.IPv6_MATCHER.match(
62
+ f"[{self.host}]"
63
+ ):
64
+ raise SmithyException(f"Invalid host: {self.host}")
65
+
66
+ @property
67
+ def netloc(self) -> str:
68
+ """Construct netloc string in format ``{username}:{password}@{host}:{port}``
69
+
70
+ ``username``, ``password``, and ``port`` are only included if set. ``password``
71
+ is ignored, unless ``username`` is also set. Add square brackets around the host
72
+ if it is a valid IPv6 endpoint URI per :rfc:`3986#section-3.2.2`.
73
+ """
74
+ return self._netloc
75
+
76
+ # cached_property does NOT behave like property, it actually allows for setting.
77
+ # Therefore we need a layer of indirection.
78
+ @cached_property
79
+ def _netloc(self) -> str:
80
+ if self.username is not None:
81
+ password = "" if self.password is None else f":{self.password}"
82
+ userinfo = f"{self.username}{password}@"
83
+ else:
84
+ userinfo = ""
85
+
86
+ if self.port is not None:
87
+ port = f":{self.port}"
88
+ else:
89
+ port = ""
90
+
91
+ if self.host_type == HostType.IPv6:
92
+ host = f"[{self.host}]"
93
+ else:
94
+ host = self.host
95
+
96
+ return f"{userinfo}{host}{port}"
97
+
98
+ @property
99
+ def host_type(self) -> HostType:
100
+ """Return the type of host."""
101
+ return self._host_type
102
+
103
+ @cached_property
104
+ def _host_type(self) -> HostType:
105
+ if rfc3986.IPv6_MATCHER.match(f"[{self.host}]"):
106
+ return HostType.IPv6
107
+ if rfc3986.IPv4_MATCHER.match(self.host):
108
+ return HostType.IPv4
109
+ if rfc3986.HOST_MATCHER.match(self.host):
110
+ return HostType.DOMAIN
111
+ return HostType.UNKNOWN
112
+
113
+ def build(self) -> str:
114
+ """Construct URI string representation.
115
+
116
+ Validate host. Returns a string of the form
117
+ ``{scheme}://{username}:{password}@{host}:{port}{path}?{query}#{fragment}``
118
+ """
119
+ components = (
120
+ self.scheme,
121
+ self.netloc,
122
+ self.path or "",
123
+ "", # params
124
+ self.query,
125
+ self.fragment,
126
+ )
127
+ return urlunparse(components)
128
+
129
+ def __eq__(self, other: object) -> bool:
130
+ if not isinstance(other, URI):
131
+ return False
132
+ return (
133
+ self.scheme == other.scheme
134
+ and self.host == other.host
135
+ and self.port == other.port
136
+ and self.path == other.path
137
+ and self.query == other.query
138
+ and self.username == other.username
139
+ and self.password == other.password
140
+ and self.fragment == other.fragment
141
+ )
@@ -0,0 +1,2 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,21 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import Any
4
+
5
+ from ..endpoints import Endpoint, EndpointResolverParams, resolve_static_uri
6
+ from ..exceptions import EndpointResolutionError
7
+ from ..interfaces import Endpoint as _Endpoint
8
+ from .interfaces import EndpointResolver
9
+
10
+
11
+ class StaticEndpointResolver(EndpointResolver):
12
+ """A basic endpoint resolver that forwards a static URI."""
13
+
14
+ async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> _Endpoint:
15
+ static_uri = resolve_static_uri(params)
16
+ if static_uri is None:
17
+ raise EndpointResolutionError(
18
+ "Unable to resolve endpoint: endpoint_uri is required"
19
+ )
20
+
21
+ return Endpoint(uri=static_uri)
@@ -0,0 +1,233 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from asyncio import Future
4
+ from typing import Any, Self
5
+
6
+ from ..deserializers import DeserializeableShape
7
+ from ..serializers import SerializeableShape
8
+ from .interfaces.eventstream import EventPublisher, EventReceiver
9
+
10
+
11
+ class DuplexEventStream[
12
+ IE: SerializeableShape,
13
+ OE: DeserializeableShape,
14
+ O: DeserializeableShape,
15
+ ]:
16
+ """An event stream that both sends and receives messages.
17
+
18
+ To ensure that streams are closed upon exiting, this class may be used as an async
19
+ context manager.
20
+
21
+ .. code-block:: python
22
+
23
+ async def main():
24
+ client = ChatClient()
25
+ input = StreamMessagesInput(chat_room="aws-python-sdk", username="hunter7")
26
+
27
+ async with client.stream_messages(input=input) as stream:
28
+ stream.input_stream.send(
29
+ MessageStreamMessage("Chat logger starting up.")
30
+ )
31
+ response_task = asyncio.create_task(handle_output(stream))
32
+ stream.input_stream.send(MessageStreamMessage("Chat logger active."))
33
+ await response_handler
34
+
35
+ async def handle_output(stream: EventStream) -> None:
36
+ _, output_stream = await stream.await_output()
37
+ async for event in output_stream:
38
+ match event:
39
+ case MessageStreamMessage():
40
+ print(event.value)
41
+ case MessageStreamShutdown():
42
+ return
43
+ case _:
44
+ stream.input_stream.send(
45
+ MessageStreamMessage(
46
+ "Unknown message type received. Shutting down."
47
+ )
48
+ )
49
+ return
50
+ """
51
+
52
+ input_stream: EventPublisher[IE]
53
+ """An event stream that sends events to the service."""
54
+
55
+ output_stream: EventReceiver[OE] | None = None
56
+ """An event stream that receives events from the service.
57
+
58
+ This value may be None until ``await_output`` has been called.
59
+
60
+ This value will also be None if the operation has no output stream.
61
+ """
62
+
63
+ output: O | None = None
64
+ """The initial response from the service.
65
+
66
+ This value may be None until ``await_output`` has been called.
67
+
68
+ This may include context necessary to interpret output events or prepare
69
+ input events. It will always be available before any events.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ *,
75
+ input_stream: EventPublisher[IE],
76
+ output_future: Future[tuple[O, EventReceiver[OE]]],
77
+ ) -> None:
78
+ self.input_stream = input_stream
79
+ self._output_future = output_future
80
+
81
+ async def await_output(self) -> tuple[O, EventReceiver[OE]]:
82
+ """Await the operation's output.
83
+
84
+ The EventStream will be returned as soon as the input stream is ready to
85
+ receive events, which may be before the initial response has been received
86
+ and the service is ready to send events.
87
+
88
+ Awaiting this method will wait until the initial response was received and the
89
+ service is ready to send events. The initial response and output stream will
90
+ be returned by this operation and also cached in ``response`` and
91
+ ``output_stream``, respectively.
92
+
93
+ The default implementation of this method performs the caching behavior,
94
+ delegating to the abstract ``_await_output`` method to actually retrieve the
95
+ initial response and output stream.
96
+
97
+ :returns: A tuple containing the initial response and output stream. If the
98
+ operation has no output stream, the second value will be None.
99
+ """
100
+ self.output, self.output_stream = await self._output_future
101
+ return self.output, self.output_stream
102
+
103
+ async def close(self) -> None:
104
+ """Closes the event stream.
105
+
106
+ This closes both the input and output streams.
107
+ """
108
+ if self.output_stream is None:
109
+ _, self.output_stream = await self.await_output()
110
+
111
+ await self.input_stream.close()
112
+ await self.output_stream.close()
113
+
114
+ async def __aenter__(self) -> Self:
115
+ return self
116
+
117
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any):
118
+ await self.close()
119
+
120
+
121
+ class InputEventStream[IE: SerializeableShape, O]:
122
+ """An event stream that streams messages to the service.
123
+
124
+ To ensure that streams are closed upon exiting, this class may be used as an async
125
+ context manager.
126
+
127
+ .. code-block:: python
128
+
129
+ async def main():
130
+ client = ChatClient()
131
+ input = PublishMessagesInput(chat_room="aws-python-sdk", username="hunter7")
132
+
133
+ async with client.publish_messages(input=input) as stream:
134
+ stream.input_stream.send(
135
+ MessageStreamMessage("High severity ticket alert!")
136
+ )
137
+ await stream.await_output()
138
+ """
139
+
140
+ input_stream: EventPublisher[IE]
141
+ """An event stream that sends events to the service."""
142
+
143
+ output: O | None = None
144
+ """The initial response from the service.
145
+
146
+ This value may be None until ``await_output`` has been called.
147
+
148
+ This may include context necessary to interpret output events or prepare
149
+ input events. It will always be available before any events.
150
+ """
151
+
152
+ def __init__(
153
+ self,
154
+ *,
155
+ input_stream: EventPublisher[IE],
156
+ output_future: Future[O],
157
+ ) -> None:
158
+ self.input_stream = input_stream
159
+ self._output_future = output_future
160
+
161
+ async def await_output(self) -> O:
162
+ """Await the operation's initial response.
163
+
164
+ The EventStream will be returned as soon as the input stream is ready to receive
165
+ events, which may be before the initial response has been received and the
166
+ service is ready to send events.
167
+
168
+ Awaiting this method will wait until the initial response was received.
169
+
170
+ :returns: The service's initial response.
171
+ """
172
+ if self.output is None:
173
+ self.output = await self._output_future
174
+ return self.output
175
+
176
+ async def close(self) -> None:
177
+ """Closes the event stream."""
178
+ await self.input_stream.close()
179
+
180
+ async def __aenter__(self) -> Self:
181
+ return self
182
+
183
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any):
184
+ await self.close()
185
+
186
+
187
+ class OutputEventStream[OE: DeserializeableShape, O: DeserializeableShape]:
188
+ """An event stream that streams messages from the service.
189
+
190
+ To ensure that streams are closed upon exiting, this class may be used as an async
191
+ context manager.
192
+
193
+ .. code-block:: python
194
+
195
+ async def main():
196
+ client = ChatClient()
197
+ input = ReceiveMessagesInput(chat_room="aws-python-sdk")
198
+
199
+ async with client.receive_messages(input=input) as stream:
200
+ async for event in stream.output_stream:
201
+ match event:
202
+ case MessageStreamMessage():
203
+ print(event.value)
204
+ case _:
205
+ return
206
+ """
207
+
208
+ output_stream: EventReceiver[OE]
209
+ """An event stream that receives events from the service.
210
+
211
+ This value will also be None if the operation has no output stream.
212
+ """
213
+
214
+ output: O
215
+ """The initial response from the service.
216
+
217
+ This may include context necessary to interpret output events or prepare input
218
+ events. It will always be available before any events.
219
+ """
220
+
221
+ def __init__(self, output_stream: EventReceiver[OE], output: O) -> None:
222
+ self.output_stream = output_stream
223
+ self.output = output
224
+
225
+ async def close(self) -> None:
226
+ """Closes the event stream."""
227
+ await self.output_stream.close()
228
+
229
+ async def __aenter__(self) -> Self:
230
+ return self
231
+
232
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any):
233
+ await self.close()
@@ -0,0 +1,200 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from collections.abc import AsyncIterable, Callable
4
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
5
+
6
+ from ...documents import TypeRegistry
7
+ from ...endpoints import EndpointResolverParams
8
+ from ...exceptions import UnsupportedStreamException
9
+ from ...interfaces import Endpoint, TypedProperties, URI
10
+ from ...interfaces import StreamingBlob as SyncStreamingBlob
11
+ from .eventstream import EventPublisher, EventReceiver
12
+
13
+ if TYPE_CHECKING:
14
+ from ...deserializers import DeserializeableShape, ShapeDeserializer
15
+ from ...schemas import APIOperation
16
+ from ...serializers import SerializeableShape
17
+ from ...shapes import ShapeID
18
+
19
+
20
+ @runtime_checkable
21
+ class AsyncByteStream(Protocol):
22
+ """A file-like object with an async read method."""
23
+
24
+ async def read(self, size: int = -1) -> bytes: ...
25
+
26
+
27
+ @runtime_checkable
28
+ class AsyncWriter(Protocol):
29
+ """An object with an async write method."""
30
+
31
+ async def write(self, data: bytes) -> None: ...
32
+
33
+
34
+ # A union of all acceptable streaming blob types. Deserialized payloads will
35
+ # always return a ByteStream, or AsyncByteStream if async is enabled.
36
+ type StreamingBlob = SyncStreamingBlob | AsyncByteStream | AsyncIterable[bytes]
37
+
38
+
39
+ class Request(Protocol):
40
+ """Protocol-agnostic representation of a request."""
41
+
42
+ destination: URI
43
+ """The URI where the request should be sent to."""
44
+
45
+ body: StreamingBlob = b""
46
+ """The request payload."""
47
+
48
+ async def consume_body_async(self) -> bytes:
49
+ """Iterate over request body and return as bytes."""
50
+ ...
51
+
52
+ def consume_body(self) -> bytes:
53
+ """Iterate over request body and return as bytes."""
54
+ ...
55
+
56
+
57
+ class Response(Protocol):
58
+ """Protocol-agnostic representation of a response."""
59
+
60
+ @property
61
+ def body(self) -> StreamingBlob:
62
+ """The response payload as iterable of chunks of bytes."""
63
+ ...
64
+
65
+ async def consume_body_async(self) -> bytes:
66
+ """Iterate over response body and return as bytes."""
67
+ ...
68
+
69
+ def consume_body(self) -> bytes:
70
+ """Iterate over request body and return as bytes."""
71
+ ...
72
+
73
+
74
+ class EndpointResolver(Protocol):
75
+ """Resolves an operation's endpoint based given parameters."""
76
+
77
+ async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint:
78
+ """Resolve an endpoint for the given operation.
79
+
80
+ :param params: The parameters available to resolve the endpoint.
81
+ """
82
+ ...
83
+
84
+
85
+ class ClientTransport[I: Request, O: Response](Protocol):
86
+ """Protocol-agnostic representation of a client tranport (e.g. an HTTP client)."""
87
+
88
+ async def send(self, request: I) -> O:
89
+ """Send a request over the transport and receive the response."""
90
+ ...
91
+
92
+
93
+ class ClientProtocol[I: Request, O: Response](Protocol):
94
+ """A protocol used by a client to communicate with a server."""
95
+
96
+ @property
97
+ def id(self) -> "ShapeID":
98
+ """The ID of the protocol."""
99
+ ...
100
+
101
+ def serialize_request[
102
+ OperationInput: "SerializeableShape",
103
+ OperationOutput: "DeserializeableShape",
104
+ ](
105
+ self,
106
+ *,
107
+ operation: "APIOperation[OperationInput, OperationOutput]",
108
+ input: OperationInput,
109
+ endpoint: URI,
110
+ context: TypedProperties,
111
+ ) -> I:
112
+ """Serialize an operation input into a transport request.
113
+
114
+ :param operation: The operation whose request is being serialized.
115
+ :param input: The input shape to be serialized.
116
+ :param endpoint: The base endpoint to serialize.
117
+ :param context: A context bag for the request.
118
+ """
119
+ ...
120
+
121
+ def set_service_endpoint(
122
+ self,
123
+ *,
124
+ request: I,
125
+ endpoint: Endpoint,
126
+ ) -> I:
127
+ """Update the endpoint of a transport request.
128
+
129
+ :param request: The request whose endpoint should be updated.
130
+ :param endpoint: The endpoint to set on the request.
131
+ """
132
+ ...
133
+
134
+ async def deserialize_response[
135
+ OperationInput: "SerializeableShape",
136
+ OperationOutput: "DeserializeableShape",
137
+ ](
138
+ self,
139
+ *,
140
+ operation: "APIOperation[OperationInput, OperationOutput]",
141
+ request: I,
142
+ response: O,
143
+ error_registry: TypeRegistry,
144
+ context: TypedProperties,
145
+ ) -> OperationOutput:
146
+ """Deserializes the output from the tranport response or throws an exception.
147
+
148
+ :param operation: The operation whose response is being deserialized.
149
+ :param request: The transport request that was sent for this response.
150
+ :param response: The response to deserialize.
151
+ :param error_registry: A TypeRegistry used to deserialize errors.
152
+ :param context: A context bag for the request.
153
+ """
154
+ ...
155
+
156
+ def create_event_publisher[
157
+ OperationInput: "SerializeableShape",
158
+ OperationOutput: "DeserializeableShape",
159
+ Event: "SerializeableShape",
160
+ ](
161
+ self,
162
+ *,
163
+ operation: "APIOperation[OperationInput, OperationOutput]",
164
+ request: I,
165
+ event_type: type[Event],
166
+ context: TypedProperties,
167
+ ) -> EventPublisher[Event]:
168
+ """Creates an event publisher for a protocol event stream.
169
+
170
+ :param operation: The event stream operation.
171
+ :param request: The transport request that was sent for this stream.
172
+ :param event_type: The type of event to publish.
173
+ :param context: A context bag for the request.
174
+ """
175
+ raise UnsupportedStreamException()
176
+
177
+ def create_event_receiver[
178
+ OperationInput: "SerializeableShape",
179
+ OperationOutput: "DeserializeableShape",
180
+ Event: "DeserializeableShape",
181
+ ](
182
+ self,
183
+ *,
184
+ operation: "APIOperation[OperationInput, OperationOutput]",
185
+ request: I,
186
+ response: O,
187
+ event_type: type[Event],
188
+ event_deserializer: Callable[["ShapeDeserializer"], Event],
189
+ context: TypedProperties,
190
+ ) -> EventReceiver[Event]:
191
+ """Creates an event receiver for a protocol event stream.
192
+
193
+ :param operation: The event stream operation.
194
+ :param request: The transport request that was sent for this stream.
195
+ :param response: The transport response that was received for this stream.
196
+ :param event_type: The type of event to publish.
197
+ :param event_deserializer: The deserializer to be used to deserialize events.
198
+ :param context: A context bag for the request.
199
+ """
200
+ raise UnsupportedStreamException()
@@ -0,0 +1,69 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import Any, Protocol, Self
4
+
5
+ from ...deserializers import DeserializeableShape
6
+ from ...serializers import SerializeableShape
7
+
8
+
9
+ class EventPublisher[E: SerializeableShape](Protocol):
10
+ """Asynchronously sends events to a service.
11
+
12
+ This may be used as a context manager to ensure the stream is closed before exiting.
13
+ """
14
+
15
+ async def send(self, event: E) -> None:
16
+ """Sends an event to the service.
17
+
18
+ :param event: The event to send.
19
+ """
20
+ ...
21
+
22
+ async def close(self) -> None:
23
+ """Closes the event stream."""
24
+ ...
25
+
26
+ async def __aenter__(self) -> Self:
27
+ return self
28
+
29
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any):
30
+ await self.close()
31
+
32
+
33
+ class EventReceiver[E: DeserializeableShape](Protocol):
34
+ """Asynchronously receives events from a service.
35
+
36
+ Events may be received via the ``receive`` method or by using this class as
37
+ an async iterable.
38
+
39
+ This may also be used as a context manager to ensure the stream is closed before
40
+ exiting.
41
+ """
42
+
43
+ async def receive(self) -> E | None:
44
+ """Receive a single event from the service.
45
+
46
+ :returns: An event or None. None indicates that no more events will be sent by
47
+ the service.
48
+ """
49
+ ...
50
+
51
+ async def close(self) -> None:
52
+ """Closes the event stream."""
53
+ ...
54
+
55
+ async def __anext__(self) -> E:
56
+ result = await self.receive()
57
+ if result is None:
58
+ await self.close()
59
+ raise StopAsyncIteration
60
+ return result
61
+
62
+ def __aiter__(self) -> Self:
63
+ return self
64
+
65
+ async def __enter__(self) -> Self:
66
+ return self
67
+
68
+ async def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
69
+ await self.close()
@@ -0,0 +1,22 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import Protocol
4
+
5
+ from ...interfaces.identity import IdentityPropertiesType_contra, IdentityType_cov
6
+
7
+
8
+ class IdentityResolver(Protocol[IdentityType_cov, IdentityPropertiesType_contra]):
9
+ """Used to load a user's `Identity` from a given source.
10
+
11
+ Each `Identity` may have one or more resolver implementations.
12
+ """
13
+
14
+ async def get_identity(
15
+ self, *, identity_properties: IdentityPropertiesType_contra
16
+ ) -> IdentityType_cov:
17
+ """Load the user's identity from this resolver.
18
+
19
+ :param identity_properties: Properties used to help determine the identity to
20
+ return.
21
+ """
22
+ ...