Skip to content

Commit bfe7d91

Browse files
committed
add docker container ops
1 parent 3d73ded commit bfe7d91

File tree

10 files changed

+342
-50
lines changed

10 files changed

+342
-50
lines changed

airbyte_cdk/cli/airbyte_cdk/_connector.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090

9191
@click.group(
9292
name="connector",
93-
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown)
93+
help=__doc__.replace("\n", "\n\n"), # Render docstring as help text (markdown) # type: ignore
9494
)
9595
def connector_cli_group() -> None:
9696
"""Connector related commands."""
@@ -114,24 +114,43 @@ def connector_cli_group() -> None:
114114
default=False,
115115
help="Only collect tests, do not run them.",
116116
)
117+
@click.option(
118+
"--use-docker-image",
119+
# is_flag=True,
120+
default=False,
121+
type=str,
122+
help="Run tests via docker.",
123+
callback=lambda ctx, param, value: (
124+
"true" if value is True else (value if isinstance(value, str) else False)
125+
),
126+
)
117127
def test(
118128
connector_name: str | None = None,
119129
connector_directory: Path | None = None,
120130
*,
121131
collect_only: bool = False,
132+
use_docker_image: str | bool = False,
122133
) -> None:
123134
"""Run connector tests.
124135
125136
This command runs the standard connector tests for a specific connector.
126137
127138
If no connector name or directory is provided, we will look within the current working
128139
directory. If the current working directory is not a connector directory (e.g. starting
129-
with 'source-') and no connector name or path is provided, the process will fail.
140+
with 'source-' or 'destination-') and no connector name or path is provided, the process
141+
will fail.
130142
"""
143+
if isinstance(
144+
use_docker_image,
145+
str,
146+
) and use_docker_image.lower() in {"true", "false"}:
147+
use_docker_image = bool(use_docker_image)
148+
131149
if pytest is None:
132150
raise ImportError(
133151
"pytest is not installed. Please install pytest to run the connector tests."
134152
)
153+
135154
click.echo("Connector test command executed.")
136155
connector_name, connector_directory = resolve_connector_name_and_directory(
137156
connector_name=connector_name,
@@ -159,9 +178,18 @@ def test(
159178
test_file_path.parent.mkdir(parents=True, exist_ok=True)
160179
test_file_path.write_text(file_text)
161180

181+
pytest_args.append("-p airbyte_cdk.test.standard_tests.pytest_hooks")
182+
162183
if collect_only:
163184
pytest_args.append("--collect-only")
164185

186+
if use_docker_image:
187+
pytest_args.append("--use-docker-image")
188+
if isinstance(use_docker_image, str):
189+
pytest_args.append(
190+
use_docker_image,
191+
)
192+
165193
pytest_args.append(str(test_file_path))
166194
click.echo(f"Running tests from connector directory: {connector_directory}...")
167195
click.echo(f"Test file: {test_file_path}")

airbyte_cdk/cli/airbyte_cdk/_image.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,11 @@ def build(
6464
connector_directory=connector_directory,
6565
)
6666

67-
metadata_file_path: Path = connector_directory / "metadata.yaml"
68-
try:
69-
metadata = MetadataFile.from_file(metadata_file_path)
70-
except (FileNotFoundError, ValueError) as e:
71-
click.echo(
72-
f"Error loading metadata file '{metadata_file_path}': {e!s}",
73-
err=True,
74-
)
75-
sys.exit(1)
76-
click.echo(f"Building Image for Connector: {metadata.data.dockerRepository}:{tag}")
67+
click.echo(f"Building '{tag}' Image for Connector '{connector_name}':")
7768
try:
7869
build_connector_image(
7970
connector_directory=connector_directory,
8071
connector_name=connector_name,
81-
metadata=metadata,
8272
tag=tag,
8373
no_verify=no_verify,
8474
)

airbyte_cdk/test/standard_tests/_job_runner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
import logging
55
import tempfile
66
import uuid
7+
from abc import abstractmethod
8+
from collections.abc import Callable
79
from dataclasses import asdict
810
from pathlib import Path
9-
from typing import Any, Callable, Literal
11+
from typing import Any, Literal
1012

1113
import orjson
1214
from typing_extensions import Protocol, runtime_checkable
1315

1416
from airbyte_cdk.models import (
1517
ConfiguredAirbyteCatalog,
18+
ConnectorSpecification,
1619
Status,
1720
)
1821
from airbyte_cdk.test import entrypoint_wrapper
@@ -50,8 +53,10 @@ class IConnector(Protocol):
5053
directly on the connector (which doesn't yet exist).
5154
"""
5255

53-
def spec(self, logger: logging.Logger) -> Any:
56+
@abstractmethod
57+
def spec(self, logger: logging.Logger) -> ConnectorSpecification:
5458
"""Connectors should have a `spec` method."""
59+
...
5560

5661

5762
def run_test_job(

airbyte_cdk/test/standard_tests/connector_base.py

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
import inspect
99
import os
1010
import sys
11-
from collections.abc import Callable
1211
from pathlib import Path
13-
from typing import cast
12+
from typing import Literal, cast
1413

14+
import pytest
1515
import yaml
1616
from boltons.typeutils import classproperty
1717

@@ -21,6 +21,7 @@
2121
)
2222
from airbyte_cdk.test import entrypoint_wrapper
2323
from airbyte_cdk.test.standard_tests._job_runner import IConnector, run_test_job
24+
from airbyte_cdk.test.standard_tests.docker_connectors import DockerConnector
2425
from airbyte_cdk.test.standard_tests.models import (
2526
ConnectorTestScenario,
2627
)
@@ -30,19 +31,42 @@
3031
)
3132

3233

34+
@pytest.fixture
35+
def use_docker_image(request: pytest.FixtureRequest) -> str | bool:
36+
"""Fixture to determine if a Docker image should be used for the test."""
37+
return request.config.getoption("use_docker_image")
38+
39+
3340
class ConnectorTestSuiteBase(abc.ABC):
3441
"""Base class for connector test suites."""
3542

36-
connector: type[IConnector] | Callable[[], IConnector] | None # type: ignore [reportRedeclaration]
43+
connector_class: type[IConnector] | None = None
3744
"""The connector class or a factory function that returns an scenario of IConnector."""
3845

39-
@classproperty # type: ignore [no-redef]
40-
def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None:
46+
@classmethod
47+
def get_test_class_dir(cls) -> Path:
48+
"""Get the file path that contains the class."""
49+
module = sys.modules[cls.__module__]
50+
# Get the directory containing the test file
51+
return Path(inspect.getfile(module)).parent
52+
53+
@classmethod
54+
def create_connector(
55+
cls,
56+
scenario: ConnectorTestScenario,
57+
use_docker_image: str | bool,
58+
) -> IConnector:
59+
"""Instantiate the connector class."""
4160
"""Get the connector class for the test suite.
4261
4362
This assumes a python connector and should be overridden by subclasses to provide the
4463
specific connector class to be tested.
4564
"""
65+
if use_docker_image:
66+
return cls.create_docker_connector(
67+
docker_image=use_docker_image,
68+
)
69+
4670
connector_root = cls.get_connector_root_dir()
4771
connector_name = connector_root.absolute().name
4872

@@ -65,7 +89,10 @@ def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None:
6589

6690
# Dynamically get the class from the module
6791
try:
68-
return cast(type[IConnector], getattr(module, expected_class_name))
92+
return cast(
93+
type[IConnector],
94+
getattr(module, expected_class_name),
95+
)()
6996
except AttributeError as e:
7097
# We did not find it based on our expectations, so let's check if we can find it
7198
# with a case-insensitive match.
@@ -77,21 +104,13 @@ def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None:
77104
raise ImportError(
78105
f"Module '{expected_module_name}' does not have a class named '{expected_class_name}'."
79106
) from e
80-
return cast(type[IConnector], getattr(module, matching_class_name))
107+
return cast(
108+
type[IConnector],
109+
getattr(module, matching_class_name),
110+
)()
81111

82-
@classmethod
83-
def get_test_class_dir(cls) -> Path:
84-
"""Get the file path that contains the class."""
85-
module = sys.modules[cls.__module__]
86-
# Get the directory containing the test file
87-
return Path(inspect.getfile(module)).parent
112+
_ = scenario
88113

89-
@classmethod
90-
def create_connector(
91-
cls,
92-
scenario: ConnectorTestScenario,
93-
) -> IConnector:
94-
"""Instantiate the connector class."""
95114
connector = cls.connector # type: ignore
96115
if connector:
97116
if callable(connector) or isinstance(connector, type):
@@ -105,15 +124,46 @@ def create_connector(
105124
"override `cls.create_connector()` to define a custom initialization process."
106125
)
107126

127+
@classmethod
128+
def create_docker_connector(
129+
cls,
130+
docker_image: str | Literal[True],
131+
) -> IConnector:
132+
"""Create a connector instance using Docker."""
133+
if not docker_image:
134+
raise ValueError("Docker image is required to create a Docker connector.")
135+
136+
# Create the connector object by building the connector
137+
if docker_image is True:
138+
return DockerConnector.from_connector_directory(
139+
connector_directory=cls.get_connector_root_dir(),
140+
)
141+
142+
if not isinstance(docker_image, str):
143+
raise ValueError(
144+
"Expected `docker_image` to be 'True' or of type `str`. "
145+
f"Type found: {type(docker_image).__name__}"
146+
)
147+
148+
# Create the connector object using the provided Docker image
149+
return DockerConnector(
150+
connector_name=cls.get_connector_root_dir().name,
151+
docker_image=docker_image,
152+
)
153+
108154
# Test Definitions
109155

110156
def test_check(
111157
self,
112158
scenario: ConnectorTestScenario,
159+
use_docker_image: str | bool,
113160
) -> None:
114161
"""Run `connection` acceptance tests."""
115162
result: entrypoint_wrapper.EntrypointOutput = run_test_job(
116-
self.create_connector(scenario),
163+
self.create_connector(
164+
scenario,
165+
use_docker_image=use_docker_image,
166+
),
117167
"check",
118168
test_scenario=scenario,
119169
)

airbyte_cdk/test/standard_tests/declarative_sources.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def components_py_path(cls) -> Path | None:
6565
def create_connector(
6666
cls,
6767
scenario: ConnectorTestScenario,
68+
use_docker_image: str | bool,
6869
) -> IConnector:
6970
"""Create a connector scenario for the test suite.
7071
@@ -73,6 +74,11 @@ def create_connector(
7374
7475
Subclasses should not need to override this method.
7576
"""
77+
if use_docker_image:
78+
return cls.create_docker_connector(
79+
docker_image=use_docker_image,
80+
)
81+
7682
config: dict[str, Any] = scenario.get_config_dict()
7783

7884
manifest_dict = yaml.safe_load(cls.manifest_yaml_path.read_text())

0 commit comments

Comments
 (0)