smithy-http 0.7.0__tar.gz → 0.8.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.8.0/.changes/0.8.0.json +92 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/CHANGELOG.md +34 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/PKG-INFO +8 -4
- {smithy_http-0.7.0 → smithy_http-0.8.0}/pyproject.toml +9 -3
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/__init__.py +9 -6
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/aiohttp.py +39 -8
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/crt.py +42 -35
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/identity/apikey.py +2 -5
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/protocols.py +7 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/testing.py +3 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/auth/apikey.py +2 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/bindings.py +1 -9
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/deserializers.py +8 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/endpoints.py +3 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/identity/__init__.py +2 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/interceptors/user_agent.py +1 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/interfaces/__init__.py +1 -1
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/plugins/__init__.py +1 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/protocols.py +216 -10
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/schema_extensions.py +5 -24
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/serializers.py +30 -83
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/testing/mockhttp.py +13 -7
- smithy_http-0.8.0/src/smithy_http/urllib3.py +266 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/user_agent.py +3 -1
- smithy_http-0.7.0/src/smithy_http/urllib3.py +0 -146
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.0.1.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.1.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.2.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.2.1.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.3.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.3.1.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.4.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.4.1.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.4.2.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.4.3.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.4.4.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.5.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.6.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.changes/0.7.0.json +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/.gitignore +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/NOTICE +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/README.md +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/auth/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/auth/apikey.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/identity/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/interfaces/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/aio/restjson.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/auth/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/exceptions.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/identity/apikey.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/interceptors/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/py.typed +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/restjson.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/testing/__init__.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/testing/utils.py +0 -0
- {smithy_http-0.7.0 → smithy_http-0.8.0}/src/smithy_http/utils.py +19 -19
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"changes": [
|
|
3
|
+
{
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"description": "Removed the eventloop parameter from AWSCRTHTTPClient."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"type": "breaking",
|
|
9
|
+
"description": "Removed `HostPrefixSerializer` and `Binding.HOST`. `@hostLabel` no longer changes where a member is serialized and smithy-core now applies host prefixes."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "feature",
|
|
13
|
+
"description": "Added support for the `aws.protocols#awsQueryCompatible` trait to `RpcV2CborClientProtocol` and `RpcV2JsonClientProtocol`."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "feature",
|
|
17
|
+
"description": "Added `RpcV2JsonClientProtocol` and `AsyncRpcV2JsonClientProtocol`, implementations of the `smithy.protocols#rpcv2Json` protocol, and a `json` extra that installs `smithy-json`."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "enhancement",
|
|
21
|
+
"description": "Documented HTTPQueryMapValueSerializer."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "enhancement",
|
|
25
|
+
"description": "Completed type annotations and __all__ declarations for the public API."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "enhancement",
|
|
29
|
+
"description": "Allow MockHTTPClient and AsyncMockHTTPClient responses to have stream bodies."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "enhancement",
|
|
33
|
+
"description": "Imported symbols from their defining modules."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "bugfix",
|
|
37
|
+
"description": "Iterating a `Urllib3HTTPClient` response body now yields 64 KiB chunks instead of 1 KiB ones."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "bugfix",
|
|
41
|
+
"description": "RPC v2 protocols (including `RpcV2CborClientProtocol` and `AsyncRpcV2CborClientProtocol`) now treat a response whose `Smithy-Protocol` header does not match the request as malformed, raising a `CallError` based solely on the HTTP status code without interpreting the body."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "bugfix",
|
|
45
|
+
"description": "Declared smithy-aws-event-stream as the eventstream extra, bounded yarl, and raised MissingDependencyError when the aiohttp extra is not installed."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "bugfix",
|
|
49
|
+
"description": "`Urllib3HTTPClient` now streams request bodies that are readers with a known length or chunked transfer encoding, such as an open file, instead of reading the whole body into memory first. Uploads are sent in 128 KiB blocks."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "bugfix",
|
|
53
|
+
"description": "Derived the CRT timeout error from SmithyHTTPError."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "bugfix",
|
|
57
|
+
"description": "`Field.as_string()` now joins values with its `delimiter` argument, a bare comma by default, instead of always using a comma and a space. The `Field` interface now declares the `delimiter` parameter. `AIOHTTPClient` and `Urllib3HTTPClient` no longer send trailer fields as headers, matching `AWSCRTHTTPClient`."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "bugfix",
|
|
61
|
+
"description": "`AIOHTTPClient` now reads request bodies that are readers, such as open files, in 128 KiB chunks instead of 1 KiB ones, which made uploads about 25 times faster on fast connections. An async iterable body is now passed to aiohttp unchanged, so each item is sent as soon as it's produced."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "bugfix",
|
|
65
|
+
"description": "`AIOHTTPClient` now sends requests that set `Transfer-Encoding: chunked` using aiohttp's chunked uploads. Previously aiohttp rejected the header with a `ValueError`."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "bugfix",
|
|
69
|
+
"description": "RPC v2 protocols now raise a `CallError` instead of an unrelated exception for error responses whose body cannot be decoded or whose `__type` is not an absolute shape ID, and resolve `__type` values with a `:`-delimited suffix to the modeled error."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "bugfix",
|
|
73
|
+
"description": "The urllib3 HTTP client now streams response bodies instead of reading each one into memory. A streaming output, such as an S3 `GetObject` body, holds its connection until it is read to the end or closed, so use it in a `with` block to release the connection early."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"type": "dependency",
|
|
77
|
+
"description": "Bump `smithy-core` from `~=0.10.0` to `~=0.11.0`."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "dependency",
|
|
81
|
+
"description": "Bump `smithy-cbor` from `~=0.2.0` to `~=0.3.0`."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "dependency",
|
|
85
|
+
"description": "Bump `smithy-json` from `~=0.4.0` to `~=0.5.0`."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "dependency",
|
|
89
|
+
"description": "Bump `smithy-aws-event-stream` from `~=0.4.0` to `~=0.5.0`."
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.8.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
* Removed the eventloop parameter from AWSCRTHTTPClient.
|
|
7
|
+
* Removed `HostPrefixSerializer` and `Binding.HOST`. `@hostLabel` no longer changes where a member is serialized and smithy-core now applies host prefixes.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
* Added support for the `aws.protocols#awsQueryCompatible` trait to `RpcV2CborClientProtocol` and `RpcV2JsonClientProtocol`.
|
|
11
|
+
* Added `RpcV2JsonClientProtocol` and `AsyncRpcV2JsonClientProtocol`, implementations of the `smithy.protocols#rpcv2Json` protocol, and a `json` extra that installs `smithy-json`.
|
|
12
|
+
|
|
13
|
+
### Enhancements
|
|
14
|
+
* Documented HTTPQueryMapValueSerializer.
|
|
15
|
+
* Completed type annotations and __all__ declarations for the public API.
|
|
16
|
+
* Allow MockHTTPClient and AsyncMockHTTPClient responses to have stream bodies.
|
|
17
|
+
* Imported symbols from their defining modules.
|
|
18
|
+
|
|
19
|
+
### Bug fixes
|
|
20
|
+
* Iterating a `Urllib3HTTPClient` response body now yields 64 KiB chunks instead of 1 KiB ones.
|
|
21
|
+
* RPC v2 protocols (including `RpcV2CborClientProtocol` and `AsyncRpcV2CborClientProtocol`) now treat a response whose `Smithy-Protocol` header does not match the request as malformed, raising a `CallError` based solely on the HTTP status code without interpreting the body.
|
|
22
|
+
* Declared smithy-aws-event-stream as the eventstream extra, bounded yarl, and raised MissingDependencyError when the aiohttp extra is not installed.
|
|
23
|
+
* `Urllib3HTTPClient` now streams request bodies that are readers with a known length or chunked transfer encoding, such as an open file, instead of reading the whole body into memory first. Uploads are sent in 128 KiB blocks.
|
|
24
|
+
* Derived the CRT timeout error from SmithyHTTPError.
|
|
25
|
+
* `Field.as_string()` now joins values with its `delimiter` argument, a bare comma by default, instead of always using a comma and a space. The `Field` interface now declares the `delimiter` parameter. `AIOHTTPClient` and `Urllib3HTTPClient` no longer send trailer fields as headers, matching `AWSCRTHTTPClient`.
|
|
26
|
+
* `AIOHTTPClient` now reads request bodies that are readers, such as open files, in 128 KiB chunks instead of 1 KiB ones, which made uploads about 25 times faster on fast connections. An async iterable body is now passed to aiohttp unchanged, so each item is sent as soon as it's produced.
|
|
27
|
+
* `AIOHTTPClient` now sends requests that set `Transfer-Encoding: chunked` using aiohttp's chunked uploads. Previously aiohttp rejected the header with a `ValueError`.
|
|
28
|
+
* RPC v2 protocols now raise a `CallError` instead of an unrelated exception for error responses whose body cannot be decoded or whose `__type` is not an absolute shape ID, and resolve `__type` values with a `:`-delimited suffix to the modeled error.
|
|
29
|
+
* The urllib3 HTTP client now streams response bodies instead of reading each one into memory. A streaming output, such as an S3 `GetObject` body, holds its connection until it is read to the end or closed, so use it in a `with` block to release the connection early.
|
|
30
|
+
|
|
31
|
+
### Dependencies
|
|
32
|
+
* Bump `smithy-core` from `~=0.10.0` to `~=0.11.0`.
|
|
33
|
+
* Bump `smithy-cbor` from `~=0.2.0` to `~=0.3.0`.
|
|
34
|
+
* Bump `smithy-json` from `~=0.4.0` to `~=0.5.0`.
|
|
35
|
+
* Bump `smithy-aws-event-stream` from `~=0.4.0` to `~=0.5.0`.
|
|
36
|
+
|
|
3
37
|
## v0.7.0
|
|
4
38
|
|
|
5
39
|
### Breaking Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: smithy-http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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/
|
|
@@ -26,14 +26,18 @@ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
|
26
26
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
27
27
|
Classifier: Topic :: Software Development :: Libraries
|
|
28
28
|
Requires-Python: >=3.12
|
|
29
|
-
Requires-Dist: smithy-core
|
|
29
|
+
Requires-Dist: smithy-core~=0.11.0
|
|
30
30
|
Provides-Extra: aiohttp
|
|
31
31
|
Requires-Dist: aiohttp<4.0,>=3.11.12; extra == 'aiohttp'
|
|
32
|
-
Requires-Dist: yarl; extra == 'aiohttp'
|
|
32
|
+
Requires-Dist: yarl>=1.17.0; extra == 'aiohttp'
|
|
33
33
|
Provides-Extra: awscrt
|
|
34
34
|
Requires-Dist: awscrt~=0.32.0; extra == 'awscrt'
|
|
35
35
|
Provides-Extra: cbor
|
|
36
|
-
Requires-Dist: smithy-cbor~=0.
|
|
36
|
+
Requires-Dist: smithy-cbor~=0.3.0; extra == 'cbor'
|
|
37
|
+
Provides-Extra: eventstream
|
|
38
|
+
Requires-Dist: smithy-aws-event-stream~=0.5.0; extra == 'eventstream'
|
|
39
|
+
Provides-Extra: json
|
|
40
|
+
Requires-Dist: smithy-json~=0.5.0; extra == 'json'
|
|
37
41
|
Provides-Extra: urllib3
|
|
38
42
|
Requires-Dist: urllib3<3.0,>=2.0; extra == 'urllib3'
|
|
39
43
|
Description-Content-Type: text/markdown
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Topic :: Software Development :: Libraries"
|
|
29
29
|
]
|
|
30
30
|
dependencies = [
|
|
31
|
-
"smithy-core",
|
|
31
|
+
"smithy-core~=0.11.0",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
@@ -41,11 +41,17 @@ awscrt = [
|
|
|
41
41
|
"awscrt~=0.32.0",
|
|
42
42
|
]
|
|
43
43
|
cbor = [
|
|
44
|
-
"smithy-cbor~=0.
|
|
44
|
+
"smithy-cbor~=0.3.0",
|
|
45
|
+
]
|
|
46
|
+
json = [
|
|
47
|
+
"smithy-json~=0.5.0",
|
|
48
|
+
]
|
|
49
|
+
eventstream = [
|
|
50
|
+
"smithy-aws-event-stream~=0.5.0",
|
|
45
51
|
]
|
|
46
52
|
aiohttp = [
|
|
47
53
|
"aiohttp>=3.11.12, <4.0",
|
|
48
|
-
"yarl"
|
|
54
|
+
"yarl>=1.17.0",
|
|
49
55
|
]
|
|
50
56
|
urllib3 = [
|
|
51
57
|
"urllib3>=2.0, <3.0",
|
|
@@ -22,7 +22,7 @@ __all__ = [
|
|
|
22
22
|
"tuples_to_fields",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.8.0"
|
|
26
26
|
|
|
27
27
|
_VALID_FIELD_POSITIONS = frozenset(get_args(FieldPosition))
|
|
28
28
|
|
|
@@ -48,7 +48,7 @@ class Field(interfaces.Field):
|
|
|
48
48
|
self.values: list[str] = list(values) if values is not None else []
|
|
49
49
|
if kind not in _VALID_FIELD_POSITIONS:
|
|
50
50
|
raise ValueError(f"Unknown field kind: {kind!r}")
|
|
51
|
-
self.kind = kind
|
|
51
|
+
self.kind: FieldPosition = kind
|
|
52
52
|
|
|
53
53
|
def __eq__(self, other: object) -> bool:
|
|
54
54
|
"""Name, values, and kind must match.
|
|
@@ -88,8 +88,8 @@ class Field(interfaces.Field):
|
|
|
88
88
|
If the ``Field`` has zero values, the empty string is returned. If the ``Field``
|
|
89
89
|
has exactly one value, the value is returned unmodified.
|
|
90
90
|
|
|
91
|
-
For ``Field``s with more than one value, the values are joined by
|
|
92
|
-
|
|
91
|
+
For ``Field``s with more than one value, the values are joined by
|
|
92
|
+
``delimiter``. For such multi-valued ``Field``s, any values that already contain
|
|
93
93
|
commas or double quotes will be surrounded by double quotes. Within any values
|
|
94
94
|
that get quoted, pre-existing double quotes and backslashes are escaped with a
|
|
95
95
|
backslash.
|
|
@@ -99,7 +99,7 @@ class Field(interfaces.Field):
|
|
|
99
99
|
return ""
|
|
100
100
|
if value_count == 1:
|
|
101
101
|
return self.values[0]
|
|
102
|
-
return
|
|
102
|
+
return delimiter.join(quote_and_escape_field_value(val) for val in self.values)
|
|
103
103
|
|
|
104
104
|
def as_tuples(self) -> list[tuple[str, str]]:
|
|
105
105
|
"""Get list of ``name``, ``value`` tuples where each tuple represents one
|
|
@@ -108,6 +108,8 @@ class Field(interfaces.Field):
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
class Fields(interfaces.Fields):
|
|
111
|
+
"""Collection of header and trailer entries mapped by name."""
|
|
112
|
+
|
|
111
113
|
def __init__(
|
|
112
114
|
self,
|
|
113
115
|
initial: Iterable[interfaces.Field] | None = None,
|
|
@@ -134,7 +136,7 @@ class Fields(interfaces.Fields):
|
|
|
134
136
|
"following normalized field names appear more than once: "
|
|
135
137
|
f"{', '.join(non_unique_names)}."
|
|
136
138
|
)
|
|
137
|
-
init_tuples = zip(init_field_names, init_fields)
|
|
139
|
+
init_tuples = zip(init_field_names, init_fields, strict=True)
|
|
138
140
|
self.entries: dict[str, interfaces.Field] = OrderedDict(init_tuples)
|
|
139
141
|
self.encoding: str = encoding
|
|
140
142
|
|
|
@@ -187,6 +189,7 @@ class Fields(interfaces.Fields):
|
|
|
187
189
|
def get(
|
|
188
190
|
self, key: str, default: interfaces.Field | None = None
|
|
189
191
|
) -> interfaces.Field | None:
|
|
192
|
+
"""Return the field named ``key``, or ``default`` if absent."""
|
|
190
193
|
return self[key] if key in self else default
|
|
191
194
|
|
|
192
195
|
def get_by_type(self, kind: FieldPosition) -> list[interfaces.Field]:
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
4
4
|
from copy import copy, deepcopy
|
|
5
5
|
from itertools import chain
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Self
|
|
7
|
-
|
|
8
|
-
import yarl
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
9
7
|
|
|
10
8
|
if TYPE_CHECKING:
|
|
11
9
|
# pyright doesn't like optional imports. This is reasonable because if we use these
|
|
12
10
|
# in type hints then they'd result in runtime errors.
|
|
13
11
|
# TODO: add integ tests that import these without the dependendency installed
|
|
14
12
|
import aiohttp
|
|
13
|
+
import yarl
|
|
15
14
|
|
|
16
15
|
try:
|
|
17
16
|
import aiohttp
|
|
17
|
+
import yarl
|
|
18
18
|
|
|
19
19
|
HAS_AIOHTTP = True
|
|
20
20
|
except ImportError:
|
|
@@ -37,10 +37,14 @@ from ..interfaces import (
|
|
|
37
37
|
)
|
|
38
38
|
from .interfaces import AsyncHTTPClient
|
|
39
39
|
|
|
40
|
-
__all__ = ["AIOHTTPClient", "AIOHTTPClientConfig"]
|
|
40
|
+
__all__ = ["HAS_AIOHTTP", "AIOHTTPClient", "AIOHTTPClientConfig"]
|
|
41
|
+
|
|
42
|
+
_UPLOAD_CHUNK_SIZE = 128 * 1024
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
class AIOHTTPClientConfig(HTTPClientConfiguration):
|
|
46
|
+
"""Configuration for the aiohttp HTTP client."""
|
|
47
|
+
|
|
44
48
|
def __post_init__(self) -> None:
|
|
45
49
|
_assert_aiohttp()
|
|
46
50
|
|
|
@@ -48,7 +52,7 @@ class AIOHTTPClientConfig(HTTPClientConfiguration):
|
|
|
48
52
|
class AIOHTTPClient(AsyncHTTPClient):
|
|
49
53
|
"""Implementation of :py:class:`.interfaces.AsyncHTTPClient` using aiohttp."""
|
|
50
54
|
|
|
51
|
-
TIMEOUT_EXCEPTIONS = (TimeoutError,)
|
|
55
|
+
TIMEOUT_EXCEPTIONS: tuple[type[Exception], ...] = (TimeoutError,)
|
|
52
56
|
|
|
53
57
|
# aiohttp has no HTTP/2 support, so it can never interleave request and
|
|
54
58
|
# response data.
|
|
@@ -107,8 +111,19 @@ class AIOHTTPClient(AsyncHTTPClient):
|
|
|
107
111
|
|
|
108
112
|
request_config = request_config or HTTPRequestConfiguration()
|
|
109
113
|
|
|
114
|
+
# aiohttp chunks a body itself when asked with chunked=True, and rejects a
|
|
115
|
+
# Transfer-Encoding: chunked header, so the header becomes that flag.
|
|
116
|
+
chunked = "transfer-encoding" in request.fields and [
|
|
117
|
+
value.strip().lower()
|
|
118
|
+
for value in request.fields["transfer-encoding"].values
|
|
119
|
+
] == ["chunked"]
|
|
120
|
+
# aiohttp can't send request trailers, and a trailer is never a header.
|
|
110
121
|
headers_list = list(
|
|
111
|
-
chain.from_iterable(
|
|
122
|
+
chain.from_iterable(
|
|
123
|
+
fld.as_tuples()
|
|
124
|
+
for fld in request.fields.get_by_type("header")
|
|
125
|
+
if not (chunked and fld.name.lower() == "transfer-encoding")
|
|
126
|
+
)
|
|
112
127
|
)
|
|
113
128
|
|
|
114
129
|
body: StreamingBlob | None = request.body
|
|
@@ -124,7 +139,8 @@ class AIOHTTPClient(AsyncHTTPClient):
|
|
|
124
139
|
method=request.method,
|
|
125
140
|
url=self._serialize_uri(request.destination),
|
|
126
141
|
headers=headers_list,
|
|
127
|
-
data=body,
|
|
142
|
+
data=_upload_data(request.body, body),
|
|
143
|
+
chunked=True if chunked else None,
|
|
128
144
|
allow_redirects=False,
|
|
129
145
|
skip_auto_headers={"Content-Type"},
|
|
130
146
|
)
|
|
@@ -154,7 +170,7 @@ class AIOHTTPClient(AsyncHTTPClient):
|
|
|
154
170
|
await body.seek(position)
|
|
155
171
|
return None if position == end else body
|
|
156
172
|
|
|
157
|
-
def _serialize_uri(self, uri: URI) -> yarl.URL:
|
|
173
|
+
def _serialize_uri(self, uri: URI) -> "yarl.URL":
|
|
158
174
|
"""Serialize the URI, preserving the already-encoded query string as-is."""
|
|
159
175
|
return yarl.URL.build(
|
|
160
176
|
scheme=uri.scheme or "",
|
|
@@ -179,6 +195,21 @@ class AIOHTTPClient(AsyncHTTPClient):
|
|
|
179
195
|
)
|
|
180
196
|
|
|
181
197
|
|
|
198
|
+
def _upload_data(source: Any, body: AsyncBytesReader | None) -> Any:
|
|
199
|
+
"""What aiohttp uploads for a request body, which it iterates.
|
|
200
|
+
|
|
201
|
+
A reader is read in 128 KiB chunks, which uploads faster than the 64 KiB that
|
|
202
|
+
AsyncBytesReader iterates in by default. An async iterable that isn't a reader
|
|
203
|
+
is passed through, so each item is sent as soon as it's produced rather than
|
|
204
|
+
held until a chunk fills.
|
|
205
|
+
"""
|
|
206
|
+
if body is None:
|
|
207
|
+
return None
|
|
208
|
+
if not hasattr(source, "read") and isinstance(source, AsyncIterable):
|
|
209
|
+
return cast(AsyncIterable[bytes], source)
|
|
210
|
+
return body.iter_chunks(_UPLOAD_CHUNK_SIZE)
|
|
211
|
+
|
|
212
|
+
|
|
182
213
|
def _assert_aiohttp() -> None:
|
|
183
214
|
if not HAS_AIOHTTP:
|
|
184
215
|
raise MissingDependencyError(
|
|
@@ -47,6 +47,7 @@ from . import interfaces as http_aio_interfaces
|
|
|
47
47
|
|
|
48
48
|
__all__ = [
|
|
49
49
|
"DEFAULT_READ_BUFFER_SIZE",
|
|
50
|
+
"HAS_CRT",
|
|
50
51
|
"AWSCRTHTTPClient",
|
|
51
52
|
"AWSCRTHTTPClientConfig",
|
|
52
53
|
"AsyncAWSCRTHTTPResponse",
|
|
@@ -56,13 +57,7 @@ __all__ = [
|
|
|
56
57
|
|
|
57
58
|
# Default buffer size for reading from streams (8 KB)
|
|
58
59
|
DEFAULT_READ_BUFFER_SIZE = 8192
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _assert_crt() -> None:
|
|
62
|
-
if not HAS_CRT:
|
|
63
|
-
raise MissingDependencyError(
|
|
64
|
-
"Attempted to use awscrt component, but awscrt is not installed."
|
|
65
|
-
)
|
|
60
|
+
"""Default number of bytes read from a request body at a time."""
|
|
66
61
|
|
|
67
62
|
|
|
68
63
|
class _AWSCRTEventLoop:
|
|
@@ -70,10 +65,12 @@ class _AWSCRTEventLoop:
|
|
|
70
65
|
_assert_crt()
|
|
71
66
|
event_loop_group = crt_io.EventLoopGroup(1)
|
|
72
67
|
host_resolver = crt_io.DefaultHostResolver(event_loop_group)
|
|
73
|
-
self.
|
|
68
|
+
self._bootstrap = crt_io.ClientBootstrap(event_loop_group, host_resolver)
|
|
74
69
|
|
|
75
70
|
|
|
76
71
|
class AsyncAWSCRTHTTPResponse(http_aio_interfaces.HTTPResponse):
|
|
72
|
+
"""HTTP response from the AWS CRT HTTP client."""
|
|
73
|
+
|
|
77
74
|
def __init__(
|
|
78
75
|
self,
|
|
79
76
|
*,
|
|
@@ -113,6 +110,7 @@ class AsyncAWSCRTHTTPResponse(http_aio_interfaces.HTTPResponse):
|
|
|
113
110
|
return None
|
|
114
111
|
|
|
115
112
|
async def chunks(self) -> AsyncGenerator[bytes, None]:
|
|
113
|
+
"""Yield the response body chunks as they arrive."""
|
|
116
114
|
while True:
|
|
117
115
|
chunk = await self._stream.get_next_response_chunk()
|
|
118
116
|
if chunk:
|
|
@@ -143,45 +141,30 @@ class AWSCRTHTTPClientConfig(http_interfaces.HTTPClientConfiguration):
|
|
|
143
141
|
_assert_crt()
|
|
144
142
|
|
|
145
143
|
|
|
146
|
-
class _CRTTimeoutError(
|
|
144
|
+
class _CRTTimeoutError(SmithyHTTPError):
|
|
147
145
|
"""Internal wrapper for CRT timeout errors."""
|
|
148
146
|
|
|
149
147
|
|
|
150
148
|
class AWSCRTHTTPClient(http_aio_interfaces.AsyncHTTPClient):
|
|
149
|
+
"""Asynchronous HTTP client backed by the AWS CRT."""
|
|
150
|
+
|
|
151
151
|
_HTTP_PORT = 80
|
|
152
152
|
_HTTPS_PORT = 443
|
|
153
153
|
_TIMEOUT_ERROR_NAMES = frozenset(["AWS_IO_SOCKET_TIMEOUT", "AWS_IO_SOCKET_CLOSED"])
|
|
154
154
|
|
|
155
|
-
TIMEOUT_EXCEPTIONS = (_CRTTimeoutError,)
|
|
155
|
+
TIMEOUT_EXCEPTIONS: tuple[type[Exception], ...] = (_CRTTimeoutError,)
|
|
156
156
|
|
|
157
157
|
# CRT can stream request and response data concurrently when the connection
|
|
158
158
|
# negotiates HTTP/2.
|
|
159
159
|
SUPPORTS_DUPLEX_STREAMING = True
|
|
160
160
|
|
|
161
|
-
def __init__(
|
|
162
|
-
self,
|
|
163
|
-
eventloop: _AWSCRTEventLoop | None = None,
|
|
164
|
-
client_config: AWSCRTHTTPClientConfig | None = None,
|
|
165
|
-
) -> None:
|
|
161
|
+
def __init__(self, client_config: AWSCRTHTTPClientConfig | None = None) -> None:
|
|
166
162
|
"""
|
|
167
163
|
:param client_config: Configuration that applies to all requests made with this
|
|
168
164
|
client.
|
|
169
165
|
"""
|
|
170
166
|
_assert_crt()
|
|
171
|
-
self.
|
|
172
|
-
if eventloop is None:
|
|
173
|
-
eventloop = _AWSCRTEventLoop()
|
|
174
|
-
self._eventloop = eventloop
|
|
175
|
-
self._client_bootstrap = self._eventloop.bootstrap
|
|
176
|
-
tls_ctx_options = crt_io.TlsContextOptions()
|
|
177
|
-
if self._config.trust_store_path is not None:
|
|
178
|
-
tls_ctx_options.override_default_trust_store_from_path(
|
|
179
|
-
None, self._config.trust_store_path
|
|
180
|
-
)
|
|
181
|
-
self._tls_ctx = crt_io.ClientTlsContext(tls_ctx_options)
|
|
182
|
-
self._socket_options = crt_io.SocketOptions()
|
|
183
|
-
self._connections: ConnectionPoolDict = {}
|
|
184
|
-
self._closed = False
|
|
167
|
+
self._init(_AWSCRTEventLoop(), client_config)
|
|
185
168
|
|
|
186
169
|
async def __aenter__(self) -> Self:
|
|
187
170
|
if self._closed:
|
|
@@ -192,10 +175,10 @@ class AWSCRTHTTPClient(http_aio_interfaces.AsyncHTTPClient):
|
|
|
192
175
|
await self.close()
|
|
193
176
|
|
|
194
177
|
def __deepcopy__(self, memo: Any) -> "AWSCRTHTTPClient":
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
178
|
+
# Config is copied per operation, so copies share the event loop's thread.
|
|
179
|
+
copied = AWSCRTHTTPClient.__new__(AWSCRTHTTPClient)
|
|
180
|
+
copied._init(self._eventloop, deepcopy(self._config))
|
|
181
|
+
return copied
|
|
199
182
|
|
|
200
183
|
async def send(
|
|
201
184
|
self,
|
|
@@ -253,6 +236,24 @@ class AWSCRTHTTPClient(http_aio_interfaces.AsyncHTTPClient):
|
|
|
253
236
|
return_exceptions=True,
|
|
254
237
|
)
|
|
255
238
|
|
|
239
|
+
def _init(
|
|
240
|
+
self,
|
|
241
|
+
eventloop: _AWSCRTEventLoop,
|
|
242
|
+
client_config: AWSCRTHTTPClientConfig | None,
|
|
243
|
+
) -> None:
|
|
244
|
+
self._config = client_config or AWSCRTHTTPClientConfig()
|
|
245
|
+
self._eventloop = eventloop
|
|
246
|
+
self._client_bootstrap = eventloop._bootstrap # pyright: ignore[reportPrivateUsage]
|
|
247
|
+
tls_ctx_options = crt_io.TlsContextOptions()
|
|
248
|
+
if self._config.trust_store_path is not None:
|
|
249
|
+
tls_ctx_options.override_default_trust_store_from_path(
|
|
250
|
+
None, self._config.trust_store_path
|
|
251
|
+
)
|
|
252
|
+
self._tls_ctx = crt_io.ClientTlsContext(tls_ctx_options)
|
|
253
|
+
self._socket_options = crt_io.SocketOptions()
|
|
254
|
+
self._connections: ConnectionPoolDict = {}
|
|
255
|
+
self._closed = False
|
|
256
|
+
|
|
256
257
|
async def _await_response(
|
|
257
258
|
self, stream: "AIOHttpClientStreamUnified"
|
|
258
259
|
) -> AsyncAWSCRTHTTPResponse:
|
|
@@ -355,8 +356,7 @@ class AWSCRTHTTPClient(http_aio_interfaces.AsyncHTTPClient):
|
|
|
355
356
|
for fld in request.fields.entries.values():
|
|
356
357
|
if fld.kind != "header":
|
|
357
358
|
continue
|
|
358
|
-
for val in fld.values
|
|
359
|
-
headers_list.append((fld.name, val))
|
|
359
|
+
headers_list.extend((fld.name, val) for val in fld.values)
|
|
360
360
|
|
|
361
361
|
path = self._render_path(request.destination)
|
|
362
362
|
headers = crt_http.HttpHeaders(headers_list)
|
|
@@ -416,3 +416,10 @@ class AWSCRTHTTPClient(http_aio_interfaces.AsyncHTTPClient):
|
|
|
416
416
|
yield bytes(chunk)
|
|
417
417
|
else:
|
|
418
418
|
yield chunk
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _assert_crt() -> None:
|
|
422
|
+
if not HAS_CRT:
|
|
423
|
+
raise MissingDependencyError(
|
|
424
|
+
"Attempted to use awscrt component, but awscrt is not installed."
|
|
425
|
+
)
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
from smithy_core.aio.interfaces.identity import AsyncIdentityResolver
|
|
4
4
|
|
|
5
|
-
from ...identity
|
|
6
|
-
|
|
7
|
-
APIKeyIdentityProperties,
|
|
8
|
-
APIKeyIdentityResolver,
|
|
9
|
-
)
|
|
5
|
+
from ...identity import APIKeyIdentity, APIKeyIdentityProperties
|
|
6
|
+
from ...identity.apikey import APIKeyIdentityResolver
|
|
10
7
|
|
|
11
8
|
__all__ = [
|
|
12
9
|
"APIKeyIdentity",
|
|
@@ -36,12 +36,14 @@ from ..protocols import (
|
|
|
36
36
|
HttpClientProtocol,
|
|
37
37
|
RpcClientProtocol,
|
|
38
38
|
RpcV2CborClientProtocol,
|
|
39
|
+
RpcV2JsonClientProtocol,
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
__all__ = [
|
|
42
43
|
"AsyncHttpEventStreamMixin",
|
|
43
44
|
"AsyncRpcEventStreamMixin",
|
|
44
45
|
"AsyncRpcV2CborClientProtocol",
|
|
46
|
+
"AsyncRpcV2JsonClientProtocol",
|
|
45
47
|
]
|
|
46
48
|
|
|
47
49
|
try:
|
|
@@ -138,7 +140,7 @@ class AsyncHttpEventStreamMixin(
|
|
|
138
140
|
auth_scheme: AsyncAuthScheme[Any, Any, Any, Any] | None = None,
|
|
139
141
|
) -> AsyncEventPublisher[Event]:
|
|
140
142
|
_assert_event_stream()
|
|
141
|
-
signing_config: SigningConfig[Any] | None = None
|
|
143
|
+
signing_config: SigningConfig[Any, Any, Any, Any] | None = None
|
|
142
144
|
if auth_scheme is not None:
|
|
143
145
|
event_signer = auth_scheme.event_signer(request=request)
|
|
144
146
|
if event_signer is not None:
|
|
@@ -292,6 +294,10 @@ class AsyncRpcV2CborClientProtocol(AsyncRpcEventStreamMixin, RpcV2CborClientProt
|
|
|
292
294
|
"""An implementation of the smithy.protocols#rpcv2Cbor protocol."""
|
|
293
295
|
|
|
294
296
|
|
|
297
|
+
class AsyncRpcV2JsonClientProtocol(AsyncRpcEventStreamMixin, RpcV2JsonClientProtocol):
|
|
298
|
+
"""An implementation of the smithy.protocols#rpcv2Json protocol."""
|
|
299
|
+
|
|
300
|
+
|
|
295
301
|
def _assert_event_stream() -> None:
|
|
296
302
|
if not _HAS_EVENT_STREAM:
|
|
297
303
|
raise MissingDependencyError(
|
|
@@ -38,4 +38,6 @@ class AsyncMockHTTPClient(MockHTTPClientBase, AsyncHTTPClient):
|
|
|
38
38
|
:raises MockHTTPClientError: If no responses are queued.
|
|
39
39
|
"""
|
|
40
40
|
status, fields, body = self._pop_response(request)
|
|
41
|
-
|
|
41
|
+
if isinstance(body, bytes | bytearray):
|
|
42
|
+
body = list_async([bytes(body)])
|
|
43
|
+
return HTTPResponse(status=status, fields=fields, body=body)
|
|
@@ -7,6 +7,7 @@ from smithy_core.exceptions import SmithyIdentityError
|
|
|
7
7
|
from smithy_core.interfaces import TypedProperties as _TypedProperties
|
|
8
8
|
from smithy_core.interfaces.auth import AuthScheme, Signer
|
|
9
9
|
from smithy_core.interfaces.identity import IdentityResolver
|
|
10
|
+
from smithy_core.shapes import ShapeID
|
|
10
11
|
from smithy_core.traits import APIKeyLocation, HTTPAPIKeyAuthTrait
|
|
11
12
|
|
|
12
13
|
from smithy_http.interfaces import HTTPRequest
|
|
@@ -77,7 +78,7 @@ class APIKeySigner(Signer[HTTPRequest, APIKeyIdentity, Any]):
|
|
|
77
78
|
class _APIKeyAuthSchemeBase:
|
|
78
79
|
"""API key auth scheme logic shared by the sync and async schemes."""
|
|
79
80
|
|
|
80
|
-
scheme_id = HTTPAPIKeyAuthTrait.id
|
|
81
|
+
scheme_id: ShapeID = HTTPAPIKeyAuthTrait.id
|
|
81
82
|
|
|
82
83
|
def __init__(
|
|
83
84
|
self, *, name: str, location: APIKeyLocation, scheme: str | None = None
|
|
@@ -8,7 +8,6 @@ from smithy_core.shapes import ShapeType
|
|
|
8
8
|
from smithy_core.traits import (
|
|
9
9
|
ErrorFault,
|
|
10
10
|
ErrorTrait,
|
|
11
|
-
HostLabelTrait,
|
|
12
11
|
HTTPErrorTrait,
|
|
13
12
|
HTTPHeaderTrait,
|
|
14
13
|
HTTPLabelTrait,
|
|
@@ -50,9 +49,6 @@ class Binding(Enum):
|
|
|
50
49
|
QUERY_PARAMS = 7
|
|
51
50
|
"""Indicates the member is bound to the query string as multiple key-value pairs."""
|
|
52
51
|
|
|
53
|
-
HOST = 8
|
|
54
|
-
"""Indicates the member is bound to a prefix to the host AND to the body."""
|
|
55
|
-
|
|
56
52
|
|
|
57
53
|
@dataclass(init=False)
|
|
58
54
|
class _BindingMatcher:
|
|
@@ -86,9 +82,7 @@ class _BindingMatcher:
|
|
|
86
82
|
for member in members:
|
|
87
83
|
binding = self._do_match(member)
|
|
88
84
|
self.bindings[member.expect_member_index()] = binding
|
|
89
|
-
found_body =
|
|
90
|
-
found_body or binding is Binding.BODY or binding is Binding.HOST
|
|
91
|
-
)
|
|
85
|
+
found_body = found_body or binding is Binding.BODY
|
|
92
86
|
if binding is Binding.PAYLOAD:
|
|
93
87
|
found_payload = True
|
|
94
88
|
self.payload_member = member
|
|
@@ -140,8 +134,6 @@ class RequestBindingMatcher(_BindingMatcher):
|
|
|
140
134
|
return Binding.PREFIX_HEADERS
|
|
141
135
|
if HTTPPayloadTrait.id in member.traits:
|
|
142
136
|
return Binding.PAYLOAD
|
|
143
|
-
if HostLabelTrait.id in member.traits:
|
|
144
|
-
return Binding.HOST
|
|
145
137
|
return Binding.BODY
|
|
146
138
|
|
|
147
139
|
|
|
@@ -35,7 +35,14 @@ if TYPE_CHECKING:
|
|
|
35
35
|
from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
__all__ = [
|
|
38
|
+
__all__ = [
|
|
39
|
+
"HTTPHeaderDeserializer",
|
|
40
|
+
"HTTPHeaderListDeserializer",
|
|
41
|
+
"HTTPHeaderMapDeserializer",
|
|
42
|
+
"HTTPResponseCodeDeserializer",
|
|
43
|
+
"HTTPResponseDeserializer",
|
|
44
|
+
"RawPayloadDeserializer",
|
|
45
|
+
]
|
|
39
46
|
|
|
40
47
|
_PREFIX_HEADER_OMISSIONS = frozenset(
|
|
41
48
|
{
|
|
@@ -27,6 +27,8 @@ class HTTPEndpoint(Endpoint):
|
|
|
27
27
|
headers: interfaces.Fields | None = None,
|
|
28
28
|
) -> None:
|
|
29
29
|
self.uri = uri
|
|
30
|
-
self.properties =
|
|
30
|
+
self.properties: _TypedProperties = (
|
|
31
|
+
properties if properties is not None else TypedProperties()
|
|
32
|
+
)
|
|
31
33
|
headers = headers if headers is not None else Fields()
|
|
32
34
|
self.properties[HEADERS] = headers
|