airbyte-cdk 6.5.3rc2__py3-none-any.whl → 6.6.0__py3-none-any.whl
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.
- airbyte_cdk/__init__.py +17 -2
- airbyte_cdk/config_observation.py +10 -3
- airbyte_cdk/connector.py +19 -9
- airbyte_cdk/connector_builder/connector_builder_handler.py +28 -8
- airbyte_cdk/connector_builder/main.py +26 -6
- airbyte_cdk/connector_builder/message_grouper.py +95 -25
- airbyte_cdk/destinations/destination.py +47 -14
- airbyte_cdk/destinations/vector_db_based/config.py +36 -14
- airbyte_cdk/destinations/vector_db_based/document_processor.py +49 -11
- airbyte_cdk/destinations/vector_db_based/embedder.py +52 -11
- airbyte_cdk/destinations/vector_db_based/test_utils.py +14 -4
- airbyte_cdk/destinations/vector_db_based/utils.py +8 -2
- airbyte_cdk/destinations/vector_db_based/writer.py +15 -4
- airbyte_cdk/entrypoint.py +82 -26
- airbyte_cdk/exception_handler.py +13 -3
- airbyte_cdk/logger.py +10 -2
- airbyte_cdk/models/airbyte_protocol.py +11 -5
- airbyte_cdk/models/airbyte_protocol_serializers.py +9 -3
- airbyte_cdk/models/well_known_types.py +1 -1
- airbyte_cdk/sources/abstract_source.py +63 -17
- airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py +47 -14
- airbyte_cdk/sources/concurrent_source/concurrent_source.py +25 -7
- airbyte_cdk/sources/concurrent_source/concurrent_source_adapter.py +27 -6
- airbyte_cdk/sources/concurrent_source/thread_pool_manager.py +9 -3
- airbyte_cdk/sources/connector_state_manager.py +32 -10
- airbyte_cdk/sources/declarative/async_job/job.py +3 -1
- airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +68 -14
- airbyte_cdk/sources/declarative/async_job/job_tracker.py +24 -6
- airbyte_cdk/sources/declarative/async_job/repository.py +3 -1
- airbyte_cdk/sources/declarative/auth/declarative_authenticator.py +3 -1
- airbyte_cdk/sources/declarative/auth/jwt.py +27 -7
- airbyte_cdk/sources/declarative/auth/oauth.py +35 -11
- airbyte_cdk/sources/declarative/auth/selective_authenticator.py +3 -1
- airbyte_cdk/sources/declarative/auth/token.py +25 -8
- airbyte_cdk/sources/declarative/checks/check_stream.py +12 -4
- airbyte_cdk/sources/declarative/checks/connection_checker.py +3 -1
- airbyte_cdk/sources/declarative/concurrency_level/concurrency_level.py +11 -3
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +106 -50
- airbyte_cdk/sources/declarative/datetime/min_max_datetime.py +20 -6
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml +43 -0
- airbyte_cdk/sources/declarative/declarative_source.py +3 -1
- airbyte_cdk/sources/declarative/declarative_stream.py +27 -6
- airbyte_cdk/sources/declarative/decoders/__init__.py +2 -2
- airbyte_cdk/sources/declarative/decoders/decoder.py +3 -1
- airbyte_cdk/sources/declarative/decoders/json_decoder.py +48 -13
- airbyte_cdk/sources/declarative/decoders/pagination_decoder_decorator.py +3 -1
- airbyte_cdk/sources/declarative/decoders/xml_decoder.py +6 -2
- airbyte_cdk/sources/declarative/extractors/dpath_extractor.py +6 -2
- airbyte_cdk/sources/declarative/extractors/record_filter.py +24 -7
- airbyte_cdk/sources/declarative/extractors/record_selector.py +10 -3
- airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py +15 -5
- airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +96 -31
- airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +22 -8
- airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +46 -15
- airbyte_cdk/sources/declarative/incremental/per_partition_with_global.py +19 -5
- airbyte_cdk/sources/declarative/incremental/resumable_full_refresh_cursor.py +3 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_boolean.py +20 -2
- airbyte_cdk/sources/declarative/interpolation/interpolated_mapping.py +5 -1
- airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py +10 -3
- airbyte_cdk/sources/declarative/interpolation/interpolated_string.py +6 -2
- airbyte_cdk/sources/declarative/interpolation/interpolation.py +7 -1
- airbyte_cdk/sources/declarative/interpolation/jinja.py +6 -2
- airbyte_cdk/sources/declarative/interpolation/macros.py +19 -4
- airbyte_cdk/sources/declarative/manifest_declarative_source.py +106 -24
- airbyte_cdk/sources/declarative/migrations/legacy_to_per_partition_state_migration.py +14 -5
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py +697 -678
- airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py +13 -4
- airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py +9 -2
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +802 -232
- airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py +29 -7
- airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +25 -7
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +54 -15
- airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/constant_backoff_strategy.py +6 -2
- airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/header_helper.py +3 -1
- airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_time_from_header_backoff_strategy.py +17 -5
- airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_until_time_from_header_backoff_strategy.py +15 -5
- airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +3 -1
- airbyte_cdk/sources/declarative/requesters/error_handlers/default_error_handler.py +18 -8
- airbyte_cdk/sources/declarative/requesters/error_handlers/default_http_response_filter.py +16 -7
- airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +51 -14
- airbyte_cdk/sources/declarative/requesters/http_job_repository.py +29 -8
- airbyte_cdk/sources/declarative/requesters/http_requester.py +58 -16
- airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +49 -14
- airbyte_cdk/sources/declarative/requesters/paginators/no_pagination.py +3 -1
- airbyte_cdk/sources/declarative/requesters/paginators/paginator.py +3 -1
- airbyte_cdk/sources/declarative/requesters/paginators/strategies/cursor_pagination_strategy.py +17 -5
- airbyte_cdk/sources/declarative/requesters/paginators/strategies/offset_increment.py +24 -7
- airbyte_cdk/sources/declarative/requesters/paginators/strategies/page_increment.py +9 -3
- airbyte_cdk/sources/declarative/requesters/paginators/strategies/pagination_strategy.py +3 -1
- airbyte_cdk/sources/declarative/requesters/paginators/strategies/stop_condition.py +6 -2
- airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +19 -6
- airbyte_cdk/sources/declarative/requesters/request_options/default_request_options_provider.py +3 -1
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +21 -7
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +18 -6
- airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +27 -8
- airbyte_cdk/sources/declarative/requesters/requester.py +3 -1
- airbyte_cdk/sources/declarative/retrievers/async_retriever.py +12 -5
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +105 -24
- airbyte_cdk/sources/declarative/schema/default_schema_loader.py +3 -1
- airbyte_cdk/sources/declarative/spec/spec.py +8 -2
- airbyte_cdk/sources/declarative/stream_slicers/stream_slicer.py +3 -1
- airbyte_cdk/sources/declarative/transformations/add_fields.py +12 -3
- airbyte_cdk/sources/declarative/transformations/remove_fields.py +6 -2
- airbyte_cdk/sources/declarative/types.py +8 -1
- airbyte_cdk/sources/declarative/yaml_declarative_source.py +3 -1
- airbyte_cdk/sources/embedded/base_integration.py +14 -4
- airbyte_cdk/sources/embedded/catalog.py +16 -4
- airbyte_cdk/sources/embedded/runner.py +19 -3
- airbyte_cdk/sources/embedded/tools.py +3 -1
- airbyte_cdk/sources/file_based/availability_strategy/abstract_file_based_availability_strategy.py +12 -4
- airbyte_cdk/sources/file_based/availability_strategy/default_file_based_availability_strategy.py +27 -7
- airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +12 -6
- airbyte_cdk/sources/file_based/config/csv_format.py +21 -9
- airbyte_cdk/sources/file_based/config/file_based_stream_config.py +6 -2
- airbyte_cdk/sources/file_based/config/unstructured_format.py +10 -3
- airbyte_cdk/sources/file_based/discovery_policy/abstract_discovery_policy.py +2 -4
- airbyte_cdk/sources/file_based/discovery_policy/default_discovery_policy.py +7 -2
- airbyte_cdk/sources/file_based/exceptions.py +13 -15
- airbyte_cdk/sources/file_based/file_based_source.py +82 -24
- airbyte_cdk/sources/file_based/file_based_stream_reader.py +16 -5
- airbyte_cdk/sources/file_based/file_types/avro_parser.py +58 -17
- airbyte_cdk/sources/file_based/file_types/csv_parser.py +89 -26
- airbyte_cdk/sources/file_based/file_types/excel_parser.py +25 -7
- airbyte_cdk/sources/file_based/file_types/file_transfer.py +8 -2
- airbyte_cdk/sources/file_based/file_types/file_type_parser.py +4 -1
- airbyte_cdk/sources/file_based/file_types/jsonl_parser.py +20 -6
- airbyte_cdk/sources/file_based/file_types/parquet_parser.py +57 -16
- airbyte_cdk/sources/file_based/file_types/unstructured_parser.py +64 -15
- airbyte_cdk/sources/file_based/schema_helpers.py +33 -10
- airbyte_cdk/sources/file_based/schema_validation_policies/abstract_schema_validation_policy.py +3 -1
- airbyte_cdk/sources/file_based/schema_validation_policies/default_schema_validation_policies.py +16 -5
- airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py +33 -10
- airbyte_cdk/sources/file_based/stream/concurrent/adapters.py +47 -11
- airbyte_cdk/sources/file_based/stream/concurrent/cursor/abstract_concurrent_file_based_cursor.py +13 -22
- airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_concurrent_cursor.py +53 -17
- airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_final_state_cursor.py +17 -5
- airbyte_cdk/sources/file_based/stream/cursor/abstract_file_based_cursor.py +3 -1
- airbyte_cdk/sources/file_based/stream/cursor/default_file_based_cursor.py +26 -9
- airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +67 -21
- airbyte_cdk/sources/http_logger.py +5 -1
- airbyte_cdk/sources/message/repository.py +18 -4
- airbyte_cdk/sources/source.py +17 -7
- airbyte_cdk/sources/streams/availability_strategy.py +9 -3
- airbyte_cdk/sources/streams/call_rate.py +63 -19
- airbyte_cdk/sources/streams/checkpoint/checkpoint_reader.py +31 -7
- airbyte_cdk/sources/streams/checkpoint/substream_resumable_full_refresh_cursor.py +6 -2
- airbyte_cdk/sources/streams/concurrent/adapters.py +77 -22
- airbyte_cdk/sources/streams/concurrent/cursor.py +56 -20
- airbyte_cdk/sources/streams/concurrent/default_stream.py +9 -2
- airbyte_cdk/sources/streams/concurrent/helpers.py +6 -2
- airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py +9 -2
- airbyte_cdk/sources/streams/concurrent/partition_reader.py +4 -1
- airbyte_cdk/sources/streams/concurrent/partitions/record.py +10 -2
- airbyte_cdk/sources/streams/concurrent/partitions/types.py +6 -2
- airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py +25 -10
- airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +32 -16
- airbyte_cdk/sources/streams/core.py +77 -22
- airbyte_cdk/sources/streams/http/availability_strategy.py +3 -1
- airbyte_cdk/sources/streams/http/error_handlers/default_error_mapping.py +4 -1
- airbyte_cdk/sources/streams/http/error_handlers/error_handler.py +3 -1
- airbyte_cdk/sources/streams/http/error_handlers/http_status_error_handler.py +16 -5
- airbyte_cdk/sources/streams/http/error_handlers/response_models.py +9 -3
- airbyte_cdk/sources/streams/http/exceptions.py +2 -2
- airbyte_cdk/sources/streams/http/http.py +133 -33
- airbyte_cdk/sources/streams/http/http_client.py +91 -29
- airbyte_cdk/sources/streams/http/rate_limiting.py +23 -7
- airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +19 -6
- airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +38 -11
- airbyte_cdk/sources/streams/http/requests_native_auth/token.py +13 -3
- airbyte_cdk/sources/types.py +5 -1
- airbyte_cdk/sources/utils/record_helper.py +12 -3
- airbyte_cdk/sources/utils/schema_helpers.py +9 -3
- airbyte_cdk/sources/utils/slice_logger.py +4 -1
- airbyte_cdk/sources/utils/transform.py +24 -9
- airbyte_cdk/sql/exceptions.py +19 -6
- airbyte_cdk/sql/secrets.py +3 -1
- airbyte_cdk/sql/shared/catalog_providers.py +13 -4
- airbyte_cdk/sql/shared/sql_processor.py +44 -14
- airbyte_cdk/test/catalog_builder.py +19 -8
- airbyte_cdk/test/entrypoint_wrapper.py +27 -8
- airbyte_cdk/test/mock_http/mocker.py +41 -11
- airbyte_cdk/test/mock_http/request.py +9 -3
- airbyte_cdk/test/mock_http/response.py +3 -1
- airbyte_cdk/test/mock_http/response_builder.py +29 -7
- airbyte_cdk/test/state_builder.py +10 -2
- airbyte_cdk/test/utils/data.py +6 -2
- airbyte_cdk/test/utils/http_mocking.py +3 -1
- airbyte_cdk/utils/airbyte_secrets_utils.py +3 -1
- airbyte_cdk/utils/analytics_message.py +10 -2
- airbyte_cdk/utils/datetime_format_inferrer.py +4 -1
- airbyte_cdk/utils/mapping_helpers.py +3 -1
- airbyte_cdk/utils/message_utils.py +11 -4
- airbyte_cdk/utils/print_buffer.py +6 -1
- airbyte_cdk/utils/schema_inferrer.py +30 -9
- airbyte_cdk/utils/spec_schema_transformations.py +3 -1
- airbyte_cdk/utils/traced_exception.py +35 -9
- {airbyte_cdk-6.5.3rc2.dist-info → airbyte_cdk-6.6.0.dist-info}/METADATA +8 -7
- {airbyte_cdk-6.5.3rc2.dist-info → airbyte_cdk-6.6.0.dist-info}/RECORD +200 -200
- {airbyte_cdk-6.5.3rc2.dist-info → airbyte_cdk-6.6.0.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.5.3rc2.dist-info → airbyte_cdk-6.6.0.dist-info}/WHEEL +0 -0
@@ -14,11 +14,22 @@ from airbyte_cdk.models import Type as MessageType
|
|
14
14
|
from airbyte_cdk.sources.message.repository import InMemoryMessageRepository
|
15
15
|
from airbyte_cdk.sources.streams.call_rate import APIBudget
|
16
16
|
from airbyte_cdk.sources.streams.checkpoint.cursor import Cursor
|
17
|
-
from airbyte_cdk.sources.streams.checkpoint.resumable_full_refresh_cursor import
|
18
|
-
|
17
|
+
from airbyte_cdk.sources.streams.checkpoint.resumable_full_refresh_cursor import (
|
18
|
+
ResumableFullRefreshCursor,
|
19
|
+
)
|
20
|
+
from airbyte_cdk.sources.streams.checkpoint.substream_resumable_full_refresh_cursor import (
|
21
|
+
SubstreamResumableFullRefreshCursor,
|
22
|
+
)
|
19
23
|
from airbyte_cdk.sources.streams.core import CheckpointMixin, Stream, StreamData
|
20
|
-
from airbyte_cdk.sources.streams.http.error_handlers import
|
21
|
-
|
24
|
+
from airbyte_cdk.sources.streams.http.error_handlers import (
|
25
|
+
BackoffStrategy,
|
26
|
+
ErrorHandler,
|
27
|
+
HttpStatusErrorHandler,
|
28
|
+
)
|
29
|
+
from airbyte_cdk.sources.streams.http.error_handlers.response_models import (
|
30
|
+
ErrorResolution,
|
31
|
+
ResponseAction,
|
32
|
+
)
|
22
33
|
from airbyte_cdk.sources.streams.http.http_client import HttpClient
|
23
34
|
from airbyte_cdk.sources.types import Record, StreamSlice
|
24
35
|
from airbyte_cdk.sources.utils.types import JsonType
|
@@ -35,9 +46,13 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
35
46
|
"""
|
36
47
|
|
37
48
|
source_defined_cursor = True # Most HTTP streams use a source defined cursor (i.e: the user can't configure it like on a SQL table)
|
38
|
-
page_size: Optional[int] =
|
49
|
+
page_size: Optional[int] = (
|
50
|
+
None # Use this variable to define page size for API http requests with pagination support
|
51
|
+
)
|
39
52
|
|
40
|
-
def __init__(
|
53
|
+
def __init__(
|
54
|
+
self, authenticator: Optional[AuthBase] = None, api_budget: Optional[APIBudget] = None
|
55
|
+
):
|
41
56
|
self._exit_on_rate_limit: bool = False
|
42
57
|
self._http_client = HttpClient(
|
43
58
|
name=self.name,
|
@@ -55,7 +70,11 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
55
70
|
# 2. Streams with at least one cursor_field are incremental and thus a superior sync to RFR.
|
56
71
|
# 3. Streams overriding read_records() do not guarantee that they will call the parent implementation which can perform
|
57
72
|
# per-page checkpointing so RFR is only supported if a stream use the default `HttpStream.read_records()` method
|
58
|
-
if
|
73
|
+
if (
|
74
|
+
not self.cursor
|
75
|
+
and len(self.cursor_field) == 0
|
76
|
+
and type(self).read_records is HttpStream.read_records
|
77
|
+
):
|
59
78
|
self.cursor = ResumableFullRefreshCursor()
|
60
79
|
|
61
80
|
@property
|
@@ -100,7 +119,10 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
100
119
|
return "GET"
|
101
120
|
|
102
121
|
@property
|
103
|
-
@deprecated(
|
122
|
+
@deprecated(
|
123
|
+
version="3.0.0",
|
124
|
+
reason="You should set error_handler explicitly in HttpStream.get_error_handler() instead.",
|
125
|
+
)
|
104
126
|
def raise_on_http_errors(self) -> bool:
|
105
127
|
"""
|
106
128
|
Override if needed. If set to False, allows opting-out of raising HTTP code exception.
|
@@ -108,7 +130,10 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
108
130
|
return True
|
109
131
|
|
110
132
|
@property
|
111
|
-
@deprecated(
|
133
|
+
@deprecated(
|
134
|
+
version="3.0.0",
|
135
|
+
reason="You should set backoff_strategies explicitly in HttpStream.get_backoff_strategy() instead.",
|
136
|
+
)
|
112
137
|
def max_retries(self) -> Union[int, None]:
|
113
138
|
"""
|
114
139
|
Override if needed. Specifies maximum amount of retries for backoff policy. Return None for no limit.
|
@@ -116,7 +141,10 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
116
141
|
return 5
|
117
142
|
|
118
143
|
@property
|
119
|
-
@deprecated(
|
144
|
+
@deprecated(
|
145
|
+
version="3.0.0",
|
146
|
+
reason="You should set backoff_strategies explicitly in HttpStream.get_backoff_strategy() instead.",
|
147
|
+
)
|
120
148
|
def max_time(self) -> Union[int, None]:
|
121
149
|
"""
|
122
150
|
Override if needed. Specifies maximum total waiting time (in seconds) for backoff policy. Return None for no limit.
|
@@ -124,7 +152,10 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
124
152
|
return 60 * 10
|
125
153
|
|
126
154
|
@property
|
127
|
-
@deprecated(
|
155
|
+
@deprecated(
|
156
|
+
version="3.0.0",
|
157
|
+
reason="You should set backoff_strategies explicitly in HttpStream.get_backoff_strategy() instead.",
|
158
|
+
)
|
128
159
|
def retry_factor(self) -> float:
|
129
160
|
"""
|
130
161
|
Override if needed. Specifies factor for backoff policy.
|
@@ -262,7 +293,10 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
262
293
|
"""
|
263
294
|
if hasattr(self, "should_retry"):
|
264
295
|
error_handler = HttpStreamAdapterHttpStatusErrorHandler(
|
265
|
-
stream=self,
|
296
|
+
stream=self,
|
297
|
+
logger=logging.getLogger(),
|
298
|
+
max_retries=self.max_retries,
|
299
|
+
max_time=timedelta(seconds=self.max_time or 0),
|
266
300
|
)
|
267
301
|
return error_handler
|
268
302
|
else:
|
@@ -333,13 +367,17 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
333
367
|
# A cursor_field indicates this is an incremental stream which offers better checkpointing than RFR enabled via the cursor
|
334
368
|
if self.cursor_field or not isinstance(self.get_cursor(), ResumableFullRefreshCursor):
|
335
369
|
yield from self._read_pages(
|
336
|
-
lambda req, res, state, _slice: self.parse_response(
|
370
|
+
lambda req, res, state, _slice: self.parse_response(
|
371
|
+
res, stream_slice=_slice, stream_state=state
|
372
|
+
),
|
337
373
|
stream_slice,
|
338
374
|
stream_state,
|
339
375
|
)
|
340
376
|
else:
|
341
377
|
yield from self._read_single_page(
|
342
|
-
lambda req, res, state, _slice: self.parse_response(
|
378
|
+
lambda req, res, state, _slice: self.parse_response(
|
379
|
+
res, stream_slice=_slice, stream_state=state
|
380
|
+
),
|
343
381
|
stream_slice,
|
344
382
|
stream_state,
|
345
383
|
)
|
@@ -373,7 +411,13 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
373
411
|
def _read_pages(
|
374
412
|
self,
|
375
413
|
records_generator_fn: Callable[
|
376
|
-
[
|
414
|
+
[
|
415
|
+
requests.PreparedRequest,
|
416
|
+
requests.Response,
|
417
|
+
Mapping[str, Any],
|
418
|
+
Optional[Mapping[str, Any]],
|
419
|
+
],
|
420
|
+
Iterable[StreamData],
|
377
421
|
],
|
378
422
|
stream_slice: Optional[Mapping[str, Any]] = None,
|
379
423
|
stream_state: Optional[Mapping[str, Any]] = None,
|
@@ -403,19 +447,29 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
403
447
|
def _read_single_page(
|
404
448
|
self,
|
405
449
|
records_generator_fn: Callable[
|
406
|
-
[
|
450
|
+
[
|
451
|
+
requests.PreparedRequest,
|
452
|
+
requests.Response,
|
453
|
+
Mapping[str, Any],
|
454
|
+
Optional[Mapping[str, Any]],
|
455
|
+
],
|
456
|
+
Iterable[StreamData],
|
407
457
|
],
|
408
458
|
stream_slice: Optional[Mapping[str, Any]] = None,
|
409
459
|
stream_state: Optional[Mapping[str, Any]] = None,
|
410
460
|
) -> Iterable[StreamData]:
|
411
|
-
partition, cursor_slice, remaining_slice = self._extract_slice_fields(
|
461
|
+
partition, cursor_slice, remaining_slice = self._extract_slice_fields(
|
462
|
+
stream_slice=stream_slice
|
463
|
+
)
|
412
464
|
stream_state = stream_state or {}
|
413
465
|
next_page_token = cursor_slice or None
|
414
466
|
|
415
467
|
request, response = self._fetch_next_page(remaining_slice, stream_state, next_page_token)
|
416
468
|
yield from records_generator_fn(request, response, stream_state, remaining_slice)
|
417
469
|
|
418
|
-
next_page_token = self.next_page_token(response) or {
|
470
|
+
next_page_token = self.next_page_token(response) or {
|
471
|
+
"__ab_full_refresh_sync_complete": True
|
472
|
+
}
|
419
473
|
|
420
474
|
cursor = self.get_cursor()
|
421
475
|
if cursor:
|
@@ -425,7 +479,9 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
425
479
|
yield from []
|
426
480
|
|
427
481
|
@staticmethod
|
428
|
-
def _extract_slice_fields(
|
482
|
+
def _extract_slice_fields(
|
483
|
+
stream_slice: Optional[Mapping[str, Any]],
|
484
|
+
) -> tuple[Mapping[str, Any], Mapping[str, Any], Mapping[str, Any]]:
|
429
485
|
if not stream_slice:
|
430
486
|
return {}, {}, {}
|
431
487
|
|
@@ -439,7 +495,11 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
439
495
|
# fields for the partition and cursor_slice value
|
440
496
|
partition = stream_slice.get("partition", {})
|
441
497
|
cursor_slice = stream_slice.get("cursor_slice", {})
|
442
|
-
remaining = {
|
498
|
+
remaining = {
|
499
|
+
key: val
|
500
|
+
for key, val in stream_slice.items()
|
501
|
+
if key != "partition" and key != "cursor_slice"
|
502
|
+
}
|
443
503
|
return partition, cursor_slice, remaining
|
444
504
|
|
445
505
|
def _fetch_next_page(
|
@@ -448,18 +508,41 @@ class HttpStream(Stream, CheckpointMixin, ABC):
|
|
448
508
|
stream_state: Optional[Mapping[str, Any]] = None,
|
449
509
|
next_page_token: Optional[Mapping[str, Any]] = None,
|
450
510
|
) -> Tuple[requests.PreparedRequest, requests.Response]:
|
451
|
-
|
452
511
|
request, response = self._http_client.send_request(
|
453
512
|
http_method=self.http_method,
|
454
513
|
url=self._join_url(
|
455
514
|
self.url_base,
|
456
|
-
self.path(
|
515
|
+
self.path(
|
516
|
+
stream_state=stream_state,
|
517
|
+
stream_slice=stream_slice,
|
518
|
+
next_page_token=next_page_token,
|
519
|
+
),
|
520
|
+
),
|
521
|
+
request_kwargs=self.request_kwargs(
|
522
|
+
stream_state=stream_state,
|
523
|
+
stream_slice=stream_slice,
|
524
|
+
next_page_token=next_page_token,
|
525
|
+
),
|
526
|
+
headers=self.request_headers(
|
527
|
+
stream_state=stream_state,
|
528
|
+
stream_slice=stream_slice,
|
529
|
+
next_page_token=next_page_token,
|
530
|
+
),
|
531
|
+
params=self.request_params(
|
532
|
+
stream_state=stream_state,
|
533
|
+
stream_slice=stream_slice,
|
534
|
+
next_page_token=next_page_token,
|
535
|
+
),
|
536
|
+
json=self.request_body_json(
|
537
|
+
stream_state=stream_state,
|
538
|
+
stream_slice=stream_slice,
|
539
|
+
next_page_token=next_page_token,
|
540
|
+
),
|
541
|
+
data=self.request_body_data(
|
542
|
+
stream_state=stream_state,
|
543
|
+
stream_slice=stream_slice,
|
544
|
+
next_page_token=next_page_token,
|
457
545
|
),
|
458
|
-
request_kwargs=self.request_kwargs(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token),
|
459
|
-
headers=self.request_headers(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token),
|
460
|
-
params=self.request_params(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token),
|
461
|
-
json=self.request_body_json(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token),
|
462
|
-
data=self.request_body_data(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token),
|
463
546
|
dedupe_query_params=True,
|
464
547
|
log_formatter=self.get_log_formatter(),
|
465
548
|
exit_on_rate_limit=self.exit_on_rate_limit,
|
@@ -482,18 +565,27 @@ class HttpSubStream(HttpStream, ABC):
|
|
482
565
|
"""
|
483
566
|
super().__init__(**kwargs)
|
484
567
|
self.parent = parent
|
485
|
-
self.has_multiple_slices =
|
568
|
+
self.has_multiple_slices = (
|
569
|
+
True # Substreams are based on parent records which implies there are multiple slices
|
570
|
+
)
|
486
571
|
|
487
572
|
# There are three conditions that dictate if RFR should automatically be applied to a stream
|
488
573
|
# 1. Streams that explicitly initialize their own cursor should defer to it and not automatically apply RFR
|
489
574
|
# 2. Streams with at least one cursor_field are incremental and thus a superior sync to RFR.
|
490
575
|
# 3. Streams overriding read_records() do not guarantee that they will call the parent implementation which can perform
|
491
576
|
# per-page checkpointing so RFR is only supported if a stream use the default `HttpStream.read_records()` method
|
492
|
-
if
|
577
|
+
if (
|
578
|
+
not self.cursor
|
579
|
+
and len(self.cursor_field) == 0
|
580
|
+
and type(self).read_records is HttpStream.read_records
|
581
|
+
):
|
493
582
|
self.cursor = SubstreamResumableFullRefreshCursor()
|
494
583
|
|
495
584
|
def stream_slices(
|
496
|
-
self,
|
585
|
+
self,
|
586
|
+
sync_mode: SyncMode,
|
587
|
+
cursor_field: Optional[List[str]] = None,
|
588
|
+
stream_state: Optional[Mapping[str, Any]] = None,
|
497
589
|
) -> Iterable[Optional[Mapping[str, Any]]]:
|
498
590
|
# read_stateless() assumes the parent is not concurrent. This is currently okay since the concurrent CDK does
|
499
591
|
# not support either substreams or RFR, but something that needs to be considered once we do
|
@@ -509,7 +601,10 @@ class HttpSubStream(HttpStream, ABC):
|
|
509
601
|
yield {"parent": parent_record}
|
510
602
|
|
511
603
|
|
512
|
-
@deprecated(
|
604
|
+
@deprecated(
|
605
|
+
version="3.0.0",
|
606
|
+
reason="You should set backoff_strategies explicitly in HttpStream.get_backoff_strategy() instead.",
|
607
|
+
)
|
513
608
|
class HttpStreamAdapterBackoffStrategy(BackoffStrategy):
|
514
609
|
def __init__(self, stream: HttpStream):
|
515
610
|
self.stream = stream
|
@@ -522,13 +617,18 @@ class HttpStreamAdapterBackoffStrategy(BackoffStrategy):
|
|
522
617
|
return self.stream.backoff_time(response_or_exception) # type: ignore # noqa # HttpStream.backoff_time has been deprecated
|
523
618
|
|
524
619
|
|
525
|
-
@deprecated(
|
620
|
+
@deprecated(
|
621
|
+
version="3.0.0",
|
622
|
+
reason="You should set error_handler explicitly in HttpStream.get_error_handler() instead.",
|
623
|
+
)
|
526
624
|
class HttpStreamAdapterHttpStatusErrorHandler(HttpStatusErrorHandler):
|
527
625
|
def __init__(self, stream: HttpStream, **kwargs): # type: ignore # noqa
|
528
626
|
self.stream = stream
|
529
627
|
super().__init__(**kwargs)
|
530
628
|
|
531
|
-
def interpret_response(
|
629
|
+
def interpret_response(
|
630
|
+
self, response_or_exception: Optional[Union[requests.Response, Exception]] = None
|
631
|
+
) -> ErrorResolution:
|
532
632
|
if isinstance(response_or_exception, Exception):
|
533
633
|
return super().interpret_response(response_or_exception)
|
534
634
|
elif isinstance(response_or_exception, requests.Response):
|
@@ -44,7 +44,9 @@ from airbyte_cdk.sources.streams.http.rate_limiting import (
|
|
44
44
|
user_defined_backoff_handler,
|
45
45
|
)
|
46
46
|
from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH
|
47
|
-
from airbyte_cdk.utils.stream_status_utils import
|
47
|
+
from airbyte_cdk.utils.stream_status_utils import (
|
48
|
+
as_airbyte_message as stream_status_as_airbyte_message,
|
49
|
+
)
|
48
50
|
from airbyte_cdk.utils.traced_exception import AirbyteTracedException
|
49
51
|
from requests.auth import AuthBase
|
50
52
|
|
@@ -69,7 +71,6 @@ class MessageRepresentationAirbyteTracedErrors(AirbyteTracedException):
|
|
69
71
|
|
70
72
|
|
71
73
|
class HttpClient:
|
72
|
-
|
73
74
|
_DEFAULT_MAX_RETRY: int = 5
|
74
75
|
_DEFAULT_MAX_TIME: int = 60 * 10
|
75
76
|
|
@@ -95,7 +96,10 @@ class HttpClient:
|
|
95
96
|
self._use_cache = use_cache
|
96
97
|
self._session = self._request_session()
|
97
98
|
self._session.mount(
|
98
|
-
"https://",
|
99
|
+
"https://",
|
100
|
+
requests.adapters.HTTPAdapter(
|
101
|
+
pool_connections=MAX_CONNECTION_POOL_SIZE, pool_maxsize=MAX_CONNECTION_POOL_SIZE
|
102
|
+
),
|
99
103
|
)
|
100
104
|
if isinstance(authenticator, AuthBase):
|
101
105
|
self._session.auth = authenticator
|
@@ -134,7 +138,9 @@ class HttpClient:
|
|
134
138
|
sqlite_path = str(Path(cache_dir) / self.cache_filename)
|
135
139
|
else:
|
136
140
|
sqlite_path = "file::memory:?cache=shared"
|
137
|
-
return CachedLimiterSession(
|
141
|
+
return CachedLimiterSession(
|
142
|
+
sqlite_path, backend="sqlite", api_budget=self._api_budget, match_headers=True
|
143
|
+
) # type: ignore # there are no typeshed stubs for requests_cache
|
138
144
|
else:
|
139
145
|
return LimiterSession(api_budget=self._api_budget)
|
140
146
|
|
@@ -145,7 +151,9 @@ class HttpClient:
|
|
145
151
|
if isinstance(self._session, requests_cache.CachedSession):
|
146
152
|
self._session.cache.clear() # type: ignore # cache.clear is not typed
|
147
153
|
|
148
|
-
def _dedupe_query_params(
|
154
|
+
def _dedupe_query_params(
|
155
|
+
self, url: str, params: Optional[Mapping[str, str]]
|
156
|
+
) -> Mapping[str, str]:
|
149
157
|
"""
|
150
158
|
Remove query parameters from params mapping if they are already encoded in the URL.
|
151
159
|
:param url: URL with
|
@@ -157,7 +165,9 @@ class HttpClient:
|
|
157
165
|
query_string = urllib.parse.urlparse(url).query
|
158
166
|
query_dict = {k: v[0] for k, v in urllib.parse.parse_qs(query_string).items()}
|
159
167
|
|
160
|
-
duplicate_keys_with_same_value = {
|
168
|
+
duplicate_keys_with_same_value = {
|
169
|
+
k for k in query_dict.keys() if str(params.get(k)) == str(query_dict[k])
|
170
|
+
}
|
161
171
|
return {k: v for k, v in params.items() if k not in duplicate_keys_with_same_value}
|
162
172
|
|
163
173
|
def _create_prepared_request(
|
@@ -184,7 +194,9 @@ class HttpClient:
|
|
184
194
|
args["json"] = json
|
185
195
|
elif data:
|
186
196
|
args["data"] = data
|
187
|
-
prepared_request: requests.PreparedRequest = self._session.prepare_request(
|
197
|
+
prepared_request: requests.PreparedRequest = self._session.prepare_request(
|
198
|
+
requests.Request(**args)
|
199
|
+
)
|
188
200
|
|
189
201
|
return prepared_request
|
190
202
|
|
@@ -205,7 +217,11 @@ class HttpClient:
|
|
205
217
|
"""
|
206
218
|
Determines the max time based on the provided error handler.
|
207
219
|
"""
|
208
|
-
return
|
220
|
+
return (
|
221
|
+
self._error_handler.max_time
|
222
|
+
if self._error_handler.max_time is not None
|
223
|
+
else self._DEFAULT_MAX_TIME
|
224
|
+
)
|
209
225
|
|
210
226
|
def _send_with_retry(
|
211
227
|
self,
|
@@ -229,11 +245,20 @@ class HttpClient:
|
|
229
245
|
max_tries = max(0, max_retries) + 1
|
230
246
|
max_time = self._max_time
|
231
247
|
|
232
|
-
user_backoff_handler = user_defined_backoff_handler(max_tries=max_tries, max_time=max_time)(
|
248
|
+
user_backoff_handler = user_defined_backoff_handler(max_tries=max_tries, max_time=max_time)(
|
249
|
+
self._send
|
250
|
+
)
|
233
251
|
rate_limit_backoff_handler = rate_limit_default_backoff_handler()
|
234
|
-
backoff_handler = http_client_default_backoff_handler(
|
252
|
+
backoff_handler = http_client_default_backoff_handler(
|
253
|
+
max_tries=max_tries, max_time=max_time
|
254
|
+
)
|
235
255
|
# backoff handlers wrap _send, so it will always return a response
|
236
|
-
response = backoff_handler(rate_limit_backoff_handler(user_backoff_handler))(
|
256
|
+
response = backoff_handler(rate_limit_backoff_handler(user_backoff_handler))(
|
257
|
+
request,
|
258
|
+
request_kwargs,
|
259
|
+
log_formatter=log_formatter,
|
260
|
+
exit_on_rate_limit=exit_on_rate_limit,
|
261
|
+
) # type: ignore # mypy can't infer that backoff_handler wraps _send
|
237
262
|
|
238
263
|
return response
|
239
264
|
|
@@ -244,7 +269,6 @@ class HttpClient:
|
|
244
269
|
log_formatter: Optional[Callable[[requests.Response], Any]] = None,
|
245
270
|
exit_on_rate_limit: Optional[bool] = False,
|
246
271
|
) -> requests.Response:
|
247
|
-
|
248
272
|
if request not in self._request_attempt_count:
|
249
273
|
self._request_attempt_count[request] = 1
|
250
274
|
else:
|
@@ -253,7 +277,8 @@ class HttpClient:
|
|
253
277
|
self._session.auth(request)
|
254
278
|
|
255
279
|
self._logger.debug(
|
256
|
-
"Making outbound API request",
|
280
|
+
"Making outbound API request",
|
281
|
+
extra={"headers": request.headers, "url": request.url, "request_body": request.body},
|
257
282
|
)
|
258
283
|
|
259
284
|
response: Optional[requests.Response] = None
|
@@ -264,7 +289,9 @@ class HttpClient:
|
|
264
289
|
except requests.RequestException as e:
|
265
290
|
exc = e
|
266
291
|
|
267
|
-
error_resolution: ErrorResolution = self._error_handler.interpret_response(
|
292
|
+
error_resolution: ErrorResolution = self._error_handler.interpret_response(
|
293
|
+
response if response is not None else exc
|
294
|
+
)
|
268
295
|
|
269
296
|
# Evaluation of response.text can be heavy, for example, if streaming a large response
|
270
297
|
# Do it only in debug mode
|
@@ -276,11 +303,20 @@ class HttpClient:
|
|
276
303
|
)
|
277
304
|
else:
|
278
305
|
self._logger.debug(
|
279
|
-
"Receiving response",
|
306
|
+
"Receiving response",
|
307
|
+
extra={
|
308
|
+
"headers": response.headers,
|
309
|
+
"status": response.status_code,
|
310
|
+
"body": response.text,
|
311
|
+
},
|
280
312
|
)
|
281
313
|
|
282
314
|
# Request/response logging for declarative cdk
|
283
|
-
if
|
315
|
+
if (
|
316
|
+
log_formatter is not None
|
317
|
+
and response is not None
|
318
|
+
and self._message_repository is not None
|
319
|
+
):
|
284
320
|
formatter = log_formatter
|
285
321
|
self._message_repository.log_message(
|
286
322
|
Level.DEBUG,
|
@@ -288,7 +324,11 @@ class HttpClient:
|
|
288
324
|
)
|
289
325
|
|
290
326
|
self._handle_error_resolution(
|
291
|
-
response=response,
|
327
|
+
response=response,
|
328
|
+
exc=exc,
|
329
|
+
request=request,
|
330
|
+
error_resolution=error_resolution,
|
331
|
+
exit_on_rate_limit=exit_on_rate_limit,
|
292
332
|
)
|
293
333
|
|
294
334
|
return response # type: ignore # will either return a valid response of type requests.Response or raise an exception
|
@@ -307,7 +347,9 @@ class HttpClient:
|
|
307
347
|
reasons = [AirbyteStreamStatusReason(type=AirbyteStreamStatusReasonType.RATE_LIMITED)]
|
308
348
|
message = orjson.dumps(
|
309
349
|
AirbyteMessageSerializer.dump(
|
310
|
-
stream_status_as_airbyte_message(
|
350
|
+
stream_status_as_airbyte_message(
|
351
|
+
StreamDescriptor(name=self._name), AirbyteStreamStatus.RUNNING, reasons
|
352
|
+
)
|
311
353
|
)
|
312
354
|
).decode()
|
313
355
|
|
@@ -321,7 +363,9 @@ class HttpClient:
|
|
321
363
|
if response is not None:
|
322
364
|
error_message = f"'{request.method}' request to '{request.url}' failed with status code '{response.status_code}' and error message '{self._error_message_parser.parse_response_error_message(response)}'"
|
323
365
|
else:
|
324
|
-
error_message =
|
366
|
+
error_message = (
|
367
|
+
f"'{request.method}' request to '{request.url}' failed with exception: '{exc}'"
|
368
|
+
)
|
325
369
|
|
326
370
|
raise MessageRepresentationAirbyteTracedErrors(
|
327
371
|
internal_message=error_message,
|
@@ -331,20 +375,22 @@ class HttpClient:
|
|
331
375
|
|
332
376
|
elif error_resolution.response_action == ResponseAction.IGNORE:
|
333
377
|
if response is not None:
|
334
|
-
log_message =
|
335
|
-
f"Ignoring response for '{request.method}' request to '{request.url}' with response code '{response.status_code}'"
|
336
|
-
)
|
378
|
+
log_message = f"Ignoring response for '{request.method}' request to '{request.url}' with response code '{response.status_code}'"
|
337
379
|
else:
|
338
380
|
log_message = f"Ignoring response for '{request.method}' request to '{request.url}' with error '{exc}'"
|
339
381
|
|
340
382
|
self._logger.info(error_resolution.error_message or log_message)
|
341
383
|
|
342
384
|
# TODO: Consider dynamic retry count depending on subsequent error codes
|
343
|
-
elif
|
385
|
+
elif (
|
386
|
+
error_resolution.response_action == ResponseAction.RETRY
|
387
|
+
or error_resolution.response_action == ResponseAction.RATE_LIMITED
|
388
|
+
):
|
344
389
|
user_defined_backoff_time = None
|
345
390
|
for backoff_strategy in self._backoff_strategies:
|
346
391
|
backoff_time = backoff_strategy.backoff_time(
|
347
|
-
response_or_exception=response if response is not None else exc,
|
392
|
+
response_or_exception=response if response is not None else exc,
|
393
|
+
attempt_count=self._request_attempt_count[request],
|
348
394
|
)
|
349
395
|
if backoff_time:
|
350
396
|
user_defined_backoff_time = backoff_time
|
@@ -354,7 +400,10 @@ class HttpClient:
|
|
354
400
|
or f"Request to {request.url} failed with failure type {error_resolution.failure_type}, response action {error_resolution.response_action}."
|
355
401
|
)
|
356
402
|
|
357
|
-
retry_endlessly =
|
403
|
+
retry_endlessly = (
|
404
|
+
error_resolution.response_action == ResponseAction.RATE_LIMITED
|
405
|
+
and not exit_on_rate_limit
|
406
|
+
)
|
358
407
|
|
359
408
|
if user_defined_backoff_time:
|
360
409
|
raise UserDefinedBackoffException(
|
@@ -365,10 +414,14 @@ class HttpClient:
|
|
365
414
|
)
|
366
415
|
|
367
416
|
elif retry_endlessly:
|
368
|
-
raise RateLimitBackoffException(
|
417
|
+
raise RateLimitBackoffException(
|
418
|
+
request=request, response=response or exc, error_message=error_message
|
419
|
+
)
|
369
420
|
|
370
421
|
raise DefaultBackoffException(
|
371
|
-
request=request,
|
422
|
+
request=request,
|
423
|
+
response=(response if response is not None else exc),
|
424
|
+
error_message=error_message,
|
372
425
|
)
|
373
426
|
|
374
427
|
elif response:
|
@@ -400,11 +453,20 @@ class HttpClient:
|
|
400
453
|
"""
|
401
454
|
|
402
455
|
request: requests.PreparedRequest = self._create_prepared_request(
|
403
|
-
http_method=http_method,
|
456
|
+
http_method=http_method,
|
457
|
+
url=url,
|
458
|
+
dedupe_query_params=dedupe_query_params,
|
459
|
+
headers=headers,
|
460
|
+
params=params,
|
461
|
+
json=json,
|
462
|
+
data=data,
|
404
463
|
)
|
405
464
|
|
406
465
|
response: requests.Response = self._send_with_retry(
|
407
|
-
request=request,
|
466
|
+
request=request,
|
467
|
+
request_kwargs=request_kwargs,
|
468
|
+
log_formatter=log_formatter,
|
469
|
+
exit_on_rate_limit=exit_on_rate_limit,
|
408
470
|
)
|
409
471
|
|
410
472
|
return request, response
|
@@ -10,7 +10,11 @@ from typing import Any, Callable, Mapping, Optional
|
|
10
10
|
import backoff
|
11
11
|
from requests import PreparedRequest, RequestException, Response, codes, exceptions
|
12
12
|
|
13
|
-
from .exceptions import
|
13
|
+
from .exceptions import (
|
14
|
+
DefaultBackoffException,
|
15
|
+
RateLimitBackoffException,
|
16
|
+
UserDefinedBackoffException,
|
17
|
+
)
|
14
18
|
|
15
19
|
TRANSIENT_EXCEPTIONS = (
|
16
20
|
DefaultBackoffException,
|
@@ -32,7 +36,9 @@ def default_backoff_handler(
|
|
32
36
|
def log_retry_attempt(details: Mapping[str, Any]) -> None:
|
33
37
|
_, exc, _ = sys.exc_info()
|
34
38
|
if isinstance(exc, RequestException) and exc.response:
|
35
|
-
logger.info(
|
39
|
+
logger.info(
|
40
|
+
f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
|
41
|
+
)
|
36
42
|
logger.info(
|
37
43
|
f"Caught retryable error '{str(exc)}' after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..."
|
38
44
|
)
|
@@ -71,7 +77,9 @@ def http_client_default_backoff_handler(
|
|
71
77
|
def log_retry_attempt(details: Mapping[str, Any]) -> None:
|
72
78
|
_, exc, _ = sys.exc_info()
|
73
79
|
if isinstance(exc, RequestException) and exc.response:
|
74
|
-
logger.info(
|
80
|
+
logger.info(
|
81
|
+
f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
|
82
|
+
)
|
75
83
|
logger.info(
|
76
84
|
f"Caught retryable error '{str(exc)}' after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..."
|
77
85
|
)
|
@@ -99,7 +107,9 @@ def user_defined_backoff_handler(
|
|
99
107
|
_, exc, _ = sys.exc_info()
|
100
108
|
if isinstance(exc, UserDefinedBackoffException):
|
101
109
|
if exc.response:
|
102
|
-
logger.info(
|
110
|
+
logger.info(
|
111
|
+
f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
|
112
|
+
)
|
103
113
|
retry_after = exc.backoff
|
104
114
|
logger.info(f"Retrying. Sleeping for {retry_after} seconds")
|
105
115
|
time.sleep(retry_after + 1) # extra second to cover any fractions of second
|
@@ -107,7 +117,9 @@ def user_defined_backoff_handler(
|
|
107
117
|
def log_give_up(details: Mapping[str, Any]) -> None:
|
108
118
|
_, exc, _ = sys.exc_info()
|
109
119
|
if isinstance(exc, RequestException):
|
110
|
-
logger.error(
|
120
|
+
logger.error(
|
121
|
+
f"Max retry limit reached in {details['elapsed']}s. Request: {exc.request}, Response: {exc.response}"
|
122
|
+
)
|
111
123
|
else:
|
112
124
|
logger.error("Max retry limit reached for unknown request and response")
|
113
125
|
|
@@ -124,11 +136,15 @@ def user_defined_backoff_handler(
|
|
124
136
|
)
|
125
137
|
|
126
138
|
|
127
|
-
def rate_limit_default_backoff_handler(
|
139
|
+
def rate_limit_default_backoff_handler(
|
140
|
+
**kwargs: Any,
|
141
|
+
) -> Callable[[SendRequestCallableType], SendRequestCallableType]:
|
128
142
|
def log_retry_attempt(details: Mapping[str, Any]) -> None:
|
129
143
|
_, exc, _ = sys.exc_info()
|
130
144
|
if isinstance(exc, RequestException) and exc.response:
|
131
|
-
logger.info(
|
145
|
+
logger.info(
|
146
|
+
f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
|
147
|
+
)
|
132
148
|
logger.info(
|
133
149
|
f"Caught retryable error '{str(exc)}' after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..."
|
134
150
|
)
|