smithy-http 0.6.0__tar.gz → 0.7.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 (61) hide show
  1. smithy_http-0.7.0/.changes/0.7.0.json +80 -0
  2. {smithy_http-0.6.0 → smithy_http-0.7.0}/.gitignore +3 -0
  3. smithy_http-0.7.0/CHANGELOG.md +128 -0
  4. {smithy_http-0.6.0 → smithy_http-0.7.0}/PKG-INFO +12 -6
  5. {smithy_http-0.6.0 → smithy_http-0.7.0}/README.md +7 -4
  6. {smithy_http-0.6.0 → smithy_http-0.7.0}/pyproject.toml +5 -1
  7. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/__init__.py +94 -41
  8. smithy_http-0.7.0/src/smithy_http/aio/__init__.py +5 -0
  9. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/aio/aiohttp.py +50 -51
  10. {smithy_http-0.6.0/src/smithy_http/interceptors → smithy_http-0.7.0/src/smithy_http/aio/auth}/__init__.py +1 -0
  11. smithy_http-0.7.0/src/smithy_http/aio/auth/apikey.py +57 -0
  12. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/aio/crt.py +78 -61
  13. {smithy_http-0.6.0/src/smithy_http/aio/auth → smithy_http-0.7.0/src/smithy_http/aio/identity}/__init__.py +1 -0
  14. smithy_http-0.7.0/src/smithy_http/aio/identity/apikey.py +28 -0
  15. smithy_http-0.7.0/src/smithy_http/aio/interfaces/__init__.py +51 -0
  16. smithy_http-0.7.0/src/smithy_http/aio/protocols.py +328 -0
  17. smithy_http-0.7.0/src/smithy_http/aio/restjson.py +24 -0
  18. smithy_http-0.7.0/src/smithy_http/aio/testing.py +41 -0
  19. smithy_http-0.7.0/src/smithy_http/auth/__init__.py +2 -0
  20. {smithy_http-0.6.0/src/smithy_http/aio → smithy_http-0.7.0/src/smithy_http}/auth/apikey.py +32 -30
  21. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/bindings.py +2 -0
  22. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/deserializers.py +60 -66
  23. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/endpoints.py +2 -0
  24. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/exceptions.py +2 -0
  25. smithy_http-0.7.0/src/smithy_http/identity/__init__.py +21 -0
  26. smithy_http-0.7.0/src/smithy_http/identity/apikey.py +25 -0
  27. {smithy_http-0.6.0/src/smithy_http/aio/identity → smithy_http-0.7.0/src/smithy_http/interceptors}/__init__.py +1 -0
  28. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/interceptors/user_agent.py +3 -1
  29. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/interfaces/__init__.py +112 -5
  30. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/plugins/__init__.py +2 -0
  31. {smithy_http-0.6.0/src/smithy_http/aio → smithy_http-0.7.0/src/smithy_http}/protocols.py +297 -81
  32. smithy_http-0.7.0/src/smithy_http/restjson.py +89 -0
  33. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/schema_extensions.py +12 -0
  34. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/serializers.py +206 -149
  35. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/testing/mockhttp.py +53 -52
  36. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/testing/utils.py +3 -2
  37. smithy_http-0.7.0/src/smithy_http/urllib3.py +146 -0
  38. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/user_agent.py +7 -0
  39. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/utils.py +2 -0
  40. smithy_http-0.6.0/CHANGELOG.md +0 -97
  41. smithy_http-0.6.0/src/smithy_http/aio/__init__.py +0 -53
  42. smithy_http-0.6.0/src/smithy_http/aio/identity/apikey.py +0 -38
  43. smithy_http-0.6.0/src/smithy_http/aio/interfaces/__init__.py +0 -110
  44. smithy_http-0.6.0/src/smithy_http/aio/restjson.py +0 -60
  45. smithy_http-0.6.0/src/smithy_http/restjson.py +0 -29
  46. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.0.1.json +0 -0
  47. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.1.0.json +0 -0
  48. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.2.0.json +0 -0
  49. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.2.1.json +0 -0
  50. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.3.0.json +0 -0
  51. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.3.1.json +0 -0
  52. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.4.0.json +0 -0
  53. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.4.1.json +0 -0
  54. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.4.2.json +0 -0
  55. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.4.3.json +0 -0
  56. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.4.4.json +0 -0
  57. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.5.0.json +0 -0
  58. {smithy_http-0.6.0 → smithy_http-0.7.0}/.changes/0.6.0.json +0 -0
  59. {smithy_http-0.6.0 → smithy_http-0.7.0}/NOTICE +0 -0
  60. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/py.typed +0 -0
  61. {smithy_http-0.6.0 → smithy_http-0.7.0}/src/smithy_http/testing/__init__.py +0 -0
