airbyte-cdk 0.35.4__py3-none-any.whl → 0.36.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -50,7 +50,9 @@ def create_source(config: Mapping[str, Any], limits: TestReadLimits) -> Manifest
50
50
  component_factory=ModelToComponentFactory(
51
51
  emit_connector_builder_messages=True,
52
52
  limit_pages_fetched_per_slice=limits.max_pages_per_slice,
53
- limit_slices_fetched=limits.max_slices)
53
+ limit_slices_fetched=limits.max_slices,
54
+ disable_retries=True
55
+ )
54
56
  )
55
57
 
56
58
 
@@ -13,6 +13,7 @@ from airbyte_cdk.models import (
13
13
  AirbyteLogMessage,
14
14
  AirbyteMessage,
15
15
  AirbyteStateMessage,
16
+ AirbyteStreamStatus,
16
17
  ConfiguredAirbyteCatalog,
17
18
  ConfiguredAirbyteStream,
18
19
  Level,
@@ -28,6 +29,7 @@ from airbyte_cdk.sources.streams.http.http import HttpStream
28
29
  from airbyte_cdk.sources.utils.record_helper import stream_data_to_airbyte_message
29
30
  from airbyte_cdk.sources.utils.schema_helpers import InternalConfig, split_config
30
31
  from airbyte_cdk.utils.event_timing import create_timer
32
+ from airbyte_cdk.utils.stream_status_utils import as_airbyte_message as stream_status_as_airbyte_message
31
33
  from airbyte_cdk.utils.traced_exception import AirbyteTracedException
32
34
 
33
35
 
@@ -113,6 +115,8 @@ class AbstractSource(Source, ABC):
113
115
  continue
114
116
  try:
115
117
  timer.start_event(f"Syncing stream {configured_stream.stream.name}")
118
+ logger.info(f"Marking stream {configured_stream.stream.name} as STARTED")
119
+ yield stream_status_as_airbyte_message(configured_stream, AirbyteStreamStatus.STARTED)
116
120
  yield from self._read_stream(
117
121
  logger=logger,
118
122
  stream_instance=stream_instance,
@@ -120,10 +124,15 @@ class AbstractSource(Source, ABC):
120
124
  state_manager=state_manager,
121
125
  internal_config=internal_config,
122
126
  )
127
+ logger.info(f"Marking stream {configured_stream.stream.name} as STOPPED")
128
+ yield stream_status_as_airbyte_message(configured_stream, AirbyteStreamStatus.COMPLETE)
123
129
  except AirbyteTracedException as e:
130
+ yield stream_status_as_airbyte_message(configured_stream, AirbyteStreamStatus.INCOMPLETE)
124
131
  raise e
125
132
  except Exception as e:
126
133
  logger.exception(f"Encountered an exception while reading stream {configured_stream.stream.name}")
134
+ logger.info(f"Marking stream {configured_stream.stream.name} as STOPPED")
135
+ yield stream_status_as_airbyte_message(configured_stream, AirbyteStreamStatus.INCOMPLETE)
127
136
  display_message = stream_instance.get_error_display_message(e)
128
137
  if display_message:
129
138
  raise AirbyteTracedException.from_exception(e, message=display_message) from e
@@ -185,6 +194,10 @@ class AbstractSource(Source, ABC):
185
194
  for record in record_iterator:
186
195
  if record.type == MessageType.RECORD:
187
196
  record_counter += 1
197
+ if record_counter == 1:
198
+ logger.info(f"Marking stream {stream_name} as RUNNING")
199
+ # If we just read the first record of the stream, emit the transition to the RUNNING state
200
+ yield stream_status_as_airbyte_message(configured_stream, AirbyteStreamStatus.RUNNING)
188
201
  yield record
189
202
 
190
203
  logger.info(f"Read {record_counter} records from {stream_name} stream")
@@ -5,14 +5,14 @@
5
5
  from dataclasses import InitVar, dataclass, field
6
6
  from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Union
7
7
 
8
- from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, AirbyteTraceMessage, SyncMode
8
+ from airbyte_cdk.models import AirbyteMessage, SyncMode
9
9
  from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
10
10
  from airbyte_cdk.sources.declarative.retrievers.retriever import Retriever
11
11
  from airbyte_cdk.sources.declarative.schema import DefaultSchemaLoader
12
12
  from airbyte_cdk.sources.declarative.schema.schema_loader import SchemaLoader
13
13
  from airbyte_cdk.sources.declarative.transformations import RecordTransformation
14
14
  from airbyte_cdk.sources.declarative.types import Config, StreamSlice
15
- from airbyte_cdk.sources.streams.core import Stream
15
+ from airbyte_cdk.sources.streams.core import Stream, StreamData
16
16
 
17
17
 
18
18
  @dataclass
@@ -102,17 +102,27 @@ class DeclarativeStream(Stream):
102
102
 
103
103
  def _apply_transformations(
104
104
  self,
105
- message_or_record_data: Union[AirbyteMessage, AirbyteLogMessage, AirbyteTraceMessage, Mapping[str, Any]],
105
+ message_or_record_data: StreamData,
106
106
  config: Config,
107
107
  stream_slice: StreamSlice,
108
108
  ):
109
- # If the input is an AirbyteRecord, transform the record's data
110
- # If the input is another type of Airbyte Message, return it as is
109
+ # If the input is an AirbyteMessage with a record, transform the record's data
110
+ # If the input is another type of AirbyteMessage, return it as is
111
111
  # If the input is a dict, transform it
112
- if isinstance(message_or_record_data, AirbyteLogMessage) or isinstance(message_or_record_data, AirbyteTraceMessage):
113
- return message_or_record_data
112
+ if isinstance(message_or_record_data, AirbyteMessage):
113
+ if message_or_record_data.record:
114
+ record = message_or_record_data.record.data
115
+ else:
116
+ return message_or_record_data
117
+ elif isinstance(message_or_record_data, dict):
118
+ record = message_or_record_data
119
+ else:
120
+ # Raise an error because this is unexpected and indicative of a typing problem in the CDK
121
+ raise ValueError(
122
+ f"Unexpected record type. Expected {StreamData}. Got {type(message_or_record_data)}. This is probably due to a bug in the CDK."
123
+ )
114
124
  for transformation in self.transformations:
115
- transformation.transform(message_or_record_data, config=config, stream_state=self.state, stream_slice=stream_slice)
125
+ transformation.transform(record, config=config, stream_state=self.state, stream_slice=stream_slice)
116
126
 
117
127
  return message_or_record_data
118
128
 
@@ -112,12 +112,17 @@ DEFAULT_BACKOFF_STRATEGY = ExponentialBackoffStrategy
112
112
 
113
113
  class ModelToComponentFactory:
114
114
  def __init__(
115
- self, limit_pages_fetched_per_slice: int = None, limit_slices_fetched: int = None, emit_connector_builder_messages: bool = False
115
+ self,
116
+ limit_pages_fetched_per_slice: int = None,
117
+ limit_slices_fetched: int = None,
118
+ emit_connector_builder_messages: bool = False,
119
+ disable_retries=False,
116
120
  ):
117
121
  self._init_mappings()
118
122
  self._limit_pages_fetched_per_slice = limit_pages_fetched_per_slice
119
123
  self._limit_slices_fetched = limit_slices_fetched
120
124
  self._emit_connector_builder_messages = emit_connector_builder_messages
125
+ self._disable_retries = disable_retries
121
126
 
122
127
  def _init_mappings(self):
123
128
  self.PYDANTIC_MODEL_TO_CONSTRUCTOR: [Type[BaseModel], Callable] = {
@@ -779,6 +784,7 @@ class ModelToComponentFactory:
779
784
  config=config,
780
785
  maximum_number_of_slices=self._limit_slices_fetched,
781
786
  parameters=model.parameters,
787
+ disable_retries=self._disable_retries,
782
788
  )
783
789
  return SimpleRetriever(
784
790
  name=name,
@@ -789,6 +795,7 @@ class ModelToComponentFactory:
789
795
  stream_slicer=stream_slicer or SinglePartitionRouter(parameters={}),
790
796
  config=config,
791
797
  parameters=model.parameters,
798
+ disable_retries=self._disable_retries,
792
799
  )
793
800
 
794
801
  @staticmethod
@@ -50,6 +50,8 @@ class SimpleRetriever(Retriever, HttpStream):
50
50
  parameters (Mapping[str, Any]): Additional runtime parameters to be used for string interpolation
51
51
  """
52
52
 
53
+ _DEFAULT_MAX_RETRY = 5
54
+
53
55
  requester: Requester
54
56
  record_selector: HttpSelector
55
57
  config: Config
@@ -61,6 +63,7 @@ class SimpleRetriever(Retriever, HttpStream):
61
63
  paginator: Optional[Paginator] = None
62
64
  stream_slicer: Optional[StreamSlicer] = SinglePartitionRouter(parameters={})
63
65
  emit_connector_builder_messages: bool = False
66
+ disable_retries: bool = False
64
67
 
65
68
  def __post_init__(self, parameters: Mapping[str, Any]):
66
69
  self.paginator = self.paginator or NoPagination(parameters=parameters)
@@ -95,6 +98,14 @@ class SimpleRetriever(Retriever, HttpStream):
95
98
  # never raise on http_errors because this overrides the error handler logic...
96
99
  return False
97
100
 
101
+ @property
102
+ def max_retries(self) -> Union[int, None]:
103
+ if self.disable_retries:
104
+ return 0
105
+ if hasattr(self.requester.error_handler, "max_retries"):
106
+ return self.requester.error_handler.max_retries
107
+ return self._DEFAULT_MAX_RETRY
108
+
98
109
  def should_retry(self, response: requests.Response) -> bool:
99
110
  """
100
111
  Specifies conditions for backoff based on the response from the server.
@@ -11,7 +11,7 @@ from functools import lru_cache
11
11
  from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union
12
12
 
13
13
  import airbyte_cdk.sources.utils.casing as casing
14
- from airbyte_cdk.models import AirbyteLogMessage, AirbyteStream, AirbyteTraceMessage, SyncMode
14
+ from airbyte_cdk.models import AirbyteMessage, AirbyteStream, SyncMode
15
15
 
16
16
  # list of all possible HTTP methods which can be used for sending of request bodies
17
17
  from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader
@@ -24,10 +24,8 @@ if typing.TYPE_CHECKING:
24
24
 
25
25
  # A stream's read method can return one of the following types:
26
26
  # Mapping[str, Any]: The content of an AirbyteRecordMessage
27
- # AirbyteRecordMessage: An AirbyteRecordMessage
28
- # AirbyteLogMessage: A log message
29
- # AirbyteTraceMessage: A trace message
30
- StreamData = Union[Mapping[str, Any], AirbyteLogMessage, AirbyteTraceMessage]
27
+ # AirbyteMessage: An AirbyteMessage. Could be of any type
28
+ StreamData = Union[Mapping[str, Any], AirbyteMessage]
31
29
 
32
30
 
33
31
  def package_name_from_class(cls: object) -> str:
@@ -0,0 +1,36 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from datetime import datetime
7
+
8
+ from airbyte_cdk.models import (
9
+ AirbyteMessage,
10
+ AirbyteStreamStatus,
11
+ AirbyteStreamStatusTraceMessage,
12
+ AirbyteTraceMessage,
13
+ ConfiguredAirbyteStream,
14
+ StreamDescriptor,
15
+ TraceType,
16
+ )
17
+ from airbyte_cdk.models import Type as MessageType
18
+
19
+
20
+ def as_airbyte_message(stream: ConfiguredAirbyteStream, current_status: AirbyteStreamStatus) -> AirbyteMessage:
21
+ """
22
+ Builds an AirbyteStreamStatusTraceMessage for the provided stream
23
+ """
24
+
25
+ now_millis = datetime.now().timestamp() * 1000.0
26
+
27
+ trace_message = AirbyteTraceMessage(
28
+ type=TraceType.STREAM_STATUS,
29
+ emitted_at=now_millis,
30
+ stream_status=AirbyteStreamStatusTraceMessage(
31
+ stream_descriptor=StreamDescriptor(name=stream.stream.name, namespace=stream.stream.namespace),
32
+ status=current_status,
33
+ ),
34
+ )
35
+
36
+ return AirbyteMessage(type=MessageType.TRACE, trace=trace_message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 0.35.4
3
+ Version: 0.36.1
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://github.com/airbytehq/airbyte
6
6
  Author: Airbyte
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.8
19
19
  Requires-Python: >=3.8
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE.txt
22
- Requires-Dist: airbyte-protocol-models (==1.0.0)
22
+ Requires-Dist: airbyte-protocol-models (==0.3.6)
23
23
  Requires-Dist: backoff
24
24
  Requires-Dist: dpath (~=2.0.1)
25
25
  Requires-Dist: isodate (~=0.6.1)
@@ -6,7 +6,7 @@ airbyte_cdk/exception_handler.py,sha256=CwkiPdZ1WMOr3CBkvKFyHiyLerXGRqBrVlB4p0OI
6
6
  airbyte_cdk/logger.py,sha256=4Mi2MEQi1uh59BP9Dxw_UEbZuxaJewqK_jvEU2b10nk,3985
7
7
  airbyte_cdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  airbyte_cdk/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
9
- airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=ytc8rNiGRNgDyY86LKnplLdQw-8k0r_J_9Bs37jMSl8,5364
9
+ airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=KYfc3yyR9hWoj06E1-j2Z0OZtjeWw3YxdRda5z-dnq4,5407
10
10
  airbyte_cdk/connector_builder/main.py,sha256=eTXmkL1GaK1SCaN4jnVLh0MBok0KvM17QzQQ6qpsTTw,2982
11
11
  airbyte_cdk/connector_builder/message_grouper.py,sha256=swO_B0FovZMEliyjeYZ7Y7DRnTTwzrexy7rmqk7VnGU,11206
12
12
  airbyte_cdk/connector_builder/models.py,sha256=y0PJ-LwJk3e1RzRmMfjQSBP9ENx_a0wBcWNCjlW72Ks,1832
@@ -16,7 +16,7 @@ airbyte_cdk/models/__init__.py,sha256=LPQcYdDPwrCXiBPe_jexO4UAcbovIb1V9tHB6I7Un3
16
16
  airbyte_cdk/models/airbyte_protocol.py,sha256=wKXV_4sCzmUyPndiW7HWAj_A6EDRJyk9cA88xvXGQN0,117
17
17
  airbyte_cdk/models/well_known_types.py,sha256=KKfNbow2gdLoC1Z4hcXy_JR8m_acsB2ol7gQuEgjobw,117
18
18
  airbyte_cdk/sources/__init__.py,sha256=4j6fLtoRCjcZnojpise4EMmQtV1RepBxoGTBgpz80JA,218
19
- airbyte_cdk/sources/abstract_source.py,sha256=TVcsu-zBYu6fZ1PBipjyrqtKktgOXfu7fqTuMlIHYAk,16081
19
+ airbyte_cdk/sources/abstract_source.py,sha256=svXe29SUHKA6WJwKlQlyKkZax2ybaG0wSvIFzaibl24,17262
20
20
  airbyte_cdk/sources/config.py,sha256=PYsY7y2u3EUwxLiEb96JnuKwH_E8CuxKggsRO2ZPSRc,856
21
21
  airbyte_cdk/sources/connector_state_manager.py,sha256=_R-2QnMGimKL0t5aV4f6P1dgd--TB3abY5Seg1xddXk,10469
22
22
  airbyte_cdk/sources/source.py,sha256=N3vHZzdUsBETFsql-YpO-LcgjolT_jcnAuHBhGD6Hqk,4278
@@ -24,7 +24,7 @@ airbyte_cdk/sources/declarative/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4G
24
24
  airbyte_cdk/sources/declarative/create_partial.py,sha256=sUJOwD8hBzW4pxw2XhYlSTMgl-WMc5WpP5Oq_jo3fHw,3371
25
25
  airbyte_cdk/sources/declarative/declarative_component_schema.yaml,sha256=DJIV9HgHfBgv4w8zg6DhLIqXjZe235UVTe79IT7O51Q,74761
26
26
  airbyte_cdk/sources/declarative/declarative_source.py,sha256=U2As9PDKmcWDgbsWUo-RetJ9fxQOBlwntWZ0NOgs5Ac,1453
27
- airbyte_cdk/sources/declarative/declarative_stream.py,sha256=dhd228ddbG2YIptSw688HSWq9Hhh8GKLRxZS0VAz6rw,6211
27
+ airbyte_cdk/sources/declarative/declarative_stream.py,sha256=0iZSpypxt8bhO3Lmf3BpGRTO7Fp0Q2GI8m8xyJJUjeM,6580
28
28
  airbyte_cdk/sources/declarative/exceptions.py,sha256=kTPUA4I2NV4J6HDz-mKPGMrfuc592akJnOyYx38l_QM,176
29
29
  airbyte_cdk/sources/declarative/manifest_declarative_source.py,sha256=vTbRNM8D9P_ChOu1GNvtNRt-PM2L9N5Y0pNRyfVFuZg,9759
30
30
  airbyte_cdk/sources/declarative/types.py,sha256=b_RJpL9TyAgxJIRYZx5BxpC39p-WccHKxbAqxWrn9oE,482
@@ -66,7 +66,7 @@ airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=y7_G5mM07zxT
66
66
  airbyte_cdk/sources/declarative/parsers/default_implementation_registry.py,sha256=W8BcK4KOg4ifNXgsdeIoV4oneHjXBKcPHEZHIC4r-hM,3801
67
67
  airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py,sha256=H23H3nURCxsvjq66Gn9naffp0HJ1fU03wLFu-5F0AhQ,7701
68
68
  airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py,sha256=6ukHx0bBrCJm9rek1l_MEfS3U_gdJcM4pJRyifJEOp0,6412
69
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=2t3Ye8Rota6X-rf8TfPj_yLPGVFRT91-KuhuWcB8DTc,46378
69
+ airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=AzSjGeLahAA1FWvzbjIOk4iLiwHcaeS-tUhjs7bnyBk,46588
70
70
  airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=27sOWhw2LBQs62HchURakHQ2M_mtnOatNgU6q8RUtpU,476
71
71
  airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py,sha256=fa6VtTwSoIkDI3SBoRtVx79opVtJX80_gU9bt31lspc,4785
72
72
  airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py,sha256=Fi3ocNZZoYkr0uvRgwoVSqne6enxRvi8DOHrASVK2PQ,1851
@@ -105,7 +105,7 @@ airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_
105
105
  airbyte_cdk/sources/declarative/requesters/request_options/request_options_provider.py,sha256=tXxM0OPop49gw_EJuhJR5vyayXcg_XORrJlp5X8KydU,2625
106
106
  airbyte_cdk/sources/declarative/retrievers/__init__.py,sha256=IiHXDeKtibRqeWcRUckmSiXfk--u-sFMw3APWK8PCGQ,339
107
107
  airbyte_cdk/sources/declarative/retrievers/retriever.py,sha256=LBxg5r2QBPY8TSyQ7FxeG-FjLYFCV3Kq5MQM4SkCbUw,2030
108
- airbyte_cdk/sources/declarative/retrievers/simple_retriever.py,sha256=BOjmMdbUjLeQ2DQKw656QreEaPZoT_enERJOEMZyuWE,20715
108
+ airbyte_cdk/sources/declarative/retrievers/simple_retriever.py,sha256=Yc5c1NQ0Jpgy5isrA6ltWIUW-Y1p9ih0aUMXNJUve0k,21057
109
109
  airbyte_cdk/sources/declarative/schema/__init__.py,sha256=ul8L9S0-__AMEdbCLHBq-PMEeA928NVp8BB83BMotfU,517
110
110
  airbyte_cdk/sources/declarative/schema/default_schema_loader.py,sha256=t0ll098cIG2Wr1rq1rZ3QDZ9WnScUuqAh42YVoTRWrU,1794
111
111
  airbyte_cdk/sources/declarative/schema/inline_schema_loader.py,sha256=bVETE10hRsatRJq3R3BeyRR0wIoK3gcP1gcpVRQ_P5U,464
@@ -128,7 +128,7 @@ airbyte_cdk/sources/singer/singer_helpers.py,sha256=q1LmgjFxSnN-dobMy7nikUwcK-9F
128
128
  airbyte_cdk/sources/singer/source.py,sha256=3YY8UTOXmctvMVUnYmIegmL3_IxF55iGP_bc_s2MZdY,8530
129
129
  airbyte_cdk/sources/streams/__init__.py,sha256=XGrzYjIkqItvnMshsOUzYhi4lC4M9kFHhxG0oCAoAyE,176
130
130
  airbyte_cdk/sources/streams/availability_strategy.py,sha256=7BM0qLvXS0QrlKvnVkBEw4Cw8i7PCENCBLcIAcuD3nY,1007
131
- airbyte_cdk/sources/streams/core.py,sha256=2yN4jCVEY1nAEpHCsRIERjMqLn6XpgEfEioLpzXlRo8,11133
131
+ airbyte_cdk/sources/streams/core.py,sha256=G0MxhVkrF1DAPoPgmRBd_wnhrI7S6ZAdtOG0h9N_zHU,11021
132
132
  airbyte_cdk/sources/streams/http/__init__.py,sha256=6hRmA0P_RhB7X54xQbtj2RTz1RGlP52AG9V4pPWLaEQ,261
133
133
  airbyte_cdk/sources/streams/http/availability_strategy.py,sha256=uYYjanBG0f-CIrUYn1SnMyJqHoQQHnTggeJiB0m4i-Y,6344
134
134
  airbyte_cdk/sources/streams/http/exceptions.py,sha256=OokLDI7W8hZvq9e15sL3em2AdwmzmcAl72Ms-i5l0Nw,1334
@@ -156,11 +156,12 @@ airbyte_cdk/utils/__init__.py,sha256=kFLcs2P-tbPyeVOJS9rOv1jZdnSpjG24ro0CHgt_CIk
156
156
  airbyte_cdk/utils/airbyte_secrets_utils.py,sha256=q3aDl8T10ufGbeqnUPqbZLxQcHdkf2kDfQK_upWzBbI,2894
157
157
  airbyte_cdk/utils/event_timing.py,sha256=Hn5kCc9xGKLcV5EYpJCZwNiz9neKKu2WG8FJF_hy278,2377
158
158
  airbyte_cdk/utils/schema_inferrer.py,sha256=LQLOlraFksg7_sqpJNhy9pS_K42GVxG634ogM_P2s5E,2361
159
+ airbyte_cdk/utils/stream_status_utils.py,sha256=X1Vy7BhglycjdIWpfKDfwJussNCxYffelKt6Utjx-qY,1005
159
160
  airbyte_cdk/utils/traced_exception.py,sha256=9G2sG9eYkvn6Aa7rMuUW_KIRszRaTc_xdnTQNDKyKGI,3216
160
161
  source_declarative_manifest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
162
  source_declarative_manifest/main.py,sha256=HXzuRsRyhHwPrGU-hc4S7RrgoOoHImqkdfbmO2geBeE,1027
162
163
  unit_tests/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
163
- unit_tests/connector_builder/test_connector_builder_handler.py,sha256=-thlNWE20J37xPqfH5z89kvz09CVSIzull82IV6mNXQ,26729
164
+ unit_tests/connector_builder/test_connector_builder_handler.py,sha256=tFcMzGHO2eLmbVtRPqLI3TKIo88tOY_ECEuG1D939p0,26796
164
165
  unit_tests/connector_builder/test_message_grouper.py,sha256=m4Iod2AwgKaRTkH8ndAC7Uq276BdzSg7SGsj1a4Jbhg,24086
165
166
  unit_tests/connector_builder/utils.py,sha256=AAggdGWP-mNuWOZUHLAVIbjTeIcdPo-3pbMm5zdYpS0,796
166
167
  unit_tests/destinations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -169,15 +170,15 @@ unit_tests/singer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
169
170
  unit_tests/singer/test_singer_helpers.py,sha256=pZV6VxJuK-3-FICNGmoGbokrA_zkaFZEd4rYZCVpSRU,1762
170
171
  unit_tests/singer/test_singer_source.py,sha256=edN_kv7dnYAdBveWdUYOs74ak0dK6p8uaX225h_ZILA,4442
171
172
  unit_tests/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
- unit_tests/sources/test_abstract_source.py,sha256=ED6OjhaM60YQVV6c6ycRqjwWN0hHfLk_7pyzlaWrqKk,39753
173
+ unit_tests/sources/test_abstract_source.py,sha256=eHZjhfSN-fzqbvdZtGqa5FVwggoFDXxi5SBA1-LQi70,44194
173
174
  unit_tests/sources/test_config.py,sha256=gFXqU_6OjwHXkV4JHMqQUznxmvTWN8nAv0w0-FFpugc,2477
174
175
  unit_tests/sources/test_connector_state_manager.py,sha256=ynFxA63Cxe6t-wMMh9C6ByTlMAuk8W7H2FikDhnUEQ0,24264
175
- unit_tests/sources/test_source.py,sha256=xE8LZrvsIp-mbsZmLQMOu3PVC2RmtIQZxjdYbNF4CGg,24151
176
+ unit_tests/sources/test_source.py,sha256=eVtU9Zuc9gBsg11Pb5xjDtyU0gVrbYqbZ4RmzPvDw_M,24695
176
177
  unit_tests/sources/declarative/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
177
178
  unit_tests/sources/declarative/external_component.py,sha256=lU2gL736bLEWtmrGm1B2k83RXt_3XkROimLIahZd5dg,293
178
179
  unit_tests/sources/declarative/test_create_partial.py,sha256=s_KIywQqt8RlauOCWNJVk3HC3KBTAtSwFTN6JVQgu80,2636
179
- unit_tests/sources/declarative/test_declarative_stream.py,sha256=o-KNEH1NXOQc3ZkThHlasMHk9dPD1pbWXtX7O1BvN9w,4815
180
- unit_tests/sources/declarative/test_manifest_declarative_source.py,sha256=mPv2BMmLbkoI1DgT08Dsy4ll6PfzG52hNeQaZ1I13As,58906
180
+ unit_tests/sources/declarative/test_declarative_stream.py,sha256=3leJnZIYHiFq8XI4jb3TjPXTubGJmvNGzABt4c01EkQ,5436
181
+ unit_tests/sources/declarative/test_manifest_declarative_source.py,sha256=GckUc3nepzZkD1UM24woHlYCVZb5DP4IAQC3IeMyZF0,58924
181
182
  unit_tests/sources/declarative/test_yaml_declarative_source.py,sha256=6HhsUFgB7ueN0yOUHWb4gpPYLng5jasxN_plvz3x37g,5097
182
183
  unit_tests/sources/declarative/auth/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
183
184
  unit_tests/sources/declarative/auth/test_oauth.py,sha256=mqXE_mQBcM78-ZaDX5GCWFOkbXPCvYeCj81aKyPZ3D8,5204
@@ -203,7 +204,7 @@ unit_tests/sources/declarative/interpolation/test_macros.py,sha256=q6kNuNfsrpupm
203
204
  unit_tests/sources/declarative/parsers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
204
205
  unit_tests/sources/declarative/parsers/test_manifest_component_transformer.py,sha256=5lHUFv2n32b6h5IRh65S7EfqPkP5-IrGE3VUxDoPflI,12483
205
206
  unit_tests/sources/declarative/parsers/test_manifest_reference_resolver.py,sha256=K3q9eyx-sJFQ8nGYjAgS7fxau4sX_FlNreEAjiCYOeE,5306
206
- unit_tests/sources/declarative/parsers/test_model_to_component_factory.py,sha256=jBRkAF9ovA6Dj6CD8ff7GbeU1sWlMydmYzIg1ExG_lk,58188
207
+ unit_tests/sources/declarative/parsers/test_model_to_component_factory.py,sha256=OKYCRk4_gK7x_9tvqUq2Yd1OXdHdPQRJcBqUj06jomA,58950
207
208
  unit_tests/sources/declarative/parsers/testing_components.py,sha256=_yUijmYRM-yYHPGDB2JsfEiOuVrgexGW9QwHf1xxNW8,1326
208
209
  unit_tests/sources/declarative/partition_routers/__init__.py,sha256=O8MZg4Bv_DghdRy9BoJCPIqdV75VtiUrhEkExQgb2nE,61
209
210
  unit_tests/sources/declarative/partition_routers/test_list_partition_router.py,sha256=gyivHDJ7iA6dslrI886GQnT_if0BfGmLPfiviVGiSqo,5118
@@ -233,7 +234,7 @@ unit_tests/sources/declarative/requesters/paginators/test_request_option.py,sha2
233
234
  unit_tests/sources/declarative/requesters/request_options/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
234
235
  unit_tests/sources/declarative/requesters/request_options/test_interpolated_request_options_provider.py,sha256=Hl7b59ix1OwCJ5c34wn83d3D_l1dccE_nXlXWUR7Zos,5607
235
236
  unit_tests/sources/declarative/retrievers/__init__.py,sha256=ZnqYNxHsKCgO38IwB34RQyRMXTs4GTvlRi3ImKnIioo,61
236
- unit_tests/sources/declarative/retrievers/test_simple_retriever.py,sha256=LhTKUN975T1af6ZqoOMkrzDq0I6wS7DGgaF15j0XS7g,27834
237
+ unit_tests/sources/declarative/retrievers/test_simple_retriever.py,sha256=EhaUAo5ElmWW8pDRCD8K_SNhN-ct0ix7f-3DGTFp8Ag,29002
237
238
  unit_tests/sources/declarative/schema/__init__.py,sha256=i-iWyCqXPVgY-4miy16FH8U06gW_1_49AVq_8S8rVWY,134
238
239
  unit_tests/sources/declarative/schema/test_default_schema_loader.py,sha256=cWOFJnT9fhcEU6XLHkoe3E83mCjWc8lEttT0PFcvAm8,1091
239
240
  unit_tests/sources/declarative/schema/test_inline_schema_loader.py,sha256=vDJauhZ8og8M9ZqKDbf12SSYSfhUZ0_LmH7zjJHCHwI,517
@@ -256,9 +257,10 @@ unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.p
256
257
  unit_tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
257
258
  unit_tests/utils/test_schema_inferrer.py,sha256=ckl17GlNOZInqgxni7Z2A0bg_p6JDy0GVFAG8ph67pw,3288
258
259
  unit_tests/utils/test_secret_utils.py,sha256=XKe0f1RHYii8iwE6ATmBr5JGDI1pzzrnZUGdUSMJQP4,4886
260
+ unit_tests/utils/test_stream_status_utils.py,sha256=NpV155JMXA6CG-2Zvofa14lItobyh3Onttc59X4m5DI,3382
259
261
  unit_tests/utils/test_traced_exception.py,sha256=bDFP5zMBizFenz6V2WvEZTRCKGB5ijh3DBezjbfoYIs,4198
260
- airbyte_cdk-0.35.4.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
261
- airbyte_cdk-0.35.4.dist-info/METADATA,sha256=K4Z7tiT6fVjZHvrnAW4WJ7bk-76ddy-2nx8zTKbboec,8902
262
- airbyte_cdk-0.35.4.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
263
- airbyte_cdk-0.35.4.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
264
- airbyte_cdk-0.35.4.dist-info/RECORD,,
262
+ airbyte_cdk-0.36.1.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
263
+ airbyte_cdk-0.36.1.dist-info/METADATA,sha256=julG0JMgo00RRk6h3nZf_h-2-wVJffJ3e_fEEsV3dhQ,8902
264
+ airbyte_cdk-0.36.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
265
+ airbyte_cdk-0.36.1.dist-info/top_level.txt,sha256=edvsDKTnE6sD2wfCUaeTfKf5gQIL6CPVMwVL2sWZzqo,51
266
+ airbyte_cdk-0.36.1.dist-info/RECORD,,
@@ -566,6 +566,7 @@ def test_create_source():
566
566
  assert isinstance(source, ManifestDeclarativeSource)
567
567
  assert source._constructor._limit_pages_fetched_per_slice == limits.max_pages_per_slice
568
568
  assert source._constructor._limit_slices_fetched == limits.max_slices
569
+ assert source.streams(config={})[0].retriever.max_retries == 0
569
570
 
570
571
 
571
572
  def request_log_message(request: dict) -> AirbyteMessage:
@@ -1360,3 +1360,29 @@ def test_simple_retriever_emit_log_messages():
1360
1360
  )
1361
1361
 
1362
1362
  assert isinstance(retriever, SimpleRetrieverTestReadDecorator)
1363
+
1364
+
1365
+ def test_ignore_retry():
1366
+ requester_model = {
1367
+ "type": "SimpleRetriever",
1368
+ "record_selector": {
1369
+ "type": "RecordSelector",
1370
+ "extractor": {
1371
+ "type": "DpathExtractor",
1372
+ "field_path": [],
1373
+ },
1374
+ },
1375
+ "requester": {"type": "HttpRequester", "name": "list", "url_base": "orange.com", "path": "/v1/api"},
1376
+ }
1377
+
1378
+ connector_builder_factory = ModelToComponentFactory(disable_retries=True)
1379
+ retriever = connector_builder_factory.create_component(
1380
+ model_type=SimpleRetrieverModel,
1381
+ component_definition=requester_model,
1382
+ config={},
1383
+ name="Test",
1384
+ primary_key="id",
1385
+ stream_slicer=None,
1386
+ )
1387
+
1388
+ assert retriever.max_retries == 0
@@ -256,6 +256,48 @@ def test_parse_response(test_name, status_code, response_status, len_expected_re
256
256
  assert len(records) == len_expected_records
257
257
 
258
258
 
259
+ def test_max_retries_given_error_handler_has_max_retries():
260
+ requester = MagicMock()
261
+ requester.error_handler = MagicMock()
262
+ requester.error_handler.max_retries = 10
263
+ retriever = SimpleRetriever(
264
+ name="stream_name",
265
+ primary_key=primary_key,
266
+ requester=requester,
267
+ record_selector=MagicMock(),
268
+ parameters={},
269
+ config={}
270
+ )
271
+ assert retriever.max_retries == 10
272
+
273
+
274
+ def test_max_retries_given_error_handler_without_max_retries():
275
+ requester = MagicMock()
276
+ requester.error_handler = MagicMock(spec=[u'without_max_retries_attribute'])
277
+ retriever = SimpleRetriever(
278
+ name="stream_name",
279
+ primary_key=primary_key,
280
+ requester=requester,
281
+ record_selector=MagicMock(),
282
+ parameters={},
283
+ config={}
284
+ )
285
+ assert retriever.max_retries == 5
286
+
287
+
288
+ def test_max_retries_given_disable_retries():
289
+ retriever = SimpleRetriever(
290
+ name="stream_name",
291
+ primary_key=primary_key,
292
+ requester=MagicMock(),
293
+ record_selector=MagicMock(),
294
+ disable_retries=True,
295
+ parameters={},
296
+ config={}
297
+ )
298
+ assert retriever.max_retries == 0
299
+
300
+
259
301
  @pytest.mark.parametrize(
260
302
  "test_name, response_action, retry_in, expected_backoff_time",
261
303
  [
@@ -5,7 +5,16 @@
5
5
  from unittest import mock
6
6
  from unittest.mock import MagicMock, call
7
7
 
8
- from airbyte_cdk.models import AirbyteLogMessage, AirbyteTraceMessage, Level, SyncMode, TraceType
8
+ from airbyte_cdk.models import (
9
+ AirbyteLogMessage,
10
+ AirbyteMessage,
11
+ AirbyteRecordMessage,
12
+ AirbyteTraceMessage,
13
+ Level,
14
+ SyncMode,
15
+ TraceType,
16
+ Type,
17
+ )
9
18
  from airbyte_cdk.sources.declarative.declarative_stream import DeclarativeStream
10
19
  from airbyte_cdk.sources.declarative.transformations import AddFields, RecordTransformation
11
20
  from airbyte_cdk.sources.declarative.transformations.add_fields import AddedFieldDefinition
@@ -24,8 +33,8 @@ def test_declarative_stream():
24
33
  records = [
25
34
  {"pk": 1234, "field": "value"},
26
35
  {"pk": 4567, "field": "different_value"},
27
- AirbyteLogMessage(level=Level.INFO, message="This is a log message"),
28
- AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345),
36
+ AirbyteMessage(type=Type.LOG, log=AirbyteLogMessage(level=Level.INFO, message="This is a log message")),
37
+ AirbyteMessage(type=Type.TRACE, trace=AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345)),
29
38
  ]
30
39
  stream_slices = [
31
40
  {"date": "2021-01-01"},
@@ -84,15 +93,17 @@ def test_declarative_stream_with_add_fields_transform():
84
93
  retriever_records = [
85
94
  {"pk": 1234, "field": "value"},
86
95
  {"pk": 4567, "field": "different_value"},
87
- AirbyteLogMessage(level=Level.INFO, message="This is a log message"),
88
- AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345),
96
+ AirbyteMessage(type=Type.RECORD, record=AirbyteRecordMessage(data={"pk": 1357, "field": "a_value"}, emitted_at=12344, stream="stream")),
97
+ AirbyteMessage(type=Type.LOG, log=AirbyteLogMessage(level=Level.INFO, message="This is a log message")),
98
+ AirbyteMessage(type=Type.TRACE, trace=AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345)),
89
99
  ]
90
100
 
91
101
  expected_records = [
92
102
  {"pk": 1234, "field": "value", "added_key": "added_value"},
93
103
  {"pk": 4567, "field": "different_value", "added_key": "added_value"},
94
- AirbyteLogMessage(level=Level.INFO, message="This is a log message"),
95
- AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345),
104
+ AirbyteMessage(type=Type.RECORD, record=AirbyteRecordMessage(data={"pk": 1357, "field": "a_value", "added_key": "added_value"}, emitted_at=12344, stream="stream")),
105
+ AirbyteMessage(type=Type.LOG, log=AirbyteLogMessage(level=Level.INFO, message="This is a log message")),
106
+ AirbyteMessage(type=Type.TRACE, trace=AirbyteTraceMessage(type=TraceType.ERROR, emitted_at=12345)),
96
107
  ]
