airbyte-cdk 0.72.1__py3-none-any.whl → 6.17.1.dev1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (518) hide show
  1. airbyte_cdk/__init__.py +355 -6
  2. airbyte_cdk/cli/__init__.py +1 -0
  3. airbyte_cdk/cli/source_declarative_manifest/__init__.py +5 -0
  4. airbyte_cdk/cli/source_declarative_manifest/_run.py +230 -0
  5. airbyte_cdk/cli/source_declarative_manifest/spec.json +17 -0
  6. airbyte_cdk/config_observation.py +29 -10
  7. airbyte_cdk/connector.py +24 -24
  8. airbyte_cdk/connector_builder/README.md +53 -0
  9. airbyte_cdk/connector_builder/connector_builder_handler.py +37 -11
  10. airbyte_cdk/connector_builder/main.py +45 -13
  11. airbyte_cdk/connector_builder/message_grouper.py +189 -50
  12. airbyte_cdk/connector_builder/models.py +3 -2
  13. airbyte_cdk/destinations/__init__.py +4 -3
  14. airbyte_cdk/destinations/destination.py +54 -20
  15. airbyte_cdk/destinations/vector_db_based/README.md +37 -0
  16. airbyte_cdk/destinations/vector_db_based/config.py +40 -17
  17. airbyte_cdk/destinations/vector_db_based/document_processor.py +56 -17
  18. airbyte_cdk/destinations/vector_db_based/embedder.py +57 -15
  19. airbyte_cdk/destinations/vector_db_based/test_utils.py +14 -4
  20. airbyte_cdk/destinations/vector_db_based/utils.py +8 -2
  21. airbyte_cdk/destinations/vector_db_based/writer.py +24 -5
  22. airbyte_cdk/entrypoint.py +153 -44
  23. airbyte_cdk/exception_handler.py +21 -3
  24. airbyte_cdk/logger.py +30 -44
  25. airbyte_cdk/models/__init__.py +13 -2
  26. airbyte_cdk/models/airbyte_protocol.py +86 -1
  27. airbyte_cdk/models/airbyte_protocol_serializers.py +44 -0
  28. airbyte_cdk/models/file_transfer_record_message.py +13 -0
  29. airbyte_cdk/models/well_known_types.py +1 -1
  30. airbyte_cdk/sources/__init__.py +5 -1
  31. airbyte_cdk/sources/abstract_source.py +125 -79
  32. airbyte_cdk/sources/concurrent_source/__init__.py +7 -2
  33. airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py +102 -36
  34. airbyte_cdk/sources/concurrent_source/concurrent_source.py +29 -36
  35. airbyte_cdk/sources/concurrent_source/concurrent_source_adapter.py +94 -10
  36. airbyte_cdk/sources/concurrent_source/stream_thread_exception.py +25 -0
  37. airbyte_cdk/sources/concurrent_source/thread_pool_manager.py +20 -14
  38. airbyte_cdk/sources/config.py +3 -2
  39. airbyte_cdk/sources/connector_state_manager.py +49 -83
  40. airbyte_cdk/sources/declarative/async_job/job.py +52 -0
  41. airbyte_cdk/sources/declarative/async_job/job_orchestrator.py +497 -0
  42. airbyte_cdk/sources/declarative/async_job/job_tracker.py +75 -0
  43. airbyte_cdk/sources/declarative/async_job/repository.py +35 -0
  44. airbyte_cdk/sources/declarative/async_job/status.py +24 -0
  45. airbyte_cdk/sources/declarative/async_job/timer.py +39 -0
  46. airbyte_cdk/sources/declarative/auth/__init__.py +2 -3
  47. airbyte_cdk/sources/declarative/auth/declarative_authenticator.py +3 -1
  48. airbyte_cdk/sources/declarative/auth/jwt.py +191 -0
  49. airbyte_cdk/sources/declarative/auth/oauth.py +60 -20
  50. airbyte_cdk/sources/declarative/auth/selective_authenticator.py +10 -2
  51. airbyte_cdk/sources/declarative/auth/token.py +28 -10
  52. airbyte_cdk/sources/declarative/auth/token_provider.py +9 -8
  53. airbyte_cdk/sources/declarative/checks/check_stream.py +16 -8
  54. airbyte_cdk/sources/declarative/checks/connection_checker.py +4 -2
  55. airbyte_cdk/sources/declarative/concurrency_level/__init__.py +7 -0
  56. airbyte_cdk/sources/declarative/concurrency_level/concurrency_level.py +50 -0
  57. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +490 -0
  58. airbyte_cdk/sources/declarative/datetime/datetime_parser.py +4 -0
  59. airbyte_cdk/sources/declarative/datetime/min_max_datetime.py +26 -6
  60. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +1185 -85
  61. airbyte_cdk/sources/declarative/declarative_source.py +5 -2
  62. airbyte_cdk/sources/declarative/declarative_stream.py +95 -9
  63. airbyte_cdk/sources/declarative/decoders/__init__.py +23 -2
  64. airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py +97 -0
  65. airbyte_cdk/sources/declarative/decoders/decoder.py +11 -4
  66. airbyte_cdk/sources/declarative/decoders/json_decoder.py +92 -5
  67. airbyte_cdk/sources/declarative/decoders/noop_decoder.py +21 -0
  68. airbyte_cdk/sources/declarative/decoders/pagination_decoder_decorator.py +39 -0
  69. airbyte_cdk/sources/declarative/decoders/xml_decoder.py +98 -0
  70. airbyte_cdk/sources/declarative/extractors/__init__.py +12 -1
  71. airbyte_cdk/sources/declarative/extractors/dpath_extractor.py +29 -24
  72. airbyte_cdk/sources/declarative/extractors/http_selector.py +4 -5
  73. airbyte_cdk/sources/declarative/extractors/record_extractor.py +2 -3
  74. airbyte_cdk/sources/declarative/extractors/record_filter.py +63 -8
  75. airbyte_cdk/sources/declarative/extractors/record_selector.py +85 -26
  76. airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py +177 -0
  77. airbyte_cdk/sources/declarative/extractors/type_transformer.py +55 -0
  78. airbyte_cdk/sources/declarative/incremental/__init__.py +31 -3
  79. airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +340 -0
  80. airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +156 -48
  81. airbyte_cdk/sources/declarative/incremental/declarative_cursor.py +13 -0
  82. airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +350 -0
  83. airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +174 -74
  84. airbyte_cdk/sources/declarative/incremental/per_partition_with_global.py +200 -0
  85. airbyte_cdk/sources/declarative/incremental/resumable_full_refresh_cursor.py +122 -0
  86. airbyte_cdk/sources/declarative/interpolation/filters.py +27 -1
  87. airbyte_cdk/sources/declarative/interpolation/interpolated_boolean.py +23 -5
  88. airbyte_cdk/sources/declarative/interpolation/interpolated_mapping.py +12 -8
  89. airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py +13 -6
  90. airbyte_cdk/sources/declarative/interpolation/interpolated_string.py +21 -6
  91. airbyte_cdk/sources/declarative/interpolation/interpolation.py +9 -3
  92. airbyte_cdk/sources/declarative/interpolation/jinja.py +72 -37
  93. airbyte_cdk/sources/declarative/interpolation/macros.py +72 -17
  94. airbyte_cdk/sources/declarative/manifest_declarative_source.py +193 -52
  95. airbyte_cdk/sources/declarative/migrations/legacy_to_per_partition_state_migration.py +98 -0
  96. airbyte_cdk/sources/declarative/migrations/state_migration.py +24 -0
  97. airbyte_cdk/sources/declarative/models/__init__.py +1 -1
  98. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +1319 -603
  99. airbyte_cdk/sources/declarative/parsers/custom_exceptions.py +2 -2
  100. airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py +26 -4
  101. airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py +26 -15
  102. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +1759 -225
  103. airbyte_cdk/sources/declarative/partition_routers/__init__.py +24 -4
  104. airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +65 -0
  105. airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py +176 -0
  106. airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +39 -9
  107. airbyte_cdk/sources/declarative/partition_routers/partition_router.py +62 -0
  108. airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py +15 -3
  109. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +222 -39
  110. airbyte_cdk/sources/declarative/requesters/error_handlers/__init__.py +19 -5
  111. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/__init__.py +3 -1
  112. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/constant_backoff_strategy.py +19 -7
  113. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/exponential_backoff_strategy.py +19 -7
  114. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/header_helper.py +4 -2
  115. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_time_from_header_backoff_strategy.py +41 -9
  116. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_until_time_from_header_backoff_strategy.py +29 -14
  117. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategy.py +5 -13
  118. airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +32 -16
  119. airbyte_cdk/sources/declarative/requesters/error_handlers/default_error_handler.py +46 -56
  120. airbyte_cdk/sources/declarative/requesters/error_handlers/default_http_response_filter.py +40 -0
  121. airbyte_cdk/sources/declarative/requesters/error_handlers/error_handler.py +6 -32
  122. airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +119 -41
  123. airbyte_cdk/sources/declarative/requesters/http_job_repository.py +228 -0
  124. airbyte_cdk/sources/declarative/requesters/http_requester.py +98 -344
  125. airbyte_cdk/sources/declarative/requesters/paginators/__init__.py +14 -3
  126. airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +105 -46
  127. airbyte_cdk/sources/declarative/requesters/paginators/no_pagination.py +14 -8
  128. airbyte_cdk/sources/declarative/requesters/paginators/paginator.py +19 -8
  129. airbyte_cdk/sources/declarative/requesters/paginators/strategies/__init__.py +9 -3
  130. airbyte_cdk/sources/declarative/requesters/paginators/strategies/cursor_pagination_strategy.py +53 -21
  131. airbyte_cdk/sources/declarative/requesters/paginators/strategies/offset_increment.py +42 -19
  132. airbyte_cdk/sources/declarative/requesters/paginators/strategies/page_increment.py +25 -12
  133. airbyte_cdk/sources/declarative/requesters/paginators/strategies/pagination_strategy.py +13 -10
  134. airbyte_cdk/sources/declarative/requesters/paginators/strategies/stop_condition.py +26 -13
  135. airbyte_cdk/sources/declarative/requesters/request_options/__init__.py +15 -2
  136. airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +91 -0
  137. airbyte_cdk/sources/declarative/requesters/request_options/default_request_options_provider.py +60 -0
  138. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +31 -14
  139. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +27 -15
  140. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +63 -10
  141. airbyte_cdk/sources/declarative/requesters/request_options/request_options_provider.py +1 -1
  142. airbyte_cdk/sources/declarative/requesters/requester.py +9 -17
  143. airbyte_cdk/sources/declarative/resolvers/__init__.py +41 -0
  144. airbyte_cdk/sources/declarative/resolvers/components_resolver.py +55 -0
  145. airbyte_cdk/sources/declarative/resolvers/config_components_resolver.py +136 -0
  146. airbyte_cdk/sources/declarative/resolvers/http_components_resolver.py +112 -0
  147. airbyte_cdk/sources/declarative/retrievers/__init__.py +6 -2
  148. airbyte_cdk/sources/declarative/retrievers/async_retriever.py +100 -0
  149. airbyte_cdk/sources/declarative/retrievers/retriever.py +1 -3
  150. airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +229 -73
  151. airbyte_cdk/sources/declarative/schema/__init__.py +14 -1
  152. airbyte_cdk/sources/declarative/schema/default_schema_loader.py +5 -3
  153. airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +236 -0
  154. airbyte_cdk/sources/declarative/schema/json_file_schema_loader.py +8 -8
  155. airbyte_cdk/sources/declarative/spec/spec.py +12 -5
  156. airbyte_cdk/sources/declarative/stream_slicers/__init__.py +1 -2
  157. airbyte_cdk/sources/declarative/stream_slicers/declarative_partition_generator.py +88 -0
  158. airbyte_cdk/sources/declarative/stream_slicers/stream_slicer.py +9 -14
  159. airbyte_cdk/sources/declarative/transformations/add_fields.py +19 -11
  160. airbyte_cdk/sources/declarative/transformations/flatten_fields.py +52 -0
  161. airbyte_cdk/sources/declarative/transformations/keys_replace_transformation.py +61 -0
  162. airbyte_cdk/sources/declarative/transformations/keys_to_lower_transformation.py +22 -0
  163. airbyte_cdk/sources/declarative/transformations/keys_to_snake_transformation.py +68 -0
  164. airbyte_cdk/sources/declarative/transformations/remove_fields.py +13 -10
  165. airbyte_cdk/sources/declarative/transformations/transformation.py +5 -5
  166. airbyte_cdk/sources/declarative/types.py +19 -110
  167. airbyte_cdk/sources/declarative/yaml_declarative_source.py +31 -10
  168. airbyte_cdk/sources/embedded/base_integration.py +16 -5
  169. airbyte_cdk/sources/embedded/catalog.py +16 -4
  170. airbyte_cdk/sources/embedded/runner.py +19 -3
  171. airbyte_cdk/sources/embedded/tools.py +5 -2
  172. airbyte_cdk/sources/file_based/README.md +152 -0
  173. airbyte_cdk/sources/file_based/__init__.py +24 -0
  174. airbyte_cdk/sources/file_based/availability_strategy/__init__.py +9 -2
  175. airbyte_cdk/sources/file_based/availability_strategy/abstract_file_based_availability_strategy.py +22 -6
  176. airbyte_cdk/sources/file_based/availability_strategy/default_file_based_availability_strategy.py +46 -10
  177. airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +47 -10
  178. airbyte_cdk/sources/file_based/config/avro_format.py +2 -1
  179. airbyte_cdk/sources/file_based/config/csv_format.py +29 -10
  180. airbyte_cdk/sources/file_based/config/excel_format.py +18 -0
  181. airbyte_cdk/sources/file_based/config/file_based_stream_config.py +16 -4
  182. airbyte_cdk/sources/file_based/config/jsonl_format.py +2 -1
  183. airbyte_cdk/sources/file_based/config/parquet_format.py +2 -1
  184. airbyte_cdk/sources/file_based/config/unstructured_format.py +13 -5
  185. airbyte_cdk/sources/file_based/discovery_policy/__init__.py +6 -2
  186. airbyte_cdk/sources/file_based/discovery_policy/abstract_discovery_policy.py +2 -4
  187. airbyte_cdk/sources/file_based/discovery_policy/default_discovery_policy.py +7 -2
  188. airbyte_cdk/sources/file_based/exceptions.py +18 -15
  189. airbyte_cdk/sources/file_based/file_based_source.py +140 -33
  190. airbyte_cdk/sources/file_based/file_based_stream_reader.py +69 -5
  191. airbyte_cdk/sources/file_based/file_types/__init__.py +14 -1
  192. airbyte_cdk/sources/file_based/file_types/avro_parser.py +75 -24
  193. airbyte_cdk/sources/file_based/file_types/csv_parser.py +116 -34
  194. airbyte_cdk/sources/file_based/file_types/excel_parser.py +196 -0
  195. airbyte_cdk/sources/file_based/file_types/file_transfer.py +37 -0
  196. airbyte_cdk/sources/file_based/file_types/file_type_parser.py +4 -1
  197. airbyte_cdk/sources/file_based/file_types/jsonl_parser.py +24 -8
  198. airbyte_cdk/sources/file_based/file_types/parquet_parser.py +60 -18
  199. airbyte_cdk/sources/file_based/file_types/unstructured_parser.py +141 -41
  200. airbyte_cdk/sources/file_based/remote_file.py +1 -1
  201. airbyte_cdk/sources/file_based/schema_helpers.py +38 -10
  202. airbyte_cdk/sources/file_based/schema_validation_policies/__init__.py +3 -1
  203. airbyte_cdk/sources/file_based/schema_validation_policies/abstract_schema_validation_policy.py +3 -1
  204. airbyte_cdk/sources/file_based/schema_validation_policies/default_schema_validation_policies.py +16 -5
  205. airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py +50 -13
  206. airbyte_cdk/sources/file_based/stream/concurrent/adapters.py +67 -27
  207. airbyte_cdk/sources/file_based/stream/concurrent/cursor/__init__.py +5 -1
  208. airbyte_cdk/sources/file_based/stream/concurrent/cursor/abstract_concurrent_file_based_cursor.py +14 -23
  209. airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_concurrent_cursor.py +54 -18
  210. airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_final_state_cursor.py +21 -9
  211. airbyte_cdk/sources/file_based/stream/cursor/abstract_file_based_cursor.py +3 -1
  212. airbyte_cdk/sources/file_based/stream/cursor/default_file_based_cursor.py +27 -10
  213. airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +147 -45
  214. airbyte_cdk/sources/http_logger.py +8 -3
  215. airbyte_cdk/sources/message/__init__.py +7 -1
  216. airbyte_cdk/sources/message/repository.py +18 -4
  217. airbyte_cdk/sources/source.py +42 -38
  218. airbyte_cdk/sources/streams/__init__.py +2 -2
  219. airbyte_cdk/sources/streams/availability_strategy.py +54 -3
  220. airbyte_cdk/sources/streams/call_rate.py +64 -21
  221. airbyte_cdk/sources/streams/checkpoint/__init__.py +26 -0
  222. airbyte_cdk/sources/streams/checkpoint/checkpoint_reader.py +335 -0
  223. airbyte_cdk/sources/{declarative/incremental → streams/checkpoint}/cursor.py +17 -14
  224. airbyte_cdk/sources/streams/checkpoint/per_partition_key_serializer.py +22 -0
  225. airbyte_cdk/sources/streams/checkpoint/resumable_full_refresh_cursor.py +51 -0
  226. airbyte_cdk/sources/streams/checkpoint/substream_resumable_full_refresh_cursor.py +110 -0
  227. airbyte_cdk/sources/streams/concurrent/README.md +7 -0
  228. airbyte_cdk/sources/streams/concurrent/abstract_stream.py +7 -2
  229. airbyte_cdk/sources/streams/concurrent/adapters.py +84 -75
  230. airbyte_cdk/sources/streams/concurrent/availability_strategy.py +30 -2
  231. airbyte_cdk/sources/streams/concurrent/cursor.py +313 -48
  232. airbyte_cdk/sources/streams/concurrent/default_stream.py +12 -3
  233. airbyte_cdk/sources/streams/concurrent/exceptions.py +3 -0
  234. airbyte_cdk/sources/streams/concurrent/helpers.py +14 -3
  235. airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py +12 -3
  236. airbyte_cdk/sources/streams/concurrent/partition_reader.py +10 -3
  237. airbyte_cdk/sources/streams/concurrent/partitions/partition.py +1 -16
  238. airbyte_cdk/sources/streams/concurrent/partitions/stream_slicer.py +21 -0
  239. airbyte_cdk/sources/streams/concurrent/partitions/types.py +15 -5
  240. airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py +109 -17
  241. airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +90 -72
  242. airbyte_cdk/sources/streams/core.py +412 -87
  243. airbyte_cdk/sources/streams/http/__init__.py +2 -1
  244. airbyte_cdk/sources/streams/http/availability_strategy.py +12 -101
  245. airbyte_cdk/sources/streams/http/error_handlers/__init__.py +22 -0
  246. airbyte_cdk/sources/streams/http/error_handlers/backoff_strategy.py +28 -0
  247. airbyte_cdk/sources/streams/http/error_handlers/default_backoff_strategy.py +17 -0
  248. airbyte_cdk/sources/streams/http/error_handlers/default_error_mapping.py +86 -0
  249. airbyte_cdk/sources/streams/http/error_handlers/error_handler.py +42 -0
  250. airbyte_cdk/sources/streams/http/error_handlers/error_message_parser.py +19 -0
  251. airbyte_cdk/sources/streams/http/error_handlers/http_status_error_handler.py +110 -0
  252. airbyte_cdk/sources/streams/http/error_handlers/json_error_message_parser.py +52 -0
  253. airbyte_cdk/sources/streams/http/error_handlers/response_models.py +65 -0
  254. airbyte_cdk/sources/streams/http/exceptions.py +27 -7
  255. airbyte_cdk/sources/streams/http/http.py +369 -246
  256. airbyte_cdk/sources/streams/http/http_client.py +531 -0
  257. airbyte_cdk/sources/streams/http/rate_limiting.py +76 -12
  258. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +28 -9
  259. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py +2 -1
  260. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +90 -35
  261. airbyte_cdk/sources/streams/http/requests_native_auth/token.py +13 -3
  262. airbyte_cdk/sources/types.py +154 -0
  263. airbyte_cdk/sources/utils/record_helper.py +36 -21
  264. airbyte_cdk/sources/utils/schema_helpers.py +13 -6
  265. airbyte_cdk/sources/utils/slice_logger.py +4 -1
  266. airbyte_cdk/sources/utils/transform.py +54 -20
  267. airbyte_cdk/sql/_util/hashing.py +34 -0
  268. airbyte_cdk/sql/_util/name_normalizers.py +92 -0
  269. airbyte_cdk/sql/constants.py +32 -0
  270. airbyte_cdk/sql/exceptions.py +235 -0
  271. airbyte_cdk/sql/secrets.py +123 -0
  272. airbyte_cdk/sql/shared/__init__.py +15 -0
  273. airbyte_cdk/sql/shared/catalog_providers.py +145 -0
  274. airbyte_cdk/sql/shared/sql_processor.py +786 -0
  275. airbyte_cdk/sql/types.py +160 -0
  276. airbyte_cdk/test/catalog_builder.py +70 -18
  277. airbyte_cdk/test/entrypoint_wrapper.py +117 -42
  278. airbyte_cdk/test/mock_http/__init__.py +1 -1
  279. airbyte_cdk/test/mock_http/matcher.py +6 -0
  280. airbyte_cdk/test/mock_http/mocker.py +57 -10
  281. airbyte_cdk/test/mock_http/request.py +19 -3
  282. airbyte_cdk/test/mock_http/response.py +3 -1
  283. airbyte_cdk/test/mock_http/response_builder.py +32 -16
  284. airbyte_cdk/test/state_builder.py +18 -10
  285. airbyte_cdk/test/utils/__init__.py +1 -0
  286. airbyte_cdk/test/utils/data.py +24 -0
  287. airbyte_cdk/test/utils/http_mocking.py +16 -0
  288. airbyte_cdk/test/utils/manifest_only_fixtures.py +60 -0
  289. airbyte_cdk/test/utils/reading.py +26 -0
  290. airbyte_cdk/utils/__init__.py +2 -1
  291. airbyte_cdk/utils/airbyte_secrets_utils.py +5 -3
  292. airbyte_cdk/utils/analytics_message.py +10 -2
  293. airbyte_cdk/utils/datetime_format_inferrer.py +4 -1
  294. airbyte_cdk/utils/event_timing.py +10 -10
  295. airbyte_cdk/utils/mapping_helpers.py +3 -1
  296. airbyte_cdk/utils/message_utils.py +20 -11
  297. airbyte_cdk/utils/print_buffer.py +75 -0
  298. airbyte_cdk/utils/schema_inferrer.py +198 -28
  299. airbyte_cdk/utils/slice_hasher.py +30 -0
  300. airbyte_cdk/utils/spec_schema_transformations.py +6 -3
  301. airbyte_cdk/utils/stream_status_utils.py +8 -1
  302. airbyte_cdk/utils/traced_exception.py +61 -21
  303. airbyte_cdk-6.17.1.dev1.dist-info/METADATA +109 -0
  304. airbyte_cdk-6.17.1.dev1.dist-info/RECORD +350 -0
  305. {airbyte_cdk-0.72.1.dist-info → airbyte_cdk-6.17.1.dev1.dist-info}/WHEEL +1 -2
  306. airbyte_cdk-6.17.1.dev1.dist-info/entry_points.txt +3 -0
  307. airbyte_cdk/sources/declarative/create_partial.py +0 -92
  308. airbyte_cdk/sources/declarative/parsers/class_types_registry.py +0 -102
  309. airbyte_cdk/sources/declarative/parsers/default_implementation_registry.py +0 -64
  310. airbyte_cdk/sources/declarative/requesters/error_handlers/response_action.py +0 -16
  311. airbyte_cdk/sources/declarative/requesters/error_handlers/response_status.py +0 -68
  312. airbyte_cdk/sources/declarative/stream_slicers/cartesian_product_stream_slicer.py +0 -114
  313. airbyte_cdk/sources/deprecated/base_source.py +0 -94
  314. airbyte_cdk/sources/deprecated/client.py +0 -99
  315. airbyte_cdk/sources/singer/__init__.py +0 -8
  316. airbyte_cdk/sources/singer/singer_helpers.py +0 -304
  317. airbyte_cdk/sources/singer/source.py +0 -186
  318. airbyte_cdk/sources/streams/concurrent/partitions/record.py +0 -23
  319. airbyte_cdk/sources/streams/http/auth/__init__.py +0 -17
  320. airbyte_cdk/sources/streams/http/auth/core.py +0 -29
  321. airbyte_cdk/sources/streams/http/auth/oauth.py +0 -113
  322. airbyte_cdk/sources/streams/http/auth/token.py +0 -47
  323. airbyte_cdk/sources/streams/utils/stream_helper.py +0 -40
  324. airbyte_cdk/sources/utils/catalog_helpers.py +0 -22
  325. airbyte_cdk/sources/utils/schema_models.py +0 -84
  326. airbyte_cdk-0.72.1.dist-info/METADATA +0 -243
  327. airbyte_cdk-0.72.1.dist-info/RECORD +0 -466
  328. airbyte_cdk-0.72.1.dist-info/top_level.txt +0 -3
  329. source_declarative_manifest/main.py +0 -29
  330. unit_tests/connector_builder/__init__.py +0 -3
  331. unit_tests/connector_builder/test_connector_builder_handler.py +0 -871
  332. unit_tests/connector_builder/test_message_grouper.py +0 -713
  333. unit_tests/connector_builder/utils.py +0 -27
  334. unit_tests/destinations/test_destination.py +0 -243
  335. unit_tests/singer/test_singer_helpers.py +0 -56
  336. unit_tests/singer/test_singer_source.py +0 -112
  337. unit_tests/sources/__init__.py +0 -0
  338. unit_tests/sources/concurrent_source/__init__.py +0 -3
  339. unit_tests/sources/concurrent_source/test_concurrent_source_adapter.py +0 -106
  340. unit_tests/sources/declarative/__init__.py +0 -3
  341. unit_tests/sources/declarative/auth/__init__.py +0 -3
  342. unit_tests/sources/declarative/auth/test_oauth.py +0 -331
  343. unit_tests/sources/declarative/auth/test_selective_authenticator.py +0 -39
  344. unit_tests/sources/declarative/auth/test_session_token_auth.py +0 -182
  345. unit_tests/sources/declarative/auth/test_token_auth.py +0 -200
  346. unit_tests/sources/declarative/auth/test_token_provider.py +0 -73
  347. unit_tests/sources/declarative/checks/__init__.py +0 -3
  348. unit_tests/sources/declarative/checks/test_check_stream.py +0 -146
  349. unit_tests/sources/declarative/decoders/__init__.py +0 -0
  350. unit_tests/sources/declarative/decoders/test_json_decoder.py +0 -16
  351. unit_tests/sources/declarative/external_component.py +0 -13
  352. unit_tests/sources/declarative/extractors/__init__.py +0 -3
  353. unit_tests/sources/declarative/extractors/test_dpath_extractor.py +0 -55
  354. unit_tests/sources/declarative/extractors/test_record_filter.py +0 -55
  355. unit_tests/sources/declarative/extractors/test_record_selector.py +0 -179
  356. unit_tests/sources/declarative/incremental/__init__.py +0 -0
  357. unit_tests/sources/declarative/incremental/test_datetime_based_cursor.py +0 -860
  358. unit_tests/sources/declarative/incremental/test_per_partition_cursor.py +0 -406
  359. unit_tests/sources/declarative/incremental/test_per_partition_cursor_integration.py +0 -332
  360. unit_tests/sources/declarative/interpolation/__init__.py +0 -3
  361. unit_tests/sources/declarative/interpolation/test_filters.py +0 -80
  362. unit_tests/sources/declarative/interpolation/test_interpolated_boolean.py +0 -40
  363. unit_tests/sources/declarative/interpolation/test_interpolated_mapping.py +0 -35
  364. unit_tests/sources/declarative/interpolation/test_interpolated_nested_mapping.py +0 -45
  365. unit_tests/sources/declarative/interpolation/test_interpolated_string.py +0 -25
  366. unit_tests/sources/declarative/interpolation/test_jinja.py +0 -240
  367. unit_tests/sources/declarative/interpolation/test_macros.py +0 -73
  368. unit_tests/sources/declarative/parsers/__init__.py +0 -3
  369. unit_tests/sources/declarative/parsers/test_manifest_component_transformer.py +0 -406
  370. unit_tests/sources/declarative/parsers/test_manifest_reference_resolver.py +0 -139
  371. unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +0 -1847
  372. unit_tests/sources/declarative/parsers/testing_components.py +0 -36
  373. unit_tests/sources/declarative/partition_routers/__init__.py +0 -3
  374. unit_tests/sources/declarative/partition_routers/test_list_partition_router.py +0 -155
  375. unit_tests/sources/declarative/partition_routers/test_single_partition_router.py +0 -14
  376. unit_tests/sources/declarative/partition_routers/test_substream_partition_router.py +0 -404
  377. unit_tests/sources/declarative/requesters/__init__.py +0 -3
  378. unit_tests/sources/declarative/requesters/error_handlers/__init__.py +0 -3
  379. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/__init__.py +0 -3
  380. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_constant_backoff.py +0 -34
  381. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_exponential_backoff.py +0 -36
  382. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_header_helper.py +0 -38
  383. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_wait_time_from_header.py +0 -35
  384. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_wait_until_time_from_header.py +0 -64
  385. unit_tests/sources/declarative/requesters/error_handlers/test_composite_error_handler.py +0 -213
  386. unit_tests/sources/declarative/requesters/error_handlers/test_default_error_handler.py +0 -178
  387. unit_tests/sources/declarative/requesters/error_handlers/test_http_response_filter.py +0 -121
  388. unit_tests/sources/declarative/requesters/error_handlers/test_response_status.py +0 -44
  389. unit_tests/sources/declarative/requesters/paginators/__init__.py +0 -3
  390. unit_tests/sources/declarative/requesters/paginators/test_cursor_pagination_strategy.py +0 -64
  391. unit_tests/sources/declarative/requesters/paginators/test_default_paginator.py +0 -313
  392. unit_tests/sources/declarative/requesters/paginators/test_no_paginator.py +0 -12
  393. unit_tests/sources/declarative/requesters/paginators/test_offset_increment.py +0 -58
  394. unit_tests/sources/declarative/requesters/paginators/test_page_increment.py +0 -70
  395. unit_tests/sources/declarative/requesters/paginators/test_request_option.py +0 -43
  396. unit_tests/sources/declarative/requesters/paginators/test_stop_condition.py +0 -105
  397. unit_tests/sources/declarative/requesters/request_options/__init__.py +0 -3
  398. unit_tests/sources/declarative/requesters/request_options/test_interpolated_request_options_provider.py +0 -101
  399. unit_tests/sources/declarative/requesters/test_http_requester.py +0 -974
  400. unit_tests/sources/declarative/requesters/test_interpolated_request_input_provider.py +0 -32
  401. unit_tests/sources/declarative/retrievers/__init__.py +0 -3
  402. unit_tests/sources/declarative/retrievers/test_simple_retriever.py +0 -542
  403. unit_tests/sources/declarative/schema/__init__.py +0 -6
  404. unit_tests/sources/declarative/schema/source_test/SourceTest.py +0 -8
  405. unit_tests/sources/declarative/schema/source_test/__init__.py +0 -3
  406. unit_tests/sources/declarative/schema/test_default_schema_loader.py +0 -32
  407. unit_tests/sources/declarative/schema/test_inline_schema_loader.py +0 -19
  408. unit_tests/sources/declarative/schema/test_json_file_schema_loader.py +0 -26
  409. unit_tests/sources/declarative/states/__init__.py +0 -3
  410. unit_tests/sources/declarative/stream_slicers/__init__.py +0 -3
  411. unit_tests/sources/declarative/stream_slicers/test_cartesian_product_stream_slicer.py +0 -225
  412. unit_tests/sources/declarative/test_create_partial.py +0 -83
  413. unit_tests/sources/declarative/test_declarative_stream.py +0 -103
  414. unit_tests/sources/declarative/test_manifest_declarative_source.py +0 -1260
  415. unit_tests/sources/declarative/test_types.py +0 -39
  416. unit_tests/sources/declarative/test_yaml_declarative_source.py +0 -148
  417. unit_tests/sources/file_based/__init__.py +0 -0
  418. unit_tests/sources/file_based/availability_strategy/__init__.py +0 -0
  419. unit_tests/sources/file_based/availability_strategy/test_default_file_based_availability_strategy.py +0 -100
  420. unit_tests/sources/file_based/config/__init__.py +0 -0
  421. unit_tests/sources/file_based/config/test_abstract_file_based_spec.py +0 -28
  422. unit_tests/sources/file_based/config/test_csv_format.py +0 -34
  423. unit_tests/sources/file_based/config/test_file_based_stream_config.py +0 -84
  424. unit_tests/sources/file_based/discovery_policy/__init__.py +0 -0
  425. unit_tests/sources/file_based/discovery_policy/test_default_discovery_policy.py +0 -31
  426. unit_tests/sources/file_based/file_types/__init__.py +0 -0
  427. unit_tests/sources/file_based/file_types/test_avro_parser.py +0 -243
  428. unit_tests/sources/file_based/file_types/test_csv_parser.py +0 -546
  429. unit_tests/sources/file_based/file_types/test_jsonl_parser.py +0 -158
  430. unit_tests/sources/file_based/file_types/test_parquet_parser.py +0 -274
  431. unit_tests/sources/file_based/file_types/test_unstructured_parser.py +0 -593
  432. unit_tests/sources/file_based/helpers.py +0 -70
  433. unit_tests/sources/file_based/in_memory_files_source.py +0 -211
  434. unit_tests/sources/file_based/scenarios/__init__.py +0 -0
  435. unit_tests/sources/file_based/scenarios/avro_scenarios.py +0 -744
  436. unit_tests/sources/file_based/scenarios/check_scenarios.py +0 -220
  437. unit_tests/sources/file_based/scenarios/concurrent_incremental_scenarios.py +0 -2844
  438. unit_tests/sources/file_based/scenarios/csv_scenarios.py +0 -3105
  439. unit_tests/sources/file_based/scenarios/file_based_source_builder.py +0 -91
  440. unit_tests/sources/file_based/scenarios/incremental_scenarios.py +0 -1926
  441. unit_tests/sources/file_based/scenarios/jsonl_scenarios.py +0 -930
  442. unit_tests/sources/file_based/scenarios/parquet_scenarios.py +0 -754
  443. unit_tests/sources/file_based/scenarios/scenario_builder.py +0 -234
  444. unit_tests/sources/file_based/scenarios/unstructured_scenarios.py +0 -608
  445. unit_tests/sources/file_based/scenarios/user_input_schema_scenarios.py +0 -746
  446. unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py +0 -726
  447. unit_tests/sources/file_based/stream/__init__.py +0 -0
  448. unit_tests/sources/file_based/stream/concurrent/__init__.py +0 -0
  449. unit_tests/sources/file_based/stream/concurrent/test_adapters.py +0 -362
  450. unit_tests/sources/file_based/stream/concurrent/test_file_based_concurrent_cursor.py +0 -458
  451. unit_tests/sources/file_based/stream/test_default_file_based_cursor.py +0 -310
  452. unit_tests/sources/file_based/stream/test_default_file_based_stream.py +0 -244
  453. unit_tests/sources/file_based/test_file_based_scenarios.py +0 -320
  454. unit_tests/sources/file_based/test_file_based_stream_reader.py +0 -272
  455. unit_tests/sources/file_based/test_scenarios.py +0 -253
  456. unit_tests/sources/file_based/test_schema_helpers.py +0 -346
  457. unit_tests/sources/fixtures/__init__.py +0 -3
  458. unit_tests/sources/fixtures/source_test_fixture.py +0 -153
  459. unit_tests/sources/message/__init__.py +0 -0
  460. unit_tests/sources/message/test_repository.py +0 -153
  461. unit_tests/sources/streams/__init__.py +0 -0
  462. unit_tests/sources/streams/concurrent/__init__.py +0 -3
  463. unit_tests/sources/streams/concurrent/scenarios/__init__.py +0 -3
  464. unit_tests/sources/streams/concurrent/scenarios/incremental_scenarios.py +0 -250
  465. unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py +0 -140
  466. unit_tests/sources/streams/concurrent/scenarios/stream_facade_scenarios.py +0 -452
  467. unit_tests/sources/streams/concurrent/scenarios/test_concurrent_scenarios.py +0 -76
  468. unit_tests/sources/streams/concurrent/scenarios/thread_based_concurrent_stream_scenarios.py +0 -418
  469. unit_tests/sources/streams/concurrent/scenarios/thread_based_concurrent_stream_source_builder.py +0 -142
  470. unit_tests/sources/streams/concurrent/scenarios/utils.py +0 -55
  471. unit_tests/sources/streams/concurrent/test_adapters.py +0 -380
  472. unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py +0 -684
  473. unit_tests/sources/streams/concurrent/test_cursor.py +0 -139
  474. unit_tests/sources/streams/concurrent/test_datetime_state_converter.py +0 -369
  475. unit_tests/sources/streams/concurrent/test_default_stream.py +0 -197
  476. unit_tests/sources/streams/concurrent/test_partition_enqueuer.py +0 -90
  477. unit_tests/sources/streams/concurrent/test_partition_reader.py +0 -67
  478. unit_tests/sources/streams/concurrent/test_thread_pool_manager.py +0 -106
  479. unit_tests/sources/streams/http/__init__.py +0 -0
  480. unit_tests/sources/streams/http/auth/__init__.py +0 -0
  481. unit_tests/sources/streams/http/auth/test_auth.py +0 -173
  482. unit_tests/sources/streams/http/requests_native_auth/__init__.py +0 -0
  483. unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py +0 -423
  484. unit_tests/sources/streams/http/test_availability_strategy.py +0 -180
  485. unit_tests/sources/streams/http/test_http.py +0 -635
  486. unit_tests/sources/streams/test_availability_strategy.py +0 -70
  487. unit_tests/sources/streams/test_call_rate.py +0 -300
  488. unit_tests/sources/streams/test_stream_read.py +0 -405
  489. unit_tests/sources/streams/test_streams_core.py +0 -184
  490. unit_tests/sources/test_abstract_source.py +0 -1442
  491. unit_tests/sources/test_concurrent_source.py +0 -112
  492. unit_tests/sources/test_config.py +0 -92
  493. unit_tests/sources/test_connector_state_manager.py +0 -482
  494. unit_tests/sources/test_http_logger.py +0 -252
  495. unit_tests/sources/test_integration_source.py +0 -86
  496. unit_tests/sources/test_source.py +0 -684
  497. unit_tests/sources/test_source_read.py +0 -460
  498. unit_tests/test/__init__.py +0 -0
  499. unit_tests/test/mock_http/__init__.py +0 -0
  500. unit_tests/test/mock_http/test_matcher.py +0 -53
  501. unit_tests/test/mock_http/test_mocker.py +0 -214
  502. unit_tests/test/mock_http/test_request.py +0 -117
  503. unit_tests/test/mock_http/test_response_builder.py +0 -177
  504. unit_tests/test/test_entrypoint_wrapper.py +0 -240
  505. unit_tests/utils/__init__.py +0 -0
  506. unit_tests/utils/test_datetime_format_inferrer.py +0 -60
  507. unit_tests/utils/test_mapping_helpers.py +0 -54
  508. unit_tests/utils/test_message_utils.py +0 -91
  509. unit_tests/utils/test_rate_limiting.py +0 -26
  510. unit_tests/utils/test_schema_inferrer.py +0 -202
  511. unit_tests/utils/test_secret_utils.py +0 -135
  512. unit_tests/utils/test_stream_status_utils.py +0 -61
  513. unit_tests/utils/test_traced_exception.py +0 -107
  514. /airbyte_cdk/sources/{deprecated → declarative/async_job}/__init__.py +0 -0
  515. {source_declarative_manifest → airbyte_cdk/sources/declarative/migrations}/__init__.py +0 -0
  516. {unit_tests/destinations → airbyte_cdk/sql}/__init__.py +0 -0
  517. {unit_tests/singer → airbyte_cdk/sql/_util}/__init__.py +0 -0
  518. {airbyte_cdk-0.72.1.dist-info → airbyte_cdk-6.17.1.dev1.dist-info}/LICENSE.txt +0 -0