@@ -0,0 +1,80 @@
1
+ {
2
+ "changes": [
3
+ {
4
+ "type": "breaking",
5
+ "description": "Replaced the async protocols with event stream mixins composed onto the sync protocols. `AsyncHttpClientProtocol`, `AsyncHttpBindingClientProtocol` and `AsyncRpcClientProtocol` are removed; use `AsyncHttpEventStreamMixin` or `AsyncRpcEventStreamMixin` with a protocol from `smithy_http.protocols`. `AsyncRpcV2CborClientProtocol` is kept as such a composition."
6
+ },
7
+ {
8
+ "type": "breaking",
9
+ "description": "Moved `HTTPRequest` and `HTTPResponse` to `smithy_http`, and their protocols and `HTTPErrorIdentifier` to `smithy_http.interfaces`. Removed `consume_body_async`."
10
+ },
11
+ {
12
+ "type": "breaking",
13
+ "description": "Removed `AsyncAPIKeySigner`; `AsyncAPIKeyAuthScheme` returns the sync `APIKeySigner`. Renamed `smithy_http.aio.restjson.parse_rest_json_error_info` to `parse_rest_json_error_info_async`."
14
+ },
15
+ {
16
+ "type": "breaking",
17
+ "description": "Serialized request bodies are now synchronous for both sync and async clients. `HTTPBindingSerializer`'s `async_body` parameter is replaced by `event_stream_body`, and `RawPayloadDeserializer` returns streaming payloads as the transport provides them."
18
+ },
19
+ {
20
+ "type": "breaking",
21
+ "description": "Moved async implementations to `smithy_http.aio` and added the `Async` prefix to their names, for example `AsyncHTTPClient`, `AsyncHttpClientProtocol`, `AsyncHttpBindingClientProtocol`, `AsyncRpcV2CborClientProtocol` and `AsyncAPIKeyAuthScheme`. The unprefixed names are reserved for sync implementations. Update imports to the `aio` paths."
22
+ },
23
+ {
24
+ "type": "breaking",
25
+ "description": "Declared explicit `__all__` exports on all public modules. Names that were previously importable only incidentally (via `from module import *` or as un-annotated module attributes) are no longer part of the public API; import public symbols by name from their documented module."
26
+ },
27
+ {
28
+ "type": "breaking",
29
+ "description": "`smithy_http.testing.MockHTTPClient` is now synchronous. Async clients use `smithy_http.aio.testing.AsyncMockHTTPClient`."
30
+ },
31
+ {
32
+ "type": "breaking",
33
+ "description": "Renamed `AWSCRTHTTPResponse` to `AsyncAWSCRTHTTPResponse`, and `smithy_http.aio.auth.apikey.APIKeyResolverConfig` to `AsyncAPIKeyResolverConfig`."
34
+ },
35
+ {
36
+ "type": "feature",
37
+ "description": "Added synchronous HTTP support: `smithy_http.urllib3.Urllib3HTTPClient`, the sync protocols in `smithy_http.protocols`, `smithy_http.restjson.parse_rest_json_error_info` and `smithy_http.identity.apikey.APIKeyIdentityResolver`."
38
+ },
39
+ {
40
+ "type": "feature",
41
+ "description": "Added `connect_timeout` and `read_timeout` to `Urllib3HTTPClientConfig`, both defaulting to 60 seconds, so sync requests no longer wait indefinitely. A request's `HTTPRequestConfiguration.read_timeout` takes precedence."
42
+ },
43
+ {
44
+ "type": "feature",
45
+ "description": "Added `max_pool_connections` to `Urllib3HTTPClientConfig`, the connections kept for reuse per host, defaulting to 10 instead of urllib3's 1."
46
+ },
47
+ {
48
+ "type": "feature",
49
+ "description": "Added a RpcClientProtocol base class and client-side event stream support for the rpcv2Cbor protocol."
50
+ },
51
+ {
52
+ "type": "enhancement",
53
+ "description": "AWSCRTHTTPClient now negotiates HTTP/2 per request for operations that require it (event streams), advertising only HTTP/2 in ALPN and pooling those connections separately from HTTP/1.1 ones, so a single client serves both HTTP/1.1 operations and HTTP/2 event streams without configuration. The client-wide force_http_2 config remains as an override."
54
+ },
55
+ {
56
+ "type": "enhancement",
57
+ "description": "Added a trust_store_path option to AWSCRTHTTPClientConfig for overriding the TLS trust store with a custom CA bundle."
58
+ },
59
+ {
60
+ "type": "enhancement",
61
+ "description": "Added support for Python 3.15."
62
+ },
63
+ {
64
+ "type": "bugfix",
65
+ "description": "Fixed HTTP serializers sending seekable streaming payloads, such as files, without a Content-Length and sending a modeled Content-Length twice."
66
+ },
67
+ {
68
+ "type": "bugfix",
69
+ "description": "Fixed API key auth setting the request path to the URI password."
70
+ },
71
+ {
72
+ "type": "bugfix",
73
+ "description": "`APIKeyIdentity` no longer includes `api_key` in its repr, which keeps it out of debug logs."
74
+ },
75
+ {
76
+ "type": "dependency",
77
+ "description": "Bump `smithy-cbor` from `~=0.1.0` to `~=0.2.0`."
78
+ }
79
+ ]
80
+ }
@@ -34,3 +34,6 @@ htmlcov
34
34
  *.egg-info
