smithy-http 0.7.0__tar.gz → 0.9.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 (58) hide show
  1. smithy_http-0.9.0/.changes/0.8.0.json +92 -0
  2. smithy_http-0.9.0/.changes/0.9.0.json +20 -0
  3. {smithy_http-0.7.0 → smithy_http-0.9.0}/CHANGELOG.md +42 -0
  4. {smithy_http-0.7.0 → smithy_http-0.9.0}/PKG-INFO +8 -4
  5. {smithy_http-0.7.0 → smithy_http-0.9.0}/pyproject.toml +9 -3
  6. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/__init__.py +9 -6
  7. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/aiohttp.py +39 -8
  8. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/crt.py +42 -35
  9. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/identity/apikey.py +2 -5
  10. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/protocols.py +7 -1
  11. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/testing.py +3 -1
  12. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/auth/apikey.py +2 -1
  13. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/bindings.py +1 -9
  14. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/deserializers.py +8 -1
  15. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/endpoints.py +3 -1
  16. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/identity/__init__.py +2 -0
  17. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/interceptors/user_agent.py +1 -0
  18. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/interfaces/__init__.py +1 -1
  19. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/plugins/__init__.py +1 -0
  20. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/protocols.py +216 -10
  21. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/schema_extensions.py +5 -24
  22. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/serializers.py +30 -83
  23. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/testing/mockhttp.py +13 -7
  24. smithy_http-0.9.0/src/smithy_http/urllib3.py +266 -0
  25. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/user_agent.py +3 -1
  26. smithy_http-0.7.0/src/smithy_http/urllib3.py +0 -146
  27. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.0.1.json +0 -0
  28. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.1.0.json +0 -0
  29. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.2.0.json +0 -0
  30. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.2.1.json +0 -0
  31. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.3.0.json +0 -0
  32. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.3.1.json +0 -0
  33. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.4.0.json +0 -0
  34. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.4.1.json +0 -0
  35. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.4.2.json +0 -0
  36. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.4.3.json +0 -0
  37. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.4.4.json +0 -0
  38. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.5.0.json +0 -0
  39. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.6.0.json +0 -0
  40. {smithy_http-0.7.0 → smithy_http-0.9.0}/.changes/0.7.0.json +0 -0
  41. {smithy_http-0.7.0 → smithy_http-0.9.0}/.gitignore +0 -0
  42. {smithy_http-0.7.0 → smithy_http-0.9.0}/NOTICE +0 -0
  43. {smithy_http-0.7.0 → smithy_http-0.9.0}/README.md +0 -0
  44. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/__init__.py +0 -0
  45. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/auth/__init__.py +0 -0
  46. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/auth/apikey.py +0 -0
  47. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/identity/__init__.py +0 -0
  48. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/interfaces/__init__.py +0 -0
  49. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/aio/restjson.py +0 -0
  50. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/auth/__init__.py +0 -0
  51. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/exceptions.py +0 -0
  52. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/identity/apikey.py +0 -0
  53. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/interceptors/__init__.py +0 -0
  54. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/py.typed +0 -0
  55. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/restjson.py +0 -0
  56. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/testing/__init__.py +0 -0
  57. {smithy_http-0.7.0 → smithy_http-0.9.0}/src/smithy_http/testing/utils.py +0 -0
  58. {smithy_http-0.7.0 → smithy_http-0.9.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
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "changes": [
3
+ {
4
+ "type": "dependency",
5
+ "description": "Bump `smithy-core` from `~=0.11.0` to `~=0.12.0`."
6
+ },
7
+ {
8
+ "type": "dependency",
9
+ "description": "Bump `smithy-cbor` from `~=0.3.0` to `~=0.4.0`."
10
+ },
11
+ {
12
+ "type": "dependency",
13
+ "description": "Bump `smithy-json` from `~=0.5.0` to `~=0.6.0`."
14
+ },
15
+ {
16
+ "type": "dependency",
17
+ "description": "Bump `smithy-aws-event-stream` from `~=0.5.0` to `~=0.6.0`."
18
+ }
19
+ ]
20
+ }
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.9.0
4
+
5
+ ### Dependencies
6
+ * Bump `smithy-core` from `~=0.11.0` to `~=0.12.0`.
7
+ * Bump `smithy-cbor` from `~=0.3.0` to `~=0.4.0`.
8
+ * Bump `smithy-json` from `~=0.5.0` to `~=0.6.0`.
9
+ * Bump `smithy-aws-event-stream` from `~=0.5.0` to `~=0.6.0`.
10
+
11
+ ## v0.8.0
12
+
13
+ ### Breaking Changes
14
+ * Removed the eventloop parameter from AWSCRTHTTPClient.
15
+ * Removed `HostPrefixSerializer` and `Binding.HOST`. `@hostLabel` no longer changes where a member is serialized and smithy-core now applies host prefixes.
16
+
17
+ ### Features
18
+ * Added support for the `aws.protocols#awsQueryCompatible` trait to `RpcV2CborClientProtocol` and `RpcV2JsonClientProtocol`.
19
+ * Added `RpcV2JsonClientProtocol` and `AsyncRpcV2JsonClientProtocol`, implementations of the `smithy.protocols#rpcv2Json` protocol, and a `json` extra that installs `smithy-json`.
20
+
21
+ ### Enhancements
22
+ * Documented HTTPQueryMapValueSerializer.
23
+ * Completed type annotations and __all__ declarations for the public API.
24
+ * Allow MockHTTPClient and AsyncMockHTTPClient responses to have stream bodies.
25
+ * Imported symbols from their defining modules.
26
+
27
+ ### Bug fixes
28
+ * Iterating a `Urllib3HTTPClient` response body now yields 64 KiB chunks instead of 1 KiB ones.
29
+ * 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.
30
+ * Declared smithy-aws-event-stream as the eventstream extra, bounded yarl, and raised MissingDependencyError when the aiohttp extra is not installed.
31
+ * `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.
32
+ * Derived the CRT timeout error from SmithyHTTPError.
33
+ * `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`.
34
+ * `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.
35
+ * `AIOHTTPClient` now sends requests that set `Transfer-Encoding: chunked` using aiohttp's chunked uploads. Previously aiohttp rejected the header with a `ValueError`.
36
+ * 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.
37
+ * 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.
38
+
39
+ ### Dependencies
40
+ * Bump `smithy-core` from `~=0.10.0` to `~=0.11.0`.
41
+ * Bump `smithy-cbor` from `~=0.2.0` to `~=0.3.0`.
42
+ * Bump `smithy-json` from `~=0.4.0` to `~=0.5.0`.
43
+ * Bump `smithy-aws-event-stream` from `~=0.4.0` to `~=0.5.0`.
44
+
3
45
  ## v0.7.0
