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.
Files changed (95) hide show
  1. {capo_keyspacesstreams-0.5.0/src/capo_keyspacesstreams.egg-info → capo_keyspacesstreams-0.6.0}/PKG-INFO +1 -1
  2. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/pyproject.toml +1 -1
  3. capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams/_aws.py +28 -0
  4. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_records.py +5 -5
  5. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_shard_iterator.py +5 -5
  6. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/get_stream.py +5 -5
  7. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_operations/keyspaces_streams/list_streams.py +5 -5
  8. capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams/_services/_pipeline.py +520 -0
  9. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/async_keyspaces_streams.py +24 -6
  10. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/keyspaces_streams.py +24 -6
  11. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/_base.py +8 -3
  12. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/access_denied_exception.py +11 -3
  13. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/internal_server_exception.py +11 -3
  14. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/resource_not_found_exception.py +11 -3
  15. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/throttling_exception.py +11 -3
  16. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/validation_exception.py +11 -3
  17. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0/src/capo_keyspacesstreams.egg-info}/PKG-INFO +1 -1
  18. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/SOURCES.txt +1 -0
  19. capo_keyspacesstreams-0.5.0/src/capo_keyspacesstreams/_services/_pipeline.py +0 -264
  20. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/LICENSE +0 -0
  21. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/README.md +0 -0
  22. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/setup.cfg +0 -0
  23. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/__init__.py +0 -0
  24. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_async.py +0 -0
  25. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_identity.py +0 -0
  26. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_providers.py +0 -0
  27. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_signers.py +0 -0
  28. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_sigv4.py +0 -0
  29. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_auth/_zapros_handler.py +0 -0
  30. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_body.py +0 -0
  31. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_iter.py +0 -0
  32. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_pagination.py +0 -0
  33. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/__init__.py +0 -0
  34. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/errors.py +0 -0
  35. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/eventstream.py +0 -0
  36. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/serialize.py +0 -0
  37. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_protocol/xml.py +0 -0
  38. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/__init__.py +0 -0
  39. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_aws_partition.py +0 -0
  40. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_endpoint_rule_set.py +0 -0
  41. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_rule_engine/_endpoint_runtime.py +0 -0
  42. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/_services/_aws_config.py +0 -0
  43. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/errors/__init__.py +0 -0
  44. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/py.typed +0 -0
  45. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/_prelude/blob.py +0 -0
  46. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/_prelude/timestamp.py +0 -0
  47. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/date.py +0 -0
  48. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_records_input.py +0 -0
  49. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_records_output.py +0 -0
  50. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_shard_iterator_input.py +0 -0
  51. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_shard_iterator_output.py +0 -0
  52. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_stream_input.py +0 -0
  53. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/get_stream_output.py +0 -0
  54. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/iterator_description.py +0 -0
  55. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/iterator_position.py +0 -0
  56. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspace_name.py +0 -0
  57. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell.py +0 -0
  58. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_list.py +0 -0
  59. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_map.py +0 -0
  60. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_map_definition.py +0 -0
  61. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cell_value.py +0 -0
  62. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_cells.py +0 -0
  63. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_keys_map.py +0 -0
  64. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_metadata.py +0 -0
  65. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_row.py +0 -0
  66. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/keyspaces_udt_map.py +0 -0
  67. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/list_streams_input.py +0 -0
  68. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/list_streams_output.py +0 -0
  69. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/origin_type.py +0 -0
  70. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/record.py +0 -0
  71. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/record_list.py +0 -0
  72. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/sequence_number.py +0 -0
  73. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/sequence_number_range.py +0 -0
  74. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard.py +0 -0
  75. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_description_list.py +0 -0
  76. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_filter.py +0 -0
  77. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_filter_type.py +0 -0
  78. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id.py +0 -0
  79. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id_list.py +0 -0
  80. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_id_token.py +0 -0
  81. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_iterator.py +0 -0
  82. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/shard_iterator_type.py +0 -0
  83. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream.py +0 -0
  84. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_arn.py +0 -0
  85. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_arn_token.py +0 -0
  86. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_list.py +0 -0
  87. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_status.py +0 -0
  88. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/stream_view_type.py +0 -0
  89. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/table_name.py +0 -0
  90. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams/types/validation_exception_type.py +0 -0
  91. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/dependency_links.txt +0 -0
  92. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/requires.txt +0 -0
  93. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/src/capo_keyspacesstreams.egg-info/top_level.txt +0 -0
  94. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/tests/test_body.py +0 -0
  95. {capo_keyspacesstreams-0.5.0 → capo_keyspacesstreams-0.6.0}/tests/test_sigv4.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capo-keyspacesstreams
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Python SDK for AWS Keyspacesstreams.
5
5
  Classifier: Development Status :: 4 - Beta
6
6
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capo-keyspacesstreams"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Python SDK for AWS Keyspacesstreams."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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