97
108
  stream_slices = [
98
109
  {"date": "2021-01-01"},
@@ -1242,7 +1242,7 @@ def _create_page(response_body):
1242
1242
  def test_read_manifest_declarative_source(test_name, manifest, pages, expected_records, expected_calls):
1243
1243
  _stream_name = "Rates"
1244
1244
  with patch.object(HttpStream, "_fetch_next_page", side_effect=pages) as mock_http_stream:
1245
- output_data = [message.record.data for message in _run_read(manifest, _stream_name)]
1245
+ output_data = [message.record.data for message in _run_read(manifest, _stream_name) if message.record]
1246
1246
  assert expected_records == output_data
1247
1247
  mock_http_stream.assert_has_calls(expected_calls)
1248
1248
 
@@ -21,6 +21,9 @@ from airbyte_cdk.models import (
21
21
  AirbyteStateType,
22
22
  AirbyteStream,
23
23
  AirbyteStreamState,
24
+ AirbyteStreamStatus,
25
+ AirbyteStreamStatusTraceMessage,
26
+ AirbyteTraceMessage,
24
27
  ConfiguredAirbyteCatalog,
25
28
  ConfiguredAirbyteStream,
26
29
  DestinationSyncMode,
@@ -28,8 +31,10 @@ from airbyte_cdk.models import (
28
31
  Status,
29
32
  StreamDescriptor,
30
33
  SyncMode,
31
- Type,
34
+ TraceType,
32
35
  )
36
+ from airbyte_cdk.models import Type
37
+ from airbyte_cdk.models import Type as MessageType
33
38
  from airbyte_cdk.sources import AbstractSource
34
39
  from airbyte_cdk.sources.connector_state_manager import ConnectorStateManager
35
40
  from airbyte_cdk.sources.streams import IncrementalMixin, Stream
@@ -250,6 +255,19 @@ def _as_records(stream: str, data: List[Dict[str, Any]]) -> List[AirbyteMessage]
250
255
  return [_as_record(stream, datum) for datum in data]
251
256
 
252
257
 
258
+ def _as_stream_status(stream: str, status: AirbyteStreamStatus) -> AirbyteMessage:
259
+ trace_message = AirbyteTraceMessage(
260
+ emitted_at=datetime.datetime.now().timestamp() * 1000.0,
261
+ type=TraceType.STREAM_STATUS,
262
+ stream_status=AirbyteStreamStatusTraceMessage(
263
+ stream_descriptor=StreamDescriptor(name=stream),
264
+ status=status,
265
+ ),
266
+ )
267
+
268
+ return AirbyteMessage(type=MessageType.TRACE, trace=trace_message)
269
+
270
+
253
271
  def _as_state(state_data: Dict[str, Any], stream_name: str = "", per_stream_state: Dict[str, Any] = None):
254
272
  if per_stream_state:
255
273
  return AirbyteMessage(
@@ -277,6 +295,8 @@ def _fix_emitted_at(messages: List[AirbyteMessage]) -> List[AirbyteMessage]:
277
295
  for msg in messages:
278
296
  if msg.type == Type.RECORD and msg.record:
279
297
  msg.record.emitted_at = GLOBAL_EMITTED_AT
298
+ if msg.type == Type.TRACE and msg.trace:
299
+ msg.trace.emitted_at = GLOBAL_EMITTED_AT
280
300
  return messages
281
301
 
282
302
 
@@ -296,7 +316,17 @@ def test_valid_full_refresh_read_no_slices(mocker):
296
316
  ]
297
317
  )
298
318
 
299
- expected = _as_records("s1", stream_output) + _as_records("s2", stream_output)
319
+ expected = _fix_emitted_at(
320
+ [
321
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
322
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
323
+ *_as_records("s1", stream_output),
324
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
325
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
326
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
327
+ *_as_records("s2", stream_output),
328
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE)
329
+ ])
300
330
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog)))
301
331
 
