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
smithy_http/aio/crt.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# pyright: reportMissingTypeStubs=false,reportUnknownMemberType=false
|
|
4
|
+
# flake8: noqa: F811
|
|
5
|
+
import asyncio
|
|
6
|
+
from asyncio import Future as AsyncFuture
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import AsyncGenerator, AsyncIterable
|
|
9
|
+
from concurrent.futures import Future as ConcurrentFuture
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from functools import partial
|
|
12
|
+
from io import BufferedIOBase, BytesIO
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
# Both of these are types that essentially are "castable to bytes/memoryview"
|
|
17
|
+
# Unfortunately they're not exposed anywhere so we have to import them from
|
|
18
|
+
# _typeshed.
|
|
19
|
+
from _typeshed import ReadableBuffer, WriteableBuffer
|
|
20
|
+
|
|
21
|
+
# pyright doesn't like optional imports. This is reasonable because if we use these
|
|
22
|
+
# in type hints then they'd result in runtime errors.
|
|
23
|
+
# TODO: add integ tests that import these without the dependendency installed
|
|
24
|
+
from awscrt import http as crt_http
|
|
25
|
+
from awscrt import io as crt_io
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from awscrt import http as crt_http
|
|
29
|
+
from awscrt import io as crt_io
|
|
30
|
+
|
|
31
|
+
HAS_CRT = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
HAS_CRT = False # type: ignore
|
|
34
|
+
|
|
35
|
+
from smithy_core import interfaces as core_interfaces
|
|
36
|
+
from smithy_core.aio.types import AsyncBytesReader
|
|
37
|
+
from smithy_core.aio.utils import close
|
|
38
|
+
from smithy_core.exceptions import MissingDependencyException
|
|
39
|
+
|
|
40
|
+
from .. import Field, Fields
|
|
41
|
+
from .. import interfaces as http_interfaces
|
|
42
|
+
from ..exceptions import SmithyHTTPException
|
|
43
|
+
from ..interfaces import FieldPosition
|
|
44
|
+
from . import interfaces as http_aio_interfaces
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _assert_crt() -> None:
|
|
48
|
+
if not HAS_CRT:
|
|
49
|
+
raise MissingDependencyException(
|
|
50
|
+
"Attempted to use awscrt component, but awscrt is not installed."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _AWSCRTEventLoop:
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
_assert_crt()
|
|
57
|
+
self.bootstrap = self._initialize_default_loop()
|
|
58
|
+
|
|
59
|
+
def _initialize_default_loop(self) -> "crt_io.ClientBootstrap":
|
|
60
|
+
event_loop_group = crt_io.EventLoopGroup(1)
|
|
61
|
+
host_resolver = crt_io.DefaultHostResolver(event_loop_group)
|
|
62
|
+
return crt_io.ClientBootstrap(event_loop_group, host_resolver)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AWSCRTHTTPResponse(http_aio_interfaces.HTTPResponse):
|
|
66
|
+
def __init__(self, *, status: int, fields: Fields, body: "CRTResponseBody") -> None:
|
|
67
|
+
_assert_crt()
|
|
68
|
+
self._status = status
|
|
69
|
+
self._fields = fields
|
|
70
|
+
self._body = body
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def status(self) -> int:
|
|
74
|
+
return self._status
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def fields(self) -> Fields:
|
|
78
|
+
return self._fields
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def body(self) -> AsyncIterable[bytes]:
|
|
82
|
+
return self.chunks()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def reason(self) -> str | None:
|
|
86
|
+
"""Optional string provided by the server explaining the status."""
|
|
87
|
+
# TODO: See how CRT exposes reason.
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
async def chunks(self) -> AsyncGenerator[bytes, None]:
|
|
91
|
+
while True:
|
|
92
|
+
chunk = await self._body.next()
|
|
93
|
+
if chunk:
|
|
94
|
+
yield chunk
|
|
95
|
+
else:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str:
|
|
99
|
+
return (
|
|
100
|
+
f"AWSCRTHTTPResponse("
|
|
101
|
+
f"status={self.status}, "
|
|
102
|
+
f"fields={self.fields!r}, body=...)"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CRTResponseBody:
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
self._stream: crt_http.HttpClientStream | None = None
|
|
109
|
+
self._completion_future: AsyncFuture[int] | None = None
|
|
110
|
+
self._chunk_futures: deque[ConcurrentFuture[bytes]] = deque()
|
|
111
|
+
|
|
112
|
+
# deque is thread safe and the crt is only going to be writing
|
|
113
|
+
# with one thread anyway, so we *shouldn't* need to gate this
|
|
114
|
+
# behind a lock. In an ideal world, the CRT would expose
|
|
115
|
+
# an interface that better matches python's async.
|
|
116
|
+
self._received_chunks: deque[bytes] = deque()
|
|
117
|
+
|
|
118
|
+
def set_stream(self, stream: "crt_http.HttpClientStream") -> None:
|
|
119
|
+
if self._stream is not None:
|
|
120
|
+
raise SmithyHTTPException("Stream already set on AWSCRTHTTPResponse object")
|
|
121
|
+
self._stream = stream
|
|
122
|
+
concurrent_future: ConcurrentFuture[int] = stream.completion_future
|
|
123
|
+
self._completion_future = asyncio.wrap_future(concurrent_future)
|
|
124
|
+
self._completion_future.add_done_callback(self._on_complete)
|
|
125
|
+
self._stream.activate()
|
|
126
|
+
|
|
127
|
+
def on_body(self, chunk: bytes, **kwargs: Any) -> None: # pragma: crt-callback
|
|
128
|
+
# TODO: update back pressure window once CRT supports it
|
|
129
|
+
if self._chunk_futures:
|
|
130
|
+
future = self._chunk_futures.popleft()
|
|
131
|
+
future.set_result(chunk)
|
|
132
|
+
else:
|
|
133
|
+
self._received_chunks.append(chunk)
|
|
134
|
+
|
|
135
|
+
async def next(self) -> bytes:
|
|
136
|
+
if self._completion_future is None:
|
|
137
|
+
raise SmithyHTTPException("Stream not set")
|
|
138
|
+
|
|
139
|
+
# TODO: update backpressure window once CRT supports it
|
|
140
|
+
if self._received_chunks:
|
|
141
|
+
return self._received_chunks.popleft()
|
|
142
|
+
elif self._completion_future.done():
|
|
143
|
+
return b""
|
|
144
|
+
else:
|
|
145
|
+
future = ConcurrentFuture[bytes]()
|
|
146
|
+
self._chunk_futures.append(future)
|
|
147
|
+
return await asyncio.wrap_future(future)
|
|
148
|
+
|
|
149
|
+
def _on_complete(
|
|
150
|
+
self, completion_future: AsyncFuture[int]
|
|
151
|
+
) -> None: # pragma: crt-callback
|
|
152
|
+
for future in self._chunk_futures:
|
|
153
|
+
future.set_result(b"")
|
|
154
|
+
self._chunk_futures.clear()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class CRTResponseFactory:
|
|
158
|
+
def __init__(self, body: CRTResponseBody) -> None:
|
|
159
|
+
self._body = body
|
|
160
|
+
self._response_future = ConcurrentFuture[AWSCRTHTTPResponse]()
|
|
161
|
+
|
|
162
|
+
def on_response(
|
|
163
|
+
self, status_code: int, headers: list[tuple[str, str]], **kwargs: Any
|
|
164
|
+
) -> None: # pragma: crt-callback
|
|
165
|
+
fields = Fields()
|
|
166
|
+
for header_name, header_val in headers:
|
|
167
|
+
try:
|
|
168
|
+
fields[header_name].add(header_val)
|
|
169
|
+
except KeyError:
|
|
170
|
+
fields[header_name] = Field(
|
|
171
|
+
name=header_name,
|
|
172
|
+
values=[header_val],
|
|
173
|
+
kind=FieldPosition.HEADER,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self._response_future.set_result(
|
|
177
|
+
AWSCRTHTTPResponse(
|
|
178
|
+
status=status_code,
|
|
179
|
+
fields=fields,
|
|
180
|
+
body=self._body,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def await_response(self) -> AWSCRTHTTPResponse:
|
|
185
|
+
return await asyncio.wrap_future(self._response_future)
|
|
186
|
+
|
|
187
|
+
def set_done_callback(self, stream: "crt_http.HttpClientStream") -> None:
|
|
188
|
+
stream.completion_future.add_done_callback(self._cancel)
|
|
189
|
+
|
|
190
|
+
def _cancel(self, completion_future: ConcurrentFuture[int | Exception]) -> None:
|
|
191
|
+
if not self._response_future.done():
|
|
192
|
+
self._response_future.cancel()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
ConnectionPoolKey = tuple[str, str, int | None]
|
|
196
|
+
ConnectionPoolDict = dict[ConnectionPoolKey, "crt_http.HttpClientConnection"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AWSCRTHTTPClientConfig(http_interfaces.HTTPClientConfiguration):
|
|
200
|
+
def __post_init__(self) -> None:
|
|
201
|
+
_assert_crt()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AWSCRTHTTPClient(http_aio_interfaces.HTTPClient):
|
|
205
|
+
_HTTP_PORT = 80
|
|
206
|
+
_HTTPS_PORT = 443
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
eventloop: _AWSCRTEventLoop | None = None,
|
|
211
|
+
client_config: AWSCRTHTTPClientConfig | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""
|
|
214
|
+
:param client_config: Configuration that applies to all requests made with this
|
|
215
|
+
client.
|
|
216
|
+
"""
|
|
217
|
+
_assert_crt()
|
|
218
|
+
self._config = client_config or AWSCRTHTTPClientConfig()
|
|
219
|
+
if eventloop is None:
|
|
220
|
+
eventloop = _AWSCRTEventLoop()
|
|
221
|
+
self._eventloop = eventloop
|
|
222
|
+
self._client_bootstrap = self._eventloop.bootstrap
|
|
223
|
+
self._tls_ctx = crt_io.ClientTlsContext(crt_io.TlsContextOptions())
|
|
224
|
+
self._socket_options = crt_io.SocketOptions()
|
|
225
|
+
self._connections: ConnectionPoolDict = {}
|
|
226
|
+
self._async_reads: set[asyncio.Task[Any]] = set()
|
|
227
|
+
|
|
228
|
+
async def send(
|
|
229
|
+
self,
|
|
230
|
+
request: http_aio_interfaces.HTTPRequest,
|
|
231
|
+
*,
|
|
232
|
+
request_config: http_aio_interfaces.HTTPRequestConfiguration | None = None,
|
|
233
|
+
) -> AWSCRTHTTPResponse:
|
|
234
|
+
"""Send HTTP request using awscrt client.
|
|
235
|
+
|
|
236
|
+
:param request: The request including destination URI, fields, payload.
|
|
237
|
+
:param request_config: Configuration specific to this request.
|
|
238
|
+
"""
|
|
239
|
+
crt_request, crt_body = await self._marshal_request(request)
|
|
240
|
+
connection = await self._get_connection(request.destination)
|
|
241
|
+
response_body = CRTResponseBody()
|
|
242
|
+
response_factory = CRTResponseFactory(response_body)
|
|
243
|
+
crt_stream = connection.request(
|
|
244
|
+
crt_request,
|
|
245
|
+
response_factory.on_response,
|
|
246
|
+
response_body.on_body,
|
|
247
|
+
)
|
|
248
|
+
response_factory.set_done_callback(crt_stream)
|
|
249
|
+
response_body.set_stream(crt_stream)
|
|
250
|
+
crt_stream.completion_future.add_done_callback(
|
|
251
|
+
partial(self._close_input_body, body=crt_body)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
response = await response_factory.await_response()
|
|
255
|
+
if response.status != 200 and response.status >= 300:
|
|
256
|
+
await close(crt_body)
|
|
257
|
+
|
|
258
|
+
return response
|
|
259
|
+
|
|
260
|
+
def _close_input_body(
|
|
261
|
+
self, future: ConcurrentFuture[int], *, body: "BufferableByteStream | BytesIO"
|
|
262
|
+
) -> None:
|
|
263
|
+
if future.exception(timeout=0):
|
|
264
|
+
body.close()
|
|
265
|
+
|
|
266
|
+
async def _create_connection(
|
|
267
|
+
self, url: core_interfaces.URI
|
|
268
|
+
) -> "crt_http.HttpClientConnection":
|
|
269
|
+
"""Builds and validates connection to ``url``"""
|
|
270
|
+
connect_future = self._build_new_connection(url)
|
|
271
|
+
connection = await asyncio.wrap_future(connect_future)
|
|
272
|
+
self._validate_connection(connection)
|
|
273
|
+
return connection
|
|
274
|
+
|
|
275
|
+
async def _get_connection(
|
|
276
|
+
self, url: core_interfaces.URI
|
|
277
|
+
) -> "crt_http.HttpClientConnection":
|
|
278
|
+
# TODO: Use CRT connection pooling instead of this basic kind
|
|
279
|
+
connection_key = (url.scheme, url.host, url.port)
|
|
280
|
+
connection = self._connections.get(connection_key)
|
|
281
|
+
|
|
282
|
+
if connection and connection.is_open():
|
|
283
|
+
return connection
|
|
284
|
+
|
|
285
|
+
connection = await self._create_connection(url)
|
|
286
|
+
self._connections[connection_key] = connection
|
|
287
|
+
return connection
|
|
288
|
+
|
|
289
|
+
def _build_new_connection(
|
|
290
|
+
self, url: core_interfaces.URI
|
|
291
|
+
) -> ConcurrentFuture["crt_http.HttpClientConnection"]:
|
|
292
|
+
if url.scheme == "http":
|
|
293
|
+
port = self._HTTP_PORT
|
|
294
|
+
tls_connection_options = None
|
|
295
|
+
elif url.scheme == "https":
|
|
296
|
+
port = self._HTTPS_PORT
|
|
297
|
+
tls_connection_options = self._tls_ctx.new_connection_options()
|
|
298
|
+
tls_connection_options.set_server_name(url.host)
|
|
299
|
+
# TODO: Support TLS configuration, including alpn
|
|
300
|
+
tls_connection_options.set_alpn_list(["h2", "http/1.1"])
|
|
301
|
+
else:
|
|
302
|
+
raise SmithyHTTPException(
|
|
303
|
+
f"AWSCRTHTTPClient does not support URL scheme {url.scheme}"
|
|
304
|
+
)
|
|
305
|
+
if url.port is not None:
|
|
306
|
+
port = url.port
|
|
307
|
+
|
|
308
|
+
connect_future: ConcurrentFuture[crt_http.HttpClientConnection] = (
|
|
309
|
+
crt_http.HttpClientConnection.new(
|
|
310
|
+
bootstrap=self._client_bootstrap,
|
|
311
|
+
host_name=url.host,
|
|
312
|
+
port=port,
|
|
313
|
+
socket_options=self._socket_options,
|
|
314
|
+
tls_connection_options=tls_connection_options,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
return connect_future
|
|
318
|
+
|
|
319
|
+
def _validate_connection(self, connection: "crt_http.HttpClientConnection") -> None:
|
|
320
|
+
"""Validates an existing connection against the client config.
|
|
321
|
+
|
|
322
|
+
Checks performed:
|
|
323
|
+
* If ``force_http_2`` is enabled: Is the connection HTTP/2?
|
|
324
|
+
"""
|
|
325
|
+
force_http_2 = self._config.force_http_2
|
|
326
|
+
if force_http_2 and connection.version is not crt_http.HttpVersion.Http2:
|
|
327
|
+
connection.close()
|
|
328
|
+
negotiated = crt_http.HttpVersion(connection.version).name
|
|
329
|
+
raise SmithyHTTPException(f"HTTP/2 could not be negotiated: {negotiated}")
|
|
330
|
+
|
|
331
|
+
def _render_path(self, url: core_interfaces.URI) -> str:
|
|
332
|
+
path = url.path if url.path is not None else "/"
|
|
333
|
+
query = f"?{url.query}" if url.query is not None else ""
|
|
334
|
+
return f"{path}{query}"
|
|
335
|
+
|
|
336
|
+
async def _marshal_request(
|
|
337
|
+
self, request: http_aio_interfaces.HTTPRequest
|
|
338
|
+
) -> tuple["crt_http.HttpRequest", "BufferableByteStream | BytesIO"]:
|
|
339
|
+
"""Create :py:class:`awscrt.http.HttpRequest` from
|
|
340
|
+
:py:class:`smithy_http.aio.HTTPRequest`"""
|
|
341
|
+
headers_list = []
|
|
342
|
+
if "host" not in request.fields:
|
|
343
|
+
request.fields.set_field(
|
|
344
|
+
Field(name="host", values=[request.destination.host])
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if "accept" not in request.fields:
|
|
348
|
+
request.fields.set_field(Field(name="accept", values=["*/*"]))
|
|
349
|
+
|
|
350
|
+
for fld in request.fields.entries.values():
|
|
351
|
+
# TODO: Use literal values for "header"/"trailer".
|
|
352
|
+
if fld.kind.value != FieldPosition.HEADER.value:
|
|
353
|
+
continue
|
|
354
|
+
for val in fld.values:
|
|
355
|
+
headers_list.append((fld.name, val))
|
|
356
|
+
|
|
357
|
+
path = self._render_path(request.destination)
|
|
358
|
+
headers = crt_http.HttpHeaders(headers_list)
|
|
359
|
+
|
|
360
|
+
body = request.body
|
|
361
|
+
if isinstance(body, bytes | bytearray):
|
|
362
|
+
# If the body is already directly in memory, wrap in a BytesIO to hand
|
|
363
|
+
# off to CRT.
|
|
364
|
+
crt_body = BytesIO(body)
|
|
365
|
+
else:
|
|
366
|
+
# If the body is async, or potentially very large, start up a task to read
|
|
367
|
+
# it into the intermediate object that CRT needs. By using
|
|
368
|
+
# asyncio.create_task we'll start the coroutine without having to
|
|
369
|
+
# explicitly await it.
|
|
370
|
+
crt_body = BufferableByteStream()
|
|
371
|
+
|
|
372
|
+
if not isinstance(body, AsyncIterable):
|
|
373
|
+
body = AsyncBytesReader(body)
|
|
374
|
+
|
|
375
|
+
# Start the read task in the background.
|
|
376
|
+
read_task = asyncio.create_task(self._consume_body_async(body, crt_body))
|
|
377
|
+
|
|
378
|
+
# Keep track of the read task so that it doesn't get garbage colllected,
|
|
379
|
+
# and stop tracking it once it's done.
|
|
380
|
+
self._async_reads.add(read_task)
|
|
381
|
+
read_task.add_done_callback(self._async_reads.discard)
|
|
382
|
+
|
|
383
|
+
crt_request = crt_http.HttpRequest(
|
|
384
|
+
method=request.method,
|
|
385
|
+
path=path,
|
|
386
|
+
headers=headers,
|
|
387
|
+
body_stream=crt_body,
|
|
388
|
+
)
|
|
389
|
+
return crt_request, crt_body
|
|
390
|
+
|
|
391
|
+
async def _consume_body_async(
|
|
392
|
+
self, source: AsyncIterable[bytes], dest: "BufferableByteStream"
|
|
393
|
+
) -> None:
|
|
394
|
+
try:
|
|
395
|
+
async for chunk in source:
|
|
396
|
+
dest.write(chunk)
|
|
397
|
+
except Exception:
|
|
398
|
+
dest.close()
|
|
399
|
+
raise
|
|
400
|
+
finally:
|
|
401
|
+
await close(source)
|
|
402
|
+
dest.end_stream()
|
|
403
|
+
|
|
404
|
+
def __deepcopy__(self, memo: Any) -> "AWSCRTHTTPClient":
|
|
405
|
+
return AWSCRTHTTPClient(
|
|
406
|
+
eventloop=self._eventloop,
|
|
407
|
+
client_config=deepcopy(self._config),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# This is adapted from the transcribe streaming sdk
|
|
412
|
+
class BufferableByteStream(BufferedIOBase):
|
|
413
|
+
"""A non-blocking bytes buffer."""
|
|
414
|
+
|
|
415
|
+
def __init__(self) -> None:
|
|
416
|
+
# We're always manipulating the front and back of the buffer, so a deque
|
|
417
|
+
# will be much more efficient than a list.
|
|
418
|
+
self._chunks: deque[bytes] = deque()
|
|
419
|
+
self._closed = False
|
|
420
|
+
self._done = False
|
|
421
|
+
|
|
422
|
+
def read(self, size: int | None = -1) -> bytes:
|
|
423
|
+
if self._closed:
|
|
424
|
+
return b""
|
|
425
|
+
|
|
426
|
+
if len(self._chunks) == 0:
|
|
427
|
+
if self._done:
|
|
428
|
+
self.close()
|
|
429
|
+
return b""
|
|
430
|
+
else:
|
|
431
|
+
# When the CRT recieves this, it'll try again
|
|
432
|
+
raise BlockingIOError("read")
|
|
433
|
+
|
|
434
|
+
# We could compile all the chunks here instead of just returning
|
|
435
|
+
# the one, BUT the CRT will keep calling read until empty bytes
|
|
436
|
+
# are returned. So it's actually better to just return one chunk
|
|
437
|
+
# since combining them would have some potentially bad memory
|
|
438
|
+
# usage issues.
|
|
439
|
+
result = self._chunks.popleft()
|
|
440
|
+
if size is not None and size > 0:
|
|
441
|
+
remainder = result[size:]
|
|
442
|
+
result = result[:size]
|
|
443
|
+
if remainder:
|
|
444
|
+
self._chunks.appendleft(remainder)
|
|
445
|
+
|
|
446
|
+
if self._done and len(self._chunks) == 0:
|
|
447
|
+
self.close()
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
def read1(self, size: int = -1) -> bytes:
|
|
452
|
+
return self.read(size)
|
|
453
|
+
|
|
454
|
+
def readinto(self, buffer: "WriteableBuffer") -> int:
|
|
455
|
+
if not isinstance(buffer, memoryview):
|
|
456
|
+
buffer = memoryview(buffer).cast("B")
|
|
457
|
+
|
|
458
|
+
data = self.read(len(buffer)) # type: ignore
|
|
459
|
+
n = len(data)
|
|
460
|
+
buffer[:n] = data
|
|
461
|
+
return n
|
|
462
|
+
|
|
463
|
+
def write(self, buffer: "ReadableBuffer") -> int:
|
|
464
|
+
if not isinstance(buffer, bytes):
|
|
465
|
+
raise ValueError(
|
|
466
|
+
f"Unexpected value written to BufferableByteStream. "
|
|
467
|
+
f"Only bytes are support but {type(buffer)} was provided."
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if self._closed:
|
|
471
|
+
raise OSError("Stream is completed and doesn't support further writes.")
|
|
472
|
+
|
|
473
|
+
if buffer:
|
|
474
|
+
self._chunks.append(buffer)
|
|
475
|
+
return len(buffer)
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def closed(self) -> bool:
|
|
479
|
+
return self._closed
|
|
480
|
+
|
|
481
|
+
def close(self) -> None:
|
|
482
|
+
self._closed = True
|
|
483
|
+
self._done = True
|
|
484
|
+
|
|
485
|
+
# Clear out the remaining chunks so that they don't sit around in memory.
|
|
486
|
+
self._chunks.clear()
|
|
487
|
+
|
|
488
|
+
def end_stream(self) -> None:
|
|
489
|
+
"""End the stream, letting any remaining chunks be read before it is closed."""
|
|
490
|
+
if len(self._chunks) == 0:
|
|
491
|
+
self.close()
|
|
492
|
+
else:
|
|
493
|
+
self._done = True
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from smithy_core.aio.interfaces.identity import IdentityResolver
|
|
4
|
+
from smithy_core.identity import Identity
|
|
5
|
+
from smithy_core.interfaces.identity import IdentityProperties
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiKeyIdentity(Identity):
|
|
9
|
+
"""The identity for auth that uses an api key."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, *, api_key: str) -> None:
|
|
12
|
+
super().__init__(expiration=None)
|
|
13
|
+
self.api_key = api_key
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApiKeyIdentityResolver(IdentityResolver[ApiKeyIdentity, IdentityProperties]):
|
|
17
|
+
"""Loads the api key identity from the configuration."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, *, api_key: str | ApiKeyIdentity) -> None:
|
|
20
|
+
"""
|
|
21
|
+
:param api_key: The API key to authenticate with.
|
|
22
|
+
"""
|
|
23
|
+
match api_key:
|
|
24
|
+
case str():
|
|
25
|
+
self._identity = ApiKeyIdentity(api_key=api_key)
|
|
26
|
+
case ApiKeyIdentity():
|
|
27
|
+
self._identity = api_key
|
|
28
|
+
|
|
29
|
+
async def get_identity(
|
|
30
|
+
self, *, identity_properties: IdentityProperties
|
|
31
|
+
) -> ApiKeyIdentity:
|
|
32
|
+
"""Load the user's api key identity from this resolver.
|
|
33
|
+
|
|
34
|
+
:param identity_properties: Properties used to help determine the identity to
|
|
35
|
+
return.
|
|
36
|
+
:returns: The api key identity.
|
|
37
|
+
"""
|
|
38
|
+
return self._identity
|
|
@@ -0,0 +1,85 @@
|
|
|
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 smithy_core.aio.interfaces import ClientTransport, Request, Response
|
|
6
|
+
from smithy_core.aio.utils import read_streaming_blob, read_streaming_blob_async
|
|
7
|
+
|
|
8
|
+
from ...interfaces import (
|
|
9
|
+
Fields,
|
|
10
|
+
HTTPClientConfiguration,
|
|
11
|
+
HTTPRequestConfiguration,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPRequest(Request, Protocol):
|
|
16
|
+
"""HTTP primitive for an Exchange to construct a version agnostic HTTP message.
|
|
17
|
+
|
|
18
|
+
:param destination: The URI where the request should be sent to.
|
|
19
|
+
:param method: The HTTP method of the request, for example "GET".
|
|
20
|
+
:param fields: ``Fields`` object containing HTTP headers and trailers.
|
|
21
|
+
:param body: A streamable collection of bytes.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
method: str
|
|
25
|
+
fields: Fields
|
|
26
|
+
|
|
27
|
+
async def consume_body_async(self) -> bytes:
|
|
28
|
+
"""Iterate over request body and return as bytes."""
|
|
29
|
+
return await read_streaming_blob_async(self.body)
|
|
30
|
+
|
|
31
|
+
def consume_body(self) -> bytes:
|
|
32
|
+
"""Iterate over request body and return as bytes."""
|
|
33
|
+
return read_streaming_blob(self.body)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HTTPResponse(Response, Protocol):
|
|
37
|
+
"""HTTP primitives returned from an Exchange, used to construct a client
|
|
38
|
+
response."""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def status(self) -> int:
|
|
42
|
+
"""The 3 digit response status code (1xx, 2xx, 3xx, 4xx, 5xx)."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def fields(self) -> Fields:
|
|
47
|
+
"""``Fields`` object containing HTTP headers and trailers."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def reason(self) -> str | None:
|
|
52
|
+
"""Optional string provided by the server explaining the status."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def consume_body_async(self) -> bytes:
|
|
56
|
+
"""Iterate over request body and return as bytes."""
|
|
57
|
+
return await read_streaming_blob_async(self.body)
|
|
58
|
+
|
|
59
|
+
def consume_body(self) -> bytes:
|
|
60
|
+
"""Iterate over request body and return as bytes."""
|
|
61
|
+
return read_streaming_blob(self.body)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class HTTPClient(ClientTransport[HTTPRequest, HTTPResponse], Protocol):
|
|
65
|
+
"""An asynchronous HTTP client interface."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, *, client_config: HTTPClientConfiguration | None) -> None:
|
|
68
|
+
"""
|
|
69
|
+
:param client_config: Configuration that applies to all requests made with this
|
|
70
|
+
client.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
async def send(
|
|
75
|
+
self,
|
|
76
|
+
request: HTTPRequest,
|
|
77
|
+
*,
|
|
78
|
+
request_config: HTTPRequestConfiguration | None = None,
|
|
79
|
+
) -> HTTPResponse:
|
|
80
|
+
"""Send HTTP request over the wire and return the response.
|
|
81
|
+
|
|
82
|
+
:param request: The request including destination URI, fields, payload.
|
|
83
|
+
:param request_config: Configuration specific to this request.
|
|
84
|
+
"""
|
|
85
|
+
...
|