From 25de86c271f5f825ed314be1462c4f889b923504 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 21:26:03 +0000 Subject: [PATCH 1/4] fix(file-based): support selecting Excel worksheets --- CHANGELOG.md | 4 + .../sources/file_based/config/excel_format.py | 5 + .../file_based/file_types/excel_parser.py | 79 +++++++++++----- .../file_types/test_excel_parser.py | 94 ++++++++++++++++++- .../file_based/scenarios/csv_scenarios.py | 8 +- 5 files changed, 163 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e73e411..82b5a99e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Newer updates can be found here: [GitHub Release Notes](https://github.com/airby # Changelog +## Unreleased + +File-based Excel streams can now select a specific worksheet or read every worksheet in a workbook. + ## 6.5.2 bugfix: Ensure that streams with partition router are not executed concurrently diff --git a/airbyte_cdk/sources/file_based/config/excel_format.py b/airbyte_cdk/sources/file_based/config/excel_format.py index 632a0bc38..0f5476294 100644 --- a/airbyte_cdk/sources/file_based/config/excel_format.py +++ b/airbyte_cdk/sources/file_based/config/excel_format.py @@ -16,3 +16,8 @@ class Config(OneOfOptionConfig): "excel", const=True, ) + sheet_name: str = Field( + default="0", + title="Sheet Name", + description='The Excel worksheet to read. Use a sheet name, a zero-indexed sheet position like "0", or "*" to read all sheets.', + ) diff --git a/airbyte_cdk/sources/file_based/file_types/excel_parser.py b/airbyte_cdk/sources/file_based/file_types/excel_parser.py index 93896f14f..1a01577b7 100644 --- a/airbyte_cdk/sources/file_based/file_types/excel_parser.py +++ b/airbyte_cdk/sources/file_based/file_types/excel_parser.py @@ -33,6 +33,7 @@ class ExcelParser(FileTypeParser): ENCODING = None + ALL_SHEETS = "*" def check_config(self, config: FileBasedStreamConfig) -> Tuple[bool, Optional[str]]: """ @@ -62,18 +63,21 @@ async def infer_schema( # Validate the format of the config self.validate_format(config.format, logger) + excel_format = config.format + if not isinstance(excel_format, ExcelFormat): + raise ConfigValidationError(FileBasedSourceError.CONFIG_VALIDATION_ERROR) fields: Dict[str, str] = {} with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: - df = self.open_and_parse_file(fp, logger, file) - for column, df_type in df.dtypes.items(): - # Choose the broadest data type if the column's data type differs in dataframes - prev_frame_column_type = fields.get(column) # type: ignore [call-overload] - fields[column] = self.dtype_to_json_type( # type: ignore [index] - prev_frame_column_type, - df_type, - ) + for df in self._parse_excel_file(fp, excel_format, logger, file).values(): + for column, df_type in df.dtypes.items(): + # Choose the broadest data type if the column's data type differs in dataframes + prev_frame_column_type = fields.get(column) # type: ignore [call-overload] + fields[column] = self.dtype_to_json_type( # type: ignore [index] + prev_frame_column_type, + df_type, + ) schema = { field: ( @@ -109,18 +113,21 @@ def parse_records( # Validate the format of the config self.validate_format(config.format, logger) + excel_format = config.format + if not isinstance(excel_format, ExcelFormat): + raise ConfigValidationError(FileBasedSourceError.CONFIG_VALIDATION_ERROR) try: # Open and parse the file using the stream reader with stream_reader.open_file(file, self.file_read_mode, self.ENCODING, logger) as fp: - df = self.open_and_parse_file(fp, logger, file) - # Yield records as dictionaries - # DataFrame.to_dict() method returns datetime values in pandas.Timestamp values, which are not serializable by orjson - # DataFrame.to_json() returns string with datetime values serialized to iso8601 with microseconds to align with pydantic behavior - # see PR description: https://github.com/airbytehq/airbyte/pull/44444/ - yield from orjson.loads( - df.to_json(orient="records", date_format="iso", date_unit="us") - ) + for df in self._parse_excel_file(fp, excel_format, logger, file).values(): + # Yield records as dictionaries + # DataFrame.to_dict() method returns datetime values in pandas.Timestamp values, which are not serializable by orjson + # DataFrame.to_json() returns string with datetime values serialized to iso8601 with microseconds to align with pydantic behavior + # see PR description: https://github.com/airbytehq/airbyte/pull/44444/ + yield from orjson.loads( + df.to_json(orient="records", date_format="iso", date_unit="us") + ) except Exception as exc: # Raise a RecordParseError if any exception occurs during parsing @@ -187,7 +194,8 @@ def _open_and_parse_file_with_calamine( fp: Union[IOBase, str, Path], logger: logging.Logger, file: RemoteFile, - ) -> pd.DataFrame: + sheet_name: int | str | None = 0, + ) -> pd.DataFrame | Dict[int | str, pd.DataFrame]: """Opens and parses Excel file using Calamine engine. Args: @@ -202,7 +210,7 @@ def _open_and_parse_file_with_calamine( ExcelCalamineParsingError: If Calamine fails to parse the file. """ try: - return pd.ExcelFile(fp, engine="calamine").parse() # type: ignore [arg-type, call-overload, no-any-return] + return pd.ExcelFile(fp, engine="calamine").parse(sheet_name=sheet_name) # type: ignore [arg-type, call-overload, no-any-return] except BaseException as exc: # Calamine engine raises PanicException(child of BaseException) if Calamine fails to parse the file. # Checking if ValueError in exception arg to know if it was actually an error during parsing due to invalid values in cells. @@ -222,7 +230,8 @@ def _open_and_parse_file_with_openpyxl( fp: Union[IOBase, str, Path], logger: logging.Logger, file: RemoteFile, - ) -> pd.DataFrame: + sheet_name: int | str | None = 0, + ) -> pd.DataFrame | Dict[int | str, pd.DataFrame]: """Opens and parses Excel file using Openpyxl engine. Args: @@ -245,19 +254,20 @@ def _open_and_parse_file_with_openpyxl( with warnings.catch_warnings(record=True) as warning_records: warnings.simplefilter("always") - df = pd.ExcelFile(fp, engine="openpyxl").parse() # type: ignore [arg-type, call-overload] + dfs = pd.ExcelFile(fp, engine="openpyxl").parse(sheet_name=sheet_name) # type: ignore [arg-type, call-overload] for warning in warning_records: logger.warning(f"Openpyxl warning for {file.file_uri_for_logging}: {warning.message}") - return df # type: ignore [no-any-return] + return dfs # type: ignore [no-any-return] def open_and_parse_file( self, fp: Union[IOBase, str, Path], logger: logging.Logger, file: RemoteFile, - ) -> pd.DataFrame: + sheet_name: int | str | None = 0, + ) -> pd.DataFrame | Dict[int | str, pd.DataFrame]: """Opens and parses the Excel file with Calamine-first and Openpyxl fallback. Args: @@ -269,6 +279,27 @@ def open_and_parse_file( pd.DataFrame: Parsed data from the Excel file. """ try: - return self._open_and_parse_file_with_calamine(fp, logger, file) + return self._open_and_parse_file_with_calamine(fp, logger, file, sheet_name) except ExcelCalamineParsingError: - return self._open_and_parse_file_with_openpyxl(fp, logger, file) + return self._open_and_parse_file_with_openpyxl(fp, logger, file, sheet_name) + + def _parse_excel_file( + self, + fp: Union[IOBase, str, Path], + excel_format: ExcelFormat, + logger: logging.Logger, + file: RemoteFile, + ) -> Dict[int | str, pd.DataFrame]: + sheet_name = self._get_sheet_name(excel_format) + parsed_file = self.open_and_parse_file(fp, logger, file, sheet_name) + if isinstance(parsed_file, pd.DataFrame): + return {excel_format.sheet_name: parsed_file} + return parsed_file + + def _get_sheet_name(self, excel_format: ExcelFormat) -> int | str | None: + sheet_name = excel_format.sheet_name + if sheet_name == self.ALL_SHEETS: + return None + if sheet_name.isdecimal(): + return int(sheet_name) + return sheet_name diff --git a/unit_tests/sources/file_based/file_types/test_excel_parser.py b/unit_tests/sources/file_based/file_types/test_excel_parser.py index 18850e9b0..ac531ace1 100644 --- a/unit_tests/sources/file_based/file_types/test_excel_parser.py +++ b/unit_tests/sources/file_based/file_types/test_excel_parser.py @@ -3,6 +3,7 @@ # +import asyncio import datetime import warnings from io import BytesIO @@ -139,6 +140,94 @@ def test_file_read_error(mock_stream_reader, mock_logger, file_config, remote_fi ) +@pytest.mark.parametrize( + "sheet_name,expected_records", + [ + pytest.param( + "0", + [ + {"sheet": "first", "value": 1}, + ], + id="default_first_sheet", + ), + pytest.param( + "Second", + [ + {"sheet": "second", "value": 2}, + ], + id="sheet_by_name", + ), + pytest.param( + "*", + [ + {"sheet": "first", "value": 1}, + {"sheet": "second", "value": 2}, + ], + id="all_sheets", + ), + ], +) +def test_parse_records_selects_configured_sheet(sheet_name, expected_records, remote_file): + parser = ExcelParser() + excel_bytes = BytesIO() + with pd.ExcelWriter(excel_bytes, engine="xlsxwriter") as writer: + pd.DataFrame({"sheet": ["first"], "value": [1]}).to_excel( + writer, index=False, sheet_name="First" + ) + pd.DataFrame({"sheet": ["second"], "value": [2]}).to_excel( + writer, index=False, sheet_name="Second" + ) + excel_bytes.seek(0) + + stream_reader = MagicMock(spec=AbstractFileBasedStreamReader) + stream_reader.open_file.return_value = BytesIO(excel_bytes.read()) + + records = list( + parser.parse_records( + FileBasedStreamConfig(name="test_stream", format=ExcelFormat(sheet_name=sheet_name)), + remote_file, + stream_reader, + MagicMock(), + ) + ) + + assert records == expected_records + + +def test_infer_schema_combines_configured_excel_sheets(remote_file): + parser = ExcelParser() + excel_bytes = BytesIO() + with pd.ExcelWriter(excel_bytes, engine="xlsxwriter") as writer: + pd.DataFrame({"first_sheet_column": ["first"]}).to_excel( + writer, index=False, sheet_name="First" + ) + pd.DataFrame({"second_sheet_column": [2]}).to_excel( + writer, index=False, sheet_name="Second" + ) + excel_bytes.seek(0) + + stream_reader = MagicMock(spec=AbstractFileBasedStreamReader) + stream_reader.open_file.return_value = BytesIO(excel_bytes.read()) + + event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(event_loop) + schema = event_loop.run_until_complete( + parser.infer_schema( + FileBasedStreamConfig(name="test_stream", format=ExcelFormat(sheet_name="*")), + remote_file, + stream_reader, + MagicMock(), + ) + ) + event_loop.close() + asyncio.set_event_loop(asyncio.new_event_loop()) + + assert schema == { + "first_sheet_column": {"type": "string"}, + "second_sheet_column": {"type": "number"}, + } + + class FakePanic(BaseException): """Simulates the PyO3 PanicException which does not inherit from Exception.""" @@ -152,7 +241,7 @@ def test_open_and_parse_file_falls_back_to_openpyxl(mock_logger): calamine_excel_file = MagicMock() - def calamine_parse_side_effect(): + def calamine_parse_side_effect(**kwargs): raise FakePanic( "failed to construct date: PyErr { type: , value: ValueError('year 20225 is out of range'), traceback: None }" ) @@ -161,7 +250,7 @@ def calamine_parse_side_effect(): openpyxl_excel_file = MagicMock() - def openpyxl_parse_side_effect(): + def openpyxl_parse_side_effect(**kwargs): warnings.warn("Cell A146 has invalid date", UserWarning) return fallback_df @@ -238,3 +327,4 @@ def seek(self, *args, **kwargs): assert "Could not rewind stream" in msg assert remote_file.file_uri_for_logging in msg mock_excel.assert_called_once_with(fp, engine="openpyxl") + openpyxl_excel_file.parse.assert_called_once_with(sheet_name=0) diff --git a/unit_tests/sources/file_based/scenarios/csv_scenarios.py b/unit_tests/sources/file_based/scenarios/csv_scenarios.py index f16d83e20..54b2f7af0 100644 --- a/unit_tests/sources/file_based/scenarios/csv_scenarios.py +++ b/unit_tests/sources/file_based/scenarios/csv_scenarios.py @@ -472,7 +472,13 @@ "default": "excel", "const": "excel", "type": "string", - } + }, + "sheet_name": { + "title": "Sheet Name", + "description": 'The Excel worksheet to read. Use a sheet name, a zero-indexed sheet position like "0", or "*" to read all sheets.', + "default": "0", + "type": "string", + }, }, "required": ["filetype"], }, From b220904bec5dd81cecffbea1e6e9cc13c411386d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 22:06:27 +0000 Subject: [PATCH 2/4] test(standard-tests): support legacy file-based source constructors Co-Authored-By: bot_apk --- .../test/standard_tests/connector_base.py | 33 ++++++++++++++--- unit_tests/test/test_standard_tests.py | 36 +++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/airbyte_cdk/test/standard_tests/connector_base.py b/airbyte_cdk/test/standard_tests/connector_base.py index dac656cf4..3bfa5ea3e 100644 --- a/airbyte_cdk/test/standard_tests/connector_base.py +++ b/airbyte_cdk/test/standard_tests/connector_base.py @@ -5,6 +5,8 @@ import importlib import os +from collections.abc import Callable +from inspect import Parameter, signature from pathlib import Path from typing import TYPE_CHECKING, cast @@ -18,8 +20,6 @@ from airbyte_cdk.test.standard_tests.docker_base import DockerConnectorTestSuite if TYPE_CHECKING: - from collections.abc import Callable - from airbyte_cdk.test import entrypoint_wrapper @@ -89,9 +89,10 @@ def create_connector( """Instantiate the connector class.""" connector = cls.connector # type: ignore if connector: - if callable(connector) or isinstance(connector, type): - # If the connector is a class or factory function, instantiate it: - return cast(IConnector, connector()) # type: ignore [redundant-cast] + if isinstance(connector, type): + return cls._instantiate_connector_class(connector) + if callable(connector): + return connector() # Otherwise, we can't instantiate the connector. Fail with a clear error message. raise NotImplementedError( @@ -100,6 +101,18 @@ def create_connector( "override `cls.create_connector()` to define a custom initialization process." ) + @staticmethod + def _instantiate_connector_class(connector_class: type[IConnector]) -> IConnector: + """Instantiate connector classes supported by standard tests.""" + if _requires_legacy_file_based_constructor_args(connector_class): + legacy_file_based_connector = cast( + Callable[[None, None, None], IConnector], + connector_class, + ) + return legacy_file_based_connector(None, None, None) + + return connector_class() + # Test Definitions def test_check( @@ -117,3 +130,13 @@ def test_check( f"Expected exactly one CONNECTION_STATUS message. " "Got: {result.connection_status_messages!s}" ) + + +def _requires_legacy_file_based_constructor_args(connector_class: type[IConnector]) -> bool: + required_positional_parameter_names = [ + parameter.name + for parameter in signature(connector_class).parameters.values() + if parameter.default is Parameter.empty + and parameter.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + ] + return required_positional_parameter_names == ["catalog", "config", "state"] diff --git a/unit_tests/test/test_standard_tests.py b/unit_tests/test/test_standard_tests.py index d5dd28277..af7473fe9 100644 --- a/unit_tests/test/test_standard_tests.py +++ b/unit_tests/test/test_standard_tests.py @@ -5,11 +5,35 @@ import pytest +from airbyte_cdk.models import AirbyteCatalog, AirbyteMessage, ConfiguredAirbyteCatalog from airbyte_cdk.sources.declarative.concurrent_declarative_source import ( ConcurrentDeclarativeSource, ) from airbyte_cdk.sources.source import Source +from airbyte_cdk.test.models import ConnectorTestScenario from airbyte_cdk.test.standard_tests._job_runner import IConnector +from airbyte_cdk.test.standard_tests.connector_base import ConnectorTestSuiteBase + + +class LegacyFileBasedConnector(Source): + def __init__( + self, + catalog: ConfiguredAirbyteCatalog | None, + config: dict[str, Any] | None, + state: list[Any] | None, + ) -> None: + self.catalog = catalog + self.config = config + self.state = state + + def check(self, **kwargs: Any) -> None: + pass + + def discover(self, **kwargs: Any) -> AirbyteCatalog: + return AirbyteCatalog(streams=[]) + + def read(self, **kwargs: Any) -> list[AirbyteMessage]: + return [] @pytest.mark.parametrize( @@ -31,3 +55,15 @@ def test_is_iconnector_check(input: Any, expected: bool) -> None: return assert isinstance(input, IConnector) == expected + + +def test_create_connector_instantiates_legacy_file_based_sources_with_empty_runtime_args() -> None: + class TestSuite(ConnectorTestSuiteBase): + connector = LegacyFileBasedConnector + + connector = TestSuite.create_connector(ConnectorTestScenario()) + + assert isinstance(connector, LegacyFileBasedConnector) + assert connector.catalog is None + assert connector.config is None + assert connector.state is None From e345ee3b85dd3c87095b9b03181efbaecaa13125 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 22:20:11 +0000 Subject: [PATCH 3/4] test(standard-tests): pass runtime args to legacy file sources Co-Authored-By: bot_apk --- .../test/standard_tests/connector_base.py | 28 ++++++++++++++--- .../test/standard_tests/source_base.py | 4 +-- unit_tests/test/test_standard_tests.py | 31 ++++++++++++++----- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/airbyte_cdk/test/standard_tests/connector_base.py b/airbyte_cdk/test/standard_tests/connector_base.py index 3bfa5ea3e..6a65eb89d 100644 --- a/airbyte_cdk/test/standard_tests/connector_base.py +++ b/airbyte_cdk/test/standard_tests/connector_base.py @@ -8,10 +8,11 @@ from collections.abc import Callable from inspect import Parameter, signature from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from boltons.typeutils import classproperty +from airbyte_cdk.models import ConfiguredAirbyteCatalog from airbyte_cdk.test import entrypoint_wrapper from airbyte_cdk.test.models import ( ConnectorTestScenario, @@ -85,12 +86,23 @@ def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None: def create_connector( cls, scenario: ConnectorTestScenario | None, + catalog: ConfiguredAirbyteCatalog | None = None, ) -> IConnector: """Instantiate the connector class.""" connector = cls.connector # type: ignore if connector: if isinstance(connector, type): - return cls._instantiate_connector_class(connector) + config = None + if scenario and _requires_legacy_file_based_constructor_args(connector): + config = ( + scenario.config_dict + if scenario.config_dict is not None + else scenario.get_config_dict( + empty_if_missing=True, + connector_root=cls.get_connector_root_dir(), + ) + ) + return cls._instantiate_connector_class(connector, catalog, config) if callable(connector): return connector() @@ -102,14 +114,20 @@ def create_connector( ) @staticmethod - def _instantiate_connector_class(connector_class: type[IConnector]) -> IConnector: + def _instantiate_connector_class( + connector_class: type[IConnector], + catalog: ConfiguredAirbyteCatalog | None = None, + config: dict[str, Any] | None = None, + ) -> IConnector: """Instantiate connector classes supported by standard tests.""" if _requires_legacy_file_based_constructor_args(connector_class): legacy_file_based_connector = cast( - Callable[[None, None, None], IConnector], + Callable[ + [ConfiguredAirbyteCatalog | None, dict[str, Any] | None, None], IConnector + ], connector_class, ) - return legacy_file_based_connector(None, None, None) + return legacy_file_based_connector(catalog, config, None) return connector_class() diff --git a/airbyte_cdk/test/standard_tests/source_base.py b/airbyte_cdk/test/standard_tests/source_base.py index faecb03c7..3c0f905c3 100644 --- a/airbyte_cdk/test/standard_tests/source_base.py +++ b/airbyte_cdk/test/standard_tests/source_base.py @@ -138,7 +138,7 @@ def test_basic_read( ] ) result = run_test_job( - self.create_connector(scenario), + self.create_connector(scenario, configured_catalog), "read", test_scenario=scenario, connector_root=self.get_connector_root_dir(), @@ -173,7 +173,7 @@ def test_fail_read_with_bad_catalog( ], ) result: entrypoint_wrapper.EntrypointOutput = run_test_job( - self.create_connector(scenario), + self.create_connector(scenario, invalid_configured_catalog), "read", connector_root=self.get_connector_root_dir(), test_scenario=scenario.with_expecting_failure(), # Expect failure due to bad catalog diff --git a/unit_tests/test/test_standard_tests.py b/unit_tests/test/test_standard_tests.py index af7473fe9..e567f6445 100644 --- a/unit_tests/test/test_standard_tests.py +++ b/unit_tests/test/test_standard_tests.py @@ -1,11 +1,18 @@ # Copyright (c) 2024 Airbyte, Inc., all rights reserved. """Unit tests for FAST Airbyte Standard Tests.""" +import logging +from collections.abc import Iterable, Mapping from typing import Any import pytest -from airbyte_cdk.models import AirbyteCatalog, AirbyteMessage, ConfiguredAirbyteCatalog +from airbyte_cdk.models import ( + AirbyteCatalog, + AirbyteMessage, + AirbyteStateMessage, + ConfiguredAirbyteCatalog, +) from airbyte_cdk.sources.declarative.concurrent_declarative_source import ( ConcurrentDeclarativeSource, ) @@ -26,13 +33,19 @@ def __init__( self.config = config self.state = state - def check(self, **kwargs: Any) -> None: + def check(self, logger: logging.Logger, config: Mapping[str, Any]) -> None: pass - def discover(self, **kwargs: Any) -> AirbyteCatalog: + def discover(self, logger: logging.Logger, config: Mapping[str, Any]) -> AirbyteCatalog: return AirbyteCatalog(streams=[]) - def read(self, **kwargs: Any) -> list[AirbyteMessage]: + def read( + self, + logger: logging.Logger, + config: Mapping[str, Any], + catalog: Any, + state: list[AirbyteStateMessage] | None = None, + ) -> Iterable[AirbyteMessage]: return [] @@ -61,9 +74,13 @@ def test_create_connector_instantiates_legacy_file_based_sources_with_empty_runt class TestSuite(ConnectorTestSuiteBase): connector = LegacyFileBasedConnector - connector = TestSuite.create_connector(ConnectorTestScenario()) + catalog = ConfiguredAirbyteCatalog(streams=[]) + connector = TestSuite.create_connector( + ConnectorTestScenario(config_dict={"folder_url": "https://example.com"}), + catalog, + ) assert isinstance(connector, LegacyFileBasedConnector) - assert connector.catalog is None - assert connector.config is None + assert connector.catalog == catalog + assert connector.config == {"folder_url": "https://example.com"} assert connector.state is None From 288277420480ba2521aaec174d640a1e173b1616 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 22:23:27 +0000 Subject: [PATCH 4/4] test(standard-tests): keep declarative source factory compatible Co-Authored-By: bot_apk --- airbyte_cdk/test/standard_tests/declarative_sources.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airbyte_cdk/test/standard_tests/declarative_sources.py b/airbyte_cdk/test/standard_tests/declarative_sources.py index 18a2a5910..73e9eab80 100644 --- a/airbyte_cdk/test/standard_tests/declarative_sources.py +++ b/airbyte_cdk/test/standard_tests/declarative_sources.py @@ -6,6 +6,7 @@ import yaml from boltons.typeutils import classproperty +from airbyte_cdk.models import ConfiguredAirbyteCatalog from airbyte_cdk.sources.declarative.concurrent_declarative_source import ( ConcurrentDeclarativeSource, ) @@ -65,6 +66,7 @@ def components_py_path(cls) -> Path | None: def create_connector( cls, scenario: ConnectorTestScenario | None, + catalog: ConfiguredAirbyteCatalog | None = None, ) -> IConnector: """Create a connector scenario for the test suite.