4
46
 
5
47
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: smithy-http
3
- Version: 0.7.0
3
+ Version: 0.9.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.12.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.2.0; extra == 'cbor'
36
+ Requires-Dist: smithy-cbor~=0.4.0; extra == 'cbor'
37
+ Provides-Extra: eventstream
38
+ Requires-Dist: smithy-aws-event-stream~=0.6.0; extra == 'eventstream'
39
+ Provides-Extra: json
40
+ Requires-Dist: smithy-json~=0.6.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.12.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.2.0",
44
+ "smithy-cbor~=0.4.0",
45
+ ]
46
+ json = [
47
+ "smithy-json~=0.6.0",
48
+ ]
49
+ eventstream = [
50
+ "smithy-aws-event-stream~=0.6.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.7.0"
25
+ __version__ = "0.9.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 a comma and a
92
- space. For such multi-valued ``Field``s, any values that already contain
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 ", ".join(quote_and_escape_field_value(val) for val in self.values)
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(fld.as_tuples() for fld in request.fields)
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.bootstrap = crt_io.ClientBootstrap(event_loop_group, host_resolver)
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(Exception):
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._config = client_config or AWSCRTHTTPClientConfig()
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
- return AWSCRTHTTPClient(
196
- eventloop=self._eventloop,
197
- client_config=deepcopy(self._config),
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.apikey import (
6
- APIKeyIdentity,
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
- return HTTPResponse(status=status, fields=fields, body=list_async([body]))
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