airbyte-cdk 6.45.4.post49.dev14495925594__py3-none-any.whl → 6.45.4.post72.dev14497997772__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.
Files changed (41) hide show
  1. airbyte_cdk/models/__init__.py +0 -1
  2. airbyte_cdk/models/airbyte_protocol.py +3 -1
  3. airbyte_cdk/models/file_transfer_record_message.py +13 -0
  4. airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py +1 -1
  5. airbyte_cdk/sources/declarative/concurrent_declarative_source.py +0 -8
  6. airbyte_cdk/sources/declarative/declarative_component_schema.yaml +0 -36
  7. airbyte_cdk/sources/declarative/extractors/record_selector.py +1 -6
  8. airbyte_cdk/sources/declarative/models/declarative_component_schema.py +0 -31
  9. airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +1 -39
  10. airbyte_cdk/sources/declarative/stream_slicers/declarative_partition_generator.py +4 -9
  11. airbyte_cdk/sources/file_based/file_based_stream_reader.py +16 -38
  12. airbyte_cdk/sources/file_based/file_types/file_transfer.py +15 -8
  13. airbyte_cdk/sources/file_based/schema_helpers.py +1 -11
  14. airbyte_cdk/sources/file_based/stream/concurrent/adapters.py +12 -3
  15. airbyte_cdk/sources/file_based/stream/default_file_based_stream.py +38 -15
  16. airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py +3 -1
  17. airbyte_cdk/sources/streams/concurrent/default_stream.py +0 -3
  18. airbyte_cdk/sources/types.py +2 -11
  19. airbyte_cdk/sources/utils/record_helper.py +8 -8
  20. airbyte_cdk/test/declarative/__init__.py +6 -0
  21. airbyte_cdk/test/declarative/models/__init__.py +7 -0
  22. airbyte_cdk/test/declarative/models/scenario.py +74 -0
  23. airbyte_cdk/test/declarative/utils/__init__.py +0 -0
  24. airbyte_cdk/test/declarative/utils/job_runner.py +159 -0
  25. airbyte_cdk/test/entrypoint_wrapper.py +4 -0
  26. airbyte_cdk/test/mock_http/response_builder.py +0 -8
  27. airbyte_cdk/test/standard_tests/__init__.py +46 -0
  28. airbyte_cdk/test/standard_tests/connector_base.py +148 -0
  29. airbyte_cdk/test/standard_tests/declarative_sources.py +92 -0
  30. airbyte_cdk/test/standard_tests/destination_base.py +16 -0
  31. airbyte_cdk/test/standard_tests/pytest_hooks.py +61 -0
  32. airbyte_cdk/test/standard_tests/source_base.py +140 -0
  33. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/METADATA +3 -2
  34. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/RECORD +38 -29
  35. airbyte_cdk/sources/declarative/retrievers/file_uploader.py +0 -89
  36. airbyte_cdk/sources/file_based/file_record_data.py +0 -23
  37. airbyte_cdk/sources/utils/files_directory.py +0 -15
  38. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/LICENSE.txt +0 -0
  39. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/LICENSE_SHORT +0 -0
  40. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/WHEEL +0 -0
  41. {airbyte_cdk-6.45.4.post49.dev14495925594.dist-info → airbyte_cdk-6.45.4.post72.dev14497997772.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,7 @@ from functools import cache
11
11
  from os import path
12
12
  from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Union
13
13
 
14
- from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, AirbyteStream, FailureType, Level
14
+ from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, FailureType, Level
15
15
  from airbyte_cdk.models import Type as MessageType
16
16
  from airbyte_cdk.sources.file_based.config.file_based_stream_config import PrimaryKeyType
17
17
  from airbyte_cdk.sources.file_based.exceptions import (
@@ -56,7 +56,6 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
56
56
  airbyte_columns = [ab_last_mod_col, ab_file_name_col]
57
57
  use_file_transfer = False
58
58
  preserve_directory_structure = True
59
- _file_transfer = FileTransfer()
60
59
 
61
60
  def __init__(self, **kwargs: Any):
62
61
  if self.FILE_TRANSFER_KW in kwargs:
@@ -94,6 +93,21 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
94
93
  self.config
95
94
  )
96
95
 
96
+ def _filter_schema_invalid_properties(
97
+ self, configured_catalog_json_schema: Dict[str, Any]
98
+ ) -> Dict[str, Any]:
99
+ if self.use_file_transfer:
100
+ return {
101
+ "type": "object",
102
+ "properties": {
103
+ "file_path": {"type": "string"},
104
+ "file_size": {"type": "string"},
105
+ self.ab_file_name_col: {"type": "string"},
106
+ },
107
+ }
108
+ else:
109
+ return super()._filter_schema_invalid_properties(configured_catalog_json_schema)
110
+
97
111
  def _duplicated_files_names(
98
112
  self, slices: List[dict[str, List[RemoteFile]]]
99
113
  ) -> List[dict[str, List[str]]]:
@@ -131,6 +145,14 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
131
145
  record[self.ab_file_name_col] = file.uri
132
146
  return record
133
147
 
148
+ def transform_record_for_file_transfer(
149
+ self, record: dict[str, Any], file: RemoteFile
150
+ ) -> dict[str, Any]:
151
+ # timstamp() returns a float representing the number of seconds since the unix epoch
152
+ record[self.modified] = int(file.last_modified.timestamp()) * 1000
153
+ record[self.source_file_url] = file.uri
154
+ return record
155
+
134
156
  def read_records_from_slice(self, stream_slice: StreamSlice) -> Iterable[AirbyteMessage]:
135
157
  """
136
158
  Yield all records from all remote files in `list_files_for_this_sync`.
@@ -151,13 +173,19 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
151
173
 
152
174
  try:
153
175
  if self.use_file_transfer:
154
- for file_record_data, file_reference in self._file_transfer.upload(
155
- file=file, stream_reader=self.stream_reader, logger=self.logger
176
+ self.logger.info(f"{self.name}: {file} file-based syncing")
177
+ # todo: complete here the code to not rely on local parser
178
+ file_transfer = FileTransfer()
179
+ for record in file_transfer.get_file(
180
+ self.config, file, self.stream_reader, self.logger
156
181
  ):
182
+ line_no += 1
183
+ if not self.record_passes_validation_policy(record):
184
+ n_skipped += 1
185
+ continue
186
+ record = self.transform_record_for_file_transfer(record, file)
157
187
  yield stream_data_to_airbyte_message(
158
- self.name,
159
- file_record_data.dict(exclude_none=True),
160
- file_reference=file_reference,
188
+ self.name, record, is_file_transfer_message=True
161
189
  )
162
190
  else:
163
191
  for record in parser.parse_records(
@@ -231,8 +259,6 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
231
259
 
232
260
  @cache
233
261
  def get_json_schema(self) -> JsonSchema:
234
- if self.use_file_transfer:
235
- return file_transfer_schema
236
262
  extra_fields = {
237
263
  self.ab_last_mod_col: {"type": "string"},
238
264
  self.ab_file_name_col: {"type": "string"},
@@ -256,7 +282,9 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
256
282
  return {"type": "object", "properties": {**extra_fields, **schema["properties"]}}
257
283
 
258
284
  def _get_raw_json_schema(self) -> JsonSchema:
259
- if self.config.input_schema:
285
+ if self.use_file_transfer:
286
+ return file_transfer_schema
287
+ elif self.config.input_schema:
260
288
  return self.config.get_input_schema() # type: ignore
261
289
  elif self.config.schemaless:
262
290
  return schemaless_schema
@@ -313,11 +341,6 @@ class DefaultFileBasedStream(AbstractFileBasedStream, IncrementalMixin):
313
341
  self.config.globs or [], self.config.legacy_prefix, self.logger
314
342
  )
315
343
 
316
- def as_airbyte_stream(self) -> AirbyteStream:
317
- file_stream = super().as_airbyte_stream()
318
- file_stream.is_file_based = self.use_file_transfer
319
- return file_stream
320
-
321
344
  def infer_schema(self, files: List[RemoteFile]) -> Mapping[str, Any]:
322
345
  loop = asyncio.get_event_loop()
323
346
  schema = loop.run_until_complete(self._infer_schema(files))
@@ -61,7 +61,9 @@ class PermissionsFileBasedStream(DefaultFileBasedStream):
61
61
  permissions_record = self.transform_record(
62
62
  permissions_record, file, file_datetime_string
63
63
  )
64
- yield stream_data_to_airbyte_message(self.name, permissions_record)
64
+ yield stream_data_to_airbyte_message(
65
+ self.name, permissions_record, is_file_transfer_message=False
66
+ )
65
67
  except Exception as e:
66
68
  self.logger.error(f"Failed to retrieve permissions for file {file.uri}: {str(e)}")
67
69
  yield AirbyteMessage(
@@ -29,7 +29,6 @@ class DefaultStream(AbstractStream):
29
29
  logger: Logger,
30
30
  cursor: Cursor,
31
31
  namespace: Optional[str] = None,
32
- supports_file_transfer: bool = False,
33
32
  ) -> None:
34
33
  self._stream_partition_generator = partition_generator
35
34
  self._name = name
@@ -40,7 +39,6 @@ class DefaultStream(AbstractStream):
40
39
  self._logger = logger
41
40
  self._cursor = cursor
42
41
  self._namespace = namespace
43
- self._supports_file_transfer = supports_file_transfer
44
42
 
45
43
  def generate_partitions(self) -> Iterable[Partition]:
46
44
  yield from self._stream_partition_generator.generate()
@@ -70,7 +68,6 @@ class DefaultStream(AbstractStream):
70
68
  json_schema=dict(self._json_schema),
71
69
  supported_sync_modes=[SyncMode.full_refresh],
72
70
  is_resumable=False,
73
- is_file_based=self._supports_file_transfer,
74
71
  )
75
72
 
76
73
  if self._namespace:
@@ -6,7 +6,6 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Any, ItemsView, Iterator, KeysView, List, Mapping, Optional, ValuesView
8
8
 
9
- from airbyte_cdk.models import AirbyteRecordMessageFileReference
10
9
  from airbyte_cdk.utils.slice_hasher import SliceHasher
11
10
 
12
11
  # A FieldPointer designates a path to a field inside a mapping. For example, retrieving ["k1", "k1.2"] in the object {"k1" :{"k1.2":
@@ -24,12 +23,12 @@ class Record(Mapping[str, Any]):
24
23
  data: Mapping[str, Any],
25
24
  stream_name: str,
26
25
  associated_slice: Optional[StreamSlice] = None,
27
- file_reference: Optional[AirbyteRecordMessageFileReference] = None,
26
+ is_file_transfer_message: bool = False,
28
27
  ):
29
28
  self._data = data
30
29
  self._associated_slice = associated_slice
31
30
  self.stream_name = stream_name
32
- self._file_reference = file_reference
31
+ self.is_file_transfer_message = is_file_transfer_message
33
32
 
34
33
  @property
35
34
  def data(self) -> Mapping[str, Any]:
@@ -39,14 +38,6 @@ class Record(Mapping[str, Any]):
39
38
  def associated_slice(self) -> Optional[StreamSlice]:
40
39
  return self._associated_slice
41
40
 
42
- @property
43
- def file_reference(self) -> AirbyteRecordMessageFileReference:
44
- return self._file_reference
45
-
46
- @file_reference.setter
47
- def file_reference(self, value: AirbyteRecordMessageFileReference) -> None:
48
- self._file_reference = value
49
-
50
41
  def __repr__(self) -> str:
51
42
  return repr(self._data)
52
43
 
@@ -9,10 +9,10 @@ from airbyte_cdk.models import (
9
9
  AirbyteLogMessage,
10
10
  AirbyteMessage,
11
11
  AirbyteRecordMessage,
12
- AirbyteRecordMessageFileReference,
13
12
  AirbyteTraceMessage,
14
13
  )
15
14
  from airbyte_cdk.models import Type as MessageType
15
+ from airbyte_cdk.models.file_transfer_record_message import AirbyteFileTransferRecordMessage
16
16
  from airbyte_cdk.sources.streams.core import StreamData
17
17
  from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer
18
18
 
@@ -22,7 +22,7 @@ def stream_data_to_airbyte_message(
22
22
  data_or_message: StreamData,
23
23
  transformer: TypeTransformer = TypeTransformer(TransformConfig.NoTransform),
24
24
  schema: Optional[Mapping[str, Any]] = None,
25
- file_reference: Optional[AirbyteRecordMessageFileReference] = None,
25
+ is_file_transfer_message: bool = False,
26
26
  ) -> AirbyteMessage:
27
27
  if schema is None:
28
28
  schema = {}
@@ -36,12 +36,12 @@ def stream_data_to_airbyte_message(
36
36
  # taken unless configured. See
37
37
  # docs/connector-development/cdk-python/schemas.md for details.
38
38
  transformer.transform(data, schema)
39
- message = AirbyteRecordMessage(
40
- stream=stream_name,
41
- data=data,
42
- emitted_at=now_millis,
43
- file_reference=file_reference,
44
- )
39
+ if is_file_transfer_message:
40
+ message = AirbyteFileTransferRecordMessage(
41
+ stream=stream_name, file=data, emitted_at=now_millis, data={}
42
+ )
43
+ else:
44
+ message = AirbyteRecordMessage(stream=stream_name, data=data, emitted_at=now_millis)
45
45
  return AirbyteMessage(type=MessageType.RECORD, record=message)
46
46
  case AirbyteTraceMessage():
47
47
  return AirbyteMessage(type=MessageType.TRACE, trace=data_or_message)
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ """Declarative tests framework.
3
+
4
+ This module provides fixtures and utilities for testing Airbyte sources and destinations
5
+ in a declarative way.
6
+ """
@@ -0,0 +1,7 @@
1
+ from airbyte_cdk.test.declarative.models.scenario import (
2
+ ConnectorTestScenario,
3
+ )
4
+
5
+ __all__ = [
6
+ "ConnectorTestScenario",
7
+ ]
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ """Run acceptance tests in PyTest.
3
+
4
+ These tests leverage the same `acceptance-test-config.yml` configuration files as the
5
+ acceptance tests in CAT, but they run in PyTest instead of CAT. This allows us to run
6
+ the acceptance tests in the same local environment as we are developing in, speeding
7
+ up iteration cycles.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import Any, Literal, cast
14
+
15
+ import yaml
16
+ from pydantic import BaseModel
17
+
18
+
19
+ class ConnectorTestScenario(BaseModel):
20
+ """Acceptance test scenario, as a Pydantic model.
21
+
22
+ This class represents an acceptance test scenario, which is a single test case
23
+ that can be run against a connector. It is used to deserialize and validate the
24
+ acceptance test configuration file.
25
+ """
26
+
27
+ class AcceptanceTestExpectRecords(BaseModel):
28
+ path: Path
29
+ exact_order: bool = False
30
+
31
+ class AcceptanceTestFileTypes(BaseModel):
32
+ skip_test: bool
33
+ bypass_reason: str
34
+
35
+ config_path: Path | None = None
36
+ config_dict: dict[str, Any] | None = None
37
+
38
+ id: str | None = None
39
+
40
+ configured_catalog_path: Path | None = None
41
+ timeout_seconds: int | None = None
42
+ expect_records: AcceptanceTestExpectRecords | None = None
43
+ file_types: AcceptanceTestFileTypes | None = None
44
+ status: Literal["succeed", "failed"] | None = None
45
+
46
+ def get_config_dict(self) -> dict[str, Any]:
47
+ """Return the config dictionary.
48
+
49
+ If a config dictionary has already been loaded, return it. Otherwise, load
50
+ the config file and return the dictionary.
51
+ """
52
+ if self.config_dict:
53
+ return self.config_dict
54
+
55
+ if self.config_path:
56
+ return cast(dict[str, Any], yaml.safe_load(self.config_path.read_text()))
57
+
58
+ raise ValueError("No config dictionary or path provided.")
59
+
60
+ @property
61
+ def expect_exception(self) -> bool:
62
+ return self.status and self.status == "failed" or False
63
+
64
+ @property
65
+ def instance_name(self) -> str:
66
+ return self.config_path.stem if self.config_path else "Unnamed Scenario"
67
+
68
+ def __str__(self) -> str:
69
+ if self.id:
70
+ return f"'{self.id}' Test Scenario"
71
+ if self.config_path:
72
+ return f"'{self.config_path.name}' Test Scenario"
73
+
74
+ return f"'{hash(self)}' Test Scenario"
File without changes
@@ -0,0 +1,159 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """Job runner for Airbyte Standard Tests."""
3
+
4
+ import logging
5
+ import tempfile
6
+ import uuid
7
+ from dataclasses import asdict
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Literal
10
+
11
+ import orjson
12
+ from typing_extensions import Protocol, runtime_checkable
13
+
14
+ from airbyte_cdk.models import (
15
+ ConfiguredAirbyteCatalog,
16
+ Status,
17
+ )
18
+ from airbyte_cdk.test import entrypoint_wrapper
19
+ from airbyte_cdk.test.declarative.models import (
20
+ ConnectorTestScenario,
21
+ )
22
+
23
+
24
+ def _errors_to_str(
25
+ entrypoint_output: entrypoint_wrapper.EntrypointOutput,
26
+ ) -> str:
27
+ """Convert errors from entrypoint output to a string."""
28
+ if not entrypoint_output.errors:
29
+ # If there are no errors, return an empty string.
30
+ return ""
31
+
32
+ return "\n" + "\n".join(
33
+ [
34
+ str(error.trace.error).replace(
35
+ "\\n",
36
+ "\n",
37
+ )
38
+ for error in entrypoint_output.errors
39
+ if error.trace
40
+ ],
41
+ )
42
+
43
+
44
+ @runtime_checkable
45
+ class IConnector(Protocol):
46
+ """A connector that can be run in a test scenario.
47
+
48
+ Note: We currently use 'spec' to determine if we have a connector object.
49
+ In the future, it would be preferred to leverage a 'launch' method instead,
50
+ directly on the connector (which doesn't yet exist).
51
+ """
52
+
53
+ def spec(self, logger: logging.Logger) -> Any:
54
+ """Connectors should have a `spec` method."""
55
+
56
+
57
+ def run_test_job(
58
+ connector: IConnector | type[IConnector] | Callable[[], IConnector],
59
+ verb: Literal["read", "check", "discover"],
60
+ test_scenario: ConnectorTestScenario,
61
+ *,
62
+ catalog: ConfiguredAirbyteCatalog | dict[str, Any] | None = None,
63
+ ) -> entrypoint_wrapper.EntrypointOutput:
64
+ """Run a test scenario from provided CLI args and return the result."""
65
+ if not connector:
66
+ raise ValueError("Connector is required")
67
+
68
+ if catalog and isinstance(catalog, ConfiguredAirbyteCatalog):
69
+ # Convert the catalog to a dict if it's already a ConfiguredAirbyteCatalog.
70
+ catalog = asdict(catalog)
71
+
72
+ connector_obj: IConnector
73
+ if isinstance(connector, type) or callable(connector):
74
+ # If the connector is a class or a factory lambda, instantiate it.
75
+ connector_obj = connector()
76
+ elif isinstance(connector, IConnector):
77
+ connector_obj = connector
78
+ else:
79
+ raise ValueError(
80
+ f"Invalid connector input: {type(connector)}",
81
+ )
82
+
83
+ args: list[str] = [verb]
84
+ if test_scenario.config_path:
85
+ args += ["--config", str(test_scenario.config_path)]
86
+ elif test_scenario.config_dict:
87
+ config_path = (
88
+ Path(tempfile.gettempdir()) / "airbyte-test" / f"temp_config_{uuid.uuid4().hex}.json"
89
+ )
90
+ config_path.parent.mkdir(parents=True, exist_ok=True)
91
+ config_path.write_text(orjson.dumps(test_scenario.config_dict).decode())
92
+ args += ["--config", str(config_path)]
93
+
94
+ catalog_path: Path | None = None
95
+ if verb not in ["discover", "check"]:
96
+ # We need a catalog for read.
97
+ if catalog:
98
+ # Write the catalog to a temp json file and pass the path to the file as an argument.
99
+ catalog_path = (
100
+ Path(tempfile.gettempdir())
101
+ / "airbyte-test"
102
+ / f"temp_catalog_{uuid.uuid4().hex}.json"
103
+ )
104
+ catalog_path.parent.mkdir(parents=True, exist_ok=True)
105
+ catalog_path.write_text(orjson.dumps(catalog).decode())
106
+ elif test_scenario.configured_catalog_path:
107
+ catalog_path = Path(test_scenario.configured_catalog_path)
108
+
109
+ if catalog_path:
110
+ args += ["--catalog", str(catalog_path)]
111
+
112
+ # This is a bit of a hack because the source needs the catalog early.
113
+ # Because it *also* can fail, we have to redundantly wrap it in a try/except block.
114
+
115
+ result: entrypoint_wrapper.EntrypointOutput = entrypoint_wrapper._run_command( # noqa: SLF001 # Non-public API
116
+ source=connector_obj, # type: ignore [arg-type]
117
+ args=args,
118
+ expecting_exception=test_scenario.expect_exception,
119
+ )
120
+ if result.errors and not test_scenario.expect_exception:
121
+ raise AssertionError(
122
+ f"Expected no errors but got {len(result.errors)}: \n" + _errors_to_str(result)
123
+ )
124
+
125
+ if verb == "check":
126
+ # Check is expected to fail gracefully without an exception.
127
+ # Instead, we assert that we have a CONNECTION_STATUS message with
128
+ # a failure status.
129
+ assert len(result.connection_status_messages) == 1, (
130
+ "Expected exactly one CONNECTION_STATUS message. Got "
131
+ f"{len(result.connection_status_messages)}:\n"
132
+ + "\n".join([str(msg) for msg in result.connection_status_messages])
133
+ + _errors_to_str(result)
134
+ )
135
+ if test_scenario.expect_exception:
136
+ conn_status = result.connection_status_messages[0].connectionStatus
137
+ assert conn_status, (
138
+ "Expected CONNECTION_STATUS message to be present. Got: \n"
139
+ + "\n".join([str(msg) for msg in result.connection_status_messages])
140
+ )
141
+ assert conn_status.status == Status.FAILED, (
142
+ "Expected CONNECTION_STATUS message to be FAILED. Got: \n"
143
+ + "\n".join([str(msg) for msg in result.connection_status_messages])
144
+ )
145
+
146
+ return result
147
+
148
+ # For all other verbs, we assert check that an exception is raised (or not).
149
+ if test_scenario.expect_exception:
150
+ if not result.errors:
151
+ raise AssertionError("Expected exception but got none.")
152
+
153
+ return result
154
+
155
+ assert not result.errors, (
156
+ f"Expected no errors but got {len(result.errors)}: \n" + _errors_to_str(result)
157
+ )
158
+
159
+ return result
@@ -82,6 +82,10 @@ class EntrypointOutput:
82
82
  def state_messages(self) -> List[AirbyteMessage]:
83
83
  return self._get_message_by_types([Type.STATE])
84
84
 
85
+ @property
86
+ def connection_status_messages(self) -> List[AirbyteMessage]:
87
+ return self._get_message_by_types([Type.CONNECTION_STATUS])
88
+
85
89
  @property
86
90
  def most_recent_state(self) -> Any:
87
91
  state_messages = self._get_message_by_types([Type.STATE])
@@ -198,14 +198,6 @@ def find_template(resource: str, execution_folder: str) -> Dict[str, Any]:
198
198
  return json.load(template_file) # type: ignore # we assume the dev correctly set up the resource file
199
199
 
200
200
 
201
- def find_binary_response(resource: str, execution_folder: str) -> bytes:
202
- response_filepath = str(
203
- get_unit_test_folder(execution_folder) / "resource" / "http" / "response" / f"{resource}"
204
- )
205
- with open(response_filepath, "rb") as response_file:
206
- return response_file.read() # type: ignore # we assume the dev correctly set up the resource file
207
-
208
-
209
201
  def create_record_builder(
210
202
  response_template: Dict[str, Any],
211
203
  records_path: Union[FieldPath, NestedPath],
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ '''FAST Airbyte Standard Tests
3
+
4
+ This module provides a set of base classes for declarative connector test suites.
5
+ The goal of this module is to provide a robust and extensible framework for testing Airbyte
6
+ connectors.
7
+
8
+ Example usage:
9
+
10
+ ```python
11
+ # `test_airbyte_standards.py`
12
+ from airbyte_cdk.test import standard_tests
13
+
14
+ pytest_plugins = [
15
+ "airbyte_cdk.test.standard_tests.pytest_hooks",
16
+ ]
17
+
18
+
19
+ class TestSuiteSourcePokeAPI(standard_tests.DeclarativeSourceTestSuite):
20
+ """Test suite for the source."""
21
+ ```
22
+
23
+ Available test suites base classes:
24
+ - `DeclarativeSourceTestSuite`: A test suite for declarative sources.
25
+ - `SourceTestSuiteBase`: A test suite for sources.
26
+ - `DestinationTestSuiteBase`: A test suite for destinations.
27
+
28
+ '''
29
+
30
+ from airbyte_cdk.test.standard_tests.connector_base import (
31
+ ConnectorTestScenario,
32
+ ConnectorTestSuiteBase,
33
+ )
34
+ from airbyte_cdk.test.standard_tests.declarative_sources import (
35
+ DeclarativeSourceTestSuite,
36
+ )
37
+ from airbyte_cdk.test.standard_tests.destination_base import DestinationTestSuiteBase
38
+ from airbyte_cdk.test.standard_tests.source_base import SourceTestSuiteBase
39
+
40
+ __all__ = [
41
+ "ConnectorTestScenario",
42
+ "ConnectorTestSuiteBase",
43
+ "DeclarativeSourceTestSuite",
44
+ "DestinationTestSuiteBase",
45
+ "SourceTestSuiteBase",
46
+ ]