smithy-http 0.1.0__tar.gz → 0.2.0__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_http-0.2.0/.changes/0.2.0.json +8 -0
- smithy_http-0.2.0/CHANGELOG.md +20 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/PKG-INFO +2 -2
- {smithy_http-0.1.0 → smithy_http-0.2.0}/pyproject.toml +1 -1
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/__init__.py +1 -1
- smithy_http-0.2.0/src/smithy_http/aio/crt.py +343 -0
- smithy_http-0.1.0/CHANGELOG.md +0 -15
- smithy_http-0.1.0/src/smithy_http/aio/crt.py +0 -494
- {smithy_http-0.1.0 → smithy_http-0.2.0}/.changes/0.0.1.json +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/.changes/0.1.0.json +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/.gitignore +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/NOTICE +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/README.md +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/aiohttp.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/auth/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/auth/apikey.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/identity/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/identity/apikey.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/interfaces/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/protocols.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/aio/restjson.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/bindings.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/deserializers.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/endpoints.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/exceptions.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/interceptors/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/interceptors/user_agent.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/interfaces/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/plugins/__init__.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/py.typed +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/restjson.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/serializers.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/user_agent.py +0 -0
- {smithy_http-0.1.0 → smithy_http-0.2.0}/src/smithy_http/utils.py +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"changes": [
|
|
3
|
+
{
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"description": "Update `AWSCRTHTTPClient` to integrate with the new AWS CRT async interfaces. ([#573](https://github.com/smithy-lang/smithy-python/pull/573)). The `AWSCRTHTTPResponse` constructor now accepts a `stream` argument of type `awscrt.aio.http.AIOHttpClientStreamUnified`. Additionally, the following classes were removed: `CRTResponseBody`, `CRTResponseFactory`, and `BufferableByteStream`."
|
|
6
|
+
}
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.2.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
* Update `AWSCRTHTTPClient` to integrate with the new AWS CRT async interfaces. ([#573](https://github.com/smithy-lang/smithy-python/pull/573)). The `AWSCRTHTTPResponse` constructor now accepts a `stream` argument of type `awscrt.aio.http.AIOHttpClientStreamUnified`. Additionally, the following classes were removed: `CRTResponseBody`, `CRTResponseFactory`, and `BufferableByteStream`.
|
|
7
|
+
|
|
8
|
+
## v0.1.0
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
* Removed identity and auth interfaces in favor of the transport-agnostic interfaces introduced in `smithy-core`.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
* Introduced schema-based serializers and deserializers for HTTP binding protocols.
|
|
15
|
+
|
|
16
|
+
## v0.0.1
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
* Added support for aiohttp and AWSCRT http clients.
|
|
20
|
+
* Added basic HTTP primitives and interfaces for Smithy clients.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smithy-http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: HTTP components for Smithy tooling.
|
|
5
5
|
Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/CHANGELOG.md
|
|
6
6
|
Project-URL: Code, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/
|
|
@@ -29,7 +29,7 @@ Provides-Extra: aiohttp
|
|
|
29
29
|
Requires-Dist: aiohttp<4.0,>=3.11.12; extra == 'aiohttp'
|
|
30
30
|
Requires-Dist: yarl; extra == 'aiohttp'
|
|
31
31
|
Provides-Extra: awscrt
|
|
32
|
-
Requires-Dist: awscrt
|
|
32
|
+
Requires-Dist: awscrt~=0.28.2; extra == 'awscrt'
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
# smithy-http
|
|
@@ -0,0 +1,343 @@
|
|
|
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
|
+
from collections.abc import AsyncGenerator, AsyncIterable
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from inspect import iscoroutinefunction
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
# pyright doesn't like optional imports. This is reasonable because if we use these
|
|
13
|
+
# in type hints then they'd result in runtime errors.
|
|
14
|
+
# TODO: add integ tests that import these without the dependendency installed
|
|
15
|
+
from awscrt import http as crt_http
|
|
16
|
+
from awscrt import io as crt_io
|
|
17
|
+
from awscrt.aio.http import (
|
|
18
|
+
AIOHttpClientConnectionUnified,
|
|
19
|
+
AIOHttpClientStreamUnified,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from awscrt import http as crt_http
|
|
24
|
+
from awscrt import io as crt_io
|
|
25
|
+
from awscrt.aio.http import (
|
|
26
|
+
AIOHttpClientConnectionUnified,
|
|
27
|
+
AIOHttpClientStreamUnified,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HAS_CRT = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_CRT = False # type: ignore
|
|
33
|
+
|
|
34
|
+
from smithy_core import interfaces as core_interfaces
|
|
35
|
+
from smithy_core.aio import interfaces as core_aio_interfaces
|
|
36
|
+
from smithy_core.aio.types import AsyncBytesReader
|
|
37
|
+
from smithy_core.exceptions import MissingDependencyError
|
|
38
|
+
|
|
39
|
+
from .. import Field, Fields
|
|
40
|
+
from .. import interfaces as http_interfaces
|
|
41
|
+
from ..exceptions import SmithyHTTPError
|
|
42
|
+
from ..interfaces import FieldPosition
|
|
43
|
+
from . import interfaces as http_aio_interfaces
|
|
44
|
+
|
|
45
|
+
# Default buffer size for reading from streams (8 KB)
|
|
46
|
+
DEFAULT_READ_BUFFER_SIZE = 8192
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _assert_crt() -> None:
|
|
50
|
+
if not HAS_CRT:
|
|
51
|
+
raise MissingDependencyError(
|
|
52
|
+
"Attempted to use awscrt component, but awscrt is not installed."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _AWSCRTEventLoop:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
_assert_crt()
|
|
59
|
+
self.bootstrap = self._initialize_default_loop()
|
|
60
|
+
|
|
61
|
+
def _initialize_default_loop(self) -> "crt_io.ClientBootstrap":
|
|
62
|
+
event_loop_group = crt_io.EventLoopGroup(1)
|
|
63
|
+
host_resolver = crt_io.DefaultHostResolver(event_loop_group)
|
|
64
|
+
return crt_io.ClientBootstrap(event_loop_group, host_resolver)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AWSCRTHTTPResponse(http_aio_interfaces.HTTPResponse):
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
status: int,
|
|
72
|
+
fields: Fields,
|
|
73
|
+
stream: "AIOHttpClientStreamUnified",
|
|
74
|
+
) -> None:
|
|
75
|
+
_assert_crt()
|
|
76
|
+
self._status = status
|
|
77
|
+
self._fields = fields
|
|
78
|
+
self._stream = stream
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def status(self) -> int:
|
|
82
|
+
return self._status
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def fields(self) -> Fields:
|
|
86
|
+
return self._fields
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def body(self) -> AsyncIterable[bytes]:
|
|
90
|
+
return self.chunks()
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def reason(self) -> str | None:
|
|
94
|
+
"""Optional string provided by the server explaining the status."""
|
|
95
|
+
# TODO: See how CRT exposes reason.
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
async def chunks(self) -> AsyncGenerator[bytes, None]:
|
|
99
|
+
while True:
|
|
100
|
+
chunk = await self._stream.get_next_response_chunk()
|
|
101
|
+
if chunk:
|
|
102
|
+
yield chunk
|
|
103
|
+
else:
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return (
|
|
108
|
+
f"AWSCRTHTTPResponse("
|
|
109
|
+
f"status={self.status}, "
|
|
110
|
+
f"fields={self.fields!r}, body=...)"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
ConnectionPoolKey = tuple[str, str, int | None]
|
|
115
|
+
ConnectionPoolDict = dict[ConnectionPoolKey, "AIOHttpClientConnectionUnified"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(kw_only=True)
|
|
119
|
+
class AWSCRTHTTPClientConfig(http_interfaces.HTTPClientConfiguration):
|
|
120
|
+
"""AWS CRT HTTP client configuration.
|
|
121
|
+
|
|
122
|
+
:param read_buffer_size: The buffer size in bytes to use when reading from streams.
|
|
123
|
+
Defaults to 8192 (8 KB).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
read_buffer_size: int = DEFAULT_READ_BUFFER_SIZE
|
|
127
|
+
|
|
128
|
+
def __post_init__(self) -> None:
|
|
129
|
+
_assert_crt()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AWSCRTHTTPClient(http_aio_interfaces.HTTPClient):
|
|
133
|
+
_HTTP_PORT = 80
|
|
134
|
+
_HTTPS_PORT = 443
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
eventloop: _AWSCRTEventLoop | None = None,
|
|
139
|
+
client_config: AWSCRTHTTPClientConfig | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""
|
|
142
|
+
:param client_config: Configuration that applies to all requests made with this
|
|
143
|
+
client.
|
|
144
|
+
"""
|
|
145
|
+
_assert_crt()
|
|
146
|
+
self._config = client_config or AWSCRTHTTPClientConfig()
|
|
147
|
+
if eventloop is None:
|
|
148
|
+
eventloop = _AWSCRTEventLoop()
|
|
149
|
+
self._eventloop = eventloop
|
|
150
|
+
self._client_bootstrap = self._eventloop.bootstrap
|
|
151
|
+
self._tls_ctx = crt_io.ClientTlsContext(crt_io.TlsContextOptions())
|
|
152
|
+
self._socket_options = crt_io.SocketOptions()
|
|
153
|
+
self._connections: ConnectionPoolDict = {}
|
|
154
|
+
|
|
155
|
+
async def send(
|
|
156
|
+
self,
|
|
157
|
+
request: http_aio_interfaces.HTTPRequest,
|
|
158
|
+
*,
|
|
159
|
+
request_config: http_aio_interfaces.HTTPRequestConfiguration | None = None,
|
|
160
|
+
) -> AWSCRTHTTPResponse:
|
|
161
|
+
"""Send HTTP request using awscrt client.
|
|
162
|
+
|
|
163
|
+
:param request: The request including destination URI, fields, payload.
|
|
164
|
+
:param request_config: Configuration specific to this request.
|
|
165
|
+
"""
|
|
166
|
+
crt_request = self._marshal_request(request)
|
|
167
|
+
connection = await self._get_connection(request.destination)
|
|
168
|
+
|
|
169
|
+
# Convert body to async iterator for request_body_generator
|
|
170
|
+
body_generator = self._create_body_generator(request.body)
|
|
171
|
+
|
|
172
|
+
crt_stream = connection.request(
|
|
173
|
+
crt_request,
|
|
174
|
+
request_body_generator=body_generator,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return await self._await_response(crt_stream)
|
|
178
|
+
|
|
179
|
+
async def _await_response(
|
|
180
|
+
self, stream: "AIOHttpClientStreamUnified"
|
|
181
|
+
) -> AWSCRTHTTPResponse:
|
|
182
|
+
status_code = await stream.get_response_status_code()
|
|
183
|
+
headers = await stream.get_response_headers()
|
|
184
|
+
fields = Fields()
|
|
185
|
+
for header_name, header_val in headers:
|
|
186
|
+
try:
|
|
187
|
+
fields[header_name].add(header_val)
|
|
188
|
+
except KeyError:
|
|
189
|
+
fields[header_name] = Field(
|
|
190
|
+
name=header_name,
|
|
191
|
+
values=[header_val],
|
|
192
|
+
kind=FieldPosition.HEADER,
|
|
193
|
+
)
|
|
194
|
+
return AWSCRTHTTPResponse(
|
|
195
|
+
status=status_code,
|
|
196
|
+
fields=fields,
|
|
197
|
+
stream=stream,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def _create_connection(
|
|
201
|
+
self, url: core_interfaces.URI
|
|
202
|
+
) -> "AIOHttpClientConnectionUnified":
|
|
203
|
+
"""Builds and validates connection to ``url``"""
|
|
204
|
+
connection = await self._build_new_connection(url)
|
|
205
|
+
await self._validate_connection(connection)
|
|
206
|
+
return connection
|
|
207
|
+
|
|
208
|
+
async def _get_connection(
|
|
209
|
+
self, url: core_interfaces.URI
|
|
210
|
+
) -> "AIOHttpClientConnectionUnified":
|
|
211
|
+
# TODO: Use CRT connection pooling instead of this basic kind
|
|
212
|
+
connection_key = (url.scheme, url.host, url.port)
|
|
213
|
+
connection = self._connections.get(connection_key)
|
|
214
|
+
|
|
215
|
+
if connection and connection.is_open():
|
|
216
|
+
return connection
|
|
217
|
+
|
|
218
|
+
connection = await self._create_connection(url)
|
|
219
|
+
self._connections[connection_key] = connection
|
|
220
|
+
return connection
|
|
221
|
+
|
|
222
|
+
async def _build_new_connection(
|
|
223
|
+
self, url: core_interfaces.URI
|
|
224
|
+
) -> "AIOHttpClientConnectionUnified":
|
|
225
|
+
if url.scheme == "http":
|
|
226
|
+
port = self._HTTP_PORT
|
|
227
|
+
tls_connection_options = None
|
|
228
|
+
elif url.scheme == "https":
|
|
229
|
+
port = self._HTTPS_PORT
|
|
230
|
+
tls_connection_options = self._tls_ctx.new_connection_options()
|
|
231
|
+
tls_connection_options.set_server_name(url.host)
|
|
232
|
+
# TODO: Support TLS configuration, including alpn
|
|
233
|
+
tls_connection_options.set_alpn_list(["h2", "http/1.1"])
|
|
234
|
+
else:
|
|
235
|
+
raise SmithyHTTPError(
|
|
236
|
+
f"AWSCRTHTTPClient does not support URL scheme {url.scheme}"
|
|
237
|
+
)
|
|
238
|
+
if url.port is not None:
|
|
239
|
+
port = url.port
|
|
240
|
+
|
|
241
|
+
return await AIOHttpClientConnectionUnified.new(
|
|
242
|
+
bootstrap=self._client_bootstrap,
|
|
243
|
+
host_name=url.host,
|
|
244
|
+
port=port,
|
|
245
|
+
socket_options=self._socket_options,
|
|
246
|
+
tls_connection_options=tls_connection_options,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def _validate_connection(
|
|
250
|
+
self, connection: "AIOHttpClientConnectionUnified"
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Validates an existing connection against the client config.
|
|
253
|
+
|
|
254
|
+
Checks performed:
|
|
255
|
+
* If ``force_http_2`` is enabled: Is the connection HTTP/2?
|
|
256
|
+
"""
|
|
257
|
+
force_http_2 = self._config.force_http_2
|
|
258
|
+
if force_http_2 and connection.version is not crt_http.HttpVersion.Http2:
|
|
259
|
+
await connection.close()
|
|
260
|
+
negotiated = crt_http.HttpVersion(connection.version).name
|
|
261
|
+
raise SmithyHTTPError(f"HTTP/2 could not be negotiated: {negotiated}")
|
|
262
|
+
|
|
263
|
+
def _render_path(self, url: core_interfaces.URI) -> str:
|
|
264
|
+
path = url.path if url.path is not None else "/"
|
|
265
|
+
query = f"?{url.query}" if url.query is not None else ""
|
|
266
|
+
return f"{path}{query}"
|
|
267
|
+
|
|
268
|
+
def _marshal_request(
|
|
269
|
+
self, request: http_aio_interfaces.HTTPRequest
|
|
270
|
+
) -> "crt_http.HttpRequest":
|
|
271
|
+
"""Create :py:class:`awscrt.http.HttpRequest` from
|
|
272
|
+
:py:class:`smithy_http.aio.HTTPRequest`"""
|
|
273
|
+
headers_list: list[tuple[str, str]] = []
|
|
274
|
+
if "host" not in request.fields:
|
|
275
|
+
request.fields.set_field(
|
|
276
|
+
Field(name="host", values=[request.destination.host])
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if "accept" not in request.fields:
|
|
280
|
+
request.fields.set_field(Field(name="accept", values=["*/*"]))
|
|
281
|
+
|
|
282
|
+
for fld in request.fields.entries.values():
|
|
283
|
+
# TODO: Use literal values for "header"/"trailer".
|
|
284
|
+
if fld.kind.value != FieldPosition.HEADER.value:
|
|
285
|
+
continue
|
|
286
|
+
for val in fld.values:
|
|
287
|
+
headers_list.append((fld.name, val))
|
|
288
|
+
|
|
289
|
+
path = self._render_path(request.destination)
|
|
290
|
+
headers = crt_http.HttpHeaders(headers_list)
|
|
291
|
+
|
|
292
|
+
crt_request = crt_http.HttpRequest(
|
|
293
|
+
method=request.method,
|
|
294
|
+
path=path,
|
|
295
|
+
headers=headers,
|
|
296
|
+
)
|
|
297
|
+
return crt_request
|
|
298
|
+
|
|
299
|
+
async def _create_body_generator(
|
|
300
|
+
self, body: core_aio_interfaces.StreamingBlob
|
|
301
|
+
) -> AsyncGenerator[bytes, None]:
|
|
302
|
+
"""Convert various body types to async generator for request_body_generator."""
|
|
303
|
+
if isinstance(body, bytes):
|
|
304
|
+
# Yield the entire body as a single chunk
|
|
305
|
+
yield body
|
|
306
|
+
elif isinstance(body, bytearray):
|
|
307
|
+
# Convert bytearray to bytes
|
|
308
|
+
yield bytes(body)
|
|
309
|
+
elif isinstance(body, AsyncIterable):
|
|
310
|
+
# Already async iterable, just yield from it.
|
|
311
|
+
# Check this before AsyncByteStream since AsyncBytesReader implements both.
|
|
312
|
+
async for chunk in body:
|
|
313
|
+
if isinstance(chunk, bytearray):
|
|
314
|
+
yield bytes(chunk)
|
|
315
|
+
else:
|
|
316
|
+
yield chunk
|
|
317
|
+
elif iscoroutinefunction(getattr(body, "read", None)) and isinstance(
|
|
318
|
+
body, # type: ignore[reportGeneralTypeIssues]
|
|
319
|
+
core_aio_interfaces.AsyncByteStream, # type: ignore[reportGeneralTypeIssues]
|
|
320
|
+
):
|
|
321
|
+
# AsyncByteStream has async read method but is not iterable
|
|
322
|
+
while True:
|
|
323
|
+
chunk = await body.read(self._config.read_buffer_size)
|
|
324
|
+
if not chunk:
|
|
325
|
+
break
|
|
326
|
+
if isinstance(chunk, bytearray):
|
|
327
|
+
yield bytes(chunk)
|
|
328
|
+
else:
|
|
329
|
+
yield chunk
|
|
330
|
+
else:
|
|
331
|
+
# Assume it's a sync BytesReader, wrap it in AsyncBytesReader
|
|
332
|
+
async_reader = AsyncBytesReader(body)
|
|
333
|
+
async for chunk in async_reader:
|
|
334
|
+
if isinstance(chunk, bytearray):
|
|
335
|
+
yield bytes(chunk)
|
|
336
|
+
else:
|
|
337
|
+
yield chunk
|
|
338
|
+
|
|
339
|
+
def __deepcopy__(self, memo: Any) -> "AWSCRTHTTPClient":
|
|
340
|
+
return AWSCRTHTTPClient(
|
|
341
|
+
eventloop=self._eventloop,
|
|
342
|
+
client_config=deepcopy(self._config),
|
|
343
|
+
)
|
smithy_http-0.1.0/CHANGELOG.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## v0.1.0
|
|
4
|
-
|
|
5
|
-
### Breaking Changes
|
|
6
|
-
* Removed identity and auth interfaces in favor of the transport-agnostic interfaces introduced in `smithy-core`.
|
|
7
|
-
|
|
8
|
-
### Features
|
|
9
|
-
* Introduced schema-based serializers and deserializers for HTTP binding protocols.
|
|
10
|
-
|
|
11
|
-
## v0.0.1
|
|
12
|
-
|
|
13
|
-
### Features
|
|
14
|
-
* Added support for aiohttp and AWSCRT http clients.
|
|
15
|
-
* Added basic HTTP primitives and interfaces for Smithy clients.
|
|
@@ -1,494 +0,0 @@
|
|
|
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, cast
|
|
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 MissingDependencyError
|
|
39
|
-
|
|
40
|
-
from .. import Field, Fields
|
|
41
|
-
from .. import interfaces as http_interfaces
|
|
42
|
-
from ..exceptions import SmithyHTTPError
|
|
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 MissingDependencyError(
|
|
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 SmithyHTTPError("Stream already set on AWSCRTHTTPResponse object")
|
|
121
|
-
self._stream = stream
|
|
122
|
-
concurrent_future = cast(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 SmithyHTTPError("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 SmithyHTTPError(
|
|
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 = cast(
|
|
309
|
-
ConcurrentFuture[crt_http.HttpClientConnection],
|
|
310
|
-
crt_http.HttpClientConnection.new(
|
|
311
|
-
bootstrap=self._client_bootstrap,
|
|
312
|
-
host_name=url.host,
|
|
313
|
-
port=port,
|
|
314
|
-
socket_options=self._socket_options,
|
|
315
|
-
tls_connection_options=tls_connection_options,
|
|
316
|
-
),
|
|
317
|
-
)
|
|
318
|
-
return connect_future
|
|
319
|
-
|
|
320
|
-
def _validate_connection(self, connection: "crt_http.HttpClientConnection") -> None:
|
|
321
|
-
"""Validates an existing connection against the client config.
|
|
322
|
-
|
|
323
|
-
Checks performed:
|
|
324
|
-
* If ``force_http_2`` is enabled: Is the connection HTTP/2?
|
|
325
|
-
"""
|
|
326
|
-
force_http_2 = self._config.force_http_2
|
|
327
|
-
if force_http_2 and connection.version is not crt_http.HttpVersion.Http2:
|
|
328
|
-
connection.close()
|
|
329
|
-
negotiated = crt_http.HttpVersion(connection.version).name
|
|
330
|
-
raise SmithyHTTPError(f"HTTP/2 could not be negotiated: {negotiated}")
|
|
331
|
-
|
|
332
|
-
def _render_path(self, url: core_interfaces.URI) -> str:
|
|
333
|
-
path = url.path if url.path is not None else "/"
|
|
334
|
-
query = f"?{url.query}" if url.query is not None else ""
|
|
335
|
-
return f"{path}{query}"
|
|
336
|
-
|
|
337
|
-
async def _marshal_request(
|
|
338
|
-
self, request: http_aio_interfaces.HTTPRequest
|
|
339
|
-
) -> tuple["crt_http.HttpRequest", "BufferableByteStream | BytesIO"]:
|
|
340
|
-
"""Create :py:class:`awscrt.http.HttpRequest` from
|
|
341
|
-
:py:class:`smithy_http.aio.HTTPRequest`"""
|
|
342
|
-
headers_list = []
|
|
343
|
-
if "host" not in request.fields:
|
|
344
|
-
request.fields.set_field(
|
|
345
|
-
Field(name="host", values=[request.destination.host])
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
if "accept" not in request.fields:
|
|
349
|
-
request.fields.set_field(Field(name="accept", values=["*/*"]))
|
|
350
|
-
|
|
351
|
-
for fld in request.fields.entries.values():
|
|
352
|
-
# TODO: Use literal values for "header"/"trailer".
|
|
353
|
-
if fld.kind.value != FieldPosition.HEADER.value:
|
|
354
|
-
continue
|
|
355
|
-
for val in fld.values:
|
|
356
|
-
headers_list.append((fld.name, val))
|
|
357
|
-
|
|
358
|
-
path = self._render_path(request.destination)
|
|
359
|
-
headers = crt_http.HttpHeaders(headers_list)
|
|
360
|
-
|
|
361
|
-
body = request.body
|
|
362
|
-
if isinstance(body, bytes | bytearray):
|
|
363
|
-
# If the body is already directly in memory, wrap in a BytesIO to hand
|
|
364
|
-
# off to CRT.
|
|
365
|
-
crt_body = BytesIO(body)
|
|
366
|
-
else:
|
|
367
|
-
# If the body is async, or potentially very large, start up a task to read
|
|
368
|
-
# it into the intermediate object that CRT needs. By using
|
|
369
|
-
# asyncio.create_task we'll start the coroutine without having to
|
|
370
|
-
# explicitly await it.
|
|
371
|
-
crt_body = BufferableByteStream()
|
|
372
|
-
|
|
373
|
-
if not isinstance(body, AsyncIterable):
|
|
374
|
-
body = AsyncBytesReader(body)
|
|
375
|
-
|
|
376
|
-
# Start the read task in the background.
|
|
377
|
-
read_task = asyncio.create_task(self._consume_body_async(body, crt_body))
|
|
378
|
-
|
|
379
|
-
# Keep track of the read task so that it doesn't get garbage colllected,
|
|
380
|
-
# and stop tracking it once it's done.
|
|
381
|
-
self._async_reads.add(read_task)
|
|
382
|
-
read_task.add_done_callback(self._async_reads.discard)
|
|
383
|
-
|
|
384
|
-
crt_request = crt_http.HttpRequest(
|
|
385
|
-
method=request.method,
|
|
386
|
-
path=path,
|
|
387
|
-
headers=headers,
|
|
388
|
-
body_stream=crt_body,
|
|
389
|
-
)
|
|
390
|
-
return crt_request, crt_body
|
|
391
|
-
|
|
392
|
-
async def _consume_body_async(
|
|
393
|
-
self, source: AsyncIterable[bytes], dest: "BufferableByteStream"
|
|
394
|
-
) -> None:
|
|
395
|
-
try:
|
|
396
|
-
async for chunk in source:
|
|
397
|
-
dest.write(chunk)
|
|
398
|
-
except Exception:
|
|
399
|
-
dest.close()
|
|
400
|
-
raise
|
|
401
|
-
finally:
|
|
402
|
-
await close(source)
|
|
403
|
-
dest.end_stream()
|
|
404
|
-
|
|
405
|
-
def __deepcopy__(self, memo: Any) -> "AWSCRTHTTPClient":
|
|
406
|
-
return AWSCRTHTTPClient(
|
|
407
|
-
eventloop=self._eventloop,
|
|
408
|
-
client_config=deepcopy(self._config),
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# This is adapted from the transcribe streaming sdk
|
|
413
|
-
class BufferableByteStream(BufferedIOBase):
|
|
414
|
-
"""A non-blocking bytes buffer."""
|
|
415
|
-
|
|
416
|
-
def __init__(self) -> None:
|
|
417
|
-
# We're always manipulating the front and back of the buffer, so a deque
|
|
418
|
-
# will be much more efficient than a list.
|
|
419
|
-
self._chunks: deque[bytes] = deque()
|
|
420
|
-
self._closed = False
|
|
421
|
-
self._done = False
|
|
422
|
-
|
|
423
|
-
def read(self, size: int | None = -1) -> bytes:
|
|
424
|
-
if self._closed:
|
|
425
|
-
return b""
|
|
426
|
-
|
|
427
|
-
if len(self._chunks) == 0:
|
|
428
|
-
if self._done:
|
|
429
|
-
self.close()
|
|
430
|
-
return b""
|
|
431
|
-
else:
|
|
432
|
-
# When the CRT recieves this, it'll try again
|
|
433
|
-
raise BlockingIOError("read")
|
|
434
|
-
|
|
435
|
-
# We could compile all the chunks here instead of just returning
|
|
436
|
-
# the one, BUT the CRT will keep calling read until empty bytes
|
|
437
|
-
# are returned. So it's actually better to just return one chunk
|
|
438
|
-
# since combining them would have some potentially bad memory
|
|
439
|
-
# usage issues.
|
|
440
|
-
result = self._chunks.popleft()
|
|
441
|
-
if size is not None and size > 0:
|
|
442
|
-
remainder = result[size:]
|
|
443
|
-
result = result[:size]
|
|
444
|
-
if remainder:
|
|
445
|
-
self._chunks.appendleft(remainder)
|
|
446
|
-
|
|
447
|
-
if self._done and len(self._chunks) == 0:
|
|
448
|
-
self.close()
|
|
449
|
-
|
|
450
|
-
return result
|
|
451
|
-
|
|
452
|
-
def read1(self, size: int = -1) -> bytes:
|
|
453
|
-
return self.read(size)
|
|
454
|
-
|
|
455
|
-
def readinto(self, buffer: "WriteableBuffer") -> int:
|
|
456
|
-
if not isinstance(buffer, memoryview):
|
|
457
|
-
buffer = memoryview(buffer).cast("B")
|
|
458
|
-
|
|
459
|
-
data = self.read(len(buffer)) # type: ignore
|
|
460
|
-
n = len(data)
|
|
461
|
-
buffer[:n] = data
|
|
462
|
-
return n
|
|
463
|
-
|
|
464
|
-
def write(self, buffer: "ReadableBuffer") -> int:
|
|
465
|
-
if not isinstance(buffer, bytes):
|
|
466
|
-
raise ValueError(
|
|
467
|
-
f"Unexpected value written to BufferableByteStream. "
|
|
468
|
-
f"Only bytes are support but {type(buffer)} was provided."
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
if self._closed:
|
|
472
|
-
raise OSError("Stream is completed and doesn't support further writes.")
|
|
473
|
-
|
|
474
|
-
if buffer:
|
|
475
|
-
self._chunks.append(buffer)
|
|
476
|
-
return len(buffer)
|
|
477
|
-
|
|
478
|
-
@property
|
|
479
|
-
def closed(self) -> bool:
|
|
480
|
-
return self._closed
|
|
481
|
-
|
|
482
|
-
def close(self) -> None:
|
|
483
|
-
self._closed = True
|
|
484
|
-
self._done = True
|
|
485
|
-
|
|
486
|
-
# Clear out the remaining chunks so that they don't sit around in memory.
|
|
487
|
-
self._chunks.clear()
|
|
488
|
-
|
|
489
|
-
def end_stream(self) -> None:
|
|
490
|
-
"""End the stream, letting any remaining chunks be read before it is closed."""
|
|
491
|
-
if len(self._chunks) == 0:
|
|
492
|
-
self.close()
|
|
493
|
-
else:
|
|
494
|
-
self._done = True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|