302
332
  assert expected == messages
@@ -326,7 +356,17 @@ def test_valid_full_refresh_read_with_slices(mocker):
326
356
  ]
327
357
  )
328
358
 
329
- expected = [*_as_records("s1", slices), *_as_records("s2", slices)]
359
+ expected = _fix_emitted_at(
360
+ [
361
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
362
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
363
+ *_as_records("s1", slices),
364
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
365
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
366
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
367
+ *_as_records("s2", slices),
368
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE)
369
+ ])
330
370
 
331
371
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog)))
332
372
 
@@ -448,18 +488,24 @@ class TestIncrementalRead:
448
488
  ]
449
489
  )
450
490
 
451
- expected = [
491
+ expected = _fix_emitted_at([
492
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
493
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
452
494
  _as_record("s1", stream_output[0]),
453
495
  _as_record("s1", stream_output[1]),
454
496
  _as_state({"s1": new_state_from_connector}, "s1", new_state_from_connector)
455
497
  if per_stream_enabled
456
498
  else _as_state({"s1": new_state_from_connector}),
499
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
500
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
501
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
457
502
  _as_record("s2", stream_output[0]),
458
503
  _as_record("s2", stream_output[1]),
459
504
  _as_state({"s1": new_state_from_connector, "s2": new_state_from_connector}, "s2", new_state_from_connector)
460
505
  if per_stream_enabled
461
506
  else _as_state({"s1": new_state_from_connector, "s2": new_state_from_connector}),
462
- ]
507
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
508
+ ])
463
509
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
464
510
 
