capo-ebs 0.1.0__tar.gz → 0.3.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.
- {capo_ebs-0.1.0/src/capo_ebs.egg-info → capo_ebs-0.3.0}/PKG-INFO +2 -2
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/pyproject.toml +2 -2
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_auth/_signers.py +41 -31
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_auth/_sigv4.py +156 -16
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/complete_snapshot.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/get_snapshot_block.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/list_changed_blocks.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/list_snapshot_blocks.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/put_snapshot_block.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_operations/ebs/start_snapshot.py +7 -3
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_protocol/eventstream.py +3 -2
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_services/_pipeline.py +17 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_services/async_ebs.py +26 -26
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_services/ebs.py +26 -26
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/get_snapshot_block_request.py +1 -1
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/put_snapshot_block_request.py +2 -2
- capo_ebs-0.3.0/src/capo_ebs/types/start_snapshot_request.py +84 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/start_snapshot_response.py +1 -1
- {capo_ebs-0.1.0 → capo_ebs-0.3.0/src/capo_ebs.egg-info}/PKG-INFO +2 -2
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/tests/test_sigv4.py +201 -17
- capo_ebs-0.1.0/src/capo_ebs/types/start_snapshot_request.py +0 -84
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/LICENSE +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/README.md +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/setup.cfg +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/__init__.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_async.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_auth/_identity.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_auth/_providers.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_auth/_zapros_handler.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_body.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_iter.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_pagination.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_protocol/__init__.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_protocol/errors.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_protocol/serialize.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_protocol/xml.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_rule_engine/__init__.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_rule_engine/_aws_partition.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_rule_engine/_endpoint_rule_set.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_rule_engine/_endpoint_runtime.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/_services/_aws_config.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/__init__.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/_base.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/access_denied_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/concurrent_limit_exceeded_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/conflict_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/internal_server_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/request_throttled_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/resource_not_found_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/service_quota_exceeded_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/errors/validation_exception.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/py.typed +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/_prelude/blob.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/_prelude/timestamp.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/access_denied_exception_reason.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/block.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/block_data.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/block_index.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/block_size.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/block_token.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/blocks.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/boolean.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/changed_block.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/changed_blocks.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/changed_blocks_count.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/checksum.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/checksum_aggregation_method.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/checksum_algorithm.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/complete_snapshot_request.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/complete_snapshot_response.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/data_length.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/description.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/error_message.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/get_snapshot_block_response.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/idempotency_token.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/kms_key_arn.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/list_changed_blocks_request.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/list_changed_blocks_response.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/list_snapshot_blocks_request.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/list_snapshot_blocks_response.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/max_results.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/owner_id.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/page_token.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/progress.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/put_snapshot_block_response.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/request_throttled_exception_reason.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/resource_not_found_exception_reason.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/service_quota_exceeded_exception_reason.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/snapshot_id.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/sse_type.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/status.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/tag.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/tag_key.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/tag_value.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/tags.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/time_stamp.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/timeout.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/validation_exception_reason.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs/types/volume_size.py +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs.egg-info/SOURCES.txt +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs.egg-info/dependency_links.txt +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs.egg-info/requires.txt +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/src/capo_ebs.egg-info/top_level.txt +0 -0
- {capo_ebs-0.1.0 → capo_ebs-0.3.0}/tests/test_body.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capo-ebs
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for EBS.
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for AWS EBS.
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -32,7 +32,9 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
32
32
|
endpoint rule-set's ``authSchemes`` property or built by the generated
|
|
33
33
|
``get_signer`` from operation defaults. ``unsigned_payload`` mirrors the
|
|
34
34
|
operation's ``aws.auth#unsignedPayload`` trait: the body is sent but left
|
|
35
|
-
out of the signature.
|
|
35
|
+
out of the signature. ``event_stream`` marks a request event stream: the
|
|
36
|
+
request is signed with the ``STREAMING-AWS4-HMAC-SHA256-EVENTS`` payload
|
|
37
|
+
marker and every event with a signature chained from it.
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
40
|
def __init__(
|
|
@@ -41,10 +43,12 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
41
43
|
*,
|
|
42
44
|
auth_scheme: dict[str, Any],
|
|
43
45
|
unsigned_payload: bool = False,
|
|
46
|
+
event_stream: bool = False,
|
|
44
47
|
) -> None:
|
|
45
48
|
super().__init__(provider)
|
|
46
49
|
self._auth_scheme = auth_scheme
|
|
47
50
|
self._unsigned_payload = unsigned_payload
|
|
51
|
+
self._event_stream = event_stream
|
|
48
52
|
|
|
49
53
|
async def asign(self, req: Request) -> Request:
|
|
50
54
|
creds = await self.provider.aresolve_identity()
|
|
@@ -62,21 +66,24 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
62
66
|
"disableNormalizePath", False
|
|
63
67
|
),
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
unsigned_payload = self._unsigned_payload
|
|
70
|
+
if (
|
|
71
|
+
not unsigned_payload
|
|
72
|
+
and not self._event_stream
|
|
73
|
+
and req.body is not None
|
|
74
|
+
and not isinstance(req.body, bytes)
|
|
75
|
+
):
|
|
76
|
+
if self._auth_scheme["signingName"] in S3_SIGNING_NAMES:
|
|
77
|
+
# S3 accepts UNSIGNED-PAYLOAD for any operation; streamed bodies rely on it.
|
|
78
|
+
unsigned_payload = True
|
|
79
|
+
else:
|
|
80
|
+
raise NotImplementedError(
|
|
81
|
+
"Currently we don't support signed chunked payloads, so buffer the body and "
|
|
82
|
+
"pass bytes as a workaround; chunked signed implementation coming soon"
|
|
83
|
+
)
|
|
84
|
+
return sign_sigv4(
|
|
85
|
+
req, ctx, unsigned_payload=unsigned_payload, event_stream=self._event_stream
|
|
86
|
+
)
|
|
80
87
|
|
|
81
88
|
def sign(self, req: Request) -> Request:
|
|
82
89
|
creds = self.provider.resolve_identity()
|
|
@@ -94,18 +101,21 @@ class SigV4Signer(Signer[Credentials]):
|
|
|
94
101
|
"disableNormalizePath", False
|
|
95
102
|
),
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
unsigned_payload = self._unsigned_payload
|
|
105
|
+
if (
|
|
106
|
+
not unsigned_payload
|
|
107
|
+
and not self._event_stream
|
|
108
|
+
and req.body is not None
|
|
109
|
+
and not isinstance(req.body, bytes)
|
|
110
|
+
):
|
|
111
|
+
if self._auth_scheme["signingName"] in S3_SIGNING_NAMES:
|
|
112
|
+
# S3 accepts UNSIGNED-PAYLOAD for any operation; streamed bodies rely on it.
|
|
113
|
+
unsigned_payload = True
|
|
114
|
+
else:
|
|
115
|
+
raise NotImplementedError(
|
|
116
|
+
"Currently we don't support signed chunked payloads, so buffer the body and "
|
|
117
|
+
"pass bytes as a workaround; chunked signed implementation coming soon"
|
|
118
|
+
)
|
|
119
|
+
return sign_sigv4(
|
|
120
|
+
req, ctx, unsigned_payload=unsigned_payload, event_stream=self._event_stream
|
|
121
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""AWS Signature Version 4 — single-chunk signing.
|
|
1
|
+
"""AWS Signature Version 4 — single-chunk and event-stream signing.
|
|
2
2
|
|
|
3
3
|
Reference:
|
|
4
4
|
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
|
|
@@ -15,7 +15,8 @@ import functools
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import hmac
|
|
17
17
|
import re
|
|
18
|
-
from
|
|
18
|
+
from collections.abc import AsyncIterator, Iterator
|
|
19
|
+
from typing import Any, Literal, TypedDict, cast
|
|
19
20
|
from urllib.parse import quote, unquote
|
|
20
21
|
|
|
21
22
|
import zapros
|
|
@@ -23,6 +24,9 @@ from pywhatwgurl import URLSearchParams
|
|
|
23
24
|
from zapros import Headers, Request
|
|
24
25
|
from zapros._utils import get_host_header_value
|
|
25
26
|
|
|
27
|
+
from .._iter import AnyIterator
|
|
28
|
+
from .._protocol.eventstream import HeaderValue, Message, encode_headers
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
def build_sigv4_auth_scheme(
|
|
28
32
|
signing_name: str, region: str | None, endpoint_scheme: dict[str, Any] | None = None
|
|
@@ -65,6 +69,10 @@ class SigV4AuthContext(TypedDict):
|
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
_SIGV4_ALGORITHM = "AWS4-HMAC-SHA256"
|
|
72
|
+
# Per-event algorithm and request payload marker of a signed event stream.
|
|
73
|
+
_EVENT_ALGORITHM = "AWS4-HMAC-SHA256-PAYLOAD"
|
|
74
|
+
_EVENT_STREAM_PAYLOAD = "STREAMING-AWS4-HMAC-SHA256-EVENTS"
|
|
75
|
+
_AMZ_DATE_FORMAT = "%Y%m%dT%H%M%SZ"
|
|
68
76
|
_EMPTY_PAYLOAD_SHA256 = hashlib.sha256(b"").hexdigest()
|
|
69
77
|
|
|
70
78
|
# Headers excluded from the signed-headers set. Mirrors botocore's denylist:
|
|
@@ -233,13 +241,20 @@ def _canonical_query_from_pairs(pairs: list[tuple[str, str]]) -> str:
|
|
|
233
241
|
def sign_sigv4(
|
|
234
242
|
request: Request,
|
|
235
243
|
ctx: SigV4AuthContext,
|
|
236
|
-
|
|
244
|
+
*,
|
|
245
|
+
unsigned_payload: bool = False,
|
|
246
|
+
event_stream: bool = False,
|
|
237
247
|
) -> Request:
|
|
238
248
|
"""Return a new ``Request`` carrying SigV4 single-chunk auth headers.
|
|
239
249
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
250
|
+
The payload hash covers ``request.body``, which must be ``bytes`` or
|
|
251
|
+
``None``. ``unsigned_payload`` signs ``UNSIGNED-PAYLOAD`` instead and
|
|
252
|
+
forwards any body unchanged (streaming S3 requests and operations carrying
|
|
253
|
+
``aws.auth#unsignedPayload``). ``event_stream`` signs a request event
|
|
254
|
+
stream: the ``STREAMING-AWS4-HMAC-SHA256-EVENTS`` marker stands in for the
|
|
255
|
+
payload hash and the body — an iterator of encoded event messages — is
|
|
256
|
+
wrapped in an :class:`EventStreamIterator` that signs every event with a
|
|
257
|
+
signature chained from this request's.
|
|
243
258
|
"""
|
|
244
259
|
service = ctx["signing_name"]
|
|
245
260
|
region = ctx["signing_region"]
|
|
@@ -253,23 +268,31 @@ def sign_sigv4(
|
|
|
253
268
|
date_stamp = amz_date[:8]
|
|
254
269
|
else:
|
|
255
270
|
now = _amz_now()
|
|
256
|
-
amz_date = now.strftime(
|
|
271
|
+
amz_date = now.strftime(_AMZ_DATE_FORMAT)
|
|
257
272
|
date_stamp = now.strftime("%Y%m%d")
|
|
258
273
|
headers["X-Amz-Date"] = amz_date
|
|
259
274
|
|
|
260
275
|
# Payload hash. For S3-family services, x-amz-content-sha256 is mandatory;
|
|
261
|
-
# for every service it is the only way to announce an unsigned payload
|
|
276
|
+
# for every service it is the only way to announce an unsigned payload or
|
|
277
|
+
# an event stream.
|
|
262
278
|
# Either way it must be set BEFORE computing the canonical request (it
|
|
263
279
|
# gets signed).
|
|
264
280
|
payload_hash = headers.get("X-Amz-Content-SHA256")
|
|
265
281
|
if payload_hash is None:
|
|
266
|
-
if
|
|
282
|
+
if event_stream:
|
|
283
|
+
payload_hash = _EVENT_STREAM_PAYLOAD
|
|
284
|
+
elif unsigned_payload:
|
|
267
285
|
payload_hash = "UNSIGNED-PAYLOAD"
|
|
286
|
+
elif request.body is None:
|
|
287
|
+
payload_hash = _EMPTY_PAYLOAD_SHA256
|
|
288
|
+
elif isinstance(request.body, bytes):
|
|
289
|
+
payload_hash = hashlib.sha256(request.body).hexdigest()
|
|
268
290
|
else:
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
raise TypeError(
|
|
292
|
+
"sign_sigv4 hashes a bytes body only; pass unsigned_payload=True "
|
|
293
|
+
"to send a streamed body without a payload hash"
|
|
271
294
|
)
|
|
272
|
-
if service in S3_SIGNING_NAMES or
|
|
295
|
+
if service in S3_SIGNING_NAMES or unsigned_payload or event_stream:
|
|
273
296
|
headers["X-Amz-Content-SHA256"] = payload_hash
|
|
274
297
|
|
|
275
298
|
# Session token (STS / assumed-role credentials).
|
|
@@ -313,18 +336,135 @@ def sign_sigv4(
|
|
|
313
336
|
f"Signature={signature}"
|
|
314
337
|
)
|
|
315
338
|
|
|
316
|
-
|
|
317
|
-
if
|
|
339
|
+
body = request.body
|
|
340
|
+
if event_stream:
|
|
341
|
+
if not isinstance(body, (Iterator, AsyncIterator)):
|
|
342
|
+
raise TypeError(
|
|
343
|
+
"event_stream requires an iterator of encoded events as the body"
|
|
344
|
+
)
|
|
345
|
+
body = EventStreamIterator(
|
|
346
|
+
cast("Iterator[bytes] | AsyncIterator[bytes]", body), ctx, signature
|
|
347
|
+
)
|
|
348
|
+
if body is not None:
|
|
318
349
|
return Request(
|
|
319
350
|
request.url,
|
|
320
351
|
request.method,
|
|
321
352
|
headers,
|
|
322
|
-
body=
|
|
353
|
+
body=body,
|
|
323
354
|
context=request.context,
|
|
324
355
|
)
|
|
325
356
|
return Request(request.url, request.method, headers, context=request.context)
|
|
326
357
|
|
|
327
358
|
|
|
359
|
+
def _sign_event(
|
|
360
|
+
ctx: SigV4AuthContext, prior_signature: str, payload: bytes
|
|
361
|
+
) -> tuple[bytes, str]:
|
|
362
|
+
"""Wrap one encoded event in a signed outer message.
|
|
363
|
+
|
|
364
|
+
Returns ``(frame, signature)``; the hex signature seeds the next event.
|
|
365
|
+
The string to sign chains every frame to its predecessor — the first one
|
|
366
|
+
to the request's ``Authorization`` signature — so the service can verify
|
|
367
|
+
the stream incrementally::
|
|
368
|
+
|
|
369
|
+
AWS4-HMAC-SHA256-PAYLOAD
|
|
370
|
+
<date, YYYYMMDDTHHMMSSZ>
|
|
371
|
+
<credential scope>
|
|
372
|
+
<prior signature, hex>
|
|
373
|
+
<hex sha256 of the encoded ``:date`` header>
|
|
374
|
+
<hex sha256 of the payload>
|
|
375
|
+
|
|
376
|
+
The AWS SDKs (and the service) use the ``-PAYLOAD`` algorithm name; the
|
|
377
|
+
Transcribe developer guide's pseudocode omits the suffix.
|
|
378
|
+
"""
|
|
379
|
+
service = ctx["signing_name"]
|
|
380
|
+
region = ctx["signing_region"]
|
|
381
|
+
now = _amz_now()
|
|
382
|
+
amz_date = now.strftime(_AMZ_DATE_FORMAT)
|
|
383
|
+
date_stamp = amz_date[:8]
|
|
384
|
+
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
|
385
|
+
date_header: dict[str, HeaderValue] = {":date": now}
|
|
386
|
+
string_to_sign = "\n".join(
|
|
387
|
+
(
|
|
388
|
+
_EVENT_ALGORITHM,
|
|
389
|
+
amz_date,
|
|
390
|
+
credential_scope,
|
|
391
|
+
prior_signature,
|
|
392
|
+
hashlib.sha256(encode_headers(date_header)).hexdigest(),
|
|
393
|
+
hashlib.sha256(payload).hexdigest(),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
signing_key = _derive_signing_key(
|
|
397
|
+
ctx["secret_access_key"], date_stamp, region, service
|
|
398
|
+
)
|
|
399
|
+
signature = hmac.new(
|
|
400
|
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
|
401
|
+
).hexdigest()
|
|
402
|
+
frame = Message(
|
|
403
|
+
{**date_header, ":chunk-signature": bytes.fromhex(signature)}, payload
|
|
404
|
+
).encode()
|
|
405
|
+
return frame, signature
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class EventStreamIterator(AnyIterator[bytes]):
|
|
409
|
+
"""Sign a request event stream, one frame per event.
|
|
410
|
+
|
|
411
|
+
Wraps an iterator of encoded event messages (as produced by the generated
|
|
412
|
+
``serialize_event_*`` functions). Each event becomes the payload of an
|
|
413
|
+
outer message carrying ``:date`` and ``:chunk-signature`` headers, the
|
|
414
|
+
signature chained from the previous frame's and seeded by the request's
|
|
415
|
+
``Authorization`` signature. Once the events are exhausted, one empty
|
|
416
|
+
signed frame is yielded to mark the end of the stream.
|
|
417
|
+
|
|
418
|
+
Like :class:`~capo._body.Body`, it is both a sync and an async iterator:
|
|
419
|
+
iterate it the way the wrapped source is iterable (``next`` over a sync
|
|
420
|
+
source, ``async for`` over an async one).
|
|
421
|
+
|
|
422
|
+
Reference:
|
|
423
|
+
https://docs.aws.amazon.com/transcribe/latest/dg/streaming-setting-up.html
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def __init__(
|
|
427
|
+
self,
|
|
428
|
+
events: Iterator[bytes] | AsyncIterator[bytes],
|
|
429
|
+
ctx: SigV4AuthContext,
|
|
430
|
+
seed_signature: str,
|
|
431
|
+
) -> None:
|
|
432
|
+
self._events = events
|
|
433
|
+
self._ctx = ctx
|
|
434
|
+
self._prior_signature = seed_signature
|
|
435
|
+
self._ended = False
|
|
436
|
+
|
|
437
|
+
def _frame(self, payload: bytes) -> bytes:
|
|
438
|
+
frame, self._prior_signature = _sign_event(
|
|
439
|
+
self._ctx, self._prior_signature, payload
|
|
440
|
+
)
|
|
441
|
+
return frame
|
|
442
|
+
|
|
443
|
+
def __next__(self) -> bytes:
|
|
444
|
+
if self._ended:
|
|
445
|
+
raise StopIteration
|
|
446
|
+
if not isinstance(self._events, Iterator):
|
|
447
|
+
raise TypeError("this event stream wraps an async source; use `async for`")
|
|
448
|
+
try:
|
|
449
|
+
payload = next(cast(Iterator[bytes], self._events))
|
|
450
|
+
except StopIteration:
|
|
451
|
+
self._ended = True
|
|
452
|
+
payload = b""
|
|
453
|
+
return self._frame(payload)
|
|
454
|
+
|
|
455
|
+
async def __anext__(self) -> bytes:
|
|
456
|
+
if self._ended:
|
|
457
|
+
raise StopAsyncIteration
|
|
458
|
+
if not isinstance(self._events, AsyncIterator):
|
|
459
|
+
raise TypeError("this event stream wraps a sync source; use `for`")
|
|
460
|
+
try:
|
|
461
|
+
payload = await cast(AsyncIterator[bytes], self._events).__anext__()
|
|
462
|
+
except StopAsyncIteration:
|
|
463
|
+
self._ended = True
|
|
464
|
+
payload = b""
|
|
465
|
+
return self._frame(payload)
|
|
466
|
+
|
|
467
|
+
|
|
328
468
|
def presign_sigv4(
|
|
329
469
|
request: Request,
|
|
330
470
|
ctx: SigV4AuthContext,
|
|
@@ -351,7 +491,7 @@ def presign_sigv4(
|
|
|
351
491
|
service = ctx["signing_name"]
|
|
352
492
|
region = ctx["signing_region"]
|
|
353
493
|
|
|
354
|
-
amz_date = sign_time.strftime(
|
|
494
|
+
amz_date = sign_time.strftime(_AMZ_DATE_FORMAT)
|
|
355
495
|
date_stamp = sign_time.strftime("%Y%m%d")
|
|
356
496
|
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
|
357
497
|
|
|
@@ -25,7 +25,11 @@ import capo_ebs.types.complete_snapshot_response
|
|
|
25
25
|
import capo_ebs.types.status
|
|
26
26
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
27
27
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
28
|
-
from capo_ebs._services._pipeline import
|
|
28
|
+
from capo_ebs._services._pipeline import (
|
|
29
|
+
AsyncOperationOptions,
|
|
30
|
+
OperationOptions,
|
|
31
|
+
raise_error,
|
|
32
|
+
)
|
|
29
33
|
from capo_ebs.errors import UnknownServiceError
|
|
30
34
|
|
|
31
35
|
|
|
@@ -166,7 +170,7 @@ def complete_snapshot(
|
|
|
166
170
|
try:
|
|
167
171
|
if response.status >= 300:
|
|
168
172
|
response.read()
|
|
169
|
-
|
|
173
|
+
raise_error(response, handle_error)
|
|
170
174
|
return handle_response(response), response
|
|
171
175
|
except BaseException:
|
|
172
176
|
response.close()
|
|
@@ -183,7 +187,7 @@ async def async_complete_snapshot(
|
|
|
183
187
|
try:
|
|
184
188
|
if response.status >= 300:
|
|
185
189
|
await response.aread()
|
|
186
|
-
|
|
190
|
+
raise_error(response, handle_error)
|
|
187
191
|
return await async_handle_response(response), response
|
|
188
192
|
except BaseException:
|
|
189
193
|
await response.aclose()
|
|
@@ -24,7 +24,11 @@ import capo_ebs.types.get_snapshot_block_request
|
|
|
24
24
|
import capo_ebs.types.get_snapshot_block_response
|
|
25
25
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
26
26
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
27
|
-
from capo_ebs._services._pipeline import
|
|
27
|
+
from capo_ebs._services._pipeline import (
|
|
28
|
+
AsyncOperationOptions,
|
|
29
|
+
OperationOptions,
|
|
30
|
+
raise_error,
|
|
31
|
+
)
|
|
28
32
|
from capo_ebs.errors import UnknownServiceError
|
|
29
33
|
|
|
30
34
|
|
|
@@ -163,7 +167,7 @@ def get_snapshot_block(
|
|
|
163
167
|
try:
|
|
164
168
|
if response.status >= 300:
|
|
165
169
|
response.read()
|
|
166
|
-
|
|
170
|
+
raise_error(response, handle_error)
|
|
167
171
|
return handle_response(response), response
|
|
168
172
|
except BaseException:
|
|
169
173
|
response.close()
|
|
@@ -180,7 +184,7 @@ async def async_get_snapshot_block(
|
|
|
180
184
|
try:
|
|
181
185
|
if response.status >= 300:
|
|
182
186
|
await response.aread()
|
|
183
|
-
|
|
187
|
+
raise_error(response, handle_error)
|
|
184
188
|
return await async_handle_response(response), response
|
|
185
189
|
except BaseException:
|
|
186
190
|
await response.aclose()
|
|
@@ -24,7 +24,11 @@ import capo_ebs.types.list_changed_blocks_response
|
|
|
24
24
|
import capo_ebs.types.time_stamp
|
|
25
25
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
26
26
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
27
|
-
from capo_ebs._services._pipeline import
|
|
27
|
+
from capo_ebs._services._pipeline import (
|
|
28
|
+
AsyncOperationOptions,
|
|
29
|
+
OperationOptions,
|
|
30
|
+
raise_error,
|
|
31
|
+
)
|
|
28
32
|
from capo_ebs.errors import UnknownServiceError
|
|
29
33
|
|
|
30
34
|
|
|
@@ -157,7 +161,7 @@ def list_changed_blocks(
|
|
|
157
161
|
try:
|
|
158
162
|
if response.status >= 300:
|
|
159
163
|
response.read()
|
|
160
|
-
|
|
164
|
+
raise_error(response, handle_error)
|
|
161
165
|
return handle_response(response), response
|
|
162
166
|
except BaseException:
|
|
163
167
|
response.close()
|
|
@@ -175,7 +179,7 @@ async def async_list_changed_blocks(
|
|
|
175
179
|
try:
|
|
176
180
|
if response.status >= 300:
|
|
177
181
|
await response.aread()
|
|
178
|
-
|
|
182
|
+
raise_error(response, handle_error)
|
|
179
183
|
return await async_handle_response(response), response
|
|
180
184
|
except BaseException:
|
|
181
185
|
await response.aclose()
|
|
@@ -24,7 +24,11 @@ import capo_ebs.types.list_snapshot_blocks_response
|
|
|
24
24
|
import capo_ebs.types.time_stamp
|
|
25
25
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
26
26
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
27
|
-
from capo_ebs._services._pipeline import
|
|
27
|
+
from capo_ebs._services._pipeline import (
|
|
28
|
+
AsyncOperationOptions,
|
|
29
|
+
OperationOptions,
|
|
30
|
+
raise_error,
|
|
31
|
+
)
|
|
28
32
|
from capo_ebs.errors import UnknownServiceError
|
|
29
33
|
|
|
30
34
|
|
|
@@ -153,7 +157,7 @@ def list_snapshot_blocks(
|
|
|
153
157
|
try:
|
|
154
158
|
if response.status >= 300:
|
|
155
159
|
response.read()
|
|
156
|
-
|
|
160
|
+
raise_error(response, handle_error)
|
|
157
161
|
return handle_response(response), response
|
|
158
162
|
except BaseException:
|
|
159
163
|
response.close()
|
|
@@ -171,7 +175,7 @@ async def async_list_snapshot_blocks(
|
|
|
171
175
|
try:
|
|
172
176
|
if response.status >= 300:
|
|
173
177
|
await response.aread()
|
|
174
|
-
|
|
178
|
+
raise_error(response, handle_error)
|
|
175
179
|
return await async_handle_response(response), response
|
|
176
180
|
except BaseException:
|
|
177
181
|
await response.aclose()
|
|
@@ -26,7 +26,11 @@ import capo_ebs.types.put_snapshot_block_request
|
|
|
26
26
|
import capo_ebs.types.put_snapshot_block_response
|
|
27
27
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
28
28
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
29
|
-
from capo_ebs._services._pipeline import
|
|
29
|
+
from capo_ebs._services._pipeline import (
|
|
30
|
+
AsyncOperationOptions,
|
|
31
|
+
OperationOptions,
|
|
32
|
+
raise_error,
|
|
33
|
+
)
|
|
30
34
|
from capo_ebs.errors import UnknownServiceError
|
|
31
35
|
|
|
32
36
|
|
|
@@ -244,7 +248,7 @@ def put_snapshot_block(
|
|
|
244
248
|
try:
|
|
245
249
|
if response.status >= 300:
|
|
246
250
|
response.read()
|
|
247
|
-
|
|
251
|
+
raise_error(response, handle_error)
|
|
248
252
|
return handle_response(response), response
|
|
249
253
|
except BaseException:
|
|
250
254
|
response.close()
|
|
@@ -263,7 +267,7 @@ async def async_put_snapshot_block(
|
|
|
263
267
|
try:
|
|
264
268
|
if response.status >= 300:
|
|
265
269
|
await response.aread()
|
|
266
|
-
|
|
270
|
+
raise_error(response, handle_error)
|
|
267
271
|
return await async_handle_response(response), response
|
|
268
272
|
except BaseException:
|
|
269
273
|
await response.aclose()
|
|
@@ -27,7 +27,11 @@ import capo_ebs.types.tags
|
|
|
27
27
|
import capo_ebs.types.time_stamp
|
|
28
28
|
from capo_ebs._protocol.errors import parse_error_metadata_json
|
|
29
29
|
from capo_ebs._rule_engine._endpoint_rule_set import EndpointParams, resolve
|
|
30
|
-
from capo_ebs._services._pipeline import
|
|
30
|
+
from capo_ebs._services._pipeline import (
|
|
31
|
+
AsyncOperationOptions,
|
|
32
|
+
OperationOptions,
|
|
33
|
+
raise_error,
|
|
34
|
+
)
|
|
31
35
|
from capo_ebs.errors import UnknownServiceError
|
|
32
36
|
|
|
33
37
|
|
|
@@ -159,7 +163,7 @@ def start_snapshot(
|
|
|
159
163
|
try:
|
|
160
164
|
if response.status >= 300:
|
|
161
165
|
response.read()
|
|
162
|
-
|
|
166
|
+
raise_error(response, handle_error)
|
|
163
167
|
return handle_response(response), response
|
|
164
168
|
except BaseException:
|
|
165
169
|
response.close()
|
|
@@ -176,7 +180,7 @@ async def async_start_snapshot(
|
|
|
176
180
|
try:
|
|
177
181
|
if response.status >= 300:
|
|
178
182
|
await response.aread()
|
|
179
|
-
|
|
183
|
+
raise_error(response, handle_error)
|
|
180
184
|
return await async_handle_response(response), response
|
|
181
185
|
except BaseException:
|
|
182
186
|
await response.aclose()
|
|
@@ -89,7 +89,7 @@ def _decode_header_value(
|
|
|
89
89
|
raise ValueError(f"unsupported eventstream header type: {type_byte:#x}")
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def encode_headers(headers: dict[str, HeaderValue]) -> bytes:
|
|
93
93
|
parts: list[bytes] = []
|
|
94
94
|
for name, value in headers.items():
|
|
95
95
|
name_bytes = name.encode("utf-8")
|
|
@@ -124,7 +124,7 @@ class Message:
|
|
|
124
124
|
|
|
125
125
|
def encode(self) -> bytes:
|
|
126
126
|
"""Serialize this message into eventstream framing bytes."""
|
|
127
|
-
headers_bytes =
|
|
127
|
+
headers_bytes = encode_headers(self.headers)
|
|
128
128
|
headers_length = len(headers_bytes)
|
|
129
129
|
payload = self.payload
|
|
130
130
|
# total_length covers the whole frame, trailing message CRC included.
|
|
@@ -234,6 +234,7 @@ __all__ = [
|
|
|
234
234
|
"MessageDecoder",
|
|
235
235
|
"async_raw_stream_to_events",
|
|
236
236
|
"async_read_messages",
|
|
237
|
+
"encode_headers",
|
|
237
238
|
"raw_stream_to_events",
|
|
238
239
|
"read_messages",
|
|
239
240
|
]
|
|
@@ -6,6 +6,7 @@ from collections.abc import AsyncIterator, Iterator, Sequence
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING, Awaitable, Callable, Generic, TypeVar, cast
|
|
8
8
|
|
|
9
|
+
from typing_extensions import Never
|
|
9
10
|
from zapros import (
|
|
10
11
|
AsyncClient,
|
|
11
12
|
Client,
|
|
@@ -135,6 +136,22 @@ async def aexecute_pipeline(
|
|
|
135
136
|
return await make_chain(0)(request)
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def raise_error(response: Response, handle_error: Callable[[Response], Never]) -> Never:
|
|
140
|
+
try:
|
|
141
|
+
handle_error(response)
|
|
142
|
+
except ServiceError as exc:
|
|
143
|
+
# The response itself can say a retry is safe, even when the error is
|
|
144
|
+
# not marked @retryable: a 429, a transient 5xx or a Retry-After header.
|
|
145
|
+
if response.status == 429:
|
|
146
|
+
exc.is_throttling_error = True
|
|
147
|
+
if (
|
|
148
|
+
response.status in (429, 500, 502, 503, 504)
|
|
149
|
+
or "retry-after" in response.headers
|
|
150
|
+
):
|
|
151
|
+
exc.is_retryable = True
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
|
|
138
155
|
def _is_retryable(exc: Exception) -> bool:
|
|
139
156
|
if isinstance(exc, ServiceError):
|
|
140
157
|
return exc.is_retryable
|