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
@@ -0,0 +1,148 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ """Base class for connector test suites."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import abc
7
+ import inspect
8
+ import sys
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import cast
12
+
13
+ import yaml
14
+ from boltons.typeutils import classproperty
15
+
16
+ from airbyte_cdk.models import (
17
+ AirbyteMessage,
18
+ Type,
19
+ )
20
+ from airbyte_cdk.test import entrypoint_wrapper
21
+ from airbyte_cdk.test.declarative.models import (
22
+ ConnectorTestScenario,
23
+ )
24
+ from airbyte_cdk.test.declarative.utils.job_runner import IConnector, run_test_job
25
+
26
+ ACCEPTANCE_TEST_CONFIG = "acceptance-test-config.yml"
27
+ MANIFEST_YAML = "manifest.yaml"
28
+
29
+
30
+ class ConnectorTestSuiteBase(abc.ABC):
31
+ """Base class for connector test suites."""
32
+
33
+ connector: type[IConnector] | Callable[[], IConnector] | None = None
34
+ """The connector class or a factory function that returns an scenario of IConnector."""
35
+
36
+ @classmethod
37
+ def get_test_class_dir(cls) -> Path:
38
+ """Get the file path that contains the class."""
39
+ module = sys.modules[cls.__module__]
40
+ # Get the directory containing the test file
41
+ return Path(inspect.getfile(module)).parent
42
+
43
+ @classmethod
44
+ def create_connector(
45
+ cls,
46
+ scenario: ConnectorTestScenario,
47
+ ) -> IConnector:
48
+ """Instantiate the connector class."""
49
+ connector = cls.connector # type: ignore
50
+ if connector:
51
+ if callable(connector) or isinstance(connector, type):
52
+ # If the connector is a class or factory function, instantiate it:
53
+ return cast(IConnector, connector()) # type: ignore [redundant-cast]
54
+
55
+ # Otherwise, we can't instantiate the connector. Fail with a clear error message.
56
+ raise NotImplementedError(
57
+ "No connector class or connector factory function provided. "
58
+ "Please provide a class or factory function in `cls.connector`, or "
59
+ "override `cls.create_connector()` to define a custom initialization process."
60
+ )
61
+
62
+ # Test Definitions
63
+
64
+ def test_check(
65
+ self,
66
+ scenario: ConnectorTestScenario,
67
+ ) -> None:
68
+ """Run `connection` acceptance tests."""
69
+ result: entrypoint_wrapper.EntrypointOutput = run_test_job(
70
+ self.create_connector(scenario),
71
+ "check",
72
+ test_scenario=scenario,
73
+ )
74
+ conn_status_messages: list[AirbyteMessage] = [
75
+ msg for msg in result._messages if msg.type == Type.CONNECTION_STATUS
76
+ ] # noqa: SLF001 # Non-public API
77
+ assert len(conn_status_messages) == 1, (
78
+ f"Expected exactly one CONNECTION_STATUS message. Got: {result._messages}"
79
+ )
80
+
81
+ @classmethod
82
+ def get_connector_root_dir(cls) -> Path:
83
+ """Get the root directory of the connector."""
84
+ for parent in cls.get_test_class_dir().parents:
85
+ if (parent / MANIFEST_YAML).exists():
86
+ return parent
87
+ if (parent / ACCEPTANCE_TEST_CONFIG).exists():
88
+ return parent
89
+ if parent.name == "airbyte_cdk":
90
+ break
91
+ # If we reach here, we didn't find the manifest file in any parent directory
92
+ # Check if the manifest file exists in the current directory
93
+ for parent in Path.cwd().parents:
94
+ if (parent / MANIFEST_YAML).exists():
95
+ return parent
96
+ if (parent / ACCEPTANCE_TEST_CONFIG).exists():
97
+ return parent
98
+ if parent.name == "airbyte_cdk":
99
+ break
100
+
101
+ raise FileNotFoundError(
102
+ "Could not find connector root directory relative to "
103
+ f"'{str(cls.get_test_class_dir())}' or '{str(Path.cwd())}'."
104
+ )
105
+
106
+ @classproperty
107
+ def acceptance_test_config_path(cls) -> Path:
108
+ """Get the path to the acceptance test config file."""
109
+ result = cls.get_connector_root_dir() / ACCEPTANCE_TEST_CONFIG
110
+ if result.exists():
111
+ return result
112
+
113
+ raise FileNotFoundError(f"Acceptance test config file not found at: {str(result)}")
114
+
115
+ @classmethod
116
+ def get_scenarios(
117
+ cls,
118
+ ) -> list[ConnectorTestScenario]:
119
+ """Get acceptance tests for a given category.
120
+
121
+ This has to be a separate function because pytest does not allow
122
+ parametrization of fixtures with arguments from the test class itself.
123
+ """
124
+ category = "connection"
125
+ all_tests_config = yaml.safe_load(cls.acceptance_test_config_path.read_text())
126
+ if "acceptance_tests" not in all_tests_config:
127
+ raise ValueError(
128
+ f"Acceptance tests config not found in {cls.acceptance_test_config_path}."
129
+ f" Found only: {str(all_tests_config)}."
130
+ )
131
+ if category not in all_tests_config["acceptance_tests"]:
132
+ return []
133
+ if "tests" not in all_tests_config["acceptance_tests"][category]:
134
+ raise ValueError(f"No tests found for category {category}")
135
+
136
+ tests_scenarios = [
137
+ ConnectorTestScenario.model_validate(test)
138
+ for test in all_tests_config["acceptance_tests"][category]["tests"]
139
+ if "iam_role" not in test["config_path"]
140
+ ]
141
+ connector_root = cls.get_connector_root_dir().absolute()
142
+ for test in tests_scenarios:
143
+ if test.config_path:
144
+ test.config_path = connector_root / test.config_path
145
+ if test.configured_catalog_path:
146
+ test.configured_catalog_path = connector_root / test.configured_catalog_path
147
+
148
+ return tests_scenarios
@@ -0,0 +1,92 @@
1
+ import os
2
+ from hashlib import md5
3
+ from pathlib import Path
4
+ from typing import Any, cast
5
+
6
+ import yaml
7
+ from boltons.typeutils import classproperty
8
+
9
+ from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
10
+ ConcurrentDeclarativeSource,
11
+ )
12
+ from airbyte_cdk.test.declarative.models import ConnectorTestScenario
13
+ from airbyte_cdk.test.declarative.utils.job_runner import IConnector
14
+ from airbyte_cdk.test.standard_tests.connector_base import MANIFEST_YAML
15
+ from airbyte_cdk.test.standard_tests.source_base import SourceTestSuiteBase
16
+
17
+
18
+ def md5_checksum(file_path: Path) -> str:
19
+ """Helper function to calculate the MD5 checksum of a file.
20
+
21
+ This is used to calculate the checksum of the `components.py` file, if it exists.
22
+ """
23
+ with open(file_path, "rb") as file:
24
+ return md5(file.read()).hexdigest()
25
+
26
+
27
+ class DeclarativeSourceTestSuite(SourceTestSuiteBase):
28
+ """Declarative source test suite.
29
+
30
+ This inherits from the Python-based source test suite and implements the
31
+ `create_connector` method to create a declarative source object instead of
32
+ requiring a custom Python source object.
33
+
34
+ The class also automatically locates the `manifest.yaml` file and the
35
+ `components.py` file (if it exists) in the connector's directory.
36
+ """
37
+
38
+ @classproperty
39
+ def manifest_yaml_path(cls) -> Path:
40
+ """Get the path to the manifest.yaml file."""
41
+ result = cls.get_connector_root_dir() / MANIFEST_YAML
42
+ if result.exists():
43
+ return result
44
+
45
+ raise FileNotFoundError(
46
+ f"Manifest YAML file not found at {result}. "
47
+ "Please ensure that the test suite is run in the correct directory.",
48
+ )
49
+
50
+ @classproperty
51
+ def components_py_path(cls) -> Path | None:
52
+ """Get the path to the `components.py` file, if one exists.
53
+
54
+ If not `components.py` file exists, return None.
55
+ """
56
+ result = cls.get_connector_root_dir() / "components.py"
57
+ if result.exists():
58
+ return result
59
+
60
+ return None
61
+
62
+ @classmethod
63
+ def create_connector(
64
+ cls,
65
+ scenario: ConnectorTestScenario,
66
+ ) -> IConnector:
67
+ """Create a connector scenario for the test suite.
68
+
69
+ This overrides `create_connector` from the create a declarative source object
70
+ instead of requiring a custom python source object.
71
+
72
+ Subclasses should not need to override this method.
73
+ """
74
+ config: dict[str, Any] = scenario.get_config_dict()
75
+
76
+ manifest_dict = yaml.safe_load(cls.manifest_yaml_path.read_text())
77
+ if cls.components_py_path and cls.components_py_path.exists():
78
+ os.environ["AIRBYTE_ENABLE_UNSAFE_CODE"] = "true"
79
+ config["__injected_components_py"] = cls.components_py_path.read_text()
80
+ config["__injected_components_py_checksums"] = {
81
+ "md5": md5_checksum(cls.components_py_path),
82
+ }
83
+
84
+ return cast(
85
+ IConnector,
86
+ ConcurrentDeclarativeSource(
87
+ config=config,
88
+ catalog=None,
89
+ state=None,
90
+ source_config=manifest_dict,
91
+ ),
92
+ )
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ """Base class for destination test suites."""
3
+
4
+ from airbyte_cdk.test.standard_tests.connector_base import ConnectorTestSuiteBase
5
+
6
+
7
+ class DestinationTestSuiteBase(ConnectorTestSuiteBase):
8
+ """Base class for destination test suites.
9
+
10
+ This class provides a base set of functionality for testing destination connectors, and it
11
+ inherits all generic connector tests from the `ConnectorTestSuiteBase` class.
12
+
13
+ TODO: As of now, this class does not add any additional functionality or tests specific to
14
+ destination connectors. However, it serves as a placeholder for future enhancements and
15
+ customizations that may be needed for destination connectors.
16
+ """
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """Pytest hooks for Airbyte CDK tests.
3
+
4
+ These hooks are used to customize the behavior of pytest during test discovery and execution.
5
+
6
+ To use these hooks within a connector, add the following lines to the connector's `conftest.py`
7
+ file, or to another file that is imported during test discovery:
8
+
9
+ ```python
10
+ pytest_plugins = [
11
+ "airbyte_cdk.test.standard_tests.pytest_hooks",
12
+ ]
13
+ ```
14
+ """
15
+
16
+ import pytest
17
+
18
+
19
+ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
20
+ """
21
+ A helper for pytest_generate_tests hook.
22
+
23
+ If a test method (in a class subclassed from our base class)
24
+ declares an argument 'scenario', this function retrieves the
25
+ 'scenarios' attribute from the test class and parametrizes that
26
+ test with the values from 'scenarios'.
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from airbyte_cdk.test.standard_tests.connector_base import (
32
+ generate_tests,
33
+ ConnectorTestSuiteBase,
34
+ )
35
+
36
+ def pytest_generate_tests(metafunc):
37
+ generate_tests(metafunc)
38
+
39
+ class TestMyConnector(ConnectorTestSuiteBase):
40
+ ...
41
+
42
+ ```
43
+ """
44
+ # Check if the test function requires an 'scenario' argument
45
+ if "scenario" in metafunc.fixturenames:
46
+ # Retrieve the test class
47
+ test_class = metafunc.cls
48
+ if test_class is None:
49
+ return
50
+
51
+ # Get the 'scenarios' attribute from the class
52
+ scenarios_attr = getattr(test_class, "get_scenarios", None)
53
+ if scenarios_attr is None:
54
+ raise ValueError(
55
+ f"Test class {test_class} does not have a 'scenarios' attribute. "
56
+ "Please define the 'scenarios' attribute in the test class."
57
+ )
58
+
59
+ scenarios = test_class.get_scenarios()
60
+ ids = [str(scenario) for scenario in scenarios]
61
+ metafunc.parametrize("scenario", scenarios, ids=ids)
@@ -0,0 +1,140 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+ """Base class for source test suites."""
3
+
4
+ from dataclasses import asdict
5
+
6
+ from airbyte_cdk.models import (
7
+ AirbyteMessage,
8
+ AirbyteStream,
9
+ ConfiguredAirbyteCatalog,
10
+ ConfiguredAirbyteStream,
11
+ DestinationSyncMode,
12
+ SyncMode,
13
+ Type,
14
+ )
15
+ from airbyte_cdk.test import entrypoint_wrapper
16
+ from airbyte_cdk.test.declarative.models import (
17
+ ConnectorTestScenario,
18
+ )
19
+ from airbyte_cdk.test.declarative.utils.job_runner import run_test_job
20
+ from airbyte_cdk.test.standard_tests.connector_base import (
21
+ ConnectorTestSuiteBase,
22
+ )
23
+
24
+
25
+ class SourceTestSuiteBase(ConnectorTestSuiteBase):
26
+ """Base class for source test suites.
27
+
28
+ This class provides a base set of functionality for testing source connectors, and it
29
+ inherits all generic connector tests from the `ConnectorTestSuiteBase` class.
30
+ """
31
+
32
+ def test_check(
33
+ self,
34
+ scenario: ConnectorTestScenario,
35
+ ) -> None:
36
+ """Run standard `check` tests on the connector.
37
+
38
+ Assert that the connector returns a single CONNECTION_STATUS message.
39
+ This test is designed to validate the connector's ability to establish a connection
40
+ and return its status with the expected message type.
41
+ """
42
+ result: entrypoint_wrapper.EntrypointOutput = run_test_job(
43
+ self.create_connector(scenario),
44
+ "check",
45
+ test_scenario=scenario,
46
+ )
47
+ conn_status_messages: list[AirbyteMessage] = [
48
+ msg for msg in result._messages if msg.type == Type.CONNECTION_STATUS
49
+ ] # noqa: SLF001 # Non-public API
50
+ num_status_messages = len(conn_status_messages)
51
+ assert num_status_messages == 1, (
52
+ f"Expected exactly one CONNECTION_STATUS message. Got {num_status_messages}: \n"
53
+ + "\n".join([str(m) for m in result._messages])
54
+ )
55
+
56
+ def test_discover(
57
+ self,
58
+ scenario: ConnectorTestScenario,
59
+ ) -> None:
60
+ """Standard test for `discover`."""
61
+ run_test_job(
62
+ self.create_connector(scenario),
63
+ "discover",
64
+ test_scenario=scenario,
65
+ )
66
+
67
+ def test_basic_read(
68
+ self,
69
+ scenario: ConnectorTestScenario,
70
+ ) -> None:
71
+ """Run standard `read` test on the connector.
72
+
73
+ This test is designed to validate the connector's ability to read data
74
+ from the source and return records. It first runs a `discover` job to
75
+ obtain the catalog of streams, and then it runs a `read` job to fetch
76
+ records from those streams.
77
+ """
78
+ discover_result = run_test_job(
79
+ self.create_connector(scenario),
80
+ "discover",
81
+ test_scenario=scenario,
82
+ )
83
+ if scenario.expect_exception:
84
+ assert discover_result.errors, "Expected exception but got none."
85
+ return
86
+
87
+ configured_catalog = ConfiguredAirbyteCatalog(
88
+ streams=[
89
+ ConfiguredAirbyteStream(
90
+ stream=stream,
91
+ sync_mode=SyncMode.full_refresh,
92
+ destination_sync_mode=DestinationSyncMode.append_dedup,
93
+ )
94
+ for stream in discover_result.catalog.catalog.streams # type: ignore [reportOptionalMemberAccess, union-attr]
95
+ ]
96
+ )
97
+ result = run_test_job(
98
+ self.create_connector(scenario),
99
+ "read",
100
+ test_scenario=scenario,
101
+ catalog=configured_catalog,
102
+ )
103
+
104
+ if not result.records:
105
+ raise AssertionError("Expected records but got none.") # noqa: TRY003
106
+
107
+ def test_fail_read_with_bad_catalog(
108
+ self,
109
+ scenario: ConnectorTestScenario,
110
+ ) -> None:
111
+ """Standard test for `read` when passed a bad catalog file."""
112
+ invalid_configured_catalog = ConfiguredAirbyteCatalog(
113
+ streams=[
114
+ # Create ConfiguredAirbyteStream which is deliberately invalid
115
+ # with regard to the Airbyte Protocol.
116
+ # This should cause the connector to fail.
117
+ ConfiguredAirbyteStream(
118
+ stream=AirbyteStream(
119
+ name="__AIRBYTE__stream_that_does_not_exist",
120
+ json_schema={
121
+ "type": "object",
122
+ "properties": {"f1": {"type": "string"}},
123
+ },
124
+ supported_sync_modes=[SyncMode.full_refresh],
125
+ ),
126
+ sync_mode="INVALID", # type: ignore [reportArgumentType]
127
+ destination_sync_mode="INVALID", # type: ignore [reportArgumentType]
128
+ )
129
+ ]
130
+ )
131
+ # Set expected status to "failed" to ensure the test fails if the connector.
132
+ scenario.status = "failed"
133
+ result: entrypoint_wrapper.EntrypointOutput = run_test_job(
134
+ self.create_connector(scenario),
135
+ "read",
136
+ test_scenario=scenario,
137
+ catalog=asdict(invalid_configured_catalog),
138
+ )
139
+ assert result.errors, "Expected errors but got none."
140
+ assert result.trace_messages, "Expected trace messages but got none."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 6.45.4.post49.dev14495925594
3
+ Version: 6.45.4.post72.dev14497997772
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -22,10 +22,11 @@ Provides-Extra: sql
22
22
  Provides-Extra: vector-db-based
23
23
  Requires-Dist: Jinja2 (>=3.1.2,<3.2.0)
24
24
  Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
25
- Requires-Dist: airbyte-protocol-models-dataclasses (>=0.15,<0.16)
25
+ Requires-Dist: airbyte-protocol-models-dataclasses (>=0.14,<0.15)
26
26
  Requires-Dist: anyascii (>=0.3.2,<0.4.0)
27
27
  Requires-Dist: avro (>=1.11.2,<1.13.0) ; extra == "file-based"
28
28
  Requires-Dist: backoff
29
+ Requires-Dist: boltons (>=25.0.0,<26.0.0)
29
30
  Requires-Dist: cachetools
30
31
  Requires-Dist: cohere (==4.21) ; extra == "vector-db-based"
31
32
  Requires-Dist: cryptography (>=44.0.0,<45.0.0)