465
511
  assert messages == expected
@@ -521,18 +567,24 @@ class TestIncrementalRead:
521
567
  ]
522
568
  )
523
569
 
524
- expected = [
570
+ expected = _fix_emitted_at([
571
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
572
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
525
573
  _as_record("s1", stream_output[0]),
526
574
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
527
575
  _as_record("s1", stream_output[1]),
528
576
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
529
577
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
578
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
579
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
580
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
530
581
  _as_record("s2", stream_output[0]),
531
582
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
532
583
  _as_record("s2", stream_output[1]),
533
584
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
534
585
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
535
- ]
586
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
587
+ ])
536
588
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
537
589
 
538
590
  assert expected == messages
@@ -582,12 +634,18 @@ class TestIncrementalRead:
582
634
  ]
583
635
  )
584
636
 
585
- expected = [
637
+ expected = _fix_emitted_at([
638
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
639
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
586
640
  *_as_records("s1", stream_output),
587
641
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
642
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
643
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
644
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
588
645
  *_as_records("s2", stream_output),
589
646
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
590
- ]
647
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
648
+ ])
591
649
 
592
650
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
593
651
 
@@ -658,20 +716,26 @@ class TestIncrementalRead:
658
716
  ]
659
717
  )
660
718
 
661
- expected = [
719
+ expected = _fix_emitted_at([
720
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
721
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
662
722
  # stream 1 slice 1
663
723
  *_as_records("s1", stream_output),
664
724
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
665
725
  # stream 1 slice 2
666
726
  *_as_records("s1", stream_output),
667
727
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
728
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
729
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
730
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
668
731
  # stream 2 slice 1
669
732
  *_as_records("s2", stream_output),
670
733
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
671
734
  # stream 2 slice 2
672
735
  *_as_records("s2", stream_output),
673
736
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
674
- ]
737
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
738
+ ])
675
739
 
