capo-keyspacesstreams 0.5.0__tar.gz → 0.6.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_keyspacesstreams-0.5.0/src/capo_keyspacesstreams.egg-info → capo_keyspacesstreams-0.6.0}/PKG-INFO +1 -1
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/pyproject.toml +1 -1
- capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams/_aws.py +28 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_records.py +5 -5
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_shard_iterator.py +5 -5
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_stream.py +5 -5
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/list_streams.py +5 -5
- capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams/_services/_pipeline.py +520 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/async_keyspaces_streams.py +24 -6
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/keyspaces_streams.py +24 -6
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/_base.py +8 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/access_denied_exception.py +11 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/internal_server_exception.py +11 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/resource_not_found_exception.py +11 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/throttling_exception.py +11 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/validation_exception.py +11 -3
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams.egg-info}/PKG-INFO +1 -1
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/SOURCES.txt +1 -0
- capo_keyspacesstreams-0.5.0/src/capo_keyspacesstreams/_services/_pipeline.py +0 -264
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/LICENSE +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/README.md +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/setup.cfg +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/__init__.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_async.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_identity.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_providers.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_signers.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_sigv4.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_zapros_handler.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_body.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_iter.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_pagination.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/__init__.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/errors.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/eventstream.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/serialize.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/xml.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/__init__.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_aws_partition.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_endpoint_rule_set.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_endpoint_runtime.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/_aws_config.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/__init__.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/py.typed +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/_prelude/blob.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/_prelude/timestamp.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/date.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_records_input.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_records_output.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_shard_iterator_input.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_shard_iterator_output.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_stream_input.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_stream_output.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/iterator_description.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/iterator_position.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspace_name.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_list.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_map.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_map_definition.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_value.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cells.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_keys_map.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_metadata.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_row.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_udt_map.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/list_streams_input.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/list_streams_output.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/origin_type.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/record.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/record_list.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/sequence_number.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/sequence_number_range.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_description_list.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_filter.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_filter_type.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id_list.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id_token.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_iterator.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_iterator_type.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_arn.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_arn_token.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_list.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_status.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_view_type.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/table_name.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/validation_exception_type.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/dependency_links.txt +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/requires.txt +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/top_level.txt +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/tests/test_body.py +0 -0
- {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/tests/test_sigv4.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Error codes and HTTP statuses that are retried whether or not the model marks them @retryable."""
|
|
2
|
+
|
|
3
|
+
# Transient: retried with the normal backoff.
|
|
4
|
+
KNOWN_RETRYABLE_STATUSES = frozenset({500, 502, 503, 504})
|
|
5
|
+
KNOWN_RETRYABLE_CODES = frozenset(
|
|
6
|
+
{"PriorRequestNotComplete", "RequestTimeout", "RequestTimeoutException"}
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Throttling: retried with the longer throttling backoff.
|
|
10
|
+
KNOWN_THROTTLING_STATUSES = frozenset({429})
|
|
11
|
+
KNOWN_THROTTLING_CODES = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"BandwidthLimitExceeded",
|
|
14
|
+
"EC2ThrottledException",
|
|
15
|
+
"LimitExceededException",
|
|
16
|
+
"PriorRequestNotComplete",
|
|
17
|
+
"ProvisionedThroughputExceededException",
|
|
18
|
+
"RequestLimitExceeded",
|
|
19
|
+
"RequestThrottled",
|
|
20
|
+
"RequestThrottledException",
|
|
21
|
+
"SlowDown",
|
|
22
|
+
"ThrottledException",
|
|
23
|
+
"Throttling",
|
|
24
|
+
"ThrottlingException",
|
|
25
|
+
"TooManyRequestsException",
|
|
26
|
+
"TransactionInProgressException",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
@@ -39,23 +39,23 @@ def handle_error(response: zapros.Response) -> Never:
|
|
|
39
39
|
match code:
|
|
40
40
|
case "AccessDeniedException":
|
|
41
41
|
raise capo_keyspacesstreams.errors.access_denied_exception.AccessDeniedException.from_aws_json_1_0(
|
|
42
|
-
data, message
|
|
42
|
+
data, message, response=response
|
|
43
43
|
)
|
|
44
44
|
case "InternalServerException":
|
|
45
45
|
raise capo_keyspacesstreams.errors.internal_server_exception.InternalServerException.from_aws_json_1_0(
|
|
46
|
-
data, message
|
|
46
|
+
data, message, response=response
|
|
47
47
|
)
|
|
48
48
|
case "ResourceNotFoundException":
|
|
49
49
|
raise capo_keyspacesstreams.errors.resource_not_found_exception.ResourceNotFoundException.from_aws_json_1_0(
|
|
50
|
-
data, message
|
|
50
|
+
data, message, response=response
|
|
51
51
|
)
|
|
52
52
|
case "ThrottlingException":
|
|
53
53
|
raise capo_keyspacesstreams.errors.throttling_exception.ThrottlingException.from_aws_json_1_0(
|
|
54
|
-
data, message
|
|
54
|
+
data, message, response=response
|
|
55
55
|
)
|
|
56
56
|
case "ValidationException":
|
|
57
57
|
raise capo_keyspacesstreams.errors.validation_exception.ValidationException.from_aws_json_1_0(
|
|
58
|
-
data, message
|
|
58
|
+
data, message, response=response
|
|
59
59
|
)
|
|
60
60
|
case _:
|
|
61
61
|
raise UnknownServiceError(code=code, message=message, response=response)
|
|
@@ -38,23 +38,23 @@ def handle_error(response: zapros.Response) -> Never:
|
|
|
38
38
|
match code:
|
|
39
39
|
case "AccessDeniedException":
|
|
40
40
|
raise capo_keyspacesstreams.errors.access_denied_exception.AccessDeniedException.from_aws_json_1_0(
|
|
41
|
-
data, message
|
|
41
|
+
data, message, response=response
|
|
42
42
|
)
|
|
43
43
|
case "InternalServerException":
|
|
44
44
|
raise capo_keyspacesstreams.errors.internal_server_exception.InternalServerException.from_aws_json_1_0(
|
|
45
|
-
data, message
|
|
45
|
+
data, message, response=response
|
|
46
46
|
)
|
|
47
47
|
case "ResourceNotFoundException":
|
|
48
48
|
raise capo_keyspacesstreams.errors.resource_not_found_exception.ResourceNotFoundException.from_aws_json_1_0(
|
|
49
|
-
data, message
|
|
49
|
+
data, message, response=response
|
|
50
50
|
)
|
|
51
51
|
case "ThrottlingException":
|
|
52
52
|
raise capo_keyspacesstreams.errors.throttling_exception.ThrottlingException.from_aws_json_1_0(
|
|
53
|
-
data, message
|
|
53
|
+
data, message, response=response
|
|
54
54
|
)
|
|
55
55
|
case "ValidationException":
|
|
56
56
|
raise capo_keyspacesstreams.errors.validation_exception.ValidationException.from_aws_json_1_0(
|
|
57
|
-
data, message
|
|
57
|
+
data, message, response=response
|
|
58
58
|
)
|
|
59
59
|
case _:
|
|
60
60
|
raise UnknownServiceError(code=code, message=message, response=response)
|
|
@@ -42,23 +42,23 @@ def handle_error(response: zapros.Response) -> Never:
|
|
|
42
42
|
match code:
|
|
43
43
|
case "AccessDeniedException":
|
|
44
44
|
raise capo_keyspacesstreams.errors.access_denied_exception.AccessDeniedException.from_aws_json_1_0(
|
|
45
|
-
data, message
|
|
45
|
+
data, message, response=response
|
|
46
46
|
)
|
|
47
47
|
case "InternalServerException":
|
|
48
48
|
raise capo_keyspacesstreams.errors.internal_server_exception.InternalServerException.from_aws_json_1_0(
|
|
49
|
-
data, message
|
|
49
|
+
data, message, response=response
|
|
50
50
|
)
|
|
51
51
|
case "ResourceNotFoundException":
|
|
52
52
|
raise capo_keyspacesstreams.errors.resource_not_found_exception.ResourceNotFoundException.from_aws_json_1_0(
|
|
53
|
-
data, message
|
|
53
|
+
data, message, response=response
|
|
54
54
|
)
|
|
55
55
|
case "ThrottlingException":
|
|
56
56
|
raise capo_keyspacesstreams.errors.throttling_exception.ThrottlingException.from_aws_json_1_0(
|
|
57
|
-
data, message
|
|
57
|
+
data, message, response=response
|
|
58
58
|
)
|
|
59
59
|
case "ValidationException":
|
|
60
60
|
raise capo_keyspacesstreams.errors.validation_exception.ValidationException.from_aws_json_1_0(
|
|
61
|
-
data, message
|
|
61
|
+
data, message, response=response
|
|
62
62
|
)
|
|
63
63
|
case _:
|
|
64
64
|
raise UnknownServiceError(code=code, message=message, response=response)
|
|
@@ -38,23 +38,23 @@ def handle_error(response: zapros.Response) -> Never:
|
|
|
38
38
|
match code:
|
|
39
39
|
case "AccessDeniedException":
|
|
40
40
|
raise capo_keyspacesstreams.errors.access_denied_exception.AccessDeniedException.from_aws_json_1_0(
|
|
41
|
-
data, message
|
|
41
|
+
data, message, response=response
|
|
42
42
|
)
|
|
43
43
|
case "InternalServerException":
|
|
44
44
|
raise capo_keyspacesstreams.errors.internal_server_exception.InternalServerException.from_aws_json_1_0(
|
|
45
|
-
data, message
|
|
45
|
+
data, message, response=response
|
|
46
46
|
)
|
|
47
47
|
case "ResourceNotFoundException":
|
|
48
48
|
raise capo_keyspacesstreams.errors.resource_not_found_exception.ResourceNotFoundException.from_aws_json_1_0(
|
|
49
|
-
data, message
|
|
49
|
+
data, message, response=response
|
|
50
50
|
)
|
|
51
51
|
case "ThrottlingException":
|
|
52
52
|
raise capo_keyspacesstreams.errors.throttling_exception.ThrottlingException.from_aws_json_1_0(
|
|
53
|
-
data, message
|
|
53
|
+
data, message, response=response
|
|
54
54
|
)
|
|
55
55
|
case "ValidationException":
|
|
56
56
|
raise capo_keyspacesstreams.errors.validation_exception.ValidationException.from_aws_json_1_0(
|
|
57
|
-
data, message
|
|
57
|
+
data, message, response=response
|
|
58
58
|
)
|
|
59
59
|
case _:
|
|
60
60
|
raise UnknownServiceError(code=code, message=message, response=response)
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Any,
|
|
12
|
+
Awaitable,
|
|
13
|
+
Callable,
|
|
14
|
+
Generic,
|
|
15
|
+
TypeVar,
|
|
16
|
+
cast,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from typing_extensions import Never
|
|
20
|
+
from zapros import (
|
|
21
|
+
AsyncClient,
|
|
22
|
+
Client,
|
|
23
|
+
ConnectionError,
|
|
24
|
+
ConnectTimeoutError,
|
|
25
|
+
PoolTimeoutError,
|
|
26
|
+
Response,
|
|
27
|
+
SSLError,
|
|
28
|
+
TimeoutError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from capo_keyspacesstreams._async import anysleep
|
|
32
|
+
from capo_keyspacesstreams._aws import (
|
|
33
|
+
KNOWN_RETRYABLE_CODES,
|
|
34
|
+
KNOWN_RETRYABLE_STATUSES,
|
|
35
|
+
KNOWN_THROTTLING_CODES,
|
|
36
|
+
KNOWN_THROTTLING_STATUSES,
|
|
37
|
+
)
|
|
38
|
+
from capo_keyspacesstreams._body import Body
|
|
39
|
+
from capo_keyspacesstreams._iter import StaticAnyIterator
|
|
40
|
+
from capo_keyspacesstreams.errors import ServiceError, UnknownServiceError
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from capo_keyspacesstreams._auth._identity import Credentials
|
|
44
|
+
from capo_keyspacesstreams._auth._providers import IdentityProvider
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("capo_keyspacesstreams.operation")
|
|
47
|
+
|
|
48
|
+
TInput = TypeVar("TInput")
|
|
49
|
+
TOutput = TypeVar("TOutput")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RetryQuota:
|
|
53
|
+
"""Retry tokens shared by all operations of one client: retries spend them, successes refund them."""
|
|
54
|
+
|
|
55
|
+
CAPACITY: int = 500
|
|
56
|
+
TRANSIENT_COST: int = 14
|
|
57
|
+
THROTTLING_COST: int = 5
|
|
58
|
+
NO_RETRY_INCREMENT: int = 1
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._available: int = self.CAPACITY
|
|
62
|
+
|
|
63
|
+
def acquire(self, is_throttling: bool) -> int | None:
|
|
64
|
+
cost = self.THROTTLING_COST if is_throttling else self.TRANSIENT_COST
|
|
65
|
+
if cost > self._available:
|
|
66
|
+
return None
|
|
67
|
+
self._available -= cost
|
|
68
|
+
return cost
|
|
69
|
+
|
|
70
|
+
def release(self, last_retry_cost: int | None) -> None:
|
|
71
|
+
refund = self.NO_RETRY_INCREMENT if last_retry_cost is None else last_retry_cost
|
|
72
|
+
self._available = min(self.CAPACITY, self._available + refund)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ThreadSafeRetryQuota(RetryQuota):
|
|
76
|
+
"""A ``RetryQuota`` that sync clients can share across threads."""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
super().__init__()
|
|
80
|
+
self._lock: threading.Lock = threading.Lock()
|
|
81
|
+
|
|
82
|
+
def acquire(self, is_throttling: bool) -> int | None:
|
|
83
|
+
with self._lock:
|
|
84
|
+
return super().acquire(is_throttling)
|
|
85
|
+
|
|
86
|
+
def release(self, last_retry_cost: int | None) -> None:
|
|
87
|
+
with self._lock:
|
|
88
|
+
super().release(last_retry_cost)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class OperationOptions:
|
|
93
|
+
client: Client
|
|
94
|
+
retry_quota: RetryQuota
|
|
95
|
+
use_fips: bool | None = None
|
|
96
|
+
endpoint: str | None = None
|
|
97
|
+
region: str | None = None
|
|
98
|
+
retry_max_attempts: int | None = None
|
|
99
|
+
credentials_provider: IdentityProvider[Credentials] | None = None
|
|
100
|
+
anonymous: bool | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class AsyncOperationOptions:
|
|
105
|
+
client: AsyncClient
|
|
106
|
+
retry_quota: RetryQuota
|
|
107
|
+
use_fips: bool | None = None
|
|
108
|
+
endpoint: str | None = None
|
|
109
|
+
region: str | None = None
|
|
110
|
+
retry_max_attempts: int | None = None
|
|
111
|
+
credentials_provider: IdentityProvider[Credentials] | None = None
|
|
112
|
+
anonymous: bool | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class OperationRequest(Generic[TInput]):
|
|
117
|
+
input: TInput
|
|
118
|
+
options: OperationOptions
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class AsyncOperationRequest(Generic[TInput]):
|
|
123
|
+
input: TInput
|
|
124
|
+
options: AsyncOperationOptions
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class OperationResponse(Generic[TOutput]):
|
|
129
|
+
output: TOutput
|
|
130
|
+
response: Response
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class AsyncOperationResponse(Generic[TOutput]):
|
|
135
|
+
output: TOutput
|
|
136
|
+
response: Response
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
NextFn = Callable[
|
|
140
|
+
[OperationRequest[TInput]],
|
|
141
|
+
OperationResponse[TOutput],
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
AsyncNextFn = Callable[
|
|
145
|
+
[AsyncOperationRequest[TInput]],
|
|
146
|
+
Awaitable[AsyncOperationResponse[TOutput]],
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
Interceptor = Callable[
|
|
150
|
+
[OperationRequest[TInput], NextFn[TInput, TOutput]],
|
|
151
|
+
OperationResponse[TOutput],
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
AsyncInterceptor = Callable[
|
|
155
|
+
[AsyncOperationRequest[TInput], AsyncNextFn[TInput, TOutput]],
|
|
156
|
+
Awaitable[AsyncOperationResponse[TOutput]],
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class OperationLogging:
|
|
162
|
+
"""What the logging interceptor needs to know about one operation call."""
|
|
163
|
+
|
|
164
|
+
operation: str
|
|
165
|
+
# Return a copy that is safe to log; None when the shape has nothing to hide.
|
|
166
|
+
redact_input: Callable[[Any], dict[str, Any]] | None = None
|
|
167
|
+
redact_output: Callable[[Any], dict[str, Any]] | None = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def execute_pipeline(
|
|
171
|
+
request: OperationRequest[TInput],
|
|
172
|
+
handler: NextFn[TInput, TOutput],
|
|
173
|
+
interceptors: Sequence[Interceptor[TInput, TOutput]],
|
|
174
|
+
) -> OperationResponse[TOutput]:
|
|
175
|
+
def make_chain(index: int) -> NextFn[TInput, TOutput]:
|
|
176
|
+
if index < len(interceptors):
|
|
177
|
+
interceptor = interceptors[index]
|
|
178
|
+
|
|
179
|
+
def next_fn(req: OperationRequest[TInput]) -> OperationResponse[TOutput]:
|
|
180
|
+
return interceptor(req, make_chain(index + 1))
|
|
181
|
+
|
|
182
|
+
return next_fn
|
|
183
|
+
return handler
|
|
184
|
+
|
|
185
|
+
return make_chain(0)(request)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def aexecute_pipeline(
|
|
189
|
+
request: AsyncOperationRequest[TInput],
|
|
190
|
+
handler: AsyncNextFn[TInput, TOutput],
|
|
191
|
+
interceptors: Sequence[AsyncInterceptor[TInput, TOutput]],
|
|
192
|
+
) -> AsyncOperationResponse[TOutput]:
|
|
193
|
+
def make_chain(index: int) -> AsyncNextFn[TInput, TOutput]:
|
|
194
|
+
if index < len(interceptors):
|
|
195
|
+
interceptor = interceptors[index]
|
|
196
|
+
|
|
197
|
+
async def next_fn(
|
|
198
|
+
req: AsyncOperationRequest[TInput],
|
|
199
|
+
) -> AsyncOperationResponse[TOutput]:
|
|
200
|
+
return await interceptor(req, make_chain(index + 1))
|
|
201
|
+
|
|
202
|
+
return next_fn
|
|
203
|
+
return handler
|
|
204
|
+
|
|
205
|
+
return await make_chain(0)(request)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def raise_error(response: Response, handle_error: Callable[[Response], Never]) -> Never:
|
|
209
|
+
try:
|
|
210
|
+
handle_error(response)
|
|
211
|
+
except ServiceError as exc:
|
|
212
|
+
_apply_retry_rules(exc, response)
|
|
213
|
+
raise
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
# The error body could not be decoded (an HTML page from a proxy, a
|
|
216
|
+
# truncated read): the status code is all there is to go on.
|
|
217
|
+
error = UnknownServiceError(code=None, message=None, response=response)
|
|
218
|
+
_apply_retry_rules(error, response)
|
|
219
|
+
raise error from exc
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _apply_retry_rules(exc: ServiceError, response: Response) -> None:
|
|
223
|
+
# A retry can be safe even when the error is not marked @retryable: a known
|
|
224
|
+
# throttling or transient status or error code, or a Retry-After header.
|
|
225
|
+
if (
|
|
226
|
+
response.status in KNOWN_THROTTLING_STATUSES
|
|
227
|
+
or exc.code in KNOWN_THROTTLING_CODES
|
|
228
|
+
):
|
|
229
|
+
exc.is_throttling_error = True
|
|
230
|
+
exc.is_retryable = True
|
|
231
|
+
if (
|
|
232
|
+
response.status in KNOWN_RETRYABLE_STATUSES
|
|
233
|
+
or exc.code in KNOWN_RETRYABLE_CODES
|
|
234
|
+
or "retry-after" in response.headers
|
|
235
|
+
):
|
|
236
|
+
exc.is_retryable = True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _is_retryable(exc: Exception) -> bool:
|
|
240
|
+
if isinstance(exc, ServiceError):
|
|
241
|
+
return exc.is_retryable
|
|
242
|
+
if isinstance(exc, SSLError):
|
|
243
|
+
return False
|
|
244
|
+
if isinstance(exc, (ConnectionError, TimeoutError)):
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_pre_transmission(exc: Exception) -> bool:
|
|
250
|
+
# True when the request body was certainly not written before the error,
|
|
251
|
+
# so the same stream can be reused as-is. A bare ConnectionError is
|
|
252
|
+
# ambiguous ("cannot be established or is lost"), so it does not qualify.
|
|
253
|
+
return isinstance(exc, (ConnectTimeoutError, PoolTimeoutError))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _prepare_retry(inp: object) -> bool:
|
|
257
|
+
if not isinstance(inp, dict):
|
|
258
|
+
return True
|
|
259
|
+
for value in inp.values():
|
|
260
|
+
if isinstance(value, Body):
|
|
261
|
+
value = cast(Body[Iterator[bytes]], value)
|
|
262
|
+
if value.rebuild() is None:
|
|
263
|
+
return False
|
|
264
|
+
elif isinstance(value, StaticAnyIterator):
|
|
265
|
+
continue
|
|
266
|
+
elif isinstance(value, (Iterator, AsyncIterator)):
|
|
267
|
+
return False
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def _aprepare_retry(inp: object) -> bool:
|
|
272
|
+
if not isinstance(inp, dict):
|
|
273
|
+
return True
|
|
274
|
+
for value in inp.values():
|
|
275
|
+
if isinstance(value, Body):
|
|
276
|
+
value = cast(Body[AsyncIterator[bytes]], value)
|
|
277
|
+
if await value.arebuild() is None:
|
|
278
|
+
return False
|
|
279
|
+
elif isinstance(value, StaticAnyIterator):
|
|
280
|
+
continue
|
|
281
|
+
elif isinstance(value, (Iterator, AsyncIterator)):
|
|
282
|
+
return False
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _retry_delay(attempt: int, is_throttling: bool) -> float:
|
|
287
|
+
base = 1.0 if is_throttling else 0.5
|
|
288
|
+
delay = base * (2.0 ** min(attempt - 1, 10))
|
|
289
|
+
delay = min(20.0, delay)
|
|
290
|
+
return random.uniform(0.0, delay)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def retry() -> Interceptor[TInput, TOutput]:
|
|
294
|
+
def interceptor(
|
|
295
|
+
request: OperationRequest[TInput], next: NextFn[TInput, TOutput]
|
|
296
|
+
) -> OperationResponse[TOutput]:
|
|
297
|
+
max_attempts = request.options.retry_max_attempts or 3
|
|
298
|
+
quota = request.options.retry_quota
|
|
299
|
+
last_retry_cost: int | None = None
|
|
300
|
+
for attempt in range(1, max_attempts + 1):
|
|
301
|
+
try:
|
|
302
|
+
response = next(request)
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
if not _is_retryable(exc) or attempt == max_attempts:
|
|
305
|
+
raise
|
|
306
|
+
if not _is_pre_transmission(exc) and not _prepare_retry(request.input):
|
|
307
|
+
logger.debug(
|
|
308
|
+
"attempt %d/%d failed: %r, not retrying because the request body cannot be replayed",
|
|
309
|
+
attempt,
|
|
310
|
+
max_attempts,
|
|
311
|
+
exc,
|
|
312
|
+
extra={
|
|
313
|
+
"attempt": attempt,
|
|
314
|
+
"max_attempts": max_attempts,
|
|
315
|
+
"error": exc,
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
raise
|
|
319
|
+
is_throttling = (
|
|
320
|
+
isinstance(exc, ServiceError) and exc.is_throttling_error
|
|
321
|
+
)
|
|
322
|
+
cost = quota.acquire(is_throttling)
|
|
323
|
+
if cost is None:
|
|
324
|
+
logger.debug(
|
|
325
|
+
"attempt %d/%d failed: %r, not retrying because the retry quota is exhausted",
|
|
326
|
+
attempt,
|
|
327
|
+
max_attempts,
|
|
328
|
+
exc,
|
|
329
|
+
extra={
|
|
330
|
+
"attempt": attempt,
|
|
331
|
+
"max_attempts": max_attempts,
|
|
332
|
+
"error": exc,
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
raise
|
|
336
|
+
last_retry_cost = cost
|
|
337
|
+
delay = _retry_delay(attempt, is_throttling)
|
|
338
|
+
logger.debug(
|
|
339
|
+
"attempt %d/%d failed: %r, retrying in %.2fs",
|
|
340
|
+
attempt,
|
|
341
|
+
max_attempts,
|
|
342
|
+
exc,
|
|
343
|
+
delay,
|
|
344
|
+
extra={
|
|
345
|
+
"attempt": attempt,
|
|
346
|
+
"max_attempts": max_attempts,
|
|
347
|
+
"error": exc,
|
|
348
|
+
"delay": delay,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
time.sleep(delay)
|
|
352
|
+
else:
|
|
353
|
+
quota.release(last_retry_cost)
|
|
354
|
+
return response
|
|
355
|
+
raise AssertionError("unreachable")
|
|
356
|
+
|
|
357
|
+
return interceptor
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def aretry() -> AsyncInterceptor[TInput, TOutput]:
|
|
361
|
+
async def interceptor(
|
|
362
|
+
request: AsyncOperationRequest[TInput], next: AsyncNextFn[TInput, TOutput]
|
|
363
|
+
) -> AsyncOperationResponse[TOutput]:
|
|
364
|
+
max_attempts = request.options.retry_max_attempts or 3
|
|
365
|
+
quota = request.options.retry_quota
|
|
366
|
+
last_retry_cost: int | None = None
|
|
367
|
+
for attempt in range(1, max_attempts + 1):
|
|
368
|
+
try:
|
|
369
|
+
response = await next(request)
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
if not _is_retryable(exc) or attempt == max_attempts:
|
|
372
|
+
raise
|
|
373
|
+
if not _is_pre_transmission(exc) and not await _aprepare_retry(
|
|
374
|
+
request.input
|
|
375
|
+
):
|
|
376
|
+
logger.debug(
|
|
377
|
+
"attempt %d/%d failed: %r, not retrying because the request body cannot be replayed",
|
|
378
|
+
attempt,
|
|
379
|
+
max_attempts,
|
|
380
|
+
exc,
|
|
381
|
+
extra={
|
|
382
|
+
"attempt": attempt,
|
|
383
|
+
"max_attempts": max_attempts,
|
|
384
|
+
"error": exc,
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
raise
|
|
388
|
+
is_throttling = (
|
|
389
|
+
isinstance(exc, ServiceError) and exc.is_throttling_error
|
|
390
|
+
)
|
|
391
|
+
cost = quota.acquire(is_throttling)
|
|
392
|
+
if cost is None:
|
|
393
|
+
logger.debug(
|
|
394
|
+
"attempt %d/%d failed: %r, not retrying because the retry quota is exhausted",
|
|
395
|
+
attempt,
|
|
396
|
+
max_attempts,
|
|
397
|
+
exc,
|
|
398
|
+
extra={
|
|
399
|
+
"attempt": attempt,
|
|
400
|
+
"max_attempts": max_attempts,
|
|
401
|
+
"error": exc,
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
raise
|
|
405
|
+
last_retry_cost = cost
|
|
406
|
+
delay = _retry_delay(attempt, is_throttling)
|
|
407
|
+
logger.debug(
|
|
408
|
+
"attempt %d/%d failed: %r, retrying in %.2fs",
|
|
409
|
+
attempt,
|
|
410
|
+
max_attempts,
|
|
411
|
+
exc,
|
|
412
|
+
delay,
|
|
413
|
+
extra={
|
|
414
|
+
"attempt": attempt,
|
|
415
|
+
"max_attempts": max_attempts,
|
|
416
|
+
"error": exc,
|
|
417
|
+
"delay": delay,
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
await anysleep(delay)
|
|
421
|
+
else:
|
|
422
|
+
quota.release(last_retry_cost)
|
|
423
|
+
return response
|
|
424
|
+
raise AssertionError("unreachable")
|
|
425
|
+
|
|
426
|
+
return interceptor
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def log_operation(operation_logging: OperationLogging) -> Interceptor[TInput, TOutput]:
|
|
430
|
+
operation = operation_logging.operation
|
|
431
|
+
redact_input = operation_logging.redact_input
|
|
432
|
+
redact_output = operation_logging.redact_output
|
|
433
|
+
|
|
434
|
+
def interceptor(
|
|
435
|
+
request: OperationRequest[TInput], next: NextFn[TInput, TOutput]
|
|
436
|
+
) -> OperationResponse[TOutput]:
|
|
437
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
438
|
+
if debug:
|
|
439
|
+
input_ = (
|
|
440
|
+
request.input if redact_input is None else redact_input(request.input)
|
|
441
|
+
)
|
|
442
|
+
logger.debug(
|
|
443
|
+
"%s input: %r",
|
|
444
|
+
operation,
|
|
445
|
+
input_,
|
|
446
|
+
extra={"operation": operation, "input": input_},
|
|
447
|
+
)
|
|
448
|
+
try:
|
|
449
|
+
response = next(request)
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
logger.debug(
|
|
452
|
+
"%s failed: %r",
|
|
453
|
+
operation,
|
|
454
|
+
exc,
|
|
455
|
+
extra={"operation": operation, "error": exc},
|
|
456
|
+
)
|
|
457
|
+
raise
|
|
458
|
+
if debug:
|
|
459
|
+
output = (
|
|
460
|
+
response.output
|
|
461
|
+
if redact_output is None
|
|
462
|
+
else redact_output(response.output)
|
|
463
|
+
)
|
|
464
|
+
logger.debug(
|
|
465
|
+
"%s output: %r",
|
|
466
|
+
operation,
|
|
467
|
+
output,
|
|
468
|
+
extra={"operation": operation, "output": output},
|
|
469
|
+
)
|
|
470
|
+
return response
|
|
471
|
+
|
|
472
|
+
return interceptor
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def alog_operation(
|
|
476
|
+
operation_logging: OperationLogging,
|
|
477
|
+
) -> AsyncInterceptor[TInput, TOutput]:
|
|
478
|
+
operation = operation_logging.operation
|
|
479
|
+
redact_input = operation_logging.redact_input
|
|
480
|
+
redact_output = operation_logging.redact_output
|
|
481
|
+
|
|
482
|
+
async def interceptor(
|
|
483
|
+
request: AsyncOperationRequest[TInput], next: AsyncNextFn[TInput, TOutput]
|
|
484
|
+
) -> AsyncOperationResponse[TOutput]:
|
|
485
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
486
|
+
if debug:
|
|
487
|
+
input_ = (
|
|
488
|
+
request.input if redact_input is None else redact_input(request.input)
|
|
489
|
+
)
|
|
490
|
+
logger.debug(
|
|
491
|
+
"%s input: %r",
|
|
492
|
+
operation,
|
|
493
|
+
input_,
|
|
494
|
+
extra={"operation": operation, "input": input_},
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
response = await next(request)
|
|
498
|
+
except Exception as exc:
|
|
499
|
+
logger.debug(
|
|
500
|
+
"%s failed: %r",
|
|
501
|
+
operation,
|
|
502
|
+
exc,
|
|
503
|
+
extra={"operation": operation, "error": exc},
|
|
504
|
+
)
|
|
505
|
+
raise
|
|
506
|
+
if debug:
|
|
507
|
+
output = (
|
|
508
|
+
response.output
|
|
509
|
+
if redact_output is None
|
|
510
|
+
else redact_output(response.output)
|
|
511
|
+
)
|
|
512
|
+
logger.debug(
|
|
513
|
+
"%s output: %r",
|
|
514
|
+
operation,
|
|
515
|
+
output,
|
|
516
|
+
extra={"operation": operation, "output": output},
|
|
517
|
+
)
|
|
518
|
+
return response
|
|
519
|
+
|
|
520
|
+
return interceptor
|