@@ -0,0 +1,531 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ import logging
6
+ import os
7
+ import urllib
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
10
+
11
+ import orjson
12
+ import requests
13
+ import requests_cache
14
+ from requests.auth import AuthBase
15
+
16
+ from airbyte_cdk.models import (
17
+ AirbyteMessageSerializer,
18
+ AirbyteStreamStatus,
19
+ AirbyteStreamStatusReason,
20
+ AirbyteStreamStatusReasonType,
21
+ Level,
22
+ StreamDescriptor,
23
+ )
24
+ from airbyte_cdk.sources.http_config import MAX_CONNECTION_POOL_SIZE
25
+ from airbyte_cdk.sources.message import MessageRepository
26
+ from airbyte_cdk.sources.streams.call_rate import APIBudget, CachedLimiterSession, LimiterSession
27
+ from airbyte_cdk.sources.streams.http.error_handlers import (
28
+ BackoffStrategy,
29
+ DefaultBackoffStrategy,
30
+ ErrorHandler,
31
+ ErrorMessageParser,
32
+ ErrorResolution,
33
+ HttpStatusErrorHandler,
34
+ JsonErrorMessageParser,
35
+ ResponseAction,
36
+ )
37
+ from airbyte_cdk.sources.streams.http.exceptions import (
38
+ DefaultBackoffException,
39
+ RateLimitBackoffException,
40
+ RequestBodyException,
41
+ UserDefinedBackoffException,
42
+ )
43
+ from airbyte_cdk.sources.streams.http.rate_limiting import (
44
+ http_client_default_backoff_handler,
45
+ rate_limit_default_backoff_handler,
46
+ user_defined_backoff_handler,
47
+ )
48
+ from airbyte_cdk.sources.utils.types import JsonType
49
+ from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets
50
+ from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH
51
+ from airbyte_cdk.utils.stream_status_utils import (
52
+ as_airbyte_message as stream_status_as_airbyte_message,
53
+ )
54
+ from airbyte_cdk.utils.traced_exception import AirbyteTracedException
55
+
56
+ BODY_REQUEST_METHODS = ("GET", "POST", "PUT", "PATCH")
57
+
58
+
59
+ class MessageRepresentationAirbyteTracedErrors(AirbyteTracedException):
60
+ """
61
+ Before the migration to the HttpClient in low-code, the exception raised was
62
+ [ReadException](https://github.com/airbytehq/airbyte/blob/8fdd9818ec16e653ba3dd2b167a74b7c07459861/airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/http_requester.py#L566).
63
+ This has been moved to a AirbyteTracedException. The printing on this is questionable (AirbyteTracedException string representation
64
+ shows the internal_message and not the message). We have already discussed moving the AirbyteTracedException string representation to
65
+ `message` but the impact is unclear and hard to quantify so we will do it here only for now.
66
+ """
67
+
68
+ def __str__(self) -> str:
69
+ if self.message:
70
+ return self.message
71
+ elif self.internal_message:
72
+ return self.internal_message
73
+ return ""
74
+
75
+
76
+ class HttpClient:
77
+ _DEFAULT_MAX_RETRY: int = 5
78
+ _DEFAULT_MAX_TIME: int = 60 * 10
79
+ _ACTIONS_TO_RETRY_ON = {ResponseAction.RETRY, ResponseAction.RATE_LIMITED}
80
+
81
+ def __init__(
82
+ self,
83
+ name: str,
84
+ logger: logging.Logger,
85
+ error_handler: Optional[ErrorHandler] = None,
86
+ api_budget: Optional[APIBudget] = None,
87
+ session: Optional[Union[requests.Session, requests_cache.CachedSession]] = None,
88
+ authenticator: Optional[AuthBase] = None,
89
+ use_cache: bool = False,
90
+ backoff_strategy: Optional[Union[BackoffStrategy, List[BackoffStrategy]]] = None,
91
+ error_message_parser: Optional[ErrorMessageParser] = None,
92
+ disable_retries: bool = False,
93
+ message_repository: Optional[MessageRepository] = None,
94
+ ):
95
+ self._name = name
96
+ self._api_budget: APIBudget = api_budget or APIBudget(policies=[])
97
+ if session:
98
+ self._session = session
99
+ else:
100
+ self._use_cache = use_cache
101
+ self._session = self._request_session()
102
+ self._session.mount(
103
+ "https://",
104
+ requests.adapters.HTTPAdapter(
105
+ pool_connections=MAX_CONNECTION_POOL_SIZE, pool_maxsize=MAX_CONNECTION_POOL_SIZE
106
+ ),
107
+ )
108
+ if isinstance(authenticator, AuthBase):
109
+ self._session.auth = authenticator
110
+ self._logger = logger
111
+ self._error_handler = error_handler or HttpStatusErrorHandler(self._logger)
112
+ if backoff_strategy is not None:
113
+ if isinstance(backoff_strategy, list):
114
+ self._backoff_strategies = backoff_strategy
115
+ else:
116
+ self._backoff_strategies = [backoff_strategy]
117
+ else:
118
+ self._backoff_strategies = [DefaultBackoffStrategy()]
119
+ self._error_message_parser = error_message_parser or JsonErrorMessageParser()
120
+ self._request_attempt_count: Dict[requests.PreparedRequest, int] = {}
121
+ self._disable_retries = disable_retries
122
+ self._message_repository = message_repository
123
+
124
+ @property
125
+ def cache_filename(self) -> str:
126
+ """
127
+ Override if needed. Return the name of cache file
128
+ Note that if the environment variable REQUEST_CACHE_PATH is not set, the cache will be in-memory only.
129
+ """
130
+ return f"{self._name}.sqlite"
131
+
132
+ def _request_session(self) -> requests.Session:
133
+ """
134
+ Session factory based on use_cache property and call rate limits (api_budget parameter)
135
+ :return: instance of request-based session
136
+ """
137
+ if self._use_cache:
138
+ cache_dir = os.getenv(ENV_REQUEST_CACHE_PATH)
139
+ # Use in-memory cache if cache_dir is not set
140
+ # This is a non-obvious interface, but it ensures we don't write sql files when running unit tests
141
+ # Use in-memory cache if cache_dir is not set
142
+ # This is a non-obvious interface, but it ensures we don't write sql files when running unit tests
143
+ sqlite_path = (
144
+ str(Path(cache_dir) / self.cache_filename)
145
+ if cache_dir
146
+ else "file::memory:?cache=shared"
147
+ )
148
+ # By using `PRAGMA synchronous=OFF` and `PRAGMA journal_mode=WAL`, we reduce the possible occurrences of `database table is locked` errors.
149
+ # Note that those were blindly added at the same time and one or the other might be sufficient to prevent the issues but we have seen good results with both. Feel free to revisit given more information.
150
+ # There are strong signals that `fast_save` might create problems but if the sync crashes, we start back from the beginning in terms of sqlite anyway so the impact should be minimal. Signals are:
151
+ # * https://github.com/requests-cache/requests-cache/commit/7fa89ffda300331c37d8fad7f773348a3b5b0236#diff-f43db4a5edf931647c32dec28ea7557aae4cae8444af4b26c8ecbe88d8c925aaR238
152
+ # * https://github.com/requests-cache/requests-cache/commit/7fa89ffda300331c37d8fad7f773348a3b5b0236#diff-2e7f95b7d7be270ff1a8118f817ea3e6663cdad273592e536a116c24e6d23c18R164-R168
153
+ # * `If the application running SQLite crashes, the data will be safe, but the database [might become corrupted](https://www.sqlite.org/howtocorrupt.html#cfgerr) if the operating system crashes or the computer loses power before that data has been written to the disk surface.` in [this description](https://www.sqlite.org/pragma.html#pragma_synchronous).
154
+ backend = requests_cache.SQLiteCache(sqlite_path, fast_save=True, wal=True)
155
+ return CachedLimiterSession(
156
+ sqlite_path, backend=backend, api_budget=self._api_budget, match_headers=True
157
+ )
158
+ else:
159
+ return LimiterSession(api_budget=self._api_budget)
160
+
161
+ def clear_cache(self) -> None:
162
+ """
163
+ Clear cached requests for current session, can be called any time
164
+ """
165
+ if isinstance(self._session, requests_cache.CachedSession):
166
+ self._session.cache.clear() # type: ignore # cache.clear is not typed
167
+
168
+ def _dedupe_query_params(
169
+ self, url: str, params: Optional[Mapping[str, str]]
170
+ ) -> Mapping[str, str]:
171
+ """
172
+ Remove query parameters from params mapping if they are already encoded in the URL.
173
+ :param url: URL with
174
+ :param params:
175
+ :return:
176
+ """
177
+ if params is None:
178
+ params = {}
179
+ query_string = urllib.parse.urlparse(url).query
180
+ query_dict = {k: v[0] for k, v in urllib.parse.parse_qs(query_string).items()}
181
+
182
+ duplicate_keys_with_same_value = {
183
+ k for k in query_dict.keys() if str(params.get(k)) == str(query_dict[k])
184
+ }
185
+ return {k: v for k, v in params.items() if k not in duplicate_keys_with_same_value}
186
+
187
+ def _create_prepared_request(
188
+ self,
189
+ http_method: str,
190
+ url: str,
191
+ dedupe_query_params: bool = False,
192
+ headers: Optional[Mapping[str, str]] = None,
193
+ params: Optional[Mapping[str, str]] = None,
194
+ json: Optional[Mapping[str, Any]] = None,
195
+ data: Optional[Union[str, Mapping[str, Any]]] = None,
196
+ ) -> requests.PreparedRequest:
197
+ if dedupe_query_params:
198
+ query_params = self._dedupe_query_params(url, params)
199
+ else:
200
+ query_params = params or {}
201
+ args = {"method": http_method, "url": url, "headers": headers, "params": query_params}
202
+ if http_method.upper() in BODY_REQUEST_METHODS:
203
+ if json and data:
204
+ raise RequestBodyException(
205
+ "At the same time only one of the 'request_body_data' and 'request_body_json' functions can return data"
206
+ )
207
+ elif json:
208
+ args["json"] = json
209
+ elif data:
210
+ args["data"] = data
211
+ prepared_request: requests.PreparedRequest = self._session.prepare_request(
212
+ requests.Request(**args)
213
+ )
214
+
215
+ return prepared_request
216
+
217
+ @property
218
+ def _max_retries(self) -> int:
219
+ """
220
+ Determines the max retries based on the provided error handler.
221
+ """
222
+ max_retries = None
223
+ if self._disable_retries:
224
+ max_retries = 0
225
+ else:
226
+ max_retries = self._error_handler.max_retries
227
+ return max_retries if max_retries is not None else self._DEFAULT_MAX_RETRY
228
+
229
+ @property
230
+ def _max_time(self) -> int:
231
+ """
232
+ Determines the max time based on the provided error handler.
233
+ """
234
+ return (
235
+ self._error_handler.max_time
236
+ if self._error_handler.max_time is not None
237
+ else self._DEFAULT_MAX_TIME
238
+ )
239
+
240
+ def _send_with_retry(
241
+ self,
242
+ request: requests.PreparedRequest,
243
+ request_kwargs: Mapping[str, Any],
244
+ log_formatter: Optional[Callable[[requests.Response], Any]] = None,
245
+ exit_on_rate_limit: Optional[bool] = False,
246
+ ) -> requests.Response:
247
+ """
248
+ Sends a request with retry logic.
249
+
250
+ Args:
251
+ request (requests.PreparedRequest): The prepared HTTP request to send.
252
+ request_kwargs (Mapping[str, Any]): Additional keyword arguments for the request.
253
+
254
+ Returns:
255
+ requests.Response: The HTTP response received from the server after retries.
256
+ """
257
+
258
+ max_retries = self._max_retries
259
+ max_tries = max(0, max_retries) + 1
260
+ max_time = self._max_time
261
+
262
+ user_backoff_handler = user_defined_backoff_handler(max_tries=max_tries, max_time=max_time)(
263
+ self._send
264
+ )
265
+ rate_limit_backoff_handler = rate_limit_default_backoff_handler(max_tries=max_tries)
266
+ backoff_handler = http_client_default_backoff_handler(
267
+ max_tries=max_tries, max_time=max_time
268
+ )
269
+ # backoff handlers wrap _send, so it will always return a response
270
+ response = backoff_handler(rate_limit_backoff_handler(user_backoff_handler))(
271
+ request,
272
+ request_kwargs,
273
+ log_formatter=log_formatter,
274
+ exit_on_rate_limit=exit_on_rate_limit,
275
+ ) # type: ignore # mypy can't infer that backoff_handler wraps _send
276
+
277
+ return response
278
+
279
+ def _send(
280
+ self,
281
+ request: requests.PreparedRequest,
282
+ request_kwargs: Mapping[str, Any],
283
+ log_formatter: Optional[Callable[[requests.Response], Any]] = None,
284
+ exit_on_rate_limit: Optional[bool] = False,
285
+ ) -> requests.Response:
286
+ if request not in self._request_attempt_count:
287
+ self._request_attempt_count[request] = 1
288
+ else:
289
+ self._request_attempt_count[request] += 1
290
+ if hasattr(self._session, "auth") and isinstance(self._session.auth, AuthBase):
291
+ self._session.auth(request)
292
+
293
+ self._logger.debug(
294
+ "Making outbound API request",
295
+ extra={"headers": request.headers, "url": request.url, "request_body": request.body},
296
+ )
297
+
298
+ response: Optional[requests.Response] = None
299
+ exc: Optional[requests.RequestException] = None
300
+
301
+ try:
302
+ response = self._session.send(request, **request_kwargs)
303
+ except requests.RequestException as e:
304
+ exc = e
305
+
306
+ error_resolution: ErrorResolution = self._error_handler.interpret_response(
307
+ response if response is not None else exc
308
+ )
309
+
310
+ # Evaluation of response.text can be heavy, for example, if streaming a large response
311
+ # Do it only in debug mode
312
+ if self._logger.isEnabledFor(logging.DEBUG) and response is not None:
313
+ if request_kwargs.get("stream"):
314
+ self._logger.debug(
315
+ "Receiving response, but not logging it as the response is streamed",
316
+ extra={"headers": response.headers, "status": response.status_code},
317
+ )
318
+ else:
319
+ self._logger.debug(
320
+ "Receiving response",
321
+ extra={
322
+ "headers": response.headers,
323
+ "status": response.status_code,
324
+ "body": response.text,
325
+ },
326
+ )
327
+
328
+ # Request/response logging for declarative cdk
329
+ if (
330
+ log_formatter is not None
331
+ and response is not None
332
+ and self._message_repository is not None
333
+ ):
334
+ formatter = log_formatter
335
+ self._message_repository.log_message(
336
+ Level.DEBUG,
337
+ lambda: formatter(response),
338
+ )
339
+
340
+ self._handle_error_resolution(
341
+ response=response,
342
+ exc=exc,
343
+ request=request,
344
+ error_resolution=error_resolution,
345
+ exit_on_rate_limit=exit_on_rate_limit,
346
+ )
347
+
348
+ return response # type: ignore # will either return a valid response of type requests.Response or raise an exception
349
+
350
+ def _get_response_body(self, response: requests.Response) -> Optional[JsonType]:
351
+ """
352
+ Extracts and returns the body of an HTTP response.
353
+
354
+ This method attempts to parse the response body as JSON. If the response
355
+ body is not valid JSON, it falls back to decoding the response content
356
+ as a UTF-8 string. If both attempts fail, it returns None.
357
+
358
+ Args:
359
+ response (requests.Response): The HTTP response object.
360
+
361
+ Returns:
362
+ Optional[JsonType]: The parsed JSON object as a string, the decoded
363
+ response content as a string, or None if both parsing attempts fail.
364
+ """
365
+ try:
366
+ return str(response.json())
367
+ except requests.exceptions.JSONDecodeError:
368
+ try:
369
+ return response.content.decode("utf-8")
370
+ except Exception:
371
+ return "The Content of the Response couldn't be decoded."
372
+
373
+ def _evict_key(self, prepared_request: requests.PreparedRequest) -> None:
374
+ """
375
+ Addresses high memory consumption when enabling concurrency in https://github.com/airbytehq/oncall/issues/6821.
376
+
377
+ The `_request_attempt_count` attribute keeps growing as multiple requests are made using the same `http_client`.
378
+ To mitigate this issue, we evict keys for completed requests once we confirm that no further retries are needed.
379
+ This helps manage memory usage more efficiently while maintaining the necessary logic for retry attempts.
380
+ """
381
+ if prepared_request in self._request_attempt_count:
382
+ del self._request_attempt_count[prepared_request]
383
+
384
+ def _handle_error_resolution(
385
+ self,
386
+ response: Optional[requests.Response],
387
+ exc: Optional[requests.RequestException],
388
+ request: requests.PreparedRequest,
389
+ error_resolution: ErrorResolution,
390
+ exit_on_rate_limit: Optional[bool] = False,
391
+ ) -> None:
392
+ if error_resolution.response_action not in self._ACTIONS_TO_RETRY_ON:
393
+ self._evict_key(request)
394
+
395
+ # Emit stream status RUNNING with the reason RATE_LIMITED to log that the rate limit has been reached
396
+ if error_resolution.response_action == ResponseAction.RATE_LIMITED:
397
+ # TODO: Update to handle with message repository when concurrent message repository is ready
398
+ reasons = [AirbyteStreamStatusReason(type=AirbyteStreamStatusReasonType.RATE_LIMITED)]
399
+ message = orjson.dumps(
400
+ AirbyteMessageSerializer.dump(
401
+ stream_status_as_airbyte_message(
402
+ StreamDescriptor(name=self._name), AirbyteStreamStatus.RUNNING, reasons
403
+ )
404
+ )
405
+ ).decode()
406
+
407
+ # Simply printing the stream status is a temporary solution and can cause future issues. Currently, the _send method is
408
+ # wrapped with backoff decorators, and we can only emit messages by iterating record_iterator in the abstract source at the
409
+ # end of the retry decorator behavior. This approach does not allow us to emit messages in the queue before exiting the
410
+ # backoff retry loop. Adding `\n` to the message and ignore 'end' ensure that few messages are printed at the same time.
411
+ print(f"{message}\n", end="", flush=True)
412
+
413
+ if error_resolution.response_action == ResponseAction.FAIL:
414
+ if response is not None:
415
+ filtered_response_message = filter_secrets(
416
+ f"Request (body): '{str(request.body)}'. Response (body): '{self._get_response_body(response)}'. Response (headers): '{response.headers}'."
417
+ )
418
+ 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)}'. {filtered_response_message}"
419
+ else:
420
+ error_message = (
421
+ f"'{request.method}' request to '{request.url}' failed with exception: '{exc}'"
422
+ )
423
+
424
+ # ensure the exception message is emitted before raised
425
+ self._logger.error(error_message)
426
+
427
+ raise MessageRepresentationAirbyteTracedErrors(
428
+ internal_message=error_message,
429
+ message=error_resolution.error_message or error_message,
430
+ failure_type=error_resolution.failure_type,
431
+ )
432
+
433
+ elif error_resolution.response_action == ResponseAction.IGNORE:
434
+ if response is not None:
435
+ log_message = f"Ignoring response for '{request.method}' request to '{request.url}' with response code '{response.status_code}'"
436
+ else:
437
+ log_message = f"Ignoring response for '{request.method}' request to '{request.url}' with error '{exc}'"
438
+
439
+ self._logger.info(error_resolution.error_message or log_message)
440
+
441
+ # TODO: Consider dynamic retry count depending on subsequent error codes
442
+ elif (
443
+ error_resolution.response_action == ResponseAction.RETRY
444
+ or error_resolution.response_action == ResponseAction.RATE_LIMITED
445
+ ):
446
+ user_defined_backoff_time = None
447
+ for backoff_strategy in self._backoff_strategies:
448
+ backoff_time = backoff_strategy.backoff_time(
449
+ response_or_exception=response if response is not None else exc,
450
+ attempt_count=self._request_attempt_count[request],
451
+ )
452
+ if backoff_time:
453
+ user_defined_backoff_time = backoff_time
454
+ break
455
+ error_message = (
456
+ error_resolution.error_message
457
+ or f"Request to {request.url} failed with failure type {error_resolution.failure_type}, response action {error_resolution.response_action}."
458
+ )
459
+
460
+ retry_endlessly = (
461
+ error_resolution.response_action == ResponseAction.RATE_LIMITED
462
+ and not exit_on_rate_limit
463
+ )
464
+
465
+ if user_defined_backoff_time:
466
+ raise UserDefinedBackoffException(
467
+ backoff=user_defined_backoff_time,
468
+ request=request,
469
+ response=(response if response is not None else exc),
470
+ error_message=error_message,
471
+ )
472
+
473
+ elif retry_endlessly:
474
+ raise RateLimitBackoffException(
475
+ request=request,
476
+ response=(response if response is not None else exc),
477
+ error_message=error_message,
478
+ )
479
+
480
+ raise DefaultBackoffException(
481
+ request=request,
482
+ response=(response if response is not None else exc),
483
+ error_message=error_message,
484
+ )
485
+
486
+ elif response:
487
+ try:
488
+ response.raise_for_status()
489
+ except requests.HTTPError as e:
490
+ self._logger.error(response.text)
491
+ raise e
492
+
493
+ @property
494
+ def name(self) -> str:
495
+ return self._name
496
+
497
+ def send_request(
498
+ self,
499
+ http_method: str,
500
+ url: str,
501
+ request_kwargs: Mapping[str, Any],
502
+ headers: Optional[Mapping[str, str]] = None,
503
+ params: Optional[Mapping[str, str]] = None,
504
+ json: Optional[Mapping[str, Any]] = None,
505
+ data: Optional[Union[str, Mapping[str, Any]]] = None,
506
+ dedupe_query_params: bool = False,
507
+ log_formatter: Optional[Callable[[requests.Response], Any]] = None,
508
+ exit_on_rate_limit: Optional[bool] = False,
509
+ ) -> Tuple[requests.PreparedRequest, requests.Response]:
510
+ """
511
+ Prepares and sends request and return request and response objects.
512
+ """
513
+
514
+ request: requests.PreparedRequest = self._create_prepared_request(
515
+ http_method=http_method,
516
+ url=url,
517
+ dedupe_query_params=dedupe_query_params,
518
+ headers=headers,
519
+ params=params,
520
+ json=json,
521
+ data=data,
522
+ )
523
+
524
+ response: requests.Response = self._send_with_retry(
525
+ request=request,
526
+ request_kwargs=request_kwargs,
527
+ log_formatter=log_formatter,
528
+ exit_on_rate_limit=exit_on_rate_limit,
529
+ )
530
+
531
+ 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 DefaultBackoffException, UserDefinedBackoffException
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(f"Status code: {exc.response.status_code}, Response Content: {exc.response.content}")
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
  )