676
740
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
677
741
 
@@ -753,10 +817,14 @@ class TestIncrementalRead:
753
817
  ]
754
818
  )
755
819
 
756
- expected = [
820
+ expected = _fix_emitted_at([
821
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
757
822
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
823
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
824
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
758
825
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
759
- ]
826
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
827
+ ])
760
828
 
761
829
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
762
830
 
@@ -837,8 +905,10 @@ class TestIncrementalRead:
837
905
  ]
838
906
  )
839
907
 
840
- expected = [
908
+ expected = _fix_emitted_at([
841
909
  # stream 1 slice 1
910
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
911
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
842
912
  _as_record("s1", stream_output[0]),
843
913
  _as_record("s1", stream_output[1]),
844
914
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
@@ -850,7 +920,10 @@ class TestIncrementalRead:
850
920
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
851
921
  _as_record("s1", stream_output[2]),
852
922
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
923
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
853
924
  # stream 2 slice 1
925
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
926
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
854
927
  _as_record("s2", stream_output[0]),
855
928
  _as_record("s2", stream_output[1]),
856
929
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
@@ -862,7 +935,8 @@ class TestIncrementalRead:
862
935
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
863
936
  _as_record("s2", stream_output[2]),
864
937
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
865
- ]
938
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
939
+ ])
866
940
 
