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.
- smithy_http-0.2.0/.changes/0.0.1.json +12 -0
- smithy_http-0.2.0/.changes/0.1.0.json +12 -0
- smithy_http-0.2.0/.changes/0.2.0.json +8 -0
- smithy_http-0.2.0/CHANGELOG.md +20 -0
- smithy_http-0.2.0/PKG-INFO +37 -0
- smithy_http-0.2.0/README.md +3 -0
- smithy_http-0.2.0/pyproject.toml +59 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/__init__.py +1 -2
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/aiohttp.py +2 -2
- smithy_http-0.2.0/src/smithy_http/aio/auth/apikey.py +122 -0
- smithy_http-0.2.0/src/smithy_http/aio/crt.py +343 -0
- smithy_http-0.2.0/src/smithy_http/aio/identity/apikey.py +38 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/interfaces/__init__.py +19 -1
- smithy_http-0.2.0/src/smithy_http/aio/protocols.py +237 -0
- smithy_http-0.2.0/src/smithy_http/bindings.py +172 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/deserializers.py +106 -44
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/exceptions.py +2 -2
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/serializers.py +253 -103
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/user_agent.py +28 -10
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/utils.py +3 -3
- smithy_http-0.0.1/CHANGES.md +0 -11
- smithy_http-0.0.1/PKG-INFO +0 -12
- smithy_http-0.0.1/README.md +0 -0
- smithy_http-0.0.1/pyproject.toml +0 -30
- smithy_http-0.0.1/src/smithy_http/aio/auth/apikey.py +0 -117
- smithy_http-0.0.1/src/smithy_http/aio/crt.py +0 -493
- smithy_http-0.0.1/src/smithy_http/aio/identity/apikey.py +0 -38
- smithy_http-0.0.1/src/smithy_http/aio/interfaces/auth.py +0 -107
- smithy_http-0.0.1/src/smithy_http/aio/protocols.py +0 -118
- {smithy_http-0.0.1 → smithy_http-0.2.0}/.gitignore +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/NOTICE +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/auth/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/identity/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/aio/restjson.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/endpoints.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interceptors/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interceptors/user_agent.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/interfaces/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/plugins/__init__.py +0 -0
- {smithy_http-0.0.1 → smithy_http-0.2.0}/src/smithy_http/py.typed +0 -0
- {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": "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,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__
|
|
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
|
|
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
|
|
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
|
+
)
|