airbyte-cdk 0.72.0__py3-none-any.whl → 6.13.1.dev4106__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (517) 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 +421 -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 +1213 -88
  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 +65 -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 +25 -3
  79. airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py +156 -48
  80. airbyte_cdk/sources/declarative/incremental/declarative_cursor.py +13 -0
  81. airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py +350 -0
  82. airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +159 -74
  83. airbyte_cdk/sources/declarative/incremental/per_partition_with_global.py +200 -0
  84. airbyte_cdk/sources/declarative/incremental/resumable_full_refresh_cursor.py +122 -0
  85. airbyte_cdk/sources/declarative/interpolation/filters.py +27 -1
  86. airbyte_cdk/sources/declarative/interpolation/interpolated_boolean.py +23 -5
  87. airbyte_cdk/sources/declarative/interpolation/interpolated_mapping.py +12 -8
  88. airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py +13 -6
  89. airbyte_cdk/sources/declarative/interpolation/interpolated_string.py +21 -6
  90. airbyte_cdk/sources/declarative/interpolation/interpolation.py +9 -3
  91. airbyte_cdk/sources/declarative/interpolation/jinja.py +72 -37
  92. airbyte_cdk/sources/declarative/interpolation/macros.py +72 -17
  93. airbyte_cdk/sources/declarative/manifest_declarative_source.py +193 -52
  94. airbyte_cdk/sources/declarative/migrations/legacy_to_per_partition_state_migration.py +98 -0
  95. airbyte_cdk/sources/declarative/migrations/state_migration.py +24 -0
  96. airbyte_cdk/sources/declarative/models/__init__.py +1 -1
  97. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +1329 -595
  98. airbyte_cdk/sources/declarative/parsers/custom_exceptions.py +2 -2
  99. airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py +26 -4
  100. airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py +26 -15
  101. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +1699 -226
  102. airbyte_cdk/sources/declarative/partition_routers/__init__.py +24 -4
  103. airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py +65 -0
  104. airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py +176 -0
  105. airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py +39 -9
  106. airbyte_cdk/sources/declarative/partition_routers/partition_router.py +62 -0
  107. airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py +15 -3
  108. airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +222 -39
  109. airbyte_cdk/sources/declarative/requesters/error_handlers/__init__.py +19 -5
  110. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/__init__.py +3 -1
  111. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/constant_backoff_strategy.py +19 -7
  112. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/exponential_backoff_strategy.py +19 -7
  113. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/header_helper.py +4 -2
  114. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_time_from_header_backoff_strategy.py +41 -9
  115. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategies/wait_until_time_from_header_backoff_strategy.py +29 -14
  116. airbyte_cdk/sources/declarative/requesters/error_handlers/backoff_strategy.py +5 -13
  117. airbyte_cdk/sources/declarative/requesters/error_handlers/composite_error_handler.py +32 -16
  118. airbyte_cdk/sources/declarative/requesters/error_handlers/default_error_handler.py +46 -56
  119. airbyte_cdk/sources/declarative/requesters/error_handlers/default_http_response_filter.py +40 -0
  120. airbyte_cdk/sources/declarative/requesters/error_handlers/error_handler.py +6 -32
  121. airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py +119 -41
  122. airbyte_cdk/sources/declarative/requesters/http_job_repository.py +228 -0
  123. airbyte_cdk/sources/declarative/requesters/http_requester.py +98 -344
  124. airbyte_cdk/sources/declarative/requesters/paginators/__init__.py +14 -3
  125. airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py +105 -46
  126. airbyte_cdk/sources/declarative/requesters/paginators/no_pagination.py +14 -8
  127. airbyte_cdk/sources/declarative/requesters/paginators/paginator.py +19 -8
  128. airbyte_cdk/sources/declarative/requesters/paginators/strategies/__init__.py +9 -3
  129. airbyte_cdk/sources/declarative/requesters/paginators/strategies/cursor_pagination_strategy.py +53 -21
  130. airbyte_cdk/sources/declarative/requesters/paginators/strategies/offset_increment.py +42 -19
  131. airbyte_cdk/sources/declarative/requesters/paginators/strategies/page_increment.py +25 -12
  132. airbyte_cdk/sources/declarative/requesters/paginators/strategies/pagination_strategy.py +13 -10
  133. airbyte_cdk/sources/declarative/requesters/paginators/strategies/stop_condition.py +26 -13
  134. airbyte_cdk/sources/declarative/requesters/request_options/__init__.py +15 -2
  135. airbyte_cdk/sources/declarative/requesters/request_options/datetime_based_request_options_provider.py +91 -0
  136. airbyte_cdk/sources/declarative/requesters/request_options/default_request_options_provider.py +60 -0
  137. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_nested_request_input_provider.py +31 -14
  138. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_input_provider.py +27 -15
  139. airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py +63 -10
  140. airbyte_cdk/sources/declarative/requesters/request_options/request_options_provider.py +1 -1
  141. airbyte_cdk/sources/declarative/requesters/requester.py +9 -17
  142. airbyte_cdk/sources/declarative/resolvers/__init__.py +41 -0
  143. airbyte_cdk/sources/declarative/resolvers/components_resolver.py +55 -0
  144. airbyte_cdk/sources/declarative/resolvers/config_components_resolver.py +136 -0
  145. airbyte_cdk/sources/declarative/resolvers/http_components_resolver.py +112 -0
  146. airbyte_cdk/sources/declarative/retrievers/__init__.py +6 -2
  147. airbyte_cdk/sources/declarative/retrievers/async_retriever.py +100 -0
  148. airbyte_cdk/sources/declarative/retrievers/retriever.py +1 -3
  149. airbyte_cdk/sources/declarative/retrievers/simple_retriever.py +228 -72
  150. airbyte_cdk/sources/declarative/schema/__init__.py +14 -1
  151. airbyte_cdk/sources/declarative/schema/default_schema_loader.py +5 -3
  152. airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +236 -0
  153. airbyte_cdk/sources/declarative/schema/json_file_schema_loader.py +8 -8
  154. airbyte_cdk/sources/declarative/spec/spec.py +12 -5
  155. airbyte_cdk/sources/declarative/stream_slicers/__init__.py +1 -2
  156. airbyte_cdk/sources/declarative/stream_slicers/declarative_partition_generator.py +88 -0
  157. airbyte_cdk/sources/declarative/stream_slicers/stream_slicer.py +9 -14
  158. airbyte_cdk/sources/declarative/transformations/add_fields.py +19 -11
  159. airbyte_cdk/sources/declarative/transformations/flatten_fields.py +52 -0
  160. airbyte_cdk/sources/declarative/transformations/keys_replace_transformation.py +61 -0
  161. airbyte_cdk/sources/declarative/transformations/keys_to_lower_transformation.py +22 -0
  162. airbyte_cdk/sources/declarative/transformations/keys_to_snake_transformation.py +68 -0
  163. airbyte_cdk/sources/declarative/transformations/remove_fields.py +13 -10
  164. airbyte_cdk/sources/declarative/transformations/transformation.py +5 -5
  165. airbyte_cdk/sources/declarative/types.py +19 -110
  166. airbyte_cdk/sources/declarative/yaml_declarative_source.py +31 -10
  167. airbyte_cdk/sources/embedded/base_integration.py +16 -5
  168. airbyte_cdk/sources/embedded/catalog.py +16 -4
  169. airbyte_cdk/sources/embedded/runner.py +19 -3
  170. airbyte_cdk/sources/embedded/tools.py +5 -2
  171. airbyte_cdk/sources/file_based/README.md +152 -0
  172. airbyte_cdk/sources/file_based/__init__.py +24 -0
  173. airbyte_cdk/sources/file_based/availability_strategy/__init__.py +9 -2
  174. airbyte_cdk/sources/file_based/availability_strategy/abstract_file_based_availability_strategy.py +22 -6
  175. airbyte_cdk/sources/file_based/availability_strategy/default_file_based_availability_strategy.py +46 -10
  176. airbyte_cdk/sources/file_based/config/abstract_file_based_spec.py +58 -10
  177. airbyte_cdk/sources/file_based/config/avro_format.py +2 -1
  178. airbyte_cdk/sources/file_based/config/csv_format.py +29 -10
  179. airbyte_cdk/sources/file_based/config/excel_format.py +18 -0
  180. airbyte_cdk/sources/file_based/config/file_based_stream_config.py +16 -4
  181. airbyte_cdk/sources/file_based/config/jsonl_format.py +2 -1
  182. airbyte_cdk/sources/file_based/config/parquet_format.py +2 -1
  183. airbyte_cdk/sources/file_based/config/unstructured_format.py +13 -5
  184. airbyte_cdk/sources/file_based/discovery_policy/__init__.py +6 -2
  185. airbyte_cdk/sources/file_based/discovery_policy/abstract_discovery_policy.py +2 -4
  186. airbyte_cdk/sources/file_based/discovery_policy/default_discovery_policy.py +7 -2
  187. airbyte_cdk/sources/file_based/exceptions.py +52 -15
  188. airbyte_cdk/sources/file_based/file_based_source.py +163 -33
  189. airbyte_cdk/sources/file_based/file_based_stream_reader.py +83 -5
  190. airbyte_cdk/sources/file_based/file_types/__init__.py +14 -1
  191. airbyte_cdk/sources/file_based/file_types/avro_parser.py +75 -24
  192. airbyte_cdk/sources/file_based/file_types/csv_parser.py +116 -34
  193. airbyte_cdk/sources/file_based/file_types/excel_parser.py +196 -0
  194. airbyte_cdk/sources/file_based/file_types/file_transfer.py +37 -0
  195. airbyte_cdk/sources/file_based/file_types/file_type_parser.py +4 -1
  196. airbyte_cdk/sources/file_based/file_types/jsonl_parser.py +24 -8
  197. airbyte_cdk/sources/file_based/file_types/parquet_parser.py +60 -18
  198. airbyte_cdk/sources/file_based/file_types/unstructured_parser.py +145 -41
  199. airbyte_cdk/sources/file_based/remote_file.py +1 -1
  200. airbyte_cdk/sources/file_based/schema_helpers.py +38 -10
  201. airbyte_cdk/sources/file_based/schema_validation_policies/__init__.py +3 -1
  202. airbyte_cdk/sources/file_based/schema_validation_policies/abstract_schema_validation_policy.py +3 -1
  203. airbyte_cdk/sources/file_based/schema_validation_policies/default_schema_validation_policies.py +16 -5
  204. airbyte_cdk/sources/file_based/stream/abstract_file_based_stream.py +50 -13
  205. airbyte_cdk/sources/file_based/stream/concurrent/adapters.py +67 -27
  206. airbyte_cdk/sources/file_based/stream/concurrent/cursor/__init__.py +5 -1
  207. airbyte_cdk/sources/file_based/stream/concurrent/cursor/abstract_concurrent_file_based_cursor.py +14 -23
  208. airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_concurrent_cursor.py +54 -18
  209. airbyte_cdk/sources/file_based/stream/concurrent/cursor/file_based_final_state_cursor.py +21 -9
  210. airbyte_cdk/sources/file_based/stream/cursor/abstract_file_based_cursor.py +3 -1
  211. airbyte_cdk/sources/file_based/stream/cursor/default_file_based_cursor.py +27 -10
  212. airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +175 -45
  213. airbyte_cdk/sources/http_logger.py +8 -3
  214. airbyte_cdk/sources/message/__init__.py +7 -1
  215. airbyte_cdk/sources/message/repository.py +18 -4
  216. airbyte_cdk/sources/source.py +42 -38
  217. airbyte_cdk/sources/streams/__init__.py +2 -2
  218. airbyte_cdk/sources/streams/availability_strategy.py +54 -3
  219. airbyte_cdk/sources/streams/call_rate.py +64 -21
  220. airbyte_cdk/sources/streams/checkpoint/__init__.py +26 -0
  221. airbyte_cdk/sources/streams/checkpoint/checkpoint_reader.py +335 -0
  222. airbyte_cdk/sources/{declarative/incremental → streams/checkpoint}/cursor.py +17 -14
  223. airbyte_cdk/sources/streams/checkpoint/per_partition_key_serializer.py +22 -0
  224. airbyte_cdk/sources/streams/checkpoint/resumable_full_refresh_cursor.py +51 -0
  225. airbyte_cdk/sources/streams/checkpoint/substream_resumable_full_refresh_cursor.py +110 -0
  226. airbyte_cdk/sources/streams/concurrent/README.md +7 -0
  227. airbyte_cdk/sources/streams/concurrent/abstract_stream.py +7 -2
  228. airbyte_cdk/sources/streams/concurrent/adapters.py +84 -75
  229. airbyte_cdk/sources/streams/concurrent/availability_strategy.py +30 -2
  230. airbyte_cdk/sources/streams/concurrent/cursor.py +298 -42
  231. airbyte_cdk/sources/streams/concurrent/default_stream.py +12 -3
  232. airbyte_cdk/sources/streams/concurrent/exceptions.py +3 -0
  233. airbyte_cdk/sources/streams/concurrent/helpers.py +14 -3
  234. airbyte_cdk/sources/streams/concurrent/partition_enqueuer.py +12 -3
  235. airbyte_cdk/sources/streams/concurrent/partition_reader.py +10 -3
  236. airbyte_cdk/sources/streams/concurrent/partitions/partition.py +1 -16
  237. airbyte_cdk/sources/streams/concurrent/partitions/stream_slicer.py +21 -0
  238. airbyte_cdk/sources/streams/concurrent/partitions/types.py +15 -5
  239. airbyte_cdk/sources/streams/concurrent/state_converters/abstract_stream_state_converter.py +109 -17
  240. airbyte_cdk/sources/streams/concurrent/state_converters/datetime_stream_state_converter.py +90 -72
  241. airbyte_cdk/sources/streams/core.py +412 -87
  242. airbyte_cdk/sources/streams/http/__init__.py +2 -1
  243. airbyte_cdk/sources/streams/http/availability_strategy.py +12 -101
  244. airbyte_cdk/sources/streams/http/error_handlers/__init__.py +22 -0
  245. airbyte_cdk/sources/streams/http/error_handlers/backoff_strategy.py +28 -0
  246. airbyte_cdk/sources/streams/http/error_handlers/default_backoff_strategy.py +17 -0
  247. airbyte_cdk/sources/streams/http/error_handlers/default_error_mapping.py +86 -0
  248. airbyte_cdk/sources/streams/http/error_handlers/error_handler.py +42 -0
  249. airbyte_cdk/sources/streams/http/error_handlers/error_message_parser.py +19 -0
  250. airbyte_cdk/sources/streams/http/error_handlers/http_status_error_handler.py +110 -0
  251. airbyte_cdk/sources/streams/http/error_handlers/json_error_message_parser.py +52 -0
  252. airbyte_cdk/sources/streams/http/error_handlers/response_models.py +65 -0
  253. airbyte_cdk/sources/streams/http/exceptions.py +27 -7
  254. airbyte_cdk/sources/streams/http/http.py +369 -246
  255. airbyte_cdk/sources/streams/http/http_client.py +531 -0
  256. airbyte_cdk/sources/streams/http/rate_limiting.py +76 -12
  257. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +28 -9
  258. airbyte_cdk/sources/streams/http/requests_native_auth/abstract_token.py +2 -1
  259. airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +90 -35
  260. airbyte_cdk/sources/streams/http/requests_native_auth/token.py +13 -3
  261. airbyte_cdk/sources/types.py +154 -0
  262. airbyte_cdk/sources/utils/record_helper.py +36 -21
  263. airbyte_cdk/sources/utils/schema_helpers.py +13 -6
  264. airbyte_cdk/sources/utils/slice_logger.py +4 -1
  265. airbyte_cdk/sources/utils/transform.py +54 -20
  266. airbyte_cdk/sql/_util/hashing.py +34 -0
  267. airbyte_cdk/sql/_util/name_normalizers.py +92 -0
  268. airbyte_cdk/sql/constants.py +32 -0
  269. airbyte_cdk/sql/exceptions.py +235 -0
  270. airbyte_cdk/sql/secrets.py +123 -0
  271. airbyte_cdk/sql/shared/__init__.py +15 -0
  272. airbyte_cdk/sql/shared/catalog_providers.py +145 -0
  273. airbyte_cdk/sql/shared/sql_processor.py +786 -0
  274. airbyte_cdk/sql/types.py +160 -0
  275. airbyte_cdk/test/catalog_builder.py +70 -18
  276. airbyte_cdk/test/entrypoint_wrapper.py +117 -42
  277. airbyte_cdk/test/mock_http/__init__.py +1 -1
  278. airbyte_cdk/test/mock_http/matcher.py +6 -0
  279. airbyte_cdk/test/mock_http/mocker.py +57 -10
  280. airbyte_cdk/test/mock_http/request.py +19 -3
  281. airbyte_cdk/test/mock_http/response.py +3 -1
  282. airbyte_cdk/test/mock_http/response_builder.py +32 -16
  283. airbyte_cdk/test/state_builder.py +18 -10
  284. airbyte_cdk/test/utils/__init__.py +1 -0
  285. airbyte_cdk/test/utils/data.py +24 -0
  286. airbyte_cdk/test/utils/http_mocking.py +16 -0
  287. airbyte_cdk/test/utils/manifest_only_fixtures.py +60 -0
  288. airbyte_cdk/test/utils/reading.py +26 -0
  289. airbyte_cdk/utils/__init__.py +2 -1
  290. airbyte_cdk/utils/airbyte_secrets_utils.py +5 -3
  291. airbyte_cdk/utils/analytics_message.py +10 -2
  292. airbyte_cdk/utils/datetime_format_inferrer.py +4 -1
  293. airbyte_cdk/utils/event_timing.py +10 -10
  294. airbyte_cdk/utils/mapping_helpers.py +3 -1
  295. airbyte_cdk/utils/message_utils.py +20 -11
  296. airbyte_cdk/utils/print_buffer.py +75 -0
  297. airbyte_cdk/utils/schema_inferrer.py +198 -28
  298. airbyte_cdk/utils/slice_hasher.py +30 -0
  299. airbyte_cdk/utils/spec_schema_transformations.py +6 -3
  300. airbyte_cdk/utils/stream_status_utils.py +8 -1
  301. airbyte_cdk/utils/traced_exception.py +61 -21
  302. airbyte_cdk-6.13.1.dev4106.dist-info/METADATA +109 -0
  303. airbyte_cdk-6.13.1.dev4106.dist-info/RECORD +349 -0
  304. {airbyte_cdk-0.72.0.dist-info → airbyte_cdk-6.13.1.dev4106.dist-info}/WHEEL +1 -2
  305. airbyte_cdk-6.13.1.dev4106.dist-info/entry_points.txt +3 -0
  306. airbyte_cdk/sources/declarative/create_partial.py +0 -92
  307. airbyte_cdk/sources/declarative/parsers/class_types_registry.py +0 -102
  308. airbyte_cdk/sources/declarative/parsers/default_implementation_registry.py +0 -64
  309. airbyte_cdk/sources/declarative/requesters/error_handlers/response_action.py +0 -16
  310. airbyte_cdk/sources/declarative/requesters/error_handlers/response_status.py +0 -68
  311. airbyte_cdk/sources/declarative/stream_slicers/cartesian_product_stream_slicer.py +0 -114
  312. airbyte_cdk/sources/deprecated/base_source.py +0 -94
  313. airbyte_cdk/sources/deprecated/client.py +0 -99
  314. airbyte_cdk/sources/singer/__init__.py +0 -8
  315. airbyte_cdk/sources/singer/singer_helpers.py +0 -304
  316. airbyte_cdk/sources/singer/source.py +0 -186
  317. airbyte_cdk/sources/streams/concurrent/partitions/record.py +0 -23
  318. airbyte_cdk/sources/streams/http/auth/__init__.py +0 -17
  319. airbyte_cdk/sources/streams/http/auth/core.py +0 -29
  320. airbyte_cdk/sources/streams/http/auth/oauth.py +0 -113
  321. airbyte_cdk/sources/streams/http/auth/token.py +0 -47
  322. airbyte_cdk/sources/streams/utils/stream_helper.py +0 -40
  323. airbyte_cdk/sources/utils/catalog_helpers.py +0 -22
  324. airbyte_cdk/sources/utils/schema_models.py +0 -84
  325. airbyte_cdk-0.72.0.dist-info/METADATA +0 -243
  326. airbyte_cdk-0.72.0.dist-info/RECORD +0 -466
  327. airbyte_cdk-0.72.0.dist-info/top_level.txt +0 -3
  328. source_declarative_manifest/main.py +0 -29
  329. unit_tests/connector_builder/__init__.py +0 -3
  330. unit_tests/connector_builder/test_connector_builder_handler.py +0 -871
  331. unit_tests/connector_builder/test_message_grouper.py +0 -713
  332. unit_tests/connector_builder/utils.py +0 -27
  333. unit_tests/destinations/test_destination.py +0 -243
  334. unit_tests/singer/test_singer_helpers.py +0 -56
  335. unit_tests/singer/test_singer_source.py +0 -112
  336. unit_tests/sources/__init__.py +0 -0
  337. unit_tests/sources/concurrent_source/__init__.py +0 -3
  338. unit_tests/sources/concurrent_source/test_concurrent_source_adapter.py +0 -106
  339. unit_tests/sources/declarative/__init__.py +0 -3
  340. unit_tests/sources/declarative/auth/__init__.py +0 -3
  341. unit_tests/sources/declarative/auth/test_oauth.py +0 -331
  342. unit_tests/sources/declarative/auth/test_selective_authenticator.py +0 -39
  343. unit_tests/sources/declarative/auth/test_session_token_auth.py +0 -182
  344. unit_tests/sources/declarative/auth/test_token_auth.py +0 -200
  345. unit_tests/sources/declarative/auth/test_token_provider.py +0 -73
  346. unit_tests/sources/declarative/checks/__init__.py +0 -3
  347. unit_tests/sources/declarative/checks/test_check_stream.py +0 -146
  348. unit_tests/sources/declarative/decoders/__init__.py +0 -0
  349. unit_tests/sources/declarative/decoders/test_json_decoder.py +0 -16
  350. unit_tests/sources/declarative/external_component.py +0 -13
  351. unit_tests/sources/declarative/extractors/__init__.py +0 -3
  352. unit_tests/sources/declarative/extractors/test_dpath_extractor.py +0 -55
  353. unit_tests/sources/declarative/extractors/test_record_filter.py +0 -55
  354. unit_tests/sources/declarative/extractors/test_record_selector.py +0 -179
  355. unit_tests/sources/declarative/incremental/__init__.py +0 -0
  356. unit_tests/sources/declarative/incremental/test_datetime_based_cursor.py +0 -860
  357. unit_tests/sources/declarative/incremental/test_per_partition_cursor.py +0 -406
  358. unit_tests/sources/declarative/incremental/test_per_partition_cursor_integration.py +0 -332
  359. unit_tests/sources/declarative/interpolation/__init__.py +0 -3
  360. unit_tests/sources/declarative/interpolation/test_filters.py +0 -80
  361. unit_tests/sources/declarative/interpolation/test_interpolated_boolean.py +0 -40
  362. unit_tests/sources/declarative/interpolation/test_interpolated_mapping.py +0 -35
  363. unit_tests/sources/declarative/interpolation/test_interpolated_nested_mapping.py +0 -45
  364. unit_tests/sources/declarative/interpolation/test_interpolated_string.py +0 -25
  365. unit_tests/sources/declarative/interpolation/test_jinja.py +0 -240
  366. unit_tests/sources/declarative/interpolation/test_macros.py +0 -73
  367. unit_tests/sources/declarative/parsers/__init__.py +0 -3
  368. unit_tests/sources/declarative/parsers/test_manifest_component_transformer.py +0 -406
  369. unit_tests/sources/declarative/parsers/test_manifest_reference_resolver.py +0 -139
  370. unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +0 -1841
  371. unit_tests/sources/declarative/parsers/testing_components.py +0 -36
  372. unit_tests/sources/declarative/partition_routers/__init__.py +0 -3
  373. unit_tests/sources/declarative/partition_routers/test_list_partition_router.py +0 -155
  374. unit_tests/sources/declarative/partition_routers/test_single_partition_router.py +0 -14
  375. unit_tests/sources/declarative/partition_routers/test_substream_partition_router.py +0 -404
  376. unit_tests/sources/declarative/requesters/__init__.py +0 -3
  377. unit_tests/sources/declarative/requesters/error_handlers/__init__.py +0 -3
  378. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/__init__.py +0 -3
  379. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_constant_backoff.py +0 -34
  380. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_exponential_backoff.py +0 -36
  381. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_header_helper.py +0 -38
  382. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_wait_time_from_header.py +0 -35
  383. unit_tests/sources/declarative/requesters/error_handlers/backoff_strategies/test_wait_until_time_from_header.py +0 -64
  384. unit_tests/sources/declarative/requesters/error_handlers/test_composite_error_handler.py +0 -213
  385. unit_tests/sources/declarative/requesters/error_handlers/test_default_error_handler.py +0 -178
  386. unit_tests/sources/declarative/requesters/error_handlers/test_http_response_filter.py +0 -121
  387. unit_tests/sources/declarative/requesters/error_handlers/test_response_status.py +0 -44
  388. unit_tests/sources/declarative/requesters/paginators/__init__.py +0 -3
  389. unit_tests/sources/declarative/requesters/paginators/test_cursor_pagination_strategy.py +0 -64
  390. unit_tests/sources/declarative/requesters/paginators/test_default_paginator.py +0 -313
  391. unit_tests/sources/declarative/requesters/paginators/test_no_paginator.py +0 -12
  392. unit_tests/sources/declarative/requesters/paginators/test_offset_increment.py +0 -58
  393. unit_tests/sources/declarative/requesters/paginators/test_page_increment.py +0 -70
  394. unit_tests/sources/declarative/requesters/paginators/test_request_option.py +0 -43
  395. unit_tests/sources/declarative/requesters/paginators/test_stop_condition.py +0 -105
  396. unit_tests/sources/declarative/requesters/request_options/__init__.py +0 -3
  397. unit_tests/sources/declarative/requesters/request_options/test_interpolated_request_options_provider.py +0 -101
  398. unit_tests/sources/declarative/requesters/test_http_requester.py +0 -974
  399. unit_tests/sources/declarative/requesters/test_interpolated_request_input_provider.py +0 -32
  400. unit_tests/sources/declarative/retrievers/__init__.py +0 -3
  401. unit_tests/sources/declarative/retrievers/test_simple_retriever.py +0 -542
  402. unit_tests/sources/declarative/schema/__init__.py +0 -6
  403. unit_tests/sources/declarative/schema/source_test/SourceTest.py +0 -8
  404. unit_tests/sources/declarative/schema/source_test/__init__.py +0 -3
  405. unit_tests/sources/declarative/schema/test_default_schema_loader.py +0 -32
  406. unit_tests/sources/declarative/schema/test_inline_schema_loader.py +0 -19
  407. unit_tests/sources/declarative/schema/test_json_file_schema_loader.py +0 -26
  408. unit_tests/sources/declarative/states/__init__.py +0 -3
  409. unit_tests/sources/declarative/stream_slicers/__init__.py +0 -3
  410. unit_tests/sources/declarative/stream_slicers/test_cartesian_product_stream_slicer.py +0 -225
  411. unit_tests/sources/declarative/test_create_partial.py +0 -83
  412. unit_tests/sources/declarative/test_declarative_stream.py +0 -103
  413. unit_tests/sources/declarative/test_manifest_declarative_source.py +0 -1260
  414. unit_tests/sources/declarative/test_types.py +0 -39
  415. unit_tests/sources/declarative/test_yaml_declarative_source.py +0 -148
  416. unit_tests/sources/file_based/__init__.py +0 -0
  417. unit_tests/sources/file_based/availability_strategy/__init__.py +0 -0
  418. unit_tests/sources/file_based/availability_strategy/test_default_file_based_availability_strategy.py +0 -100
  419. unit_tests/sources/file_based/config/__init__.py +0 -0
  420. unit_tests/sources/file_based/config/test_abstract_file_based_spec.py +0 -28
  421. unit_tests/sources/file_based/config/test_csv_format.py +0 -34
  422. unit_tests/sources/file_based/config/test_file_based_stream_config.py +0 -84
  423. unit_tests/sources/file_based/discovery_policy/__init__.py +0 -0
  424. unit_tests/sources/file_based/discovery_policy/test_default_discovery_policy.py +0 -31
  425. unit_tests/sources/file_based/file_types/__init__.py +0 -0
  426. unit_tests/sources/file_based/file_types/test_avro_parser.py +0 -243
  427. unit_tests/sources/file_based/file_types/test_csv_parser.py +0 -546
  428. unit_tests/sources/file_based/file_types/test_jsonl_parser.py +0 -158
  429. unit_tests/sources/file_based/file_types/test_parquet_parser.py +0 -274
  430. unit_tests/sources/file_based/file_types/test_unstructured_parser.py +0 -593
  431. unit_tests/sources/file_based/helpers.py +0 -70
  432. unit_tests/sources/file_based/in_memory_files_source.py +0 -211
  433. unit_tests/sources/file_based/scenarios/__init__.py +0 -0
  434. unit_tests/sources/file_based/scenarios/avro_scenarios.py +0 -744
  435. unit_tests/sources/file_based/scenarios/check_scenarios.py +0 -220
  436. unit_tests/sources/file_based/scenarios/concurrent_incremental_scenarios.py +0 -2844
  437. unit_tests/sources/file_based/scenarios/csv_scenarios.py +0 -3105
  438. unit_tests/sources/file_based/scenarios/file_based_source_builder.py +0 -91
  439. unit_tests/sources/file_based/scenarios/incremental_scenarios.py +0 -1926
  440. unit_tests/sources/file_based/scenarios/jsonl_scenarios.py +0 -930
  441. unit_tests/sources/file_based/scenarios/parquet_scenarios.py +0 -754
  442. unit_tests/sources/file_based/scenarios/scenario_builder.py +0 -234
  443. unit_tests/sources/file_based/scenarios/unstructured_scenarios.py +0 -608
  444. unit_tests/sources/file_based/scenarios/user_input_schema_scenarios.py +0 -746
  445. unit_tests/sources/file_based/scenarios/validation_policy_scenarios.py +0 -726
  446. unit_tests/sources/file_based/stream/__init__.py +0 -0
  447. unit_tests/sources/file_based/stream/concurrent/__init__.py +0 -0
  448. unit_tests/sources/file_based/stream/concurrent/test_adapters.py +0 -362
  449. unit_tests/sources/file_based/stream/concurrent/test_file_based_concurrent_cursor.py +0 -458
  450. unit_tests/sources/file_based/stream/test_default_file_based_cursor.py +0 -310
  451. unit_tests/sources/file_based/stream/test_default_file_based_stream.py +0 -244
  452. unit_tests/sources/file_based/test_file_based_scenarios.py +0 -320
  453. unit_tests/sources/file_based/test_file_based_stream_reader.py +0 -272
  454. unit_tests/sources/file_based/test_scenarios.py +0 -253
  455. unit_tests/sources/file_based/test_schema_helpers.py +0 -346
  456. unit_tests/sources/fixtures/__init__.py +0 -3
  457. unit_tests/sources/fixtures/source_test_fixture.py +0 -153
  458. unit_tests/sources/message/__init__.py +0 -0
  459. unit_tests/sources/message/test_repository.py +0 -153
  460. unit_tests/sources/streams/__init__.py +0 -0
  461. unit_tests/sources/streams/concurrent/__init__.py +0 -3
  462. unit_tests/sources/streams/concurrent/scenarios/__init__.py +0 -3
  463. unit_tests/sources/streams/concurrent/scenarios/incremental_scenarios.py +0 -250
  464. unit_tests/sources/streams/concurrent/scenarios/stream_facade_builder.py +0 -140
  465. unit_tests/sources/streams/concurrent/scenarios/stream_facade_scenarios.py +0 -452
  466. unit_tests/sources/streams/concurrent/scenarios/test_concurrent_scenarios.py +0 -76
  467. unit_tests/sources/streams/concurrent/scenarios/thread_based_concurrent_stream_scenarios.py +0 -418
  468. unit_tests/sources/streams/concurrent/scenarios/thread_based_concurrent_stream_source_builder.py +0 -142
  469. unit_tests/sources/streams/concurrent/scenarios/utils.py +0 -55
  470. unit_tests/sources/streams/concurrent/test_adapters.py +0 -380
  471. unit_tests/sources/streams/concurrent/test_concurrent_read_processor.py +0 -684
  472. unit_tests/sources/streams/concurrent/test_cursor.py +0 -139
  473. unit_tests/sources/streams/concurrent/test_datetime_state_converter.py +0 -369
  474. unit_tests/sources/streams/concurrent/test_default_stream.py +0 -197
  475. unit_tests/sources/streams/concurrent/test_partition_enqueuer.py +0 -90
  476. unit_tests/sources/streams/concurrent/test_partition_reader.py +0 -67
  477. unit_tests/sources/streams/concurrent/test_thread_pool_manager.py +0 -106
  478. unit_tests/sources/streams/http/__init__.py +0 -0
  479. unit_tests/sources/streams/http/auth/__init__.py +0 -0
  480. unit_tests/sources/streams/http/auth/test_auth.py +0 -173
  481. unit_tests/sources/streams/http/requests_native_auth/__init__.py +0 -0
  482. unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py +0 -423
  483. unit_tests/sources/streams/http/test_availability_strategy.py +0 -180
  484. unit_tests/sources/streams/http/test_http.py +0 -635
  485. unit_tests/sources/streams/test_availability_strategy.py +0 -70
  486. unit_tests/sources/streams/test_call_rate.py +0 -300
  487. unit_tests/sources/streams/test_stream_read.py +0 -405
  488. unit_tests/sources/streams/test_streams_core.py +0 -184
  489. unit_tests/sources/test_abstract_source.py +0 -1442
  490. unit_tests/sources/test_concurrent_source.py +0 -112
  491. unit_tests/sources/test_config.py +0 -92
  492. unit_tests/sources/test_connector_state_manager.py +0 -482
  493. unit_tests/sources/test_http_logger.py +0 -252
  494. unit_tests/sources/test_integration_source.py +0 -86
  495. unit_tests/sources/test_source.py +0 -684
  496. unit_tests/sources/test_source_read.py +0 -460
  497. unit_tests/test/__init__.py +0 -0
  498. unit_tests/test/mock_http/__init__.py +0 -0
  499. unit_tests/test/mock_http/test_matcher.py +0 -53
  500. unit_tests/test/mock_http/test_mocker.py +0 -214
  501. unit_tests/test/mock_http/test_request.py +0 -117
  502. unit_tests/test/mock_http/test_response_builder.py +0 -177
  503. unit_tests/test/test_entrypoint_wrapper.py +0 -240
  504. unit_tests/utils/__init__.py +0 -0
  505. unit_tests/utils/test_datetime_format_inferrer.py +0 -60
  506. unit_tests/utils/test_mapping_helpers.py +0 -54
  507. unit_tests/utils/test_message_utils.py +0 -91
  508. unit_tests/utils/test_rate_limiting.py +0 -26
  509. unit_tests/utils/test_schema_inferrer.py +0 -202
  510. unit_tests/utils/test_secret_utils.py +0 -135
  511. unit_tests/utils/test_stream_status_utils.py +0 -61
  512. unit_tests/utils/test_traced_exception.py +0 -107
  513. /airbyte_cdk/sources/{deprecated → declarative/async_job}/__init__.py +0 -0
  514. {source_declarative_manifest → airbyte_cdk/sources/declarative/migrations}/__init__.py +0 -0
  515. {unit_tests/destinations → airbyte_cdk/sql}/__init__.py +0 -0
  516. {unit_tests/singer → airbyte_cdk/sql/_util}/__init__.py +0 -0
  517. {airbyte_cdk-0.72.0.dist-info → airbyte_cdk-6.13.1.dev4106.dist-info}/LICENSE.txt +0 -0