867
941
  messages = _fix_emitted_at(list(src.read(logger, {}, catalog, state=input_state)))
868
942
 
@@ -942,6 +1016,8 @@ class TestIncrementalRead:
942
1016
 
943
1017
  expected = _fix_emitted_at(
944
1018
  [
1019
+ _as_stream_status("s1", AirbyteStreamStatus.STARTED),
1020
+ _as_stream_status("s1", AirbyteStreamStatus.RUNNING),
945
1021
  # stream 1 slice 1
946
1022
  stream_data_to_airbyte_message("s1", stream_output[0]),
947
1023
  stream_data_to_airbyte_message("s1", stream_output[1]),
@@ -956,7 +1032,10 @@ class TestIncrementalRead:
956
1032
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
957
1033
  stream_data_to_airbyte_message("s1", stream_output[3]),
958
1034
  _as_state({"s1": state}, "s1", state) if per_stream_enabled else _as_state({"s1": state}),
1035
+ _as_stream_status("s1", AirbyteStreamStatus.COMPLETE),
959
1036
  # stream 2 slice 1
1037
+ _as_stream_status("s2", AirbyteStreamStatus.STARTED),
1038
+ _as_stream_status("s2", AirbyteStreamStatus.RUNNING),
960
1039
  stream_data_to_airbyte_message("s2", stream_output[0]),
961
1040
  stream_data_to_airbyte_message("s2", stream_output[1]),
962
1041
  stream_data_to_airbyte_message("s2", stream_output[2]),
@@ -970,6 +1049,7 @@ class TestIncrementalRead:
970
1049
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
971
1050
  stream_data_to_airbyte_message("s2", stream_output[3]),
972
1051
  _as_state({"s1": state, "s2": state}, "s2", state) if per_stream_enabled else _as_state({"s1": state, "s2": state}),
1052
+ _as_stream_status("s2", AirbyteStreamStatus.COMPLETE),
973
1053
  ]
974
1054
  )
