smithy-http 0.0.1__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.
Files changed (42) hide show
  1. smithy_http-0.2.0/.changes/0.0.1.json +12 -0
  2. smithy_http-0.2.0/.changes/0.1.0.json +12 -0
  3. smithy_http-0.2.0/.changes/0.2.0.json +8 -0
  4. smithy_http-0.2.0/CHANGELOG.md +20 -0
  5. smithy_http-0.2.0/PKG-INFO +37 -0
  6. smithy_http-0.2.0/README.md +3 -0
  7. smithy_http-0.2.0/pyproject.toml +59 -0
  8. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/__init__.py +1 -2
  9. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/aiohttp.py +2 -2
  10. smithy_http-0.2.0/src/smithy_http/aio/auth/apikey.py +122 -0
  11. smithy_http-0.2.0/src/smithy_http/aio/crt.py +343 -0
  12. smithy_http-0.2.0/src/smithy_http/aio/identity/apikey.py +38 -0
  13. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/interfaces/__init__.py +19 -1
  14. smithy_http-0.2.0/src/smithy_http/aio/protocols.py +237 -0
  15. smithy_http-0.2.0/src/smithy_http/bindings.py +172 -0
  16. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/deserializers.py +106 -44
  17. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/exceptions.py +2 -2
  18. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/serializers.py +253 -103
  19. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/user_agent.py +28 -10
  20. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/utils.py +3 -3
  21. smithy_http-0.0.1/CHANGES.md +0 -11
  22. smithy_http-0.0.1/PKG-INFO +0 -12
  23. smithy_http-0.0.1/README.md +0 -0
  24. smithy_http-0.0.1/pyproject.toml +0 -30
  25. smithy_http-0.0.1/src/smithy_http/aio/auth/apikey.py +0 -117
  26. smithy_http-0.0.1/src/smithy_http/aio/crt.py +0 -493
  27. smithy_http-0.0.1/src/smithy_http/aio/identity/apikey.py +0 -38
  28. smithy_http-0.0.1/src/smithy_http/aio/interfaces/auth.py +0 -107
  29. smithy_http-0.0.1/src/smithy_http/aio/protocols.py +0 -118
  30. {smithy_http-0.0.1 → smithy_http-0.2.0}/.gitignore +0 -0
  31. {smithy_http-0.0.1 → smithy_http-0.2.0}/NOTICE +0 -0
  32. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/__init__.py +0 -0
  33. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/auth/__init__.py +0 -0
  34. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/identity/__init__.py +0 -0
  35. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/restjson.py +0 -0
  36. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/endpoints.py +0 -0
  37. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interceptors/__init__.py +0 -0
  38. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interceptors/user_agent.py +0 -0
  39. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interfaces/__init__.py +0 -0
  40. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/plugins/__init__.py +0 -0
  41. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/py.typed +0 -0
  42. {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/restjson.py +0 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "changes": [
3
+ {
4
+ "type": "feature",
5
+ "description": "Added support for aiohttp and AWSCRT http clients."
6
+ },
7
+ {
8
+ "type": "feature",
9
+ "description": "Added basic HTTP primitives and interfaces for Smithy clients."
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "changes": [
3
+ {
4
+ "type": "breaking",
5
+ "description": "Removed identity and auth interfaces in favor of the transport-agnostic interfaces introduced in `smithy-core`."
6
+ },
7
+ {
8
+ "type": "feature",
9
+ "description": "Introduced schema-based serializers and deserializers for HTTP binding protocols."
10
+ }
11
+ ]
12
+ }
@@ -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.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: smithy-http
3
+ Version: 0.2.0
4
+ Summary: HTTP components for Smithy tooling.
5
+ Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/CHANGELOG.md
6
+ Project-URL: Code, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/
7
+ Project-URL: Issue tracker, https://github.com/smithy-lang/smithy-python/issues
8
+ Author: Amazon Web Services
9
+ License: Apache License 2.0
10
+ License-File: NOTICE
11
+ Keywords: http,sdk,smithy
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: smithy-core~=0.1.0
28
+ Provides-Extra: aiohttp
29
+ Requires-Dist: aiohttp<4.0,>=3.11.12; extra == 'aiohttp'
30
+ Requires-Dist: yarl; extra == 'aiohttp'
31
+ Provides-Extra: awscrt
32
+ Requires-Dist: awscrt~=0.28.2; extra == 'awscrt'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # smithy-http
36
+
37
+ This package provides primitives and interfaces for http functionality in tooling generated by Smithy.
@@ -0,0 +1,3 @@
1
+ # smithy-http
2
+
3
+ This package provides primitives and interfaces for http functionality in tooling generated by Smithy.
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "smithy-http"
3
+ dynamic = ["version"]
4
+ requires-python = ">=3.12"
5
+ authors = [
6
+ {name = "Amazon Web Services"},
7
+ ]
8
+ description = "HTTP components for Smithy tooling."
9
+ readme = "README.md"
10
+ license = {text = "Apache License 2.0"}
11
+ keywords = ["smithy", "sdk", "http"]
12
+ classifiers = [
13
+ "Development Status :: 2 - Pre-Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: System Administrators",
16
+ "Natural Language :: English",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Programming Language :: Python :: Implementation :: CPython",
26
+ "Topic :: Software Development :: Libraries"
27
+ ]
28
+ dependencies = [
29
+ "smithy-core~=0.1.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ "Changelog" = "https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/CHANGELOG.md"
34
+ "Code" = "https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-http/"
35
+ "Issue tracker" = "https://github.com/smithy-lang/smithy-python/issues"
36
+
37
+ [project.optional-dependencies]
38
+ awscrt = [
39
+ "awscrt~=0.28.2",
40
+ ]
41
+ aiohttp = [
42
+ "aiohttp>=3.11.12, <4.0",
43
+ "yarl"
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
49
+
50
+ [tool.hatch.version]
51
+ path = "src/smithy_http/__init__.py"
52
+
53
+ [tool.hatch.build]
54
+ exclude = [
55
+ "tests",
56
+ ]
57
+
58
+ [tool.ruff]
59
+ src = ["src"]
@@ -1,13 +1,12 @@
1
1
  # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
- import importlib.metadata
4
3
  from collections import Counter, OrderedDict
5
4
  from collections.abc import Iterable, Iterator
6
5
 
7
6
  from . import interfaces
8
7
  from .interfaces import FieldPosition
9
8
 
10
- __version__: str = importlib.metadata.version("smithy-http")
9
+ __version__ = "0.2.0"
11
10
 
12
11
 
13
12
  class Field(interfaces.Field):
@@ -23,7 +23,7 @@ except ImportError:
23
23
  from smithy_core.aio.interfaces import StreamingBlob
24
24
  from smithy_core.aio.types import AsyncBytesReader
25
25
  from smithy_core.aio.utils import async_list
26
- from smithy_core.exceptions import MissingDependencyException
26
+ from smithy_core.exceptions import MissingDependencyError
27
27
  from smithy_core.interfaces import URI
28
28
 
29
29
  from .. import Field, Fields
@@ -39,7 +39,7 @@ from .interfaces import HTTPResponse as HTTPResponseInterface
39
39
 
40
40
  def _assert_aiohttp() -> None:
41
41
  if not HAS_AIOHTTP:
42
- raise MissingDependencyException(
42
+ raise MissingDependencyError(
43
43
  "Attempted to use aiohttp component, but aiohttp is not installed."
44
44
  )
45
45
 
@@ -0,0 +1,122 @@
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 smithy_core import URI
6
+ from smithy_core.aio.interfaces.auth import AuthScheme, Signer
7
+ from smithy_core.aio.interfaces.identity import IdentityResolver
8
+ from smithy_core.exceptions import SmithyIdentityError
9
+ from smithy_core.interfaces import TypedProperties as _TypedProperties
10
+ from smithy_core.traits import APIKeyLocation, HTTPAPIKeyAuthTrait
11
+ from smithy_core.types import PropertyKey
12
+
13
+ from ... import Field
14
+ from ..identity.apikey import APIKeyIdentity, APIKeyIdentityProperties
15
+ from ..interfaces import HTTPRequest
16
+
17
+
18
+ class APIKeyResolverConfig(Protocol):
19
+ """A config bearing API key properties."""
20
+
21
+ api_key: str | None
22
+ """An explicit API key.
23
+
24
+ If not set, it MAY be retrieved from elsewhere by the resolver.
25
+ """
26
+
27
+ api_key_identity_resolver: (
28
+ IdentityResolver[APIKeyIdentity, APIKeyIdentityProperties] | None
29
+ )
30
+ """An API key identity resolver.
31
+
32
+ The default implementation only checks the explicitly configured key.
33
+ """
34
+
35
+
36
+ API_KEY_RESOLVER_CONFIG = PropertyKey(key="config", value_type=APIKeyResolverConfig)
37
+ """A context property bearing an API key config."""
38
+
39
+
40
+ class APIKeySigner(Signer[HTTPRequest, APIKeyIdentity, Any]):
41
+ """A signer that signs http requests with an api key."""
42
+
43
+ def __init__(
44
+ self, *, name: str, location: APIKeyLocation, scheme: str | None = None
45
+ ) -> None:
46
+ self._name = name
47
+ self._location = location
48
+ self._scheme = scheme
49
+
50
+ async def sign(
51
+ self,
52
+ *,
53
+ request: HTTPRequest,
54
+ identity: APIKeyIdentity,
55
+ properties: Any,
56
+ ) -> HTTPRequest:
57
+ match self._location:
58
+ case APIKeyLocation.QUERY:
59
+ query = request.destination.query or ""
60
+ if query:
61
+ query += "&"
62
+ query += f"{self._name}={identity.api_key}"
63
+ request.destination = URI(
64
+ scheme=request.destination.scheme,
65
+ username=request.destination.username,
66
+ password=request.destination.password,
67
+ host=request.destination.host,
68
+ port=request.destination.port,
69
+ path=request.destination.password,
70
+ query=query,
71
+ fragment=request.destination.fragment,
72
+ )
73
+ case APIKeyLocation.HEADER:
74
+ value = identity.api_key
75
+ if self._scheme is not None:
76
+ value = f"{self._scheme} {value}"
77
+ request.fields.set_field(Field(name=self._name, values=[value]))
78
+
79
+ return request
80
+
81
+
82
+ class APIKeyAuthScheme(
83
+ AuthScheme[HTTPRequest, APIKeyIdentity, APIKeyIdentityProperties, Any]
84
+ ):
85
+ """An auth scheme containing necessary data and tools for api key auth."""
86
+
87
+ scheme_id = HTTPAPIKeyAuthTrait.id
88
+ _signer: APIKeySigner
89
+
90
+ def __init__(
91
+ self, *, name: str, location: APIKeyLocation, scheme: str | None = None
92
+ ) -> None:
93
+ self._signer = APIKeySigner(name=name, location=location, scheme=scheme)
94
+
95
+ def identity_properties(
96
+ self, *, context: _TypedProperties
97
+ ) -> APIKeyIdentityProperties:
98
+ config = context.get(API_KEY_RESOLVER_CONFIG)
99
+ if config is not None and config.api_key is not None:
100
+ return {"api_key": config.api_key}
101
+ return {}
102
+
103
+ def identity_resolver(
104
+ self, *, context: _TypedProperties
105
+ ) -> IdentityResolver[APIKeyIdentity, APIKeyIdentityProperties]:
106
+ config = context.get(API_KEY_RESOLVER_CONFIG)
107
+ if config is None or config.api_key_identity_resolver is None:
108
+ raise SmithyIdentityError(
109
+ "Attempted to use API key auth, but api_key_identity_resolver was not "
110
+ "set on the config."
111
+ )
112
+ return config.api_key_identity_resolver
113
+
114
+ def signer_properties(self, *, context: _TypedProperties) -> Any:
115
+ return {}
116
+
117
+ def signer(self) -> Signer[HTTPRequest, APIKeyIdentity, Any]:
118
+ return self._signer
119
+
120
+ @classmethod
121
+ def from_trait(cls, trait: HTTPAPIKeyAuthTrait, /) -> Self:
122
+ return cls(name=trait.name, location=trait.location, scheme=trait.scheme)
@@ -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
+ )