35
35
  dist
36
36
  .venv
37
+
38
+ # make gen
39
+ /workspace/
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ ## v0.7.0
4
+
5
+ ### Breaking Changes
6
+ * Replaced the async protocols with event stream mixins composed onto the sync protocols. `AsyncHttpClientProtocol`, `AsyncHttpBindingClientProtocol` and `AsyncRpcClientProtocol` are removed; use `AsyncHttpEventStreamMixin` or `AsyncRpcEventStreamMixin` with a protocol from `smithy_http.protocols`. `AsyncRpcV2CborClientProtocol` is kept as such a composition.
7
+ * Moved `HTTPRequest` and `HTTPResponse` to `smithy_http`, and their protocols and `HTTPErrorIdentifier` to `smithy_http.interfaces`. Removed `consume_body_async`.
8
+ * Removed `AsyncAPIKeySigner`; `AsyncAPIKeyAuthScheme` returns the sync `APIKeySigner`. Renamed `smithy_http.aio.restjson.parse_rest_json_error_info` to `parse_rest_json_error_info_async`.
9
+ * Serialized request bodies are now synchronous for both sync and async clients. `HTTPBindingSerializer`'s `async_body` parameter is replaced by `event_stream_body`, and `RawPayloadDeserializer` returns streaming payloads as the transport provides them.
10
+ * Moved async implementations to `smithy_http.aio` and added the `Async` prefix to their names, for example `AsyncHTTPClient`, `AsyncHttpClientProtocol`, `AsyncHttpBindingClientProtocol`, `AsyncRpcV2CborClientProtocol` and `AsyncAPIKeyAuthScheme`. The unprefixed names are reserved for sync implementations. Update imports to the `aio` paths.
11
+ * Declared explicit `__all__` exports on all public modules. Names that were previously importable only incidentally (via `from module import *` or as un-annotated module attributes) are no longer part of the public API; import public symbols by name from their documented module.
12
+ * `smithy_http.testing.MockHTTPClient` is now synchronous. Async clients use `smithy_http.aio.testing.AsyncMockHTTPClient`.
13
+ * Renamed `AWSCRTHTTPResponse` to `AsyncAWSCRTHTTPResponse`, and `smithy_http.aio.auth.apikey.APIKeyResolverConfig` to `AsyncAPIKeyResolverConfig`.
14
+
15
+ ### Features
16
+ * Added synchronous HTTP support: `smithy_http.urllib3.Urllib3HTTPClient`, the sync protocols in `smithy_http.protocols`, `smithy_http.restjson.parse_rest_json_error_info` and `smithy_http.identity.apikey.APIKeyIdentityResolver`.
17
+ * Added `connect_timeout` and `read_timeout` to `Urllib3HTTPClientConfig`, both defaulting to 60 seconds, so sync requests no longer wait indefinitely. A request's `HTTPRequestConfiguration.read_timeout` takes precedence.
18
+ * Added `max_pool_connections` to `Urllib3HTTPClientConfig`, the connections kept for reuse per host, defaulting to 10 instead of urllib3's 1.
19
+ * Added a RpcClientProtocol base class and client-side event stream support for the rpcv2Cbor protocol.
20
+
21
+ ### Enhancements
22
+ * AWSCRTHTTPClient now negotiates HTTP/2 per request for operations that require it (event streams), advertising only HTTP/2 in ALPN and pooling those connections separately from HTTP/1.1 ones, so a single client serves both HTTP/1.1 operations and HTTP/2 event streams without configuration. The client-wide force_http_2 config remains as an override.
23
+ * Added a trust_store_path option to AWSCRTHTTPClientConfig for overriding the TLS trust store with a custom CA bundle.
24
+ * Added support for Python 3.15.
25
+
26
+ ### Bug fixes
27
+ * Fixed HTTP serializers sending seekable streaming payloads, such as files, without a Content-Length and sending a modeled Content-Length twice.
28
+ * Fixed API key auth setting the request path to the URI password.
29
+ * `APIKeyIdentity` no longer includes `api_key` in its repr, which keeps it out of debug logs.
30
+
31
+ ### Dependencies
32
+ * Bump `smithy-cbor` from `~=0.1.0` to `~=0.2.0`.
33
+
34
+ ## v0.6.0
35
+
36
+ ### Features
37
+ * `HttpClientProtocol` implements `extract_response_metadata()`, reporting the HTTP status code.
38
+ * Added a naive `RpcV2CborClientProtocol` for the `smithy.protocols#rpcv2Cbor` protocol (optional `cbor` extra).
39
+ * Added schema-cached HTTP binding plans for members, operations, payloads, and request and response dispatch.
40
+
41
+ ### Dependencies
42
+ * Bump `smithy-cbor` from `~=0.0.0` to `~=0.1.0`.
43
+
44
+ ## v0.5.0
45
+
46
+ ### Features
47
+ * Added deterministic connection-pool cleanup and async context manager support to HTTP clients.
48
+
49
+ ### Bug fixes
50
+ * Stream aiohttp response bodies incrementally to enable true output event streams.
51
+ * Fixed `AIOHTTPClient` to stop dropping valueless query parameters and automatically adding a `Content-Type` to bodyless POST, PUT, and PATCH requests.
52
+ * Fixed duplicate Content-Type headers when a member is bound to Content-Type.
53
+
54
+ ## v0.4.4
55
+
56
+ ### Enhancements
57
+ * Added a protocol hook for extracting server-provided retry delays from error responses.
58
+ * Declared `AWSCRTHTTPClient` as duplex (bidirectional) streaming capable via `SUPPORTS_DUPLEX_STREAMING`. `AIOHTTPClient` explicitly does not support duplex streaming.
59
+ * Disabled automatic redirects in `AIOHTTPClient` so that redirect responses are returned to the caller instead of being followed transparently.
60
+
61
+ ## v0.4.3
62
+
63
+ ### Enhancements
64
+ * Enabled HTTP binding protocols to resolve modeled response errors when wire identifiers do not exactly match registered shape IDs. ([#742](https://github.com/smithy-lang/smithy-python/pull/742))
65
+
66
+ ### Bug fixes
67
+ * Fixed `AIOHTTPClient` sending omitted request payloads with chunked transfer encoding.
68
+
69
+ ## v0.4.2
70
+
71
+ ### Bug fixes
72
+ * Disabled automatic response decompression in `AIOHTTPClient`.
73
+
74
+ ## v0.4.1
75
+
76
+ ### Bug fixes
77
+ * Fixed `AWSCRTHTTPClient` to send HTTP/1.1 request bodies via `body_stream` instead of the HTTP/2-only `request_body_generator`.
78
+
79
+ ## v0.4.0
80
+
81
+ ### Breaking Changes
82
+ * Replace `FieldPosition` enum values with string literals for `Field.kind`. Use "header" and "trailer" instead of `FieldPosition.HEADER` and `FieldPosition.TRAILER`.
83
+
84
+ ### Dependencies
85
+ * Upgrade CRT to 0.32.0 to fix bug where HTTP2 exceptions could hang indefinitely
86
+
87
+ ## v0.3.1
88
+
89
+ ### Bug fixes
90
+ * Fixed empty error messaging for CRT-based timeout errors.
91
+
92
+ ### Dependencies
93
+ * Removed strict pinning on `smithy-core` in favor of client managed versions.
94
+
95
+ ## v0.3.0
96
+
97
+ ### Features
98
+ * Added `MockHTTPClient` for testing SDK clients without making real HTTP requests.
99
+
100
+ ### Enhancements
101
+ * Added timeout error detection for HTTP clients
102
+
103
+ ### Dependencies
104
+ * Bump `smithy-core` from `~=0.1.0` to `~=0.2.0`.
105
+
106
+ ## v0.2.1
107
+
108
+ ### Bug fixes
109
+ * Add port to CRT HTTP client's host header.
110
+
111
+ ## v0.2.0
112
+
113
+ ### Breaking Changes
114
+ * 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`.
115
+
116
+ ## v0.1.0
117
+
118
+ ### Breaking Changes
119
+ * Removed identity and auth interfaces in favor of the transport-agnostic interfaces introduced in `smithy-core`.
120
+
121
+ ### Features
122
+ * Introduced schema-based serializers and deserializers for HTTP binding protocols.
123
+
124
+ ## v0.0.1
125
+
126
+ ### Features
127
+ * Added support for aiohttp and AWSCRT http clients.
128
+ * Added basic HTTP primitives and interfaces for Smithy clients.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: smithy-http
3
- Version: 0.6.0
3
+ Version: 0.7.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/
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
24
25
  Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
25
26
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
27
  Classifier: Topic :: Software Development :: Libraries
@@ -32,7 +33,9 @@ Requires-Dist: yarl; extra == 'aiohttp'
32
33
  Provides-Extra: awscrt
33
34
  Requires-Dist: awscrt~=0.32.0; extra == 'awscrt'
34
35
  Provides-Extra: cbor
35
- Requires-Dist: smithy-cbor~=0.1.0; extra == 'cbor'
36
+ Requires-Dist: smithy-cbor~=0.2.0; extra == 'cbor'
37
+ Provides-Extra: urllib3
38
+ Requires-Dist: urllib3<3.0,>=2.0; extra == 'urllib3'
36
39
  Description-Content-Type: text/markdown
37
40
 
38
41
  # smithy-http
@@ -47,11 +50,14 @@ The `smithy_http.testing` module provides shared utilities for testing HTTP func
47
50
 
48
51
  ### MockHTTPClient
49
52
 
50
- The `MockHTTPClient` allows you to test smithy-python clients without making actual network calls. It implements the `HTTPClient` interface and provides configurable responses for functional testing.
53
+ `MockHTTPClient` lets you test smithy-python clients without making actual network
54
+ calls. It implements the `HTTPClient` interface and returns queued responses. For async
55
+ clients, use `smithy_http.aio.testing.AsyncMockHTTPClient`, which implements
56
+ `AsyncHTTPClient` the same way.
51
57
 
52
- #### Basic Usage
58
+ #### Basic Usage
53
59
 
54
- ```python
60
+ ```python
55
61
  from smithy_http.testing import MockHTTPClient
56
62
 
57
63
  # Create mock client and configure responses
@@ -67,7 +73,7 @@ config = Config(transport=mock_client)
67
73
  client = TestSmithyServiceClient(config=config)
68
74
 
69
75
  # Test your client logic
70
- result = await client.some_operation({"input": "data"})
76
+ result = client.some_operation({"input": "data"})
71
77
 
72
78
  # Inspect what requests were made
73
79
  assert mock_client.call_count == 1
@@ -10,11 +10,14 @@ The `smithy_http.testing` module provides shared utilities for testing HTTP func
10
10
 
11
11
  ### MockHTTPClient
12
12
 
13
- The `MockHTTPClient` allows you to test smithy-python clients without making actual network calls. It implements the `HTTPClient` interface and provides configurable responses for functional testing.
13
+ `MockHTTPClient` lets you test smithy-python clients without making actual network
14
+ calls. It implements the `HTTPClient` interface and returns queued responses. For async
15
+ clients, use `smithy_http.aio.testing.AsyncMockHTTPClient`, which implements
16
+ `AsyncHTTPClient` the same way.
14
17
 
15
- #### Basic Usage
18
+ #### Basic Usage
16
19
 
17
- ```python
20
+ ```python
18
21
  from smithy_http.testing import MockHTTPClient
19
22
 
20
23
  # Create mock client and configure responses
@@ -30,7 +33,7 @@ config = Config(transport=mock_client)
30
33
  client = TestSmithyServiceClient(config=config)
31
34
 
32
35
  # Test your client logic
33
- result = await client.some_operation({"input": "data"})
36
+ result = client.some_operation({"input": "data"})
34
37
 
35
38
  # Inspect what requests were made
36
39
  assert mock_client.call_count == 1
@@ -22,6 +22,7 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.12",
23
23
  "Programming Language :: Python :: 3.13",
24
24
  "Programming Language :: Python :: 3.14",
25
+ "Programming Language :: Python :: 3.15",
25
26
  "Programming Language :: Python :: Implementation :: CPython",
26
27
  "Programming Language :: Python :: Free Threading :: 2 - Beta",
27
28
  "Topic :: Software Development :: Libraries"
@@ -40,12 +41,15 @@ awscrt = [
40
41
  "awscrt~=0.32.0",
41
42
  ]
42
43
  cbor = [
43
- "smithy-cbor~=0.1.0",
44
+ "smithy-cbor~=0.2.0",
44
45
  ]
45
46
  aiohttp = [
46
47
  "aiohttp>=3.11.12, <4.0",
47
48
  "yarl"
48
49
  ]
50
+ urllib3 = [
51
+ "urllib3>=2.0, <3.0",
52
+ ]
49
53
 
50
54
  [build-system]
51
55
  requires = ["hatchling"]
@@ -2,12 +2,27 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from collections import Counter, OrderedDict
4
4
  from collections.abc import Iterable, Iterator
5
+ from dataclasses import dataclass, field
5
6
  from typing import get_args
6
7
 
8
+ from smithy_core.interfaces import AnyStreamingBlob, URI
9
+ from smithy_core.utils import read_streaming_blob
10
+
7
11
  from . import interfaces
8
12
  from .interfaces import FieldPosition
9
13
 
10
- __version__ = "0.6.0"
14
+ __all__ = [
15
+ "Field",
16
+ "FieldPosition",
17
+ "Fields",
18
+ "HTTPRequest",
19
+ "HTTPResponse",
20
+ "interfaces",
21
+ "quote_and_escape_field_value",
22
+ "tuples_to_fields",
23
+ ]
24
+
25
+ __version__ = "0.7.0"
11
26
 
12
27
  _VALID_FIELD_POSITIONS = frozenset(get_args(FieldPosition))
13
28
 
@@ -35,6 +50,22 @@ class Field(interfaces.Field):
35
50
  raise ValueError(f"Unknown field kind: {kind!r}")
36
51
  self.kind = kind
37
52
 
53
+ def __eq__(self, other: object) -> bool:
54
+ """Name, values, and kind must match.
55
+
56
+ Values order must match.
57
+ """
58
+ if not isinstance(other, Field):
59
+ return False
60
+ return (
61
+ self.name == other.name
62
+ and self.kind == other.kind
63
+ and self.values == other.values
64
+ )
65
+
66
+ def __repr__(self) -> str:
67
+ return f"Field(name={self.name!r}, value={self.values!r}, kind={self.kind!r})"
68
+
38
69
  def add(self, value: str) -> None:
39
70
  """Append a value to a field."""
40
71
  self.values.append(value)
@@ -75,22 +106,6 @@ class Field(interfaces.Field):
75
106
  value."""
76
107
  return [(self.name, val) for val in self.values]
77
108
 
78
- def __eq__(self, other: object) -> bool:
79
- """Name, values, and kind must match.
80
-
81
- Values order must match.
82
- """
83
- if not isinstance(other, Field):
84
- return False
85
- return (
86
- self.name == other.name
87
- and self.kind == other.kind
88
- and self.values == other.values
89
- )
90
-
91
- def __repr__(self) -> str:
92
- return f"Field(name={self.name!r}, value={self.values!r}, kind={self.kind!r})"
93
-
94
109
 
95
110
  class Fields(interfaces.Fields):
96
111
  def __init__(
@@ -123,10 +138,6 @@ class Fields(interfaces.Fields):
123
138
  self.entries: dict[str, interfaces.Field] = OrderedDict(init_tuples)
124
139
  self.encoding: str = encoding
125
140
 
126
- def set_field(self, field: interfaces.Field) -> None:
127
- """Alias for __setitem__ to utilize the field.name for the entry key."""
128
- self.__setitem__(field.name, field)
129
-
130
141
  def __setitem__(self, name: str, field: interfaces.Field) -> None:
131
142
  """Set or override entry for a Field name."""
132
143
  normalized_name = self._normalize_field_name(name)
@@ -138,11 +149,6 @@ class Fields(interfaces.Fields):
138
149
  )
139
150
  self.entries[normalized_name] = field
140
151
 
141
- def get(
142
- self, key: str, default: interfaces.Field | None = None
143
- ) -> interfaces.Field | None:
144
- return self[key] if key in self else default
145
-
146
152
  def __getitem__(self, name: str) -> interfaces.Field:
147
153
  """Retrieve Field entry."""
148
154
  normalized_name = self._normalize_field_name(name)
@@ -153,6 +159,36 @@ class Fields(interfaces.Fields):
153
159
  normalized_name = self._normalize_field_name(name)
154
160
  del self.entries[normalized_name]
155
161
 
162
+ def __eq__(self, other: object) -> bool:
163
+ """Encoding must match.
164
+
165
+ Entries must match in values and order.
166
+ """
167
+ if not isinstance(other, Fields):
168
+ return False
169
+ return self.encoding == other.encoding and self.entries == other.entries
170
+
171
+ def __iter__(self) -> Iterator[interfaces.Field]:
172
+ yield from self.entries.values()
173
+
174
+ def __len__(self) -> int:
175
+ return len(self.entries)
176
+
177
+ def __repr__(self) -> str:
178
+ return f"Fields({self.entries})"
179
+
180
+ def __contains__(self, key: str) -> bool:
181
+ return self._normalize_field_name(key) in self.entries
182
+
183
+ def set_field(self, field: interfaces.Field) -> None:
184
+ """Alias for __setitem__ to utilize the field.name for the entry key."""
185
+ self.__setitem__(field.name, field)
186
+
187
+ def get(
188
+ self, key: str, default: interfaces.Field | None = None
189
+ ) -> interfaces.Field | None:
190
+ return self[key] if key in self else default
191
+
156
192
  def get_by_type(self, kind: FieldPosition) -> list[interfaces.Field]:
157
193
  """Helper function for retrieving specific types of fields.
158
194
 
@@ -182,26 +218,43 @@ class Fields(interfaces.Fields):
182
218
  """
183
219
  return name.lower()
184
220
 
185
- def __eq__(self, other: object) -> bool:
186
- """Encoding must match.
187
221
 
188
- Entries must match in values and order.
189
- """
190
- if not isinstance(other, Fields):
191
- return False
192
- return self.encoding == other.encoding and self.entries == other.entries
222
+ @dataclass(kw_only=True)
223
+ class HTTPRequest(interfaces.HTTPRequest):
224
+ """HTTP primitives for an Exchange to construct a version agnostic HTTP message."""
193
225
 
194
- def __iter__(self) -> Iterator[interfaces.Field]:
195
- yield from self.entries.values()
226
+ destination: URI
227
+ body: AnyStreamingBlob = field(repr=False, default=b"")
228
+ method: str
229
+ fields: interfaces.Fields
230
+ http2_required: bool = False
231
+ """Transport hint: negotiate HTTP/2 for this request and don't reuse a non-h2
232
+ connection for it. Set for event-stream operations whose protocol permits h2;
233
+ not sent on the wire."""
196
234
 
197
- def __len__(self) -> int:
198
- return len(self.entries)
199
235
 
200
- def __repr__(self) -> str:
201
- return f"Fields({self.entries})"
236
+ # CAUTION: implements interfaces.HTTPResponse structurally; subclassing the protocol
237
+ # makes its read-only properties collide with the dataclass fields (AttributeError).
238
+ # See https://github.com/python/typing/discussions/903#discussioncomment-4866851.
239
+ @dataclass(kw_only=True)
240
+ class HTTPResponse:
241
+ """Basic implementation of :py:class:`.interfaces.HTTPResponse`."""
202
242
 
203
- def __contains__(self, key: str) -> bool:
204
- return self._normalize_field_name(key) in self.entries
243
+ body: AnyStreamingBlob = field(repr=False, default=b"")
244
+ """The response payload."""
245
+
246
+ status: int
247
+ """The 3 digit response status code (1xx, 2xx, 3xx, 4xx, 5xx)."""
248
+
249
+ fields: interfaces.Fields
250
+ """HTTP header and trailer fields."""
251
+
252
+ reason: str | None = None
253
+ """Optional string provided by the server explaining the status."""
254
+
255
+ def consume_body(self) -> bytes:
256
+ """Read the response body into bytes."""
257
+ return read_streaming_blob(self.body)
205
258
 
206
259
 
207
260
  def quote_and_escape_field_value(value: str) -> str:
@@ -0,0 +1,5 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from .. import HTTPRequest, HTTPResponse
4
+
5
+ __all__ = ["HTTPRequest", "HTTPResponse"]
@@ -25,22 +25,19 @@ from smithy_core.aio.types import AsyncBytesReader
25
25
  from smithy_core.exceptions import MissingDependencyError
26
26
  from smithy_core.interfaces import URI
27
27
 
28
- from .. import Field, Fields
28
+ from smithy_http import HTTPResponse
29
+ from smithy_http.interfaces import HTTPRequest
30
+ from smithy_http.interfaces import HTTPResponse as HTTPResponseInterface
31
+
32
+ from .. import tuples_to_fields
29
33
  from ..exceptions import SmithyHTTPError
30
34
  from ..interfaces import (
31
35
  HTTPClientConfiguration,
32
36
  HTTPRequestConfiguration,
33
37
  )
34
- from . import HTTPResponse
35
- from .interfaces import HTTPClient, HTTPRequest
36
- from .interfaces import HTTPResponse as HTTPResponseInterface
38
+ from .interfaces import AsyncHTTPClient
37
39
 
38
-
39
- def _assert_aiohttp() -> None:
40
- if not HAS_AIOHTTP:
41
- raise MissingDependencyError(
42
- "Attempted to use aiohttp component, but aiohttp is not installed."
43
- )
40
+ __all__ = ["AIOHTTPClient", "AIOHTTPClientConfig"]
44
41
 
45
42
 
46
43
  class AIOHTTPClientConfig(HTTPClientConfiguration):
@@ -48,23 +45,8 @@ class AIOHTTPClientConfig(HTTPClientConfiguration):
48
45
  _assert_aiohttp()
49
46
 
50
47
 
51
- class _AIOHTTPStreamingBody(AsyncIterable[bytes]):
52
- """Streams a response body, releasing the response once it is done."""
53
-
54
- def __init__(self, response: "aiohttp.ClientResponse") -> None:
55
- self._response = response
56
-
57
- def __aiter__(self) -> AsyncIterator[bytes]:
58
- # The reader iterates by line, so use iter_any to get raw chunks.
59
- return self._response.content.iter_any()
60
-
61
- async def close(self) -> None:
62
- # Pools the connection if the body was read to completion, closes it if not.
63
- self._response.release()
64
-
65
-
66
- class AIOHTTPClient(HTTPClient):
67
- """Implementation of :py:class:`.interfaces.HTTPClient` using aiohttp."""
48
+ class AIOHTTPClient(AsyncHTTPClient):
49
+ """Implementation of :py:class:`.interfaces.AsyncHTTPClient` using aiohttp."""
68
50
 
69
51
  TIMEOUT_EXCEPTIONS = (TimeoutError,)
70
52
 
@@ -93,6 +75,20 @@ class AIOHTTPClient(HTTPClient):
93
75
  headers={"Accept-Encoding": "identity"},
94
76
  )
95
77
 
78
+ async def __aenter__(self) -> Self:
79
+ if self._closed:
80
+ raise SmithyHTTPError("Cannot enter an HTTP client that has been closed.")
81
+ return self
82
+
83
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
84
+ await self.close()
85
+
86
+ def __deepcopy__(self, memo: Any) -> "AIOHTTPClient":
87
+ return AIOHTTPClient(
88
+ client_config=deepcopy(self._config),
89
+ _session=copy(self._session),
90
+ )
91
+
96
92
  async def send(
97
93
  self,
98
94
  request: HTTPRequest,
@@ -145,14 +141,6 @@ class AIOHTTPClient(HTTPClient):
145
141
  self._closed = True
146
142
  await self._session.close()
147
143
 
148
- async def __aenter__(self) -> Self:
149
- if self._closed:
150
- raise SmithyHTTPError("Cannot enter an HTTP client that has been closed.")
151
- return self
152
-
153
- async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
154
- await self.close()
155
-
156
144
  async def _prepare_body(self, body: StreamingBlob) -> AsyncBytesReader | None:
157
145
  """Convert a body for aiohttp, omitting seekable bodies with no data."""
158
146
  if not isinstance(body, AsyncBytesReader):
@@ -183,26 +171,37 @@ class AIOHTTPClient(HTTPClient):
183
171
  self, aiohttp_resp: "aiohttp.ClientResponse"
184
172
  ) -> HTTPResponseInterface:
185
173
  """Convert a ``aiohttp.ClientResponse`` to a ``smithy_http.aio.HTTPResponse``"""
186
- headers = Fields()
187
- for header_name, header_val in aiohttp_resp.headers.items():
188
- try:
189
- headers[header_name].add(header_val)
190
- except KeyError:
191
- headers[header_name] = Field(
192
- name=header_name,
193
- values=[header_val],
194
- kind="header",
195
- )
196
-
197
174
  return HTTPResponse(
198
175
  status=aiohttp_resp.status,
199
- fields=headers,
176
+ fields=tuples_to_fields(aiohttp_resp.headers.items()),
200
177
  body=_AIOHTTPStreamingBody(aiohttp_resp),
201
178
  reason=aiohttp_resp.reason,
202
179
  )
203
180
 
204
- def __deepcopy__(self, memo: Any) -> "AIOHTTPClient":
205
- return AIOHTTPClient(
206
- client_config=deepcopy(self._config),
207
- _session=copy(self._session),
181
+
182
+ def _assert_aiohttp() -> None:
183
+ if not HAS_AIOHTTP:
184
+ raise MissingDependencyError(
185
+ "Attempted to use aiohttp component, but aiohttp is not installed."
208
186
  )
187
+
188
+
189
+ class _AIOHTTPStreamingBody(AsyncIterable[bytes]):
190
+ """Streams a response body, releasing the response once it is done."""
191
+
192
+ def __init__(self, response: "aiohttp.ClientResponse") -> None:
193
+ self._response = response
194
+ self._reader: AsyncBytesReader | None = None
195
+
196
+ def __aiter__(self) -> AsyncIterator[bytes]:
197
+ # The reader iterates by line, so use iter_any to get raw chunks.
198
+ return self._response.content.iter_any()
199
+
200
+ async def read(self, size: int = -1) -> bytes:
201
+ if self._reader is None:
202
+ self._reader = AsyncBytesReader(self._response.content.iter_any())
203
+ return await self._reader.read(size)
204
+
205
+ async def close(self) -> None:
206
+ # Pools the connection if the body was read to completion, closes it if not.
207
+ self._response.release()
@@ -1,2 +1,3 @@
1
1
  # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
+ __all__: list[str] = []