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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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