975
1055
 
@@ -365,8 +365,8 @@ def test_internal_config(abstract_source, catalog):
365
365
  # Test with empty config
366
366
  logger = logging.getLogger(f"airbyte.{getattr(abstract_source, 'name', '')}")
367
367
  records = [r for r in abstract_source.read(logger=logger, config={}, catalog=catalog, state={})]
368
- # 3 for http stream and 3 for non http stream
369
- assert len(records) == 3 + 3
368
+ # 3 for http stream, 3 for non http stream and 3 for stream status messages for each stream (2x)
369
+ assert len(records) == 3 + 3 + 3 + 3
370
370
  assert http_stream.read_records.called
371
371
  assert non_http_stream.read_records.called
372
372
  # Make sure page_size havent been set
@@ -375,21 +375,21 @@ def test_internal_config(abstract_source, catalog):
375
375
  # Test with records limit set to 1
376
376
  internal_config = {"some_config": 100, "_limit": 1}
377
377
  records = [r for r in abstract_source.read(logger=logger, config=internal_config, catalog=catalog, state={})]
378
- # 1 from http stream + 1 from non http stream
379
- assert len(records) == 1 + 1
378
+ # 1 from http stream + 1 from non http stream and 3 for stream status messages for each stream (2x)
379
+ assert len(records) == 1 + 1 + 3 + 3
380
380
  assert "_limit" not in abstract_source.streams_config
381
381
  assert "some_config" in abstract_source.streams_config
382
382
  # Test with records limit set to number that exceeds expceted records
383
383
  internal_config = {"some_config": 100, "_limit": 20}
384
384
  records = [r for r in abstract_source.read(logger=logger, config=internal_config, catalog=catalog, state={})]
385
- assert len(records) == 3 + 3
385
+ assert len(records) == 3 + 3 + 3 + 3
386
386
 
387
387
  # Check if page_size paramter is set to http instance only
388
388
  internal_config = {"some_config": 100, "_page_size": 2}
389
389
  records = [r for r in abstract_source.read(logger=logger, config=internal_config, catalog=catalog, state={})]
390
390
  assert "_page_size" not in abstract_source.streams_config
391
391
  assert "some_config" in abstract_source.streams_config
392
- assert len(records) == 3 + 3
392
+ assert len(records) == 3 + 3 + 3 + 3
393
393
  assert http_stream.page_size == 2
394
394
  # Make sure page_size havent been set for non http streams
395
395
  assert not non_http_stream.page_size
@@ -402,6 +402,7 @@ def test_internal_config_limit(mocker, abstract_source, catalog):
402
402
  STREAM_LIMIT = 2
403
403
  SLICE_DEBUG_LOG_COUNT = 1
404
404
  FULL_RECORDS_NUMBER = 3
405
+ TRACE_STATUS_COUNT = 3
405
406
  streams = abstract_source.streams(None)
406
407
  http_stream = streams[0]
407
408
  http_stream.read_records.return_value = [{}] * FULL_RECORDS_NUMBER
@@ -409,7 +410,7 @@ def test_internal_config_limit(mocker, abstract_source, catalog):
409
410
 
410
411
  catalog.streams[0].sync_mode = SyncMode.full_refresh
411
412
  records = [r for r in abstract_source.read(logger=logger_mock, config=internal_config, catalog=catalog, state={})]
412
- assert len(records) == STREAM_LIMIT + SLICE_DEBUG_LOG_COUNT
413
+ assert len(records) == STREAM_LIMIT + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT
413
414
  logger_info_args = [call[0][0] for call in logger_mock.info.call_args_list]
414
415
  # Check if log line matches number of limit
415
416
  read_log_record = [_l for _l in logger_info_args if _l.startswith("Read")]
@@ -418,14 +419,16 @@ def test_internal_config_limit(mocker, abstract_source, catalog):
418
419
  # No limit, check if state record produced for incremental stream
419
420
  catalog.streams[0].sync_mode = SyncMode.incremental
420
421
  records = [r for r in abstract_source.read(logger=logger_mock, config={}, catalog=catalog, state={})]
421
- assert len(records) == FULL_RECORDS_NUMBER + SLICE_DEBUG_LOG_COUNT + 1
422
- assert records[-1].type == Type.STATE
422
+ assert len(records) == FULL_RECORDS_NUMBER + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT + 1
423
+ assert records[-2].type == Type.STATE
424
+ assert records[-1].type == Type.TRACE
423
425
 
424
426
  # Set limit and check if state is produced when limit is set for incremental stream
425
427
  logger_mock.reset_mock()
426
428
  records = [r for r in abstract_source.read(logger=logger_mock, config=internal_config, catalog=catalog, state={})]
427
- assert len(records) == STREAM_LIMIT + SLICE_DEBUG_LOG_COUNT + 1
428
- assert records[-1].type == Type.STATE
429
+ assert len(records) == STREAM_LIMIT + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT + 1
430
+ assert records[-2].type == Type.STATE
431
+ assert records[-1].type == Type.TRACE
429
432
  logger_info_args = [call[0][0] for call in logger_mock.info.call_args_list]
430
433
  read_log_record = [_l for _l in logger_info_args if _l.startswith("Read")]
431
434
  assert read_log_record[0].startswith(f"Read {STREAM_LIMIT} ")
@@ -436,6 +439,7 @@ SCHEMA = {"type": "object", "properties": {"value": {"type": "string"}}}
436
439
 
437
440
  def test_source_config_no_transform(mocker, abstract_source, catalog):
438
441
  SLICE_DEBUG_LOG_COUNT = 1
442
+ TRACE_STATUS_COUNT = 3
439
443
  logger_mock = mocker.MagicMock()
440
444
  logger_mock.level = logging.DEBUG
441
445
  streams = abstract_source.streams(None)
@@ -443,7 +447,7 @@ def test_source_config_no_transform(mocker, abstract_source, catalog):
443
447
  http_stream.get_json_schema.return_value = non_http_stream.get_json_schema.return_value = SCHEMA
444
448
  http_stream.read_records.return_value, non_http_stream.read_records.return_value = [[{"value": 23}] * 5] * 2
445
449
  records = [r for r in abstract_source.read(logger=logger_mock, config={}, catalog=catalog, state={})]
446
- assert len(records) == 2 * (5 + SLICE_DEBUG_LOG_COUNT)
450
+ assert len(records) == 2 * (5 + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT)
447
451
  assert [r.record.data for r in records if r.type == Type.RECORD] == [{"value": 23}] * 2 * 5
448
452
  assert http_stream.get_json_schema.call_count == 5
449
453
  assert non_http_stream.get_json_schema.call_count == 5
@@ -453,6 +457,7 @@ def test_source_config_transform(mocker, abstract_source, catalog):
453
457
  logger_mock = mocker.MagicMock()
454
458
  logger_mock.level = logging.DEBUG
455
459
  SLICE_DEBUG_LOG_COUNT = 2
460
+ TRACE_STATUS_COUNT = 6
456
461
  streams = abstract_source.streams(None)
457
462
  http_stream, non_http_stream = streams
458
463
  http_stream.transformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization)
@@ -460,7 +465,7 @@ def test_source_config_transform(mocker, abstract_source, catalog):
460
465
  http_stream.get_json_schema.return_value = non_http_stream.get_json_schema.return_value = SCHEMA
461
466
  http_stream.read_records.return_value, non_http_stream.read_records.return_value = [{"value": 23}], [{"value": 23}]
462
467
  records = [r for r in abstract_source.read(logger=logger_mock, config={}, catalog=catalog, state={})]
463
- assert len(records) == 2 + SLICE_DEBUG_LOG_COUNT
468
+ assert len(records) == 2 + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT
464
469
  assert [r.record.data for r in records if r.type == Type.RECORD] == [{"value": "23"}] * 2
465
470
 
466
471
 
@@ -468,13 +473,14 @@ def test_source_config_transform_and_no_transform(mocker, abstract_source, catal
468
473
  logger_mock = mocker.MagicMock()
469
474
  logger_mock.level = logging.DEBUG
470
475
  SLICE_DEBUG_LOG_COUNT = 2
476
+ TRACE_STATUS_COUNT = 6
471
477
  streams = abstract_source.streams(None)
472
478
  http_stream, non_http_stream = streams
473
479
  http_stream.transformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization)
474
480
  http_stream.get_json_schema.return_value = non_http_stream.get_json_schema.return_value = SCHEMA
475
481
  http_stream.read_records.return_value, non_http_stream.read_records.return_value = [{"value": 23}], [{"value": 23}]
476
482
  records = [r for r in abstract_source.read(logger=logger_mock, config={}, catalog=catalog, state={})]
477
- assert len(records) == 2 + SLICE_DEBUG_LOG_COUNT
483
+ assert len(records) == 2 + SLICE_DEBUG_LOG_COUNT + TRACE_STATUS_COUNT
478
484
  assert [r.record.data for r in records if r.type == Type.RECORD] == [{"value": "23"}, {"value": 23}]
479
485
 
480
486
 
@@ -520,8 +526,8 @@ def test_read_default_http_availability_strategy_stream_available(catalog, mocke
520
526
  source = MockAbstractSource(streams=streams)
521
527
  logger = logging.getLogger(f"airbyte.{getattr(abstract_source, 'name', '')}")
522
528
  records = [r for r in source.read(logger=logger, config={}, catalog=catalog, state={})]
523
- # 3 for http stream and 3 for non http stream
524
- assert len(records) == 3 + 3
529
+ # 3 for http stream, 3 for non http stream and 3 for stream status messages for each stream (2x)
530
+ assert len(records) == 3 + 3 + 3 + 3
525
531
  assert http_stream.read_records.called
526
532
  assert non_http_stream.read_records.called
527
533
 
@@ -578,8 +584,8 @@ def test_read_default_http_availability_strategy_stream_unavailable(catalog, moc
578
584
  with caplog.at_level(logging.WARNING):
579
585
  records = [r for r in source.read(logger=logger, config={}, catalog=catalog, state={})]
580
586
 
581
- # 0 for http stream and 3 for non http stream
582
- assert len(records) == 0 + 3
587
+ # 0 for http stream, 3 for non http stream and 3 status trace meessages
588
+ assert len(records) == 0 + 3 + 3
583
589
  assert non_http_stream.read_records.called
584
590
  expected_logs = [
585
591
  f"Skipped syncing stream '{http_stream.name}' because it was unavailable.",
@@ -0,0 +1,70 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ from airbyte_cdk.models import (
6
+ AirbyteMessage,
7
+ AirbyteStream,
8
+ AirbyteStreamStatus,
9
+ ConfiguredAirbyteStream,
10
+ DestinationSyncMode,
11
+ SyncMode,
12
+ TraceType,
13
+ )
14
+ from airbyte_cdk.models import Type as MessageType
15
+ from airbyte_cdk.utils.stream_status_utils import as_airbyte_message as stream_status_as_airbyte_message
16
+
17
+ stream = AirbyteStream(name="name", namespace="namespace", json_schema={}, supported_sync_modes=[SyncMode.full_refresh])
18
+ configured_stream = ConfiguredAirbyteStream(stream=stream, sync_mode=SyncMode.full_refresh, destination_sync_mode=DestinationSyncMode.overwrite)
19
+
20
+
21
+ def test_started_as_message():
22
+ stream_status = AirbyteStreamStatus.STARTED
23
+ airbyte_message = stream_status_as_airbyte_message(configured_stream, stream_status)
24
+
25
+ assert type(airbyte_message) == AirbyteMessage
26
+ assert airbyte_message.type == MessageType.TRACE
27
+ assert airbyte_message.trace.type == TraceType.STREAM_STATUS
28
+ assert airbyte_message.trace.emitted_at > 0
29
+ assert airbyte_message.trace.stream_status.stream_descriptor.name == configured_stream.stream.name
30
+ assert airbyte_message.trace.stream_status.stream_descriptor.namespace == configured_stream.stream.namespace
31
+ assert airbyte_message.trace.stream_status.status == stream_status
32
+
33
+
34
+ def test_running_as_message():
35
+ stream_status = AirbyteStreamStatus.RUNNING
36
+ airbyte_message = stream_status_as_airbyte_message(configured_stream, stream_status)
37
+
38
+ assert type(airbyte_message) == AirbyteMessage
39
+ assert airbyte_message.type == MessageType.TRACE
40
+ assert airbyte_message.trace.type == TraceType.STREAM_STATUS
41
+ assert airbyte_message.trace.emitted_at > 0
42
+ assert airbyte_message.trace.stream_status.stream_descriptor.name == configured_stream.stream.name
43
+ assert airbyte_message.trace.stream_status.stream_descriptor.namespace == configured_stream.stream.namespace
44
+ assert airbyte_message.trace.stream_status.status == stream_status
45
+
46
+
47
+ def test_complete_as_message():
48
+ stream_status = AirbyteStreamStatus.COMPLETE
49
+ airbyte_message = stream_status_as_airbyte_message(configured_stream, stream_status)
50
+
51
+ assert type(airbyte_message) == AirbyteMessage
52
+ assert airbyte_message.type == MessageType.TRACE
53
+ assert airbyte_message.trace.type == TraceType.STREAM_STATUS
54
+ assert airbyte_message.trace.emitted_at > 0
55
+ assert airbyte_message.trace.stream_status.stream_descriptor.name == configured_stream.stream.name
56
+ assert airbyte_message.trace.stream_status.stream_descriptor.namespace == configured_stream.stream.namespace
57
+ assert airbyte_message.trace.stream_status.status == stream_status
58
+
59
+
60
+ def test_incomplete_failed_as_message():
61
+ stream_status = AirbyteStreamStatus.INCOMPLETE
62
+ airbyte_message = stream_status_as_airbyte_message(configured_stream, stream_status)
63
+
64
+ assert type(airbyte_message) == AirbyteMessage
65
+ assert airbyte_message.type == MessageType.TRACE
66
+ assert airbyte_message.trace.type == TraceType.STREAM_STATUS
67
+ assert airbyte_message.trace.emitted_at > 0
68
+ assert airbyte_message.trace.stream_status.stream_descriptor.name == configured_stream.stream.name
69
+ assert airbyte_message.trace.stream_status.stream_descriptor.namespace == configured_stream.stream.namespace
70
+ assert airbyte_message.trace.stream_status.status == stream_status