@@ -1,974 +0,0 @@
1
- #
2
- # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
- #
4
-
5
- from http import HTTPStatus
6
- from typing import Any, Mapping, Optional
7
- from unittest import mock
8
- from unittest.mock import MagicMock
9
- from urllib.parse import parse_qs, urlparse
10
-
11
- import pytest as pytest
12
- import requests
13
- import requests_cache
14
- from airbyte_cdk.sources.declarative.auth.declarative_authenticator import DeclarativeAuthenticator, NoAuth
15
- from airbyte_cdk.sources.declarative.auth.token import BearerAuthenticator
16
- from airbyte_cdk.sources.declarative.exceptions import ReadException
17
- from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
18
- from airbyte_cdk.sources.declarative.requesters.error_handlers.default_error_handler import DefaultErrorHandler
19
- from airbyte_cdk.sources.declarative.requesters.error_handlers.error_handler import ErrorHandler
20
- from airbyte_cdk.sources.declarative.requesters.http_requester import HttpMethod, HttpRequester
21
- from airbyte_cdk.sources.declarative.requesters.request_options import InterpolatedRequestOptionsProvider
22
- from airbyte_cdk.sources.declarative.types import Config
23
- from airbyte_cdk.sources.message import MessageRepository
24
- from airbyte_cdk.sources.streams.http.exceptions import DefaultBackoffException, RequestBodyException, UserDefinedBackoffException
25
- from requests import PreparedRequest
26
- from requests_cache import CachedResponse
27
-
28
-
29
- @pytest.fixture
30
- def http_requester_factory():
31
- def factory(
32
- name: str = "name",
33
- url_base: str = "https://test_base_url.com",
34
- path: str = "/",
35
- http_method: str = HttpMethod.GET,
36
- request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None,
37
- authenticator: Optional[DeclarativeAuthenticator] = None,
38
- error_handler: Optional[ErrorHandler] = None,
39
- config: Optional[Config] = None,
40
- parameters: Mapping[str, Any] = None,
41
- disable_retries: bool = False,
42
- message_repository: Optional[MessageRepository] = None,
43
- use_cache: bool = False,
44
- ) -> HttpRequester:
45
- return HttpRequester(
46
- name=name,
47
- url_base=url_base,
48
- path=path,
49
- config=config or {},
50
- parameters=parameters or {},
51
- authenticator=authenticator,
52
- http_method=http_method,
53
- request_options_provider=request_options_provider,
54
- error_handler=error_handler,
55
- disable_retries=disable_retries,
56
- message_repository=message_repository or MagicMock(),
57
- use_cache=use_cache,
58
- )
59
-
60
- return factory
61
-
62
-
63
- def test_http_requester():
64
- http_method = HttpMethod.GET
65
-
66
- request_options_provider = MagicMock()
67
- request_params = {"param": "value"}
68
- request_body_data = "body_key_1=value_1&body_key_2=value2"
69
- request_body_json = {"body_field": "body_value"}
70
- request_options_provider.get_request_params.return_value = request_params
71
- request_options_provider.get_request_body_data.return_value = request_body_data
72
- request_options_provider.get_request_body_json.return_value = request_body_json
73
-
74
- request_headers_provider = MagicMock()
75
- request_headers = {"header": "value"}
76
- request_headers_provider.get_request_headers.return_value = request_headers
77
-
78
- authenticator = MagicMock()
79
-
80
- error_handler = MagicMock()
81
- max_retries = 10
82
- backoff_time = 1000
83
- response_status = MagicMock()
84
- response_status.retry_in.return_value = 10
85
- error_handler.max_retries = max_retries
86
- error_handler.interpret_response.return_value = response_status
87
- error_handler.backoff_time.return_value = backoff_time
88
-
89
- config = {"url": "https://airbyte.io"}
90
- stream_slice = {"id": "1234"}
91
-
92
- name = "stream_name"
93
-
94
- requester = HttpRequester(
95
- name=name,
96
- url_base=InterpolatedString.create("{{ config['url'] }}", parameters={}),
97
- path=InterpolatedString.create("v1/{{ stream_slice['id'] }}", parameters={}),
98
- http_method=http_method,
99
- request_options_provider=request_options_provider,
100
- authenticator=authenticator,
101
- error_handler=error_handler,
102
- config=config,
103
- parameters={},
104
- )
105
-
106
- assert requester.get_url_base() == "https://airbyte.io/"
107
- assert requester.get_path(stream_state={}, stream_slice=stream_slice, next_page_token={}) == "v1/1234"
108
- assert requester.get_authenticator() == authenticator
109
- assert requester.get_method() == http_method
110
- assert requester.get_request_params(stream_state={}, stream_slice=None, next_page_token=None) == request_params
111
- assert requester.get_request_body_data(stream_state={}, stream_slice=None, next_page_token=None) == request_body_data
112
- assert requester.get_request_body_json(stream_state={}, stream_slice=None, next_page_token=None) == request_body_json
113
- assert requester.interpret_response_status(requests.Response()) == response_status
114
-
115
-
116
- @pytest.mark.parametrize(
117
- "test_name, base_url, expected_base_url",
118
- [
119
- ("test_no_trailing_slash", "https://example.com", "https://example.com/"),
120
- ("test_with_trailing_slash", "https://example.com/", "https://example.com/"),
121
- ("test_with_v1_no_trailing_slash", "https://example.com/v1", "https://example.com/v1/"),
122
- ("test_with_v1_with_trailing_slash", "https://example.com/v1/", "https://example.com/v1/"),
123
- ],
124
- )
125
- def test_base_url_has_a_trailing_slash(test_name, base_url, expected_base_url):
126
- requester = HttpRequester(
127
- name="name",
128
- url_base=base_url,
129
- path="deals",
130
- http_method=HttpMethod.GET,
131
- request_options_provider=MagicMock(),
132
- authenticator=MagicMock(),
133
- error_handler=MagicMock(),
134
- config={},
135
- parameters={},
136
- )
137
- assert requester.get_url_base() == expected_base_url
138
-
139
-
140
- @pytest.mark.parametrize(
141
- "test_name, path, expected_path",
142
- [
143
- ("test_no_leading_slash", "deals", "deals"),
144
- ("test_with_leading_slash", "/deals", "deals"),
145
- ("test_with_v1_no_leading_slash", "v1/deals", "v1/deals"),
146
- ("test_with_v1_with_leading_slash", "/v1/deals", "v1/deals"),
147
- ("test_with_v1_with_trailing_slash", "v1/deals/", "v1/deals/"),
148
- ],
149
- )
150
- def test_path(test_name, path, expected_path):
151
- requester = HttpRequester(
152
- name="name",
153
- url_base="https://example.com",
154
- path=path,
155
- http_method=HttpMethod.GET,
156
- request_options_provider=MagicMock(),
157
- authenticator=MagicMock(),
158
- error_handler=MagicMock(),
159
- config={},
160
- parameters={},
161
- )
162
- assert requester.get_path(stream_state={}, stream_slice={}, next_page_token={}) == expected_path
163
-
164
-
165
- def create_requester(
166
- url_base: Optional[str] = None,
167
- parameters: Optional[Mapping[str, Any]] = {},
168
- config: Optional[Config] = None,
169
- path: Optional[str] = None,
170
- authenticator: Optional[DeclarativeAuthenticator] = None,
171
- error_handler: Optional[ErrorHandler] = None,
172
- ) -> HttpRequester:
173
- requester = HttpRequester(
174
- name="name",
175
- url_base=url_base or "https://example.com",
176
- path=path or "deals",
177
- http_method=HttpMethod.GET,
178
- request_options_provider=None,
179
- authenticator=authenticator,
180
- error_handler=error_handler,
181
- config=config or {},
182
- parameters=parameters or {},
183
- )
184
- requester._session.send = MagicMock()
185
- req = requests.Response()
186
- req.status_code = 200
187
- requester._session.send.return_value = req
188
- return requester
189
-
190
-
191
- def test_basic_send_request():
192
- options_provider = MagicMock()
193
- options_provider.get_request_headers.return_value = {"my_header": "my_value"}
194
- requester = create_requester()
195
- requester._request_options_provider = options_provider
196
- requester.send_request()
197
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
198
- assert sent_request.method == "GET"
199
- assert sent_request.url == "https://example.com/deals"
200
- assert sent_request.headers["my_header"] == "my_value"
201
- assert sent_request.body is None
202
-
203
-
204
- @pytest.mark.parametrize(
205
- "provider_data, provider_json, param_data, param_json, authenticator_data, authenticator_json, expected_exception, expected_body",
206
- [
207
- # merging data params from the three sources
208
- ({"field": "value"}, None, None, None, None, None, None, "field=value"),
209
- ({"field": "value"}, None, {"field2": "value"}, None, None, None, None, "field=value&field2=value"),
210
- ({"field": "value"}, None, {"field2": "value"}, None, {"authfield": "val"}, None, None, "field=value&field2=value&authfield=val"),
211
- ({"field": "value"}, None, {"field": "value"}, None, None, None, ValueError, None),
212
- ({"field": "value"}, None, None, None, {"field": "value"}, None, ValueError, None),
213
- ({"field": "value"}, None, {"field2": "value"}, None, {"field": "value"}, None, ValueError, None),
214
- # merging json params from the three sources
215
- (None, {"field": "value"}, None, None, None, None, None, '{"field": "value"}'),
216
- (None, {"field": "value"}, None, {"field2": "value"}, None, None, None, '{"field": "value", "field2": "value"}'),
217
- (
218
- None,
219
- {"field": "value"},
220
- None,
221
- {"field2": "value"},
222
- None,
223
- {"authfield": "val"},
224
- None,
225
- '{"field": "value", "field2": "value", "authfield": "val"}',
226
- ),
227
- (None, {"field": "value"}, None, {"field": "value"}, None, None, ValueError, None),
228
- (None, {"field": "value"}, None, None, None, {"field": "value"}, ValueError, None),
229
- # raise on mixed data and json params
230
- ({"field": "value"}, {"field": "value"}, None, None, None, None, RequestBodyException, None),
231
- ({"field": "value"}, None, None, {"field": "value"}, None, None, RequestBodyException, None),
232
- (None, None, {"field": "value"}, {"field": "value"}, None, None, RequestBodyException, None),
233
- (None, None, None, None, {"field": "value"}, {"field": "value"}, RequestBodyException, None),
234
- ({"field": "value"}, None, None, None, None, {"field": "value"}, RequestBodyException, None),
235
- ],
236
- )
237
- def test_send_request_data_json(
238
- provider_data, provider_json, param_data, param_json, authenticator_data, authenticator_json, expected_exception, expected_body
239
- ):
240
- options_provider = MagicMock()
241
- options_provider.get_request_body_data.return_value = provider_data
242
- options_provider.get_request_body_json.return_value = provider_json
243
- authenticator = MagicMock()
244
- authenticator.get_request_body_data.return_value = authenticator_data
245
- authenticator.get_request_body_json.return_value = authenticator_json
246
- requester = create_requester(authenticator=authenticator)
247
- requester._request_options_provider = options_provider
248
- if expected_exception is not None:
249
- with pytest.raises(expected_exception):
250
- requester.send_request(request_body_data=param_data, request_body_json=param_json)
251
- else:
252
- requester.send_request(request_body_data=param_data, request_body_json=param_json)
253
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
254
- if expected_body is not None:
255
- assert sent_request.body == expected_body.decode("UTF-8") if not isinstance(expected_body, str) else expected_body
256
-
257
-
258
- @pytest.mark.parametrize(
259
- "provider_data, param_data, authenticator_data, expected_exception, expected_body",
260
- [
261
- # assert body string from one source works
262
- ("field=value", None, None, None, "field=value"),
263
- (None, "field=value", None, None, "field=value"),
264
- (None, None, "field=value", None, "field=value"),
265
- # assert body string from multiple sources fails
266
- ("field=value", "field=value", None, ValueError, None),
267
- ("field=value", None, "field=value", ValueError, None),
268
- (None, "field=value", "field=value", ValueError, None),
269
- ("field=value", "field=value", "field=value", ValueError, None),
270
- # assert body string and mapping from different source fails
271
- ("field=value", {"abc": "def"}, None, ValueError, None),
272
- ({"abc": "def"}, "field=value", None, ValueError, None),
273
- ("field=value", None, {"abc": "def"}, ValueError, None),
274
- ],
275
- )
276
- def test_send_request_string_data(provider_data, param_data, authenticator_data, expected_exception, expected_body):
277
- options_provider = MagicMock()
278
- options_provider.get_request_body_data.return_value = provider_data
279
- authenticator = MagicMock()
280
- authenticator.get_request_body_data.return_value = authenticator_data
281
- requester = create_requester(authenticator=authenticator)
282
- requester._request_options_provider = options_provider
283
- if expected_exception is not None:
284
- with pytest.raises(expected_exception):
285
- requester.send_request(request_body_data=param_data)
286
- else:
287
- requester.send_request(request_body_data=param_data)
288
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
289
- if expected_body is not None:
290
- assert sent_request.body == expected_body
291
-
292
-
293
- @pytest.mark.parametrize(
294
- "provider_headers, param_headers, authenticator_headers, expected_exception, expected_headers",
295
- [
296
- # merging headers from the three sources
297
- ({"header": "value"}, None, None, None, {"header": "value"}),
298
- ({"header": "value"}, {"header2": "value"}, None, None, {"header": "value", "header2": "value"}),
299
- (
300
- {"header": "value"},
301
- {"header2": "value"},
302
- {"authheader": "val"},
303
- None,
304
- {"header": "value", "header2": "value", "authheader": "val"},
305
- ),
306
- # raise on conflicting headers
307
- ({"header": "value"}, {"header": "value"}, None, ValueError, None),
308
- ({"header": "value"}, None, {"header": "value"}, ValueError, None),
309
- ({"header": "value"}, {"header2": "value"}, {"header": "value"}, ValueError, None),
310
- ],
311
- )
312
- def test_send_request_headers(provider_headers, param_headers, authenticator_headers, expected_exception, expected_headers):
313
- # headers set by the requests framework, do not validate
314
- default_headers = {"User-Agent": mock.ANY, "Accept-Encoding": mock.ANY, "Accept": mock.ANY, "Connection": mock.ANY}
315
- options_provider = MagicMock()
316
- options_provider.get_request_headers.return_value = provider_headers
317
- authenticator = MagicMock()
318
- authenticator.get_auth_header.return_value = authenticator_headers or {}
319
- requester = create_requester(authenticator=authenticator)
320
- requester._request_options_provider = options_provider
321
- if expected_exception is not None:
322
- with pytest.raises(expected_exception):
323
- requester.send_request(request_headers=param_headers)
324
- else:
325
- requester.send_request(request_headers=param_headers)
326
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
327
- assert sent_request.headers == {**default_headers, **expected_headers}
328
-
329
-
330
- @pytest.mark.parametrize(
331
- "provider_params, param_params, authenticator_params, expected_exception, expected_params",
332
- [
333
- # merging params from the three sources
334
- ({"param": "value"}, None, None, None, {"param": "value"}),
335
- ({"param": "value"}, {"param2": "value"}, None, None, {"param": "value", "param2": "value"}),
336
- ({"param": "value"}, {"param2": "value"}, {"authparam": "val"}, None, {"param": "value", "param2": "value", "authparam": "val"}),
337
- # raise on conflicting params
338
- ({"param": "value"}, {"param": "value"}, None, ValueError, None),
339
- ({"param": "value"}, None, {"param": "value"}, ValueError, None),
340
- ({"param": "value"}, {"param2": "value"}, {"param": "value"}, ValueError, None),
341
- ],
342
- )
343
- def test_send_request_params(provider_params, param_params, authenticator_params, expected_exception, expected_params):
344
- options_provider = MagicMock()
345
- options_provider.get_request_params.return_value = provider_params
346
- authenticator = MagicMock()
347
- authenticator.get_request_params.return_value = authenticator_params
348
- requester = create_requester(authenticator=authenticator)
349
- requester._request_options_provider = options_provider
350
- if expected_exception is not None:
351
- with pytest.raises(expected_exception):
352
- requester.send_request(request_params=param_params)
353
- else:
354
- requester.send_request(request_params=param_params)
355
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
356
- parsed_url = urlparse(sent_request.url)
357
- query_params = {key: value[0] for key, value in parse_qs(parsed_url.query).items()}
358
- assert query_params == expected_params
359
-
360
-
361
- @pytest.mark.parametrize(
362
- "request_parameters, config, expected_query_params",
363
- [
364
- pytest.param(
365
- {"k": '{"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}'},
366
- {},
367
- "k=%7B%22updatedDateFrom%22%3A+%222023-08-20T00%3A00%3A00Z%22%2C+%22updatedDateTo%22%3A+%222023-08-20T23%3A59%3A59Z%22%7D",
368
- id="test-request-parameter-dictionary",
369
- ),
370
- pytest.param(
371
- {"k": "1,2"},
372
- {},
373
- "k=1%2C2", # k=1,2
374
- id="test-request-parameter-comma-separated-numbers",
375
- ),
376
- pytest.param(
377
- {"k": "a,b"},
378
- {},
379
- "k=a%2Cb", # k=a,b
380
- id="test-request-parameter-comma-separated-strings",
381
- ),
382
- pytest.param(
383
- {"k": '{{ config["k"] }}'},
384
- {"k": {"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}},
385
- # {'updatedDateFrom': '2023-08-20T00:00:00Z', 'updatedDateTo': '2023-08-20T23:59:59Z'}
386
- "k=%7B%27updatedDateFrom%27%3A+%272023-08-20T00%3A00%3A00Z%27%2C+%27updatedDateTo%27%3A+%272023-08-20T23%3A59%3A59Z%27%7D",
387
- id="test-request-parameter-from-config-object",
388
- ),
389
- ],
390
- )
391
- def test_request_param_interpolation(request_parameters, config, expected_query_params):
392
- options_provider = InterpolatedRequestOptionsProvider(
393
- config=config,
394
- request_parameters=request_parameters,
395
- request_body_data={},
396
- request_headers={},
397
- parameters={},
398
- )
399
- requester = create_requester()
400
- requester._request_options_provider = options_provider
401
- requester.send_request()
402
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
403
- assert sent_request.url.split("?", 1)[-1] == expected_query_params
404
-
405
-
406
- @pytest.mark.parametrize(
407
- "request_parameters, config, invalid_value_for_key",
408
- [
409
- pytest.param(
410
- {"k": "[1,2]"},
411
- {},
412
- "k",
413
- id="test-request-parameter-list-of-numbers",
414
- ),
415
- pytest.param(
416
- {"k": {"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}},
417
- {},
418
- "k",
419
- id="test-request-parameter-object-of-the-updated-info",
420
- ),
421
- pytest.param(
422
- {"k": '["a", "b"]'},
423
- {},
424
- "k",
425
- id="test-request-parameter-list-of-strings",
426
- ),
427
- pytest.param(
428
- {"k": '{{ config["k"] }}'},
429
- {"k": [1, 2]},
430
- "k",
431
- id="test-request-parameter-from-config-list-of-numbers",
432
- ),
433
- pytest.param(
434
- {"k": '{{ config["k"] }}'},
435
- {"k": ["a", "b"]},
436
- "k",
437
- id="test-request-parameter-from-config-list-of-strings",
438
- ),
439
- pytest.param(
440
- {"k": '{{ config["k"] }}'},
441
- {"k": ["a,b"]},
442
- "k",
443
- id="test-request-parameter-from-config-comma-separated-strings",
444
- ),
445
- pytest.param(
446
- {'["a", "b"]': '{{ config["k"] }}'},
447
- {"k": [1, 2]},
448
- '["a", "b"]',
449
- id="test-key-with-list-is-not-interpolated",
450
- ),
451
- pytest.param(
452
- {"a": '{{ config["k"] }}', "b": {"end_timestamp": 1699109113}},
453
- {"k": 1699108113},
454
- "b",
455
- id="test-key-with-multiple-keys",
456
- ),
457
- ],
458
- )
459
- def test_request_param_interpolation_with_incorrect_values(request_parameters, config, invalid_value_for_key):
460
- options_provider = InterpolatedRequestOptionsProvider(
461
- config=config,
462
- request_parameters=request_parameters,
463
- request_body_data={},
464
- request_headers={},
465
- parameters={},
466
- )
467
- requester = create_requester()
468
- requester._request_options_provider = options_provider
469
- with pytest.raises(ValueError) as error:
470
- requester.send_request()
471
-
472
- assert (
473
- error.value.args[0]
474
- == f"Invalid value for `{invalid_value_for_key}` parameter. The values of request params cannot be an array or object."
475
- )
476
-
477
-
478
- @pytest.mark.parametrize(
479
- "request_body_data, config, expected_request_body_data",
480
- [
481
- pytest.param(
482
- {"k": '{"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}'},
483
- {},
484
- # k={"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}
485
- "k=%7B%22updatedDateFrom%22%3A+%222023-08-20T00%3A00%3A00Z%22%2C+%22updatedDateTo%22%3A+%222023-08-20T23%3A59%3A59Z%22%7D",
486
- id="test-request-body-dictionary",
487
- ),
488
- pytest.param(
489
- {"k": "1,2"},
490
- {},
491
- "k=1%2C2", # k=1,2
492
- id="test-request-body-comma-separated-numbers",
493
- ),
494
- pytest.param(
495
- {"k": "a,b"},
496
- {},
497
- "k=a%2Cb", # k=a,b
498
- id="test-request-body-comma-separated-strings",
499
- ),
500
- pytest.param(
501
- {"k": "[1,2]"},
502
- {},
503
- "k=1&k=2",
504
- id="test-request-body-list-of-numbers",
505
- ),
506
- pytest.param(
507
- {"k": '["a", "b"]'},
508
- {},
509
- "k=a&k=b",
510
- id="test-request-body-list-of-strings",
511
- ),
512
- pytest.param(
513
- {"k": '{{ config["k"] }}'},
514
- {"k": {"updatedDateFrom": "2023-08-20T00:00:00Z", "updatedDateTo": "2023-08-20T23:59:59Z"}},
515
- # k={'updatedDateFrom': '2023-08-20T00:00:00Z', 'updatedDateTo': '2023-08-20T23:59:59Z'}
516
- "k=%7B%27updatedDateFrom%27%3A+%272023-08-20T00%3A00%3A00Z%27%2C+%27updatedDateTo%27%3A+%272023-08-20T23%3A59%3A59Z%27%7D",
517
- id="test-request-body-from-config-object",
518
- ),
519
- pytest.param(
520
- {"k": '{{ config["k"] }}'},
521
- {"k": [1, 2]},
522
- "k=1&k=2",
523
- id="test-request-body-from-config-list-of-numbers",
524
- ),
525
- pytest.param(
526
- {"k": '{{ config["k"] }}'},
527
- {"k": ["a", "b"]},
528
- "k=a&k=b",
529
- id="test-request-body-from-config-list-of-strings",
530
- ),
531
- pytest.param(
532
- {"k": '{{ config["k"] }}'},
533
- {"k": ["a,b"]},
534
- "k=a%2Cb", # k=a,b
535
- id="test-request-body-from-config-comma-separated-strings",
536
- ),
537
- pytest.param(
538
- {'["a", "b"]': '{{ config["k"] }}'},
539
- {"k": [1, 2]},
540
- "%5B%22a%22%2C+%22b%22%5D=1&%5B%22a%22%2C+%22b%22%5D=2", # ["a", "b"]=1&["a", "b"]=2
541
- id="test-key-with-list-is-not-interpolated",
542
- ),
543
- pytest.param(
544
- {"k": "{'updatedDateFrom': '2023-08-20T00:00:00Z', 'updatedDateTo': '2023-08-20T23:59:59Z'}"},
545
- {},
546
- # k={'updatedDateFrom': '2023-08-20T00:00:00Z', 'updatedDateTo': '2023-08-20T23:59:59Z'}
547
- "k=%7B%27updatedDateFrom%27%3A+%272023-08-20T00%3A00%3A00Z%27%2C+%27updatedDateTo%27%3A+%272023-08-20T23%3A59%3A59Z%27%7D",
548
- id="test-single-quotes-are-retained",
549
- ),
550
- ],
551
- )
552
- def test_request_body_interpolation(request_body_data, config, expected_request_body_data):
553
- options_provider = InterpolatedRequestOptionsProvider(
554
- config=config,
555
- request_parameters={},
556
- request_body_data=request_body_data,
557
- request_headers={},
558
- parameters={},
559
- )
560
- requester = create_requester()
561
- requester._request_options_provider = options_provider
562
- requester.send_request()
563
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
564
- assert sent_request.body == expected_request_body_data
565
-
566
-
567
- @pytest.mark.parametrize(
568
- "requester_path, param_path, expected_path",
569
- [
570
- ("deals", None, "/deals"),
571
- ("deals", "deals2", "/deals2"),
572
- ("deals", "/deals2", "/deals2"),
573
- (
574
- "deals/{{ stream_slice.start }}/{{ next_page_token.next_page_token }}/{{ config.config_key }}/{{ parameters.param_key }}",
575
- None,
576
- "/deals/2012/pagetoken/config_value/param_value",
577
- ),
578
- ],
579
- )
580
- def test_send_request_path(requester_path, param_path, expected_path):
581
- requester = create_requester(config={"config_key": "config_value"}, path=requester_path, parameters={"param_key": "param_value"})
582
- requester.send_request(stream_slice={"start": "2012"}, next_page_token={"next_page_token": "pagetoken"}, path=param_path)
583
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
584
- parsed_url = urlparse(sent_request.url)
585
- assert parsed_url.path == expected_path
586
-
587
-
588
- def test_send_request_url_base():
589
- requester = create_requester(
590
- url_base="https://example.org/{{ config.config_key }}/{{ parameters.param_key }}",
591
- config={"config_key": "config_value"},
592
- parameters={"param_key": "param_value"},
593
- )
594
- requester.send_request()
595
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
596
- assert sent_request.url == "https://example.org/config_value/param_value/deals"
597
-
598
-
599
- def test_send_request_stream_slice_next_page_token():
600
- options_provider = MagicMock()
601
- requester = create_requester()
602
- requester._request_options_provider = options_provider
603
- stream_slice = {"id": "1234"}
604
- next_page_token = {"next_page_token": "next_page_token"}
605
- requester.send_request(stream_slice=stream_slice, next_page_token=next_page_token)
606
- options_provider.get_request_params.assert_called_once_with(
607
- stream_state=None, stream_slice=stream_slice, next_page_token=next_page_token
608
- )
609
- options_provider.get_request_body_data.assert_called_once_with(
610
- stream_state=None, stream_slice=stream_slice, next_page_token=next_page_token
611
- )
612
- options_provider.get_request_body_json.assert_called_once_with(
613
- stream_state=None, stream_slice=stream_slice, next_page_token=next_page_token
614
- )
615
- options_provider.get_request_headers.assert_called_once_with(
616
- stream_state=None, stream_slice=stream_slice, next_page_token=next_page_token
617
- )
618
-
619
-
620
- def test_default_authenticator():
621
- requester = create_requester()
622
- assert isinstance(requester._authenticator, NoAuth)
623
- assert isinstance(requester._session.auth, NoAuth)
624
-
625
-
626
- def test_token_authenticator():
627
- requester = create_requester(authenticator=BearerAuthenticator(token_provider=MagicMock(), config={}, parameters={}))
628
- assert isinstance(requester.authenticator, BearerAuthenticator)
629
- assert isinstance(requester._session.auth, BearerAuthenticator)
630
-
631
-
632
- def test_stub_custom_backoff_http_stream(mocker):
633
- mocker.patch("time.sleep", lambda x: None)
634
- req = requests.Response()
635
- req.status_code = 429
636
-
637
- requester = create_requester()
638
- requester._backoff_time = lambda _: 0.5
639
-
640
- requester._session.send.return_value = req
641
-
642
- with pytest.raises(UserDefinedBackoffException):
643
- requester.send_request()
644
- assert requester._session.send.call_count == requester.max_retries + 1
645
-
646
-
647
- @pytest.mark.parametrize("retries", [-20, -1, 0, 1, 2, 10])
648
- def test_stub_custom_backoff_http_stream_retries(mocker, retries):
649
- mocker.patch("time.sleep", lambda x: None)
650
- error_handler = DefaultErrorHandler(parameters={}, config={}, max_retries=retries)
651
- requester = create_requester(error_handler=error_handler)
652
- req = requests.Response()
653
- req.status_code = HTTPStatus.TOO_MANY_REQUESTS
654
- requester._session.send.return_value = req
655
-
656
- with pytest.raises(UserDefinedBackoffException, match="Request URL: https://example.com/deals, Response Code: 429") as excinfo:
657
- requester.send_request()
658
- assert isinstance(excinfo.value.request, requests.PreparedRequest)
659
- assert isinstance(excinfo.value.response, requests.Response)
660
- if retries <= 0:
661
- assert requester._session.send.call_count == 1
662
- else:
663
- assert requester._session.send.call_count == requester.max_retries + 1
664
-
665
-
666
- def test_stub_custom_backoff_http_stream_endless_retries(mocker):
667
- mocker.patch("time.sleep", lambda x: None)
668
- error_handler = DefaultErrorHandler(parameters={}, config={}, max_retries=None)
669
- requester = create_requester(error_handler=error_handler)
670
- req = requests.Response()
671
- req.status_code = HTTPStatus.TOO_MANY_REQUESTS
672
- infinite_number = 20
673
-
674
- req = requests.Response()
675
- req.status_code = HTTPStatus.TOO_MANY_REQUESTS
676
- send_mock = mocker.patch.object(requester._session, "send", side_effect=[req] * infinite_number)
677
-
678
- # Expecting mock object to raise a RuntimeError when the end of side_effect list parameter reached.
679
- with pytest.raises(StopIteration):
680
- requester.send_request()
681
- assert send_mock.call_count == infinite_number + 1
682
-
683
-
684
- @pytest.mark.parametrize("http_code", [400, 401, 403])
685
- def test_4xx_error_codes_http_stream(mocker, http_code):
686
- requester = create_requester(error_handler=DefaultErrorHandler(parameters={}, config={}, max_retries=0))
687
- requester._DEFAULT_RETRY_FACTOR = 0.01
688
- req = requests.Response()
689
- req.request = requests.Request()
690
- req.status_code = http_code
691
- requester._session.send.return_value = req
692
-
693
- with pytest.raises(ReadException):
694
- requester.send_request()
695
-
696
-
697
- def test_raise_on_http_errors_off_429(mocker):
698
- requester = create_requester()
699
- requester._DEFAULT_RETRY_FACTOR = 0.01
700
- req = requests.Response()
701
- req.status_code = 429
702
- requester._session.send.return_value = req
703
-
704
- with pytest.raises(DefaultBackoffException, match="Request URL: https://example.com/deals, Response Code: 429"):
705
- requester.send_request()
706
-
707
-
708
- @pytest.mark.parametrize("status_code", [500, 501, 503, 504])
709
- def test_raise_on_http_errors_off_5xx(mocker, status_code):
710
- requester = create_requester()
711
- req = requests.Response()
712
- req.status_code = status_code
713
- requester._session.send.return_value = req
714
- requester._DEFAULT_RETRY_FACTOR = 0.01
715
-
716
- with pytest.raises(DefaultBackoffException):
717
- requester.send_request()
718
- assert requester._session.send.call_count == requester.max_retries + 1
719
-
720
-
721
- @pytest.mark.parametrize("status_code", [400, 401, 402, 403, 416])
722
- def test_raise_on_http_errors_off_non_retryable_4xx(mocker, status_code):
723
- requester = create_requester()
724
- req = requests.Response()
725
- req.status_code = status_code
726
- requester._session.send.return_value = req
727
- requester._DEFAULT_RETRY_FACTOR = 0.01
728
-
729
- response = requester.send_request()
730
- assert response.status_code == status_code
731
-
732
-
733
- @pytest.mark.parametrize(
734
- "error",
735
- (
736
- requests.exceptions.ConnectTimeout,
737
- requests.exceptions.ConnectionError,
738
- requests.exceptions.ChunkedEncodingError,
739
- requests.exceptions.ReadTimeout,
740
- ),
741
- )
742
- def test_raise_on_http_errors(mocker, error):
743
- requester = create_requester()
744
- req = requests.Response()
745
- req.status_code = 200
746
- requester._session.send.return_value = req
747
- requester._DEFAULT_RETRY_FACTOR = 0.01
748
- mocker.patch.object(requester._session, "send", side_effect=error())
749
-
750
- with pytest.raises(error):
751
- requester.send_request()
752
- assert requester._session.send.call_count == requester.max_retries + 1
753
-
754
-
755
- @pytest.mark.parametrize(
756
- "api_response, expected_message",
757
- [
758
- ({"error": "something broke"}, "something broke"),
759
- ({"error": {"message": "something broke"}}, "something broke"),
760
- ({"error": "err-001", "message": "something broke"}, "something broke"),
761
- ({"failure": {"message": "something broke"}}, "something broke"),
762
- ({"detail": {"message": "something broke"}}, "something broke"),
763
- ({"error": {"errors": [{"message": "one"}, {"message": "two"}, {"message": "three"}]}}, "one, two, three"),
764
- ({"errors": ["one", "two", "three"]}, "one, two, three"),
765
- ({"errors": [None, {}, "third error", 9002.09]}, "third error"),
766
- ({"messages": ["one", "two", "three"]}, "one, two, three"),
767
- ({"errors": [{"message": "one"}, {"message": "two"}, {"message": "three"}]}, "one, two, three"),
768
- ({"error": [{"message": "one"}, {"message": "two"}, {"message": "three"}]}, "one, two, three"),
769
- ({"errors": [{"error": "one"}, {"error": "two"}, {"error": "three"}]}, "one, two, three"),
770
- ({"failures": [{"message": "one"}, {"message": "two"}, {"message": "three"}]}, "one, two, three"),
771
- ({"details": [{"message": "one"}, {"message": "two"}, {"message": "three"}]}, "one, two, three"),
772
- ({"details": ["one", 10087, True]}, "one"),
773
- (["one", "two", "three"], "one, two, three"),
774
- ({"detail": False}, None),
775
- ([{"error": "one"}, {"error": "two"}, {"error": "three"}], "one, two, three"),
776
- ({"error": True}, None),
777
- ({"something_else": "hi"}, None),
778
- ({}, None),
779
- ],
780
- )
781
- def test_default_parse_response_error_message(api_response: dict, expected_message: Optional[str]):
782
- response = MagicMock()
783
- response.json.return_value = api_response
784
-
785
- message = HttpRequester.parse_response_error_message(response)
786
- assert message == expected_message
787
-
788
-
789
- def test_default_parse_response_error_message_not_json(requests_mock):
790
- requests_mock.register_uri("GET", "mock://test.com/not_json", text="this is not json")
791
- response = requests.get("mock://test.com/not_json")
792
-
793
- message = HttpRequester.parse_response_error_message(response)
794
- assert message is None
795
-
796
-
797
- @pytest.mark.parametrize(
798
- "test_name, base_url, path, expected_full_url",
799
- [
800
- ("test_no_slashes", "https://airbyte.io", "my_endpoint", "https://airbyte.io/my_endpoint"),
801
- ("test_trailing_slash_on_base_url", "https://airbyte.io/", "my_endpoint", "https://airbyte.io/my_endpoint"),
802
- (
803
- "test_trailing_slash_on_base_url_and_leading_slash_on_path",
804
- "https://airbyte.io/",
805
- "/my_endpoint",
806
- "https://airbyte.io/my_endpoint",
807
- ),
808
- ("test_leading_slash_on_path", "https://airbyte.io", "/my_endpoint", "https://airbyte.io/my_endpoint"),
809
- ("test_trailing_slash_on_path", "https://airbyte.io", "/my_endpoint/", "https://airbyte.io/my_endpoint/"),
810
- ("test_nested_path_no_leading_slash", "https://airbyte.io", "v1/my_endpoint", "https://airbyte.io/v1/my_endpoint"),
811
- ("test_nested_path_with_leading_slash", "https://airbyte.io", "/v1/my_endpoint", "https://airbyte.io/v1/my_endpoint"),
812
- ],
813
- )
814
- def test_join_url(test_name, base_url, path, expected_full_url):
815
- requester = HttpRequester(
816
- name="name",
817
- url_base=base_url,
818
- path=path,
819
- http_method=HttpMethod.GET,
820
- request_options_provider=None,
821
- config={},
822
- parameters={},
823
- )
824
- requester._session.send = MagicMock()
825
- response = requests.Response()
826
- response.status_code = 200
827
- requester._session.send.return_value = response
828
- requester.send_request()
829
- sent_request: PreparedRequest = requester._session.send.call_args_list[0][0][0]
830
- assert sent_request.url == expected_full_url
831
-
832
-
833
- @pytest.mark.parametrize(
834
- "path, params, expected_url",
835
- [
836
- pytest.param("v1/endpoint?param1=value1", {}, "https://test_base_url.com/v1/endpoint?param1=value1", id="test_params_only_in_path"),
837
- pytest.param(
838
- "v1/endpoint", {"param1": "value1"}, "https://test_base_url.com/v1/endpoint?param1=value1", id="test_params_only_in_path"
839
- ),
840
- pytest.param("v1/endpoint", None, "https://test_base_url.com/v1/endpoint", id="test_params_is_none_and_no_params_in_path"),
841
- pytest.param(
842
- "v1/endpoint?param1=value1",
843
- None,
844
- "https://test_base_url.com/v1/endpoint?param1=value1",
845
- id="test_params_is_none_and_no_params_in_path",
846
- ),
847
- pytest.param(
848
- "v1/endpoint?param1=value1",
849
- {"param2": "value2"},
850
- "https://test_base_url.com/v1/endpoint?param1=value1&param2=value2",
851
- id="test_no_duplicate_params",
852
- ),
853
- pytest.param(
854
- "v1/endpoint?param1=value1",
855
- {"param1": "value1"},
856
- "https://test_base_url.com/v1/endpoint?param1=value1",
857
- id="test_duplicate_params_same_value",
858
- ),
859
- pytest.param(
860
- "v1/endpoint?param1=1",
861
- {"param1": 1},
862
- "https://test_base_url.com/v1/endpoint?param1=1",
863
- id="test_duplicate_params_same_value_not_string",
864
- ),
865
- pytest.param(
866
- "v1/endpoint?param1=value1",
867
- {"param1": "value2"},
868
- "https://test_base_url.com/v1/endpoint?param1=value1&param1=value2",
869
- id="test_duplicate_params_different_value",
870
- ),
871
- ],
872
- )
873
- def test_duplicate_request_params_are_deduped(path, params, expected_url):
874
- requester = HttpRequester(
875
- name="name",
876
- url_base="https://test_base_url.com",
877
- path=path,
878
- http_method=HttpMethod.GET,
879
- request_options_provider=None,
880
- config={},
881
- parameters={},
882
- )
883
-
884
- if expected_url is None:
885
- with pytest.raises(ValueError):
886
- requester._create_prepared_request(path=path, params=params)
887
- else:
888
- prepared_request = requester._create_prepared_request(path=path, params=params)
889
- assert prepared_request.url == expected_url
890
-
891
-
892
- @pytest.mark.parametrize(
893
- "should_log, status_code, should_throw",
894
- [
895
- (True, 200, False),
896
- (True, 400, False),
897
- (True, 500, True),
898
- (False, 200, False),
899
- (False, 400, False),
900
- (False, 500, True),
901
- ],
902
- )
903
- def test_log_requests(should_log, status_code, should_throw):
904
- repository = MagicMock()
905
- requester = HttpRequester(
906
- name="name",
907
- url_base="https://test_base_url.com",
908
- path="/",
909
- http_method=HttpMethod.GET,
910
- request_options_provider=None,
911
- config={},
912
- parameters={},
913
- message_repository=repository,
914
- disable_retries=True,
915
- )
916
- requester._session.send = MagicMock()
917
- response = requests.Response()
918
- response.status_code = status_code
919
- requester._session.send.return_value = response
920
- formatter = MagicMock()
921
- formatter.return_value = "formatted_response"
922
- if should_throw:
923
- with pytest.raises(DefaultBackoffException):
924
- requester.send_request(log_formatter=formatter if should_log else None)
925
- else:
926
- requester.send_request(log_formatter=formatter if should_log else None)
927
- if should_log:
928
- assert repository.log_message.call_args_list[0].args[1]() == "formatted_response"
929
- formatter.assert_called_once_with(response)
930
-
931
-
932
- def test_connection_pool():
933
- requester = HttpRequester(
934
- name="name",
935
- url_base="https://test_base_url.com",
936
- path="/",
937
- http_method=HttpMethod.GET,
938
- request_options_provider=None,
939
- config={},
940
- parameters={},
941
- message_repository=MagicMock(),
942
- disable_retries=True,
943
- )
944
- assert requester._session.adapters["https://"]._pool_connections == 20
945
-
946
-
947
- def test_caching_filename(http_requester_factory):
948
- http_requester = http_requester_factory()
949
- assert http_requester.cache_filename == f"{http_requester.name}.sqlite"
950
-
951
-
952
- def test_caching_session_with_enable_use_cache(http_requester_factory):
953
- http_requester = http_requester_factory(use_cache=True)
954
- assert isinstance(http_requester._session, requests_cache.CachedSession)
955
-
956
-
957
- def test_response_caching_with_enable_use_cache(http_requester_factory, requests_mock):
958
- http_requester = http_requester_factory(use_cache=True)
959
-
960
- requests_mock.register_uri("GET", http_requester.url_base, json=[{"id": 12, "title": "test_record"}])
961
- http_requester.clear_cache()
962
-
963
- response = http_requester.send_request()
964
-
965
- assert requests_mock.called
966
- assert isinstance(response, requests.Response)
967
-
968
- requests_mock.reset_mock()
969
- new_response = http_requester.send_request()
970
-
971
- assert not requests_mock.called
972
- assert isinstance(new_response, CachedResponse)
973
-
974
- assert len(response.json()) == len(new_response.json())