@@ -40,16 +46,19 @@ def default_backoff_handler(
40
46
  def should_give_up(exc: Exception) -> bool:
41
47
  # If a non-rate-limiting related 4XX error makes it this far, it means it was unexpected and probably consistent, so we shouldn't back off
42
48
  if isinstance(exc, RequestException):
43
- give_up: bool = (
44
- exc.response is not None and exc.response.status_code != codes.too_many_requests and 400 <= exc.response.status_code < 500
45
- )
46
- if give_up:
47
- logger.info(f"Giving up for returned HTTP status: {exc.response.status_code}")
48
- return give_up
49
+ if exc.response is not None:
50
+ give_up: bool = (
51
+ exc.response is not None
52
+ and exc.response.status_code != codes.too_many_requests
53
+ and 400 <= exc.response.status_code < 500
54
+ )
55
+ if give_up:
56
+ logger.info(f"Giving up for returned HTTP status: {exc.response.status_code!r}")
57
+ return give_up
49
58
  # Only RequestExceptions are retryable, so if we get here, it's not retryable
50
59
  return False
51
60
 
52
- return backoff.on_exception(
61
+ return backoff.on_exception( # type: ignore # Decorator function returns a function with a different signature than the input function, so mypy can't infer the type of the returned function
53
62
  backoff.expo,
54
63
  TRANSIENT_EXCEPTIONS,
55
64
  jitter=None,
@@ -62,6 +71,35 @@ def default_backoff_handler(
62
71
  )
63
72
 
64
73
 
74
+ def http_client_default_backoff_handler(
75
+ max_tries: Optional[int], max_time: Optional[int] = None, **kwargs: Any
76
+ ) -> Callable[[SendRequestCallableType], SendRequestCallableType]:
77
+ def log_retry_attempt(details: Mapping[str, Any]) -> None:
78
+ _, exc, _ = sys.exc_info()
79
+ if isinstance(exc, RequestException) and exc.response:
80
+ logger.info(
81
+ f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
82
+ )
83
+ logger.info(
84
+ f"Caught retryable error '{str(exc)}' after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..."
85
+ )
86
+
87
+ def should_give_up(exc: Exception) -> bool:
88
+ # If made it here, the ResponseAction was RETRY and therefore should not give up
89
+ return False
90
+
91
+ return backoff.on_exception( # type: ignore # Decorator function returns a function with a different signature than the input function, so mypy can't infer the type of the returned function
92
+ backoff.expo,
93
+ TRANSIENT_EXCEPTIONS,
94
+ jitter=None,
95
+ on_backoff=log_retry_attempt,
96
+ giveup=should_give_up,
97
+ max_tries=max_tries,
98
+ max_time=max_time,
99
+ **kwargs,
100
+ )
101
+
102
+
65
103
  def user_defined_backoff_handler(
66
104
  max_tries: Optional[int], max_time: Optional[int] = None, **kwargs: Any
67
105
  ) -> Callable[[SendRequestCallableType], SendRequestCallableType]:
@@ -69,7 +107,9 @@ def user_defined_backoff_handler(
69
107
  _, exc, _ = sys.exc_info()
70
108
  if isinstance(exc, UserDefinedBackoffException):
71
109
  if exc.response:
72
- logger.info(f"Status code: {exc.response.status_code}, Response Content: {exc.response.content}")
110
+ logger.info(
111
+ f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
112
+ )
73
113
  retry_after = exc.backoff
74
114
  logger.info(f"Retrying. Sleeping for {retry_after} seconds")
75
115
  time.sleep(retry_after + 1) # extra second to cover any fractions of second
@@ -77,11 +117,13 @@ def user_defined_backoff_handler(
77
117
  def log_give_up(details: Mapping[str, Any]) -> None:
78
118
  _, exc, _ = sys.exc_info()
79
119
  if isinstance(exc, RequestException):
80
- logger.error(f"Max retry limit reached. Request: {exc.request}, Response: {exc.response}")
120
+ logger.error(
121
+ f"Max retry limit reached in {details['elapsed']}s. Request: {exc.request}, Response: {exc.response}"
122
+ )
81
123
  else:
82
124
  logger.error("Max retry limit reached for unknown request and response")
83
125
 
84
- return backoff.on_exception(
126
+ return backoff.on_exception( # type: ignore # Decorator function returns a function with a different signature than the input function, so mypy can't infer the type of the returned function
85
127
  backoff.constant,
86
128
  UserDefinedBackoffException,
87
129
  interval=0, # skip waiting, we'll wait in on_backoff handler
@@ -92,3 +134,25 @@ def user_defined_backoff_handler(
92
134
  max_time=max_time,
93
135
  **kwargs,
94
136
  )
137
+
138
+
139
+ def rate_limit_default_backoff_handler(
140
+ **kwargs: Any,
141
+ ) -> Callable[[SendRequestCallableType], SendRequestCallableType]:
142
+ def log_retry_attempt(details: Mapping[str, Any]) -> None:
143
+ _, exc, _ = sys.exc_info()
144
+ if isinstance(exc, RequestException) and exc.response:
145
+ logger.info(
146
+ f"Status code: {exc.response.status_code!r}, Response Content: {exc.response.content!r}"
147
+ )
148
+ logger.info(
149
+ f"Caught retryable error '{str(exc)}' after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..."
150
+ )
151
+
152
+ return backoff.on_exception( # type: ignore # Decorator function returns a function with a different signature than the input function, so mypy can't infer the type of the returned function
153
+ backoff.expo,
154
+ RateLimitBackoffException,
155
+ jitter=None,
156
+ on_backoff=log_retry_attempt,
157
+ **kwargs,
158
+ )