airbyte-cdk 6.48.7.post2.dev14899985028__py3-none-any.whl → 6.48.9__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.
@@ -44,11 +44,14 @@ from types import ModuleType
44
44
 
45
45
  import rich_click as click
46
46
 
47
- # from airbyte_cdk.test.standard_tests import pytest_hooks
48
- from airbyte_cdk.cli.airbyte_cdk._util import resolve_connector_name_and_directory
49
- from airbyte_cdk.test.standard_tests.test_resources import find_connector_root_from_name
50
47
  from airbyte_cdk.test.standard_tests.util import create_connector_test_suite
51
48
 
49
+ # from airbyte_cdk.test.standard_tests import pytest_hooks
50
+ from airbyte_cdk.utils.connector_paths import (
51
+ find_connector_root_from_name,
52
+ resolve_connector_name_and_directory,
53
+ )
54
+
52
55
  click.rich_click.TEXT_MARKUP = "markdown"
53
56
 
54
57
  pytest: ModuleType | None
@@ -98,15 +101,11 @@ def connector_cli_group() -> None:
98
101
 
99
102
 
100
103
  @connector_cli_group.command()
101
- @click.option(
102
- "--connector-name",
104
+ @click.argument(
105
+ "connector",
106
+ required=False,
103
107
  type=str,
104
- help="Name of the connector to test. Ignored if --connector-directory is provided.",
105
- )
106
- @click.option(
107
- "--connector-directory",
108
- type=click.Path(exists=True, file_okay=False, path_type=Path),
109
- help="Path to the connector directory.",
108
+ metavar="[CONNECTOR]",
110
109
  )
111
110
  @click.option(
112
111
  "--collect-only",
@@ -115,8 +114,7 @@ def connector_cli_group() -> None:
115
114
  help="Only collect tests, do not run them.",
116
115
  )
117
116
  def test(
118
- connector_name: str | None = None,
119
- connector_directory: Path | None = None,
117
+ connector: str | Path | None = None,
120
118
  *,
121
119
  collect_only: bool = False,
122
120
  ) -> None:
@@ -124,6 +122,9 @@ def test(
124
122
 
125
123
  This command runs the standard connector tests for a specific connector.
126
124
 
125
+ [CONNECTOR] can be a connector name (e.g. 'source-pokeapi'), a path to a connector directory, or omitted to use the current working directory.
126
+ If a string containing '/' is provided, it is treated as a path. Otherwise, it is treated as a connector name.
127
+
127
128
  If no connector name or directory is provided, we will look within the current working
128
129
  directory. If the current working directory is not a connector directory (e.g. starting
129
130
  with 'source-') and no connector name or path is provided, the process will fail.
@@ -133,10 +134,7 @@ def test(
133
134
  "pytest is not installed. Please install pytest to run the connector tests."
134
135
  )
135
136
  click.echo("Connector test command executed.")
136
- connector_name, connector_directory = resolve_connector_name_and_directory(
137
- connector_name=connector_name,
138
- connector_directory=connector_directory,
139
- )
137
+ connector_name, connector_directory = resolve_connector_name_and_directory(connector)
140
138
 
141
139
  connector_test_suite = create_connector_test_suite(
142
140
  connector_name=connector_name if not connector_directory else None,
@@ -10,8 +10,8 @@ from pathlib import Path
10
10
 
11
11
  import rich_click as click
12
12
 
13
- from airbyte_cdk.cli.airbyte_cdk._util import resolve_connector_name_and_directory
14
13
  from airbyte_cdk.models.connector_metadata import MetadataFile
14
+ from airbyte_cdk.utils.connector_paths import resolve_connector_name_and_directory
15
15
  from airbyte_cdk.utils.docker import (
16
16
  ConnectorImageBuildError,
17
17
  build_connector_image,
@@ -28,30 +28,30 @@ def image_cli_group() -> None:
28
28
 
29
29
 
30
30
  @image_cli_group.command()
31
- @click.option(
32
- "--connector-name",
31
+ @click.argument(
32
+ "connector",
33
+ required=False,
33
34
  type=str,
34
- help="Name of the connector to test. Ignored if --connector-directory is provided.",
35
- )
36
- @click.option(
37
- "--connector-directory",
38
- type=click.Path(exists=True, file_okay=False, path_type=Path),
39
- help="Path to the connector directory.",
35
+ metavar="[CONNECTOR]",
40
36
  )
41
37
  @click.option("--tag", default="dev", help="Tag to apply to the built image (default: dev)")
42
38
  @click.option("--no-verify", is_flag=True, help="Skip verification of the built image")
39
+ @click.option(
40
+ "--dockerfile",
41
+ type=click.Path(exists=True, file_okay=True, path_type=Path),
42
+ help="Optional. Override the Dockerfile used for building the image.",
43
+ )
43
44
  def build(
44
- connector_name: str | None = None,
45
- connector_directory: Path | None = None,
45
+ connector: str | None = None,
46
46
  *,
47
47
  tag: str = "dev",
48
48
  no_verify: bool = False,
49
+ dockerfile: Path | None = None,
49
50
  ) -> None:
50
51
  """Build a connector Docker image.
51
52
 
52
- This command builds a Docker image for a connector, using either
53
- the connector's Dockerfile or a base image specified in the metadata.
54
- The image is built for both AMD64 and ARM64 architectures.
53
+ [CONNECTOR] can be a connector name (e.g. 'source-pokeapi'), a path to a connector directory, or omitted to use the current working directory.
54
+ If a string containing '/' is provided, it is treated as a path. Otherwise, it is treated as a connector name.
55
55
  """
56
56
  if not verify_docker_installation():
57
57
  click.echo(
@@ -59,10 +59,7 @@ def build(
59
59
  )
60
60
  sys.exit(1)
61
61
 
62
- connector_name, connector_directory = resolve_connector_name_and_directory(
63
- connector_name=connector_name,
64
- connector_directory=connector_directory,
65
- )
62
+ connector_name, connector_directory = resolve_connector_name_and_directory(connector)
66
63
 
67
64
  metadata_file_path: Path = connector_directory / "metadata.yaml"
68
65
  try:
@@ -81,6 +78,7 @@ def build(
81
78
  metadata=metadata,
82
79
  tag=tag,
83
80
  no_verify=no_verify,
81
+ dockerfile_override=dockerfile or None,
84
82
  )
85
83
  except ConnectorImageBuildError as e:
86
84
  click.echo(
@@ -43,7 +43,7 @@ from click import style
43
43
  from rich.console import Console
44
44
  from rich.table import Table
45
45
 
46
- from airbyte_cdk.cli.airbyte_cdk._util import (
46
+ from airbyte_cdk.utils.connector_paths import (
47
47
  resolve_connector_name,
48
48
  resolve_connector_name_and_directory,
49
49
  )
@@ -73,15 +73,11 @@ def secrets_cli_group() -> None:
73
73
 
74
74
 
75
75
  @secrets_cli_group.command()
76
- @click.option(
77
- "--connector-name",
76
+ @click.argument(
77
+ "connector",
78
+ required=False,
78
79
  type=str,
79
- help="Name of the connector to fetch secrets for. Ignored if --connector-directory is provided.",
80
- )
81
- @click.option(
82
- "--connector-directory",
83
- type=click.Path(exists=True, file_okay=False, path_type=Path),
84
- help="Path to the connector directory.",
80
+ metavar="[CONNECTOR]",
85
81
  )
86
82
  @click.option(
87
83
  "--gcp-project-id",
@@ -97,8 +93,7 @@ def secrets_cli_group() -> None:
97
93
  default=False,
98
94
  )
99
95
  def fetch(
100
- connector_name: str | None = None,
101
- connector_directory: Path | None = None,
96
+ connector: str | Path | None = None,
102
97
  gcp_project_id: str = AIRBYTE_INTERNAL_GCP_PROJECT,
103
98
  print_ci_secrets_masks: bool = False,
104
99
  ) -> None:
@@ -107,6 +102,9 @@ def fetch(
107
102
  This command fetches secrets for a connector from Google Secret Manager and writes them
108
103
  to the connector's secrets directory.
109
104
 
105
+ [CONNECTOR] can be a connector name (e.g. 'source-pokeapi'), a path to a connector directory, or omitted to use the current working directory.
106
+ If a string containing '/' is provided, it is treated as a path. Otherwise, it is treated as a connector name.
107
+
110
108
  If no connector name or directory is provided, we will look within the current working
111
109
  directory. If the current working directory is not a connector directory (e.g. starting
112
110
  with 'source-') and no connector name or path is provided, the process will fail.
@@ -114,17 +112,14 @@ def fetch(
114
112
  The `--print-ci-secrets-masks` option will print the GitHub CI mask for the secrets.
115
113
  This is useful for masking secrets in CI logs.
116
114
 
117
- WARNING: This action causes the secrets to be printed in clear text to `STDOUT`. For security
118
- reasons, this function will only execute if the `CI` environment variable is set. Otherwise,
119
- masks will not be printed.
115
+ WARNING: The `--print-ci-secrets-masks` option causes the secrets to be printed in clear text to
116
+ `STDOUT`. For security reasons, this argument will be ignored if the `CI` environment
117
+ variable is not set.
120
118
  """
121
119
  click.echo("Fetching secrets...", err=True)
122
120
 
123
121
  client = _get_gsm_secrets_client()
124
- connector_name, connector_directory = resolve_connector_name_and_directory(
125
- connector_name=connector_name,
126
- connector_directory=connector_directory,
127
- )
122
+ connector_name, connector_directory = resolve_connector_name_and_directory(connector)
128
123
  secrets_dir = _get_secrets_dir(
129
124
  connector_directory=connector_directory,
130
125
  connector_name=connector_name,
@@ -289,21 +284,7 @@ def _get_secrets_dir(
289
284
  connector_name: str,
290
285
  ensure_exists: bool = True,
291
286
  ) -> Path:
292
- try:
293
- connector_name, connector_directory = resolve_connector_name_and_directory(
294
- connector_name=connector_name,
295
- connector_directory=connector_directory,
296
- )
297
- except FileNotFoundError as e:
298
- raise FileNotFoundError(
299
- f"Could not find connector directory for '{connector_name}'. "
300
- "Please provide the --connector-directory option with the path to the connector. "
301
- "Note: This command requires either running from within a connector directory, "
302
- "being in the airbyte monorepo, or explicitly providing the connector directory path."
303
- ) from e
304
- except ValueError as e:
305
- raise ValueError(str(e))
306
-
287
+ _ = connector_name # Unused, but it may be used in the future for logging
307
288
  secrets_dir = connector_directory / "secrets"
308
289
  if ensure_exists:
309
290
  secrets_dir.mkdir(parents=True, exist_ok=True)
@@ -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 spec_messages(self) -> List[AirbyteMessage]:
87
+ return self._get_message_by_types([Type.SPEC])
88
+
85
89
  @property
86
90
  def connection_status_messages(self) -> List[AirbyteMessage]:
87
91
  return self._get_message_by_types([Type.CONNECTION_STATUS])
@@ -56,12 +56,15 @@ class IConnector(Protocol):
56
56
 
57
57
  def run_test_job(
58
58
  connector: IConnector | type[IConnector] | Callable[[], IConnector],
59
- verb: Literal["read", "check", "discover"],
60
- test_scenario: ConnectorTestScenario,
59
+ verb: Literal["spec", "read", "check", "discover"],
61
60
  *,
61
+ test_scenario: ConnectorTestScenario | None = None,
62
62
  catalog: ConfiguredAirbyteCatalog | dict[str, Any] | None = None,
63
63
  ) -> entrypoint_wrapper.EntrypointOutput:
64
64
  """Run a test scenario from provided CLI args and return the result."""
65
+ # Use default (empty) scenario if not provided:
66
+ test_scenario = test_scenario or ConnectorTestScenario()
67
+
65
68
  if not connector:
66
69
  raise ValueError("Connector is required")
67
70
 
@@ -81,14 +84,14 @@ def run_test_job(
81
84
  )
82
85
 
83
86
  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_dict = test_scenario.get_config_dict(empty_if_missing=True)
88
+ if config_dict and verb != "spec":
89
+ # Write the config to a temp json file and pass the path to the file as an argument.
87
90
  config_path = (
88
91
  Path(tempfile.gettempdir()) / "airbyte-test" / f"temp_config_{uuid.uuid4().hex}.json"
89
92
  )
90
93
  config_path.parent.mkdir(parents=True, exist_ok=True)
91
- config_path.write_text(orjson.dumps(test_scenario.config_dict).decode())
94
+ config_path.write_text(orjson.dumps(config_dict).decode())
92
95
  args += ["--config", str(config_path)]
93
96
 
94
97
  catalog_path: Path | None = None
@@ -24,7 +24,7 @@ from airbyte_cdk.test.standard_tests._job_runner import IConnector, run_test_job
24
24
  from airbyte_cdk.test.standard_tests.models import (
25
25
  ConnectorTestScenario,
26
26
  )
27
- from airbyte_cdk.test.standard_tests.test_resources import (
27
+ from airbyte_cdk.utils.connector_paths import (
28
28
  ACCEPTANCE_TEST_CONFIG,
29
29
  find_connector_root,
30
30
  )
@@ -89,7 +89,7 @@ class ConnectorTestSuiteBase(abc.ABC):
89
89
  @classmethod
90
90
  def create_connector(
91
91
  cls,
92
- scenario: ConnectorTestScenario,
92
+ scenario: ConnectorTestScenario | None,
93
93
  ) -> IConnector:
94
94
  """Instantiate the connector class."""
95
95
  connector = cls.connector # type: ignore
@@ -147,28 +147,35 @@ class ConnectorTestSuiteBase(abc.ABC):
147
147
  This has to be a separate function because pytest does not allow
148
148
  parametrization of fixtures with arguments from the test class itself.
149
149
  """
150
- category = "connection"
150
+ categories = ["connection", "spec"]
151
151
  all_tests_config = yaml.safe_load(cls.acceptance_test_config_path.read_text())
152
152
  if "acceptance_tests" not in all_tests_config:
153
153
  raise ValueError(
154
154
  f"Acceptance tests config not found in {cls.acceptance_test_config_path}."
155
155
  f" Found only: {str(all_tests_config)}."
156
156
  )
157
- if category not in all_tests_config["acceptance_tests"]:
158
- return []
159
- if "tests" not in all_tests_config["acceptance_tests"][category]:
160
- raise ValueError(f"No tests found for category {category}")
161
-
162
- tests_scenarios = [
163
- ConnectorTestScenario.model_validate(test)
164
- for test in all_tests_config["acceptance_tests"][category]["tests"]
165
- if "iam_role" not in test["config_path"]
166
- ]
157
+
158
+ test_scenarios: list[ConnectorTestScenario] = []
159
+ for category in categories:
160
+ if (
161
+ category not in all_tests_config["acceptance_tests"]
162
+ or "tests" not in all_tests_config["acceptance_tests"][category]
163
+ ):
164
+ continue
165
+
166
+ test_scenarios.extend(
167
+ [
168
+ ConnectorTestScenario.model_validate(test)
169
+ for test in all_tests_config["acceptance_tests"][category]["tests"]
170
+ if "config_path" in test and "iam_role" not in test["config_path"]
171
+ ]
172
+ )
173
+
167
174
  connector_root = cls.get_connector_root_dir().absolute()
168
- for test in tests_scenarios:
175
+ for test in test_scenarios:
169
176
  if test.config_path:
170
177
  test.config_path = connector_root / test.config_path
171
178
  if test.configured_catalog_path:
172
179
  test.configured_catalog_path = connector_root / test.configured_catalog_path
173
180
 
174
- return tests_scenarios
181
+ return test_scenarios
@@ -12,7 +12,7 @@ from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
12
12
  from airbyte_cdk.test.standard_tests._job_runner import IConnector
13
13
  from airbyte_cdk.test.standard_tests.models import ConnectorTestScenario
14
14
  from airbyte_cdk.test.standard_tests.source_base import SourceTestSuiteBase
15
- from airbyte_cdk.test.standard_tests.test_resources import MANIFEST_YAML
15
+ from airbyte_cdk.utils.connector_paths import MANIFEST_YAML
16
16
 
17
17
 
18
18
  def md5_checksum(file_path: Path) -> str:
@@ -64,7 +64,7 @@ class DeclarativeSourceTestSuite(SourceTestSuiteBase):
64
64
  @classmethod
65
65
  def create_connector(
66
66
  cls,
67
- scenario: ConnectorTestScenario,
67
+ scenario: ConnectorTestScenario | None,
68
68
  ) -> IConnector:
69
69
  """Create a connector scenario for the test suite.
70
70
 
@@ -73,9 +73,13 @@ class DeclarativeSourceTestSuite(SourceTestSuiteBase):
73
73
 
74
74
  Subclasses should not need to override this method.
75
75
  """
76
- config: dict[str, Any] = scenario.get_config_dict()
77
-
76
+ scenario = scenario or ConnectorTestScenario() # Use default (empty) scenario if None
78
77
  manifest_dict = yaml.safe_load(cls.manifest_yaml_path.read_text())
78
+ config = {
79
+ "__injected_manifest": manifest_dict,
80
+ }
81
+ config.update(scenario.get_config_dict(empty_if_missing=True))
82
+
79
83
  if cls.components_py_path and cls.components_py_path.exists():
80
84
  os.environ["AIRBYTE_ENABLE_UNSAFE_CODE"] = "true"
81
85
  config["__injected_components_py"] = cls.components_py_path.read_text()
@@ -43,18 +43,29 @@ class ConnectorTestScenario(BaseModel):
43
43
  file_types: AcceptanceTestFileTypes | None = None
44
44
  status: Literal["succeed", "failed"] | None = None
45
45
 
46
- def get_config_dict(self) -> dict[str, Any]:
46
+ def get_config_dict(
47
+ self,
48
+ *,
49
+ empty_if_missing: bool,
50
+ ) -> dict[str, Any]:
47
51
  """Return the config dictionary.
48
52
 
49
53
  If a config dictionary has already been loaded, return it. Otherwise, load
50
54
  the config file and return the dictionary.
55
+
56
+ If `self.config_dict` and `self.config_path` are both `None`:
57
+ - return an empty dictionary if `empty_if_missing` is True
58
+ - raise a ValueError if `empty_if_missing` is False
51
59
  """
52
- if self.config_dict:
60
+ if self.config_dict is not None:
53
61
  return self.config_dict
54
62
 
55
- if self.config_path:
63
+ if self.config_path is not None:
56
64
  return cast(dict[str, Any], yaml.safe_load(self.config_path.read_text()))
57
65
 
66
+ if empty_if_missing:
67
+ return {}
68
+
58
69
  raise ValueError("No config dictionary or path provided.")
59
70
 
60
71
  @property
@@ -64,6 +64,30 @@ class SourceTestSuiteBase(ConnectorTestSuiteBase):
64
64
  test_scenario=scenario,
65
65
  )
66
66
 
67
+ def test_spec(self) -> None:
68
+ """Standard test for `spec`.
69
+
70
+ This test does not require a `scenario` input, since `spec`
71
+ does not require any inputs.
72
+
73
+ We assume `spec` should always succeed and it should always generate
74
+ a valid `SPEC` message.
75
+
76
+ Note: the parsing of messages by type also implicitly validates that
77
+ the generated `SPEC` message is valid JSON.
78
+ """
79
+ result = run_test_job(
80
+ verb="spec",
81
+ test_scenario=None,
82
+ connector=self.create_connector(scenario=None),
83
+ )
84
+ # If an error occurs, it will be raised above.
85
+
86
+ assert len(result.spec_messages) == 1, (
87
+ "Expected exactly 1 spec message but got {len(result.spec_messages)}",
88
+ result.errors,
89
+ )
90
+
67
91
  def test_basic_read(
68
92
  self,
69
93
  scenario: ConnectorTestScenario,
@@ -12,7 +12,7 @@ from airbyte_cdk.test.standard_tests.declarative_sources import (
12
12
  )
13
13
  from airbyte_cdk.test.standard_tests.destination_base import DestinationTestSuiteBase
14
14
  from airbyte_cdk.test.standard_tests.source_base import SourceTestSuiteBase
15
- from airbyte_cdk.test.standard_tests.test_resources import (
15
+ from airbyte_cdk.utils.connector_paths import (
16
16
  METADATA_YAML,
17
17
  find_connector_root_from_name,
18
18
  )
@@ -0,0 +1,223 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """Resources and utilities for locating Airbyte Connectors."""
3
+
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+
7
+ ACCEPTANCE_TEST_CONFIG = "acceptance-test-config.yml"
8
+ MANIFEST_YAML = "manifest.yaml"
9
+ METADATA_YAML = "metadata.yaml"
10
+
11
+
12
+ def resolve_airbyte_repo_root(
13
+ from_dir: Path,
14
+ ) -> Path:
15
+ """Resolve the Airbyte repository root directory.
16
+
17
+ This function will resolve the Airbyte repository root directory based on the
18
+ current working directory. If the current working directory is not within the
19
+ Airbyte repository, it will look for the 'airbyte' or 'airbyte-enterprise'
20
+ directory in the parent directories.
21
+
22
+ Sibling directories are also considered, so if the working directory is '~/repos/airbyte-cdk',
23
+ it will find the 'airbyte' directory in '~/repos/airbyte'. The 'airbyte' directory
24
+ will be preferred over 'airbyte-enterprise' if both are present as sibling directories and
25
+ neither is a parent directory.
26
+
27
+ If we reach the root of the filesystem without finding the 'airbyte' directory,
28
+ a FileNotFoundError will be raised.
29
+
30
+ Raises:
31
+ FileNotFoundError: If the Airbyte repository root directory cannot be found.
32
+ """
33
+
34
+ def _is_airbyte_repo_root(path: Path) -> bool:
35
+ """Check if the given path is the Airbyte repository root."""
36
+ return all(
37
+ [
38
+ (path.name == "airbyte" or path.name == "airbyte-enterprise"),
39
+ (path / "airbyte-integrations").is_dir(),
40
+ ]
41
+ )
42
+
43
+ def _find_in_adjacent_dirs(current_dir: Path) -> Path | None:
44
+ """Check if 'airbyte' or 'airbyte-enterprise' exists as a sibling, parent, or child."""
45
+ # Check parents
46
+ parent_dir = current_dir.parent
47
+ if _is_airbyte_repo_root(parent_dir):
48
+ return parent_dir
49
+
50
+ # Check siblings
51
+ if _is_airbyte_repo_root(parent_dir / "airbyte"):
52
+ return parent_dir / "airbyte"
53
+ if _is_airbyte_repo_root(parent_dir / "airbyte-enterprise"):
54
+ return parent_dir / "airbyte-enterprise"
55
+
56
+ # Check children only if no "airbyte" or "airbyte-enterprise" in parent
57
+ if not any(
58
+ [
59
+ "airbyte" in current_dir.parts,
60
+ "airbyte-enterprise" in current_dir.parts,
61
+ ]
62
+ ):
63
+ if _is_airbyte_repo_root(current_dir / "airbyte"):
64
+ return current_dir / "airbyte"
65
+ if _is_airbyte_repo_root(current_dir / "airbyte-enterprise"):
66
+ return current_dir / "airbyte-enterprise"
67
+
68
+ return None
69
+
70
+ current_dir = from_dir.resolve().absolute()
71
+ while current_dir != current_dir.parent: # abort when we reach file system root
72
+ found_dir = _find_in_adjacent_dirs(current_dir)
73
+ if found_dir:
74
+ return found_dir
75
+
76
+ # Move up one directory
77
+ current_dir = current_dir.parent
78
+
79
+ raise FileNotFoundError(
80
+ f"Could not find the Airbyte repository root directory. Current directory: {from_dir}"
81
+ )
82
+
83
+
84
+ def resolve_connector_name_and_directory(
85
+ connector_ref: str | Path | None = None,
86
+ *,
87
+ connector_directory: Path | None = None,
88
+ ) -> tuple[str, Path]:
89
+ """Resolve the connector name and directory.
90
+
91
+ This function will resolve the connector name and directory based on the provided
92
+ reference. If no input ref is provided, it will look within the
93
+ current working directory. If the current working directory is not a connector
94
+ directory (e.g. starting with 'source-') and no connector name or path is provided,
95
+ the process will fail.
96
+ If ref is sent as a string containing "/" or "\\", it will be treated as a path to the
97
+ connector directory.
98
+
99
+ raises:
100
+ ValueError: If the connector name or directory cannot be resolved.
101
+ FileNotFoundError: If the connector directory does not exist or cannot be found.
102
+ """
103
+ connector_name: str | None = None
104
+
105
+ # Resolve connector_ref to connector_name or connector_directory (if provided)
106
+ if connector_ref:
107
+ if isinstance(connector_ref, str):
108
+ if "/" in connector_ref or "\\" in connector_ref:
109
+ # If the connector name is a path, treat it as a directory
110
+ connector_directory = Path(connector_ref)
111
+ else:
112
+ # Otherwise, treat it as a connector name
113
+ connector_name = connector_ref
114
+ elif isinstance(connector_ref, Path):
115
+ connector_directory = connector_ref
116
+ else:
117
+ raise ValueError(
118
+ "connector_ref must be a string or Path. "
119
+ f"Received type '{type(connector_ref).__name__}': {connector_ref!r}",
120
+ )
121
+
122
+ if not connector_directory:
123
+ if connector_name:
124
+ connector_directory = find_connector_root_from_name(connector_name)
125
+ else:
126
+ cwd = Path().resolve().absolute()
127
+ if cwd.name.startswith("source-") or cwd.name.startswith("destination-"):
128
+ connector_directory = cwd
129
+ else:
130
+ raise ValueError(
131
+ "The 'connector' input must be provided if not "
132
+ "running from a connector directory. "
133
+ f"Could not infer connector directory from: {cwd}"
134
+ )
135
+
136
+ if not connector_name:
137
+ connector_name = connector_directory.name
138
+
139
+ if connector_directory:
140
+ connector_directory = connector_directory.resolve().absolute()
141
+ elif connector_name:
142
+ connector_directory = find_connector_root_from_name(connector_name)
143
+ else:
144
+ raise ValueError(
145
+ f"Could not infer connector_name or connector_directory from input ref: {connector_ref}",
146
+ )
147
+
148
+ return connector_name, connector_directory
149
+
150
+
151
+ def resolve_connector_name(
152
+ connector_directory: Path,
153
+ ) -> str:
154
+ """Resolve the connector name.
155
+
156
+ This function will resolve the connector name based on the provided connector directory.
157
+ If the current working directory is not a connector directory
158
+ (e.g. starting with 'source-'), the process will fail.
159
+
160
+ Raises:
161
+ FileNotFoundError: If the connector directory does not exist or cannot be found.
162
+ """
163
+ if not connector_directory:
164
+ raise FileNotFoundError(
165
+ "Connector directory does not exist or cannot be found. Please provide a valid "
166
+ "connector directory."
167
+ )
168
+ connector_name = connector_directory.absolute().name
169
+ if not connector_name.startswith("source-") and not connector_name.startswith("destination-"):
170
+ raise ValueError(
171
+ f"Connector directory '{connector_name}' does not look like a valid connector directory. "
172
+ f"Full path: {connector_directory.absolute()}"
173
+ )
174
+ return connector_name
175
+
176
+
177
+ def find_connector_root(from_paths: list[Path]) -> Path:
178
+ """Find the root directory of the connector."""
179
+ for path in from_paths:
180
+ # If we reach here, we didn't find the manifest file in any parent directory
181
+ # Check if the manifest file exists in the current directory
182
+ for parent in [path, *path.parents]:
183
+ if (parent / METADATA_YAML).exists():
184
+ return parent
185
+ if (parent / MANIFEST_YAML).exists():
186
+ return parent
187
+ if (parent / ACCEPTANCE_TEST_CONFIG).exists():
188
+ return parent
189
+ if parent.name == "airbyte_cdk":
190
+ break
191
+
192
+ raise FileNotFoundError(
193
+ "Could not find connector root directory relative to the provided directories: "
194
+ f"'{str(from_paths)}'."
195
+ )
196
+
197
+
198
+ def find_connector_root_from_name(connector_name: str) -> Path:
199
+ """Find the root directory of the connector from its name."""
200
+ with suppress(FileNotFoundError):
201
+ return find_connector_root([Path(connector_name)])
202
+
203
+ # If the connector name is not found, check if we are in the airbyte monorepo
204
+ # and try to find the connector root from the current directory.
205
+
206
+ cwd: Path = Path().absolute()
207
+
208
+ try:
209
+ airbyte_repo_root: Path = resolve_airbyte_repo_root(cwd)
210
+ except FileNotFoundError as ex:
211
+ raise FileNotFoundError(
212
+ "Could not find connector root directory relative and we are not in the airbyte repo."
213
+ ) from ex
214
+
215
+ expected_connector_dir: Path = (
216
+ airbyte_repo_root / "airbyte-integrations" / "connectors" / connector_name
217
+ )
218
+ if not expected_connector_dir.exists():
219
+ raise FileNotFoundError(
220
+ f"Could not find connector directory '{expected_connector_dir}' relative to the airbyte repo."
221
+ )
222
+
223
+ return expected_connector_dir
@@ -12,14 +12,10 @@ from enum import Enum
12
12
  from pathlib import Path
13
13
 
14
14
  import click
15
+ import requests
15
16
 
16
17
  from airbyte_cdk.models.connector_metadata import ConnectorLanguage, MetadataFile
17
- from airbyte_cdk.utils.docker_image_templates import (
18
- DOCKERIGNORE_TEMPLATE,
19
- JAVA_CONNECTOR_DOCKERFILE_TEMPLATE,
20
- MANIFEST_ONLY_DOCKERFILE_TEMPLATE,
21
- PYTHON_CONNECTOR_DOCKERFILE_TEMPLATE,
22
- )
18
+ from airbyte_cdk.utils.connector_paths import resolve_airbyte_repo_root
23
19
 
24
20
 
25
21
  @dataclass(kw_only=True)
@@ -145,6 +141,7 @@ def build_connector_image(
145
141
  tag: str,
146
142
  primary_arch: ArchEnum = ArchEnum.ARM64, # Assume MacBook M series by default
147
143
  no_verify: bool = False,
144
+ dockerfile_override: Path | None = None,
148
145
  ) -> None:
149
146
  """Build a connector Docker image.
150
147
 
@@ -167,10 +164,36 @@ def build_connector_image(
167
164
  ConnectorImageBuildError: If the image build or tag operation fails.
168
165
  """
169
166
  connector_kebab_name = connector_name
170
- connector_snake_name = connector_kebab_name.replace("-", "_")
171
167
 
172
- dockerfile_path = connector_directory / "build" / "docker" / "Dockerfile"
173
- dockerignore_path = connector_directory / "build" / "docker" / "Dockerfile.dockerignore"
168
+ if dockerfile_override:
169
+ dockerfile_path = dockerfile_override
170
+ else:
171
+ dockerfile_path = connector_directory / "build" / "docker" / "Dockerfile"
172
+ dockerignore_path = connector_directory / "build" / "docker" / "Dockerfile.dockerignore"
173
+ try:
174
+ dockerfile_text, dockerignore_text = get_dockerfile_templates(
175
+ metadata=metadata,
176
+ connector_directory=connector_directory,
177
+ )
178
+ except FileNotFoundError:
179
+ # If the Dockerfile and .dockerignore are not found in the connector directory,
180
+ # download the templates from the Airbyte repo. This is a fallback
181
+ # in case the Airbyte repo not checked out locally.
182
+ try:
183
+ dockerfile_text, dockerignore_text = _download_dockerfile_defs(
184
+ connector_language=metadata.data.language,
185
+ )
186
+ except requests.HTTPError as e:
187
+ raise ConnectorImageBuildError(
188
+ build_args=[],
189
+ error_text=(
190
+ "Could not locate local dockerfile templates and "
191
+ f"failed to download Dockerfile templates from github: {e}"
192
+ ),
193
+ ) from e
194
+
195
+ dockerfile_path.write_text(dockerfile_text)
196
+ dockerignore_path.write_text(dockerignore_text)
174
197
 
175
198
  extra_build_script: str = ""
176
199
  build_customization_path = connector_directory / "build_customization.py"
@@ -185,14 +208,9 @@ def build_connector_image(
185
208
  )
186
209
 
187
210
  base_image = metadata.data.connectorBuildOptions.baseImage
188
-
189
- dockerfile_path.write_text(get_dockerfile_template(metadata))
190
- dockerignore_path.write_text(DOCKERIGNORE_TEMPLATE)
191
-
192
211
  build_args: dict[str, str | None] = {
193
212
  "BASE_IMAGE": base_image,
194
- "CONNECTOR_SNAKE_NAME": connector_snake_name,
195
- "CONNECTOR_KEBAB_NAME": connector_kebab_name,
213
+ "CONNECTOR_NAME": connector_kebab_name,
196
214
  "EXTRA_BUILD_SCRIPT": extra_build_script,
197
215
  }
198
216
 
@@ -246,31 +264,100 @@ def build_connector_image(
246
264
  sys.exit(0)
247
265
 
248
266
 
249
- def get_dockerfile_template(
267
+ def _download_dockerfile_defs(
268
+ connector_language: ConnectorLanguage,
269
+ ) -> tuple[str, str]:
270
+ """Download the Dockerfile and .dockerignore templates for the specified connector language.
271
+
272
+ We use the requests library to download from the master branch hosted on GitHub.
273
+
274
+ Args:
275
+ connector_language: The language of the connector.
276
+
277
+ Returns:
278
+ A tuple containing the Dockerfile and .dockerignore templates as strings.
279
+
280
+ Raises:
281
+ ValueError: If the connector language is not supported.
282
+ requests.HTTPError: If the download fails.
283
+ """
284
+ print("Downloading Dockerfile and .dockerignore templates from GitHub...")
285
+ # Map ConnectorLanguage to template directory
286
+ language_to_template_suffix = {
287
+ ConnectorLanguage.PYTHON: "python-connector",
288
+ ConnectorLanguage.JAVA: "java-connector",
289
+ ConnectorLanguage.MANIFEST_ONLY: "manifest-only-connector",
290
+ }
291
+
292
+ if connector_language not in language_to_template_suffix:
293
+ raise ValueError(f"Unsupported connector language: {connector_language}")
294
+
295
+ template_suffix = language_to_template_suffix[connector_language]
296
+ base_url = f"https://github.com/airbytehq/airbyte/raw/master/docker-images/"
297
+
298
+ dockerfile_url = f"{base_url}/Dockerfile.{template_suffix}"
299
+ dockerignore_url = f"{base_url}/Dockerfile.{template_suffix}.dockerignore"
300
+
301
+ dockerfile_resp = requests.get(dockerfile_url)
302
+ dockerfile_resp.raise_for_status()
303
+ dockerfile_text = dockerfile_resp.text
304
+
305
+ dockerignore_resp = requests.get(dockerignore_url)
306
+ dockerignore_resp.raise_for_status()
307
+ dockerignore_text = dockerignore_resp.text
308
+
309
+ return dockerfile_text, dockerignore_text
310
+
311
+
312
+ def get_dockerfile_templates(
250
313
  metadata: MetadataFile,
251
- ) -> str:
314
+ connector_directory: Path,
315
+ ) -> tuple[str, str]:
252
316
  """Get the Dockerfile template for the connector.
253
317
 
254
318
  Args:
255
319
  metadata: The metadata of the connector.
256
320
  connector_name: The name of the connector.
257
321
 
322
+ Raises:
323
+ ValueError: If the connector language is not supported.
324
+ FileNotFoundError: If the Dockerfile or .dockerignore is not found.
325
+
258
326
  Returns:
259
- The Dockerfile template as a string.
327
+ A tuple containing the Dockerfile and .dockerignore templates as strings.
260
328
  """
261
- if metadata.data.language == ConnectorLanguage.PYTHON:
262
- return PYTHON_CONNECTOR_DOCKERFILE_TEMPLATE
263
-
264
- if metadata.data.language == ConnectorLanguage.MANIFEST_ONLY:
265
- return MANIFEST_ONLY_DOCKERFILE_TEMPLATE
266
-
267
- if metadata.data.language == ConnectorLanguage.JAVA:
268
- return JAVA_CONNECTOR_DOCKERFILE_TEMPLATE
329
+ if metadata.data.language not in [
330
+ ConnectorLanguage.PYTHON,
331
+ ConnectorLanguage.MANIFEST_ONLY,
332
+ ConnectorLanguage.JAVA,
333
+ ]:
334
+ raise ValueError(
335
+ f"Unsupported connector language: {metadata.data.language}. "
336
+ "Please check the connector's metadata file."
337
+ )
269
338
 
270
- raise ValueError(
271
- f"Unsupported connector language: {metadata.data.language}. "
272
- "Please check the connector's metadata file."
339
+ airbyte_repo_root = resolve_airbyte_repo_root(
340
+ from_dir=connector_directory,
341
+ )
342
+ # airbyte_repo_root successfully resolved
343
+ dockerfile_path = (
344
+ airbyte_repo_root / "docker-images" / f"Dockerfile.{metadata.data.language.value}-connector"
273
345
  )
346
+ dockerignore_path = (
347
+ airbyte_repo_root
348
+ / "docker-images"
349
+ / f"Dockerfile.{metadata.data.language.value}-connector.dockerignore"
350
+ )
351
+ if not dockerfile_path.exists():
352
+ raise FileNotFoundError(
353
+ f"Dockerfile for {metadata.data.language.value} connector not found at {dockerfile_path}"
354
+ )
355
+ if not dockerignore_path.exists():
356
+ raise FileNotFoundError(
357
+ f".dockerignore for {metadata.data.language.value} connector not found at {dockerignore_path}"
358
+ )
359
+
360
+ return dockerfile_path.read_text(), dockerignore_path.read_text()
274
361
 
275
362
 
276
363
  def run_docker_command(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 6.48.7.post2.dev14899985028
3
+ Version: 6.48.9
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -1,11 +1,10 @@
1
1
  airbyte_cdk/__init__.py,sha256=52uncJvDQNHvwKxaqzXgnMYTptIl65LDJr2fvlk8-DU,11707
2
2
  airbyte_cdk/cli/__init__.py,sha256=CXsai3MYMLZ_sqi2vPAIVcKDun8VRqlv0cKffBI0iSY,346
3
3
  airbyte_cdk/cli/airbyte_cdk/__init__.py,sha256=8IoEcbdYr7CMAh97Xut5__uHH9vV4LKUtSBNTk3qEWY,2031
4
- airbyte_cdk/cli/airbyte_cdk/_connector.py,sha256=wXwt7tbO-9qFYDDK4P8wbKJX43kbeoRZvK5SNX-Wnfs,5300
5
- airbyte_cdk/cli/airbyte_cdk/_image.py,sha256=F0XtvR2CyFi1EPIUBiEnDia9NfCuM7T_itdNj9yyb2E,2907
4
+ airbyte_cdk/cli/airbyte_cdk/_connector.py,sha256=drKb_EXJOFX-cSeLwJB8WXE-lesOL0dx2ziWSmW3Jkg,5187
5
+ airbyte_cdk/cli/airbyte_cdk/_image.py,sha256=AkBEZrRYXEwvhW7hPOPRWeYEZutzi2PIzjpl7_yaE8I,2890
6
6
  airbyte_cdk/cli/airbyte_cdk/_manifest.py,sha256=aFdeeWgek7oXR3YfZPxk7kBZ64Blmsr0dAXN6BVGiIA,482
7
- airbyte_cdk/cli/airbyte_cdk/_secrets.py,sha256=iRA8435sYDcWe6IBv4VAo-3yTIAFqySbDvmUsgpEInA,14712
8
- airbyte_cdk/cli/airbyte_cdk/_util.py,sha256=bnzBIAIvuukbOwYwHXay33yAkyyOVJw24xNjNXsPG74,2732
7
+ airbyte_cdk/cli/airbyte_cdk/_secrets.py,sha256=u8-G44SoVg19FCWjcNDT0brFUZa6g_-He58Nd-VuZHI,13991
9
8
  airbyte_cdk/cli/airbyte_cdk/_version.py,sha256=ohZNIktLFk91sdzqFW5idaNrZAPX2dIRnz---_fcKOE,352
10
9
  airbyte_cdk/cli/source_declarative_manifest/__init__.py,sha256=-0ST722Nj65bgRokzpzPkD1NBBW5CytEHFUe38cB86Q,91
11
10
  airbyte_cdk/cli/source_declarative_manifest/_run.py,sha256=9qtbjt-I_stGWzWX6yVUKO_eE-Ga7g-uTuibML9qLBs,8330
@@ -366,7 +365,7 @@ airbyte_cdk/sql/shared/sql_processor.py,sha256=1CwfC3fp9dWnHBpKtly7vGduf9ho_Mahi
366
365
  airbyte_cdk/sql/types.py,sha256=XEIhRAo_ASd0kVLBkdLf5bHiRhNple-IJrC9TibcDdY,5880
367
366
  airbyte_cdk/test/__init__.py,sha256=f_XdkOg4_63QT2k3BbKY34209lppwgw-svzfZstQEq4,199
368
367
  airbyte_cdk/test/catalog_builder.py,sha256=-y05Cz1x0Dlk6oE9LSKhCozssV2gYBNtMdV5YYOPOtk,3015
369
- airbyte_cdk/test/entrypoint_wrapper.py,sha256=TyUmVJyIuGelAv6y8Wy_BnwqIRw_drjfZWKlroljCuQ,9951
368
+ airbyte_cdk/test/entrypoint_wrapper.py,sha256=UsuvbgXDki00-hgpQgTeoSTr_Sxv6UXYfymHM0jhGwg,10074
370
369
  airbyte_cdk/test/mock_http/__init__.py,sha256=jE5kC6CQ0OXkTqKhciDnNVZHesBFVIA2YvkdFGwva7k,322
371
370
  airbyte_cdk/test/mock_http/matcher.py,sha256=4Qj8UnJKZIs-eodshryce3SN1Ayc8GZpBETmP6hTEyc,1446
372
371
  airbyte_cdk/test/mock_http/mocker.py,sha256=XgsjMtVoeMpRELPyALgrkHFauH9H5irxrz1Kcxh2yFY,8013
@@ -374,16 +373,15 @@ airbyte_cdk/test/mock_http/request.py,sha256=tdB8cqk2vLgCDTOKffBKsM06llYs4ZecgtH
374
373
  airbyte_cdk/test/mock_http/response.py,sha256=s4-cQQqTtmeej0pQDWqmG0vUWpHS-93lIWMpW3zSVyU,662
375
374
  airbyte_cdk/test/mock_http/response_builder.py,sha256=F-v7ebftqGj7YVIMLKdodmU9U8Dq8aIyllWGo2NGwHc,8331
376
375
  airbyte_cdk/test/standard_tests/__init__.py,sha256=YS2bghoGmQ-4GNIbe6RuEmvV-V1kpM1OyxTpebrs0Ig,1338
377
- airbyte_cdk/test/standard_tests/_job_runner.py,sha256=d2JkwxJilYIJNmyVH946YMn8x1pnP3JaNT865V8vZzQ,5820
378
- airbyte_cdk/test/standard_tests/connector_base.py,sha256=WXzlfv8b7AsfEssfTm7dOg3KH114qXqHN2SLdPAvYwE,6800
379
- airbyte_cdk/test/standard_tests/declarative_sources.py,sha256=PQschGGdVgduPfkEYHwNPAz7kxbhKZYAoSmxryqJ5yc,3223
376
+ airbyte_cdk/test/standard_tests/_job_runner.py,sha256=k-gwwXtIJbh0BwH71uTST9LLx246RrnZMchACz-vIS8,6014
377
+ airbyte_cdk/test/standard_tests/connector_base.py,sha256=NMA0x8CA1tQYRhSv1x9lBxkveGfqaRHHt7ggxKAXgfA,6970
378
+ airbyte_cdk/test/standard_tests/declarative_sources.py,sha256=vzpW507HUPtYgP5VEK1MAR_mt1Gq2nk40sjNhKie5yM,3402
380
379
  airbyte_cdk/test/standard_tests/destination_base.py,sha256=MARZip2mdo_PzGvzf2VBTAfrP4tbjrJYgeJUApnAArA,731
381
380
  airbyte_cdk/test/standard_tests/models/__init__.py,sha256=bS25WlzQwPNxpU5DHtUDZo1DuXd0LkEv9qesNhY1jkY,135
382
- airbyte_cdk/test/standard_tests/models/scenario.py,sha256=loOfIeKWbzhqyY3JiLU-sE7RKDAsvz3SLpI9APeTPiw,2378
381
+ airbyte_cdk/test/standard_tests/models/scenario.py,sha256=kvuc5oIH-TA3TS7xtiz1RQ8tcY_WC0aI6-TsDQAqhUA,2710
383
382
  airbyte_cdk/test/standard_tests/pytest_hooks.py,sha256=4OMy2jNQThS8y7Tyj8MiMy2-SWjoefD4lGo-zQmCUfU,1886
384
- airbyte_cdk/test/standard_tests/source_base.py,sha256=B30Jduz5n7Z8oM3y9RF6B5upTZN3am6rFod6mqgIizo,5235
385
- airbyte_cdk/test/standard_tests/test_resources.py,sha256=fYYMRa50L_kUqXwr_aj9FuHhfcVEf4WLGocoRT1bnZI,2448
386
- airbyte_cdk/test/standard_tests/util.py,sha256=eeRP_WFEaIs4x-JnpQ7wWIA938zdDyDhvOeeAf0AnOs,2933
383
+ airbyte_cdk/test/standard_tests/source_base.py,sha256=cprlvFIRy0xqsLwAC38I9zWlxC2-PJapdje9kRGjTek,6047
384
+ airbyte_cdk/test/standard_tests/util.py,sha256=JrTXxLpcMvRJhnJDVzY8Y-TmcWhW7GGfNOnbS3v0pR4,2920
387
385
  airbyte_cdk/test/state_builder.py,sha256=kLPql9lNzUJaBg5YYRLJlY_Hy5JLHJDVyKPMZMoYM44,946
388
386
  airbyte_cdk/test/utils/__init__.py,sha256=Hu-1XT2KDoYjDF7-_ziDwv5bY3PueGjANOCbzeOegDg,57
389
387
  airbyte_cdk/test/utils/data.py,sha256=CkCR1_-rujWNmPXFR1IXTMwx1rAl06wAyIKWpDcN02w,820
@@ -393,11 +391,11 @@ airbyte_cdk/test/utils/reading.py,sha256=SOTDYlps6Te9KumfTJ3vVDSm9EUXhvKtE8aD7gv
393
391
  airbyte_cdk/utils/__init__.py,sha256=qhnC02DbS35OY8oB_tkYHwZzHed2FZeBM__G8IOgckY,347
394
392
  airbyte_cdk/utils/airbyte_secrets_utils.py,sha256=wEtRnl5KRhN6eLJwrDrC4FJjyqt_4vkA1F65mdl8c24,3142
395
393
  airbyte_cdk/utils/analytics_message.py,sha256=bi3uugQ2NjecnwTnz63iD5D1M8ZR8mXPbdtt6w5cC4s,653
394
+ airbyte_cdk/utils/connector_paths.py,sha256=3yIa3855Cf3EQiiQ6ILMovsjf8UcPsdAuQGfryOhYgE,8675
396
395
  airbyte_cdk/utils/constants.py,sha256=QzCi7j5SqpI5I06uRvQ8FC73JVJi7rXaRnR3E_gro5c,108
397
396
  airbyte_cdk/utils/datetime_format_inferrer.py,sha256=Ne2cpk7Tx3eZDEW2Q3O7jnNOY9g-w-AUMt3Ltvwg1tY,3989
398
397
  airbyte_cdk/utils/datetime_helpers.py,sha256=8mqzZ67Or2PBp7tLtrhh6XFv4wFzYsjCL_DOQJRaftI,17751
399
- airbyte_cdk/utils/docker.py,sha256=kM8DmRsA5a4_kJmZYwlAIaSjHeTfkVV-Gmp10ADkh80,11062
400
- airbyte_cdk/utils/docker_image_templates.py,sha256=X8f_aRgt_x8yIbrTfsSWw_oIERu65a8hNttcgXA7hlw,3417
398
+ airbyte_cdk/utils/docker.py,sha256=NS35XgIc6FbFg9IxzDazHrxqmzmi9fjA46qdDfTQ4e0,14585
401
399
  airbyte_cdk/utils/event_timing.py,sha256=aiuFmPU80buLlNdKq4fDTEqqhEIelHPF6AalFGwY8as,2557
402
400
  airbyte_cdk/utils/is_cloud_environment.py,sha256=DayV32Irh-SdnJ0MnjvstwCJ66_l5oEsd8l85rZtHoc,574
403
401
  airbyte_cdk/utils/mapping_helpers.py,sha256=nWjOpnz_5QE9tY9-GtSWMPvYQL95kDD6k8KwwjUmrh0,6526
@@ -409,9 +407,9 @@ airbyte_cdk/utils/slice_hasher.py,sha256=EDxgROHDbfG-QKQb59m7h_7crN1tRiawdf5uU7G
409
407
  airbyte_cdk/utils/spec_schema_transformations.py,sha256=-5HTuNsnDBAhj-oLeQXwpTGA0HdcjFOf2zTEMUTTg_Y,816
410
408
  airbyte_cdk/utils/stream_status_utils.py,sha256=ZmBoiy5HVbUEHAMrUONxZvxnvfV9CesmQJLDTAIWnWw,1171
411
409
  airbyte_cdk/utils/traced_exception.py,sha256=C8uIBuCL_E4WnBAOPSxBicD06JAldoN9fGsQDp463OY,6292
412
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
413
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
414
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/METADATA,sha256=g78XypWk60LikFsBy2VCCPA6OIMx1UC-pqGoknnOUuc,6364
415
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
416
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/entry_points.txt,sha256=AKWbEkHfpzzk9nF9tqBUaw1MbvTM4mGtEzmZQm0ZWvM,139
417
- airbyte_cdk-6.48.7.post2.dev14899985028.dist-info/RECORD,,
410
+ airbyte_cdk-6.48.9.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
411
+ airbyte_cdk-6.48.9.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
412
+ airbyte_cdk-6.48.9.dist-info/METADATA,sha256=gTA6WrjXLh_UFtrwmxoS5Ci4swMfmFMrnUta4edFt9A,6343
413
+ airbyte_cdk-6.48.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
414
+ airbyte_cdk-6.48.9.dist-info/entry_points.txt,sha256=AKWbEkHfpzzk9nF9tqBUaw1MbvTM4mGtEzmZQm0ZWvM,139
415
+ airbyte_cdk-6.48.9.dist-info/RECORD,,
@@ -1,69 +0,0 @@
1
- """Common utilities for Airbyte CDK CLI."""
2
-
3
- from pathlib import Path
4
-
5
- from airbyte_cdk.test.standard_tests.test_resources import find_connector_root_from_name
6
-
7
-
8
- def resolve_connector_name_and_directory(
9
- connector_name: str | None = None,
10
- connector_directory: Path | None = None,
11
- ) -> tuple[str, Path]:
12
- """Resolve the connector name and directory.
13
-
14
- This function will resolve the connector name and directory based on the provided
15
- arguments. If no connector name or directory is provided, it will look within the
16
- current working directory. If the current working directory is not a connector
17
- directory (e.g. starting with 'source-') and no connector name or path is provided,
18
- the process will fail.
19
- """
20
- if not connector_directory:
21
- if connector_name:
22
- connector_directory = find_connector_root_from_name(connector_name)
23
- else:
24
- cwd = Path().resolve().absolute()
25
- if cwd.name.startswith("source-") or cwd.name.startswith("destination-"):
26
- connector_directory = cwd
27
- else:
28
- raise ValueError(
29
- "Either connector_name or connector_directory must be provided if not "
30
- "running from a connector directory."
31
- )
32
-
33
- if not connector_name:
34
- connector_name = connector_directory.name
35
-
36
- if connector_directory:
37
- connector_directory = connector_directory.resolve().absolute()
38
- elif connector_name:
39
- connector_directory = find_connector_root_from_name(connector_name)
40
- else:
41
- raise ValueError("Either connector_name or connector_directory must be provided.")
42
-
43
- return connector_name, connector_directory
44
-
45
-
46
- def resolve_connector_name(
47
- connector_directory: Path,
48
- ) -> str:
49
- """Resolve the connector name.
50
-
51
- This function will resolve the connector name based on the provided connector directory.
52
- If the current working directory is not a connector directory
53
- (e.g. starting with 'source-'), the process will fail.
54
-
55
- Raises:
56
- FileNotFoundError: If the connector directory does not exist or cannot be found.
57
- """
58
- if not connector_directory:
59
- raise FileNotFoundError(
60
- "Connector directory does not exist or cannot be found. Please provide a valid "
61
- "connector directory."
62
- )
63
- connector_name = connector_directory.absolute().name
64
- if not connector_name.startswith("source-") and not connector_name.startswith("destination-"):
65
- raise ValueError(
66
- f"Connector directory '{connector_name}' does not look like a valid connector directory. "
67
- f"Full path: {connector_directory.absolute()}"
68
- )
69
- return connector_name
@@ -1,69 +0,0 @@
1
- # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
- """Resources for Airbyte CDK tests."""
3
-
4
- from contextlib import suppress
5
- from pathlib import Path
6
-
7
- ACCEPTANCE_TEST_CONFIG = "acceptance-test-config.yml"
8
- MANIFEST_YAML = "manifest.yaml"
9
- METADATA_YAML = "metadata.yaml"
10
-
11
-
12
- def find_connector_root(from_paths: list[Path]) -> Path:
13
- """Find the root directory of the connector."""
14
- for path in from_paths:
15
- # If we reach here, we didn't find the manifest file in any parent directory
16
- # Check if the manifest file exists in the current directory
17
- for parent in [path, *path.parents]:
18
- if (parent / METADATA_YAML).exists():
19
- return parent
20
- if (parent / MANIFEST_YAML).exists():
21
- return parent
22
- if (parent / ACCEPTANCE_TEST_CONFIG).exists():
23
- return parent
24
- if parent.name == "airbyte_cdk":
25
- break
26
-
27
- raise FileNotFoundError(
28
- "Could not find connector root directory relative to the provided directories: "
29
- f"'{str(from_paths)}'."
30
- )
31
-
32
-
33
- def find_connector_root_from_name(connector_name: str) -> Path:
34
- """Find the root directory of the connector from its name."""
35
- with suppress(FileNotFoundError):
36
- return find_connector_root([Path(connector_name)])
37
-
38
- # If the connector name is not found, check if we are in the airbyte monorepo
39
- # and try to find the connector root from the current directory.
40
-
41
- cwd: Path = Path().absolute()
42
-
43
- if "airbyte" not in cwd.parts:
44
- raise FileNotFoundError(
45
- "Could not find connector root directory relative and we are not in the airbyte repo. "
46
- f"Current directory: {cwd} "
47
- )
48
-
49
- # Find the connector root from the current directory
50
-
51
- airbyte_repo_root: Path
52
- for parent in [cwd, *cwd.parents]:
53
- if parent.name == "airbyte":
54
- airbyte_repo_root = parent
55
- break
56
- else:
57
- raise FileNotFoundError(
58
- "Could not find connector root directory relative and we are not in the airbyte repo."
59
- )
60
-
61
- expected_connector_dir: Path = (
62
- airbyte_repo_root / "airbyte-integrations" / "connectors" / connector_name
63
- )
64
- if not expected_connector_dir.exists():
65
- raise FileNotFoundError(
66
- f"Could not find connector directory '{expected_connector_dir}' relative to the airbyte repo."
67
- )
68
-
69
- return expected_connector_dir
@@ -1,136 +0,0 @@
1
- # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
- """A collection of Dockerfile templates for building Airbyte connectors.
3
-
4
- The templates are designed to be used with the Airbyte CDK and can be customized
5
- for different connectors and architectures.
6
-
7
- These templates are used to generate connector images.
8
- """
9
-
10
- ##############################
11
- ## GLOBAL DOCKERIGNORE FILE ##
12
- ##############################
13
-
14
- DOCKERIGNORE_TEMPLATE: str = "\n".join(
15
- [
16
- "# This file is auto-generated. Do not edit.",
17
- "*," # Ignore everything not explicitly allowed below
18
- "build/",
19
- "!build/distributions/*.tar",
20
- ".venv/",
21
- "secrets/",
22
- "!setup.py",
23
- "!pyproject.toml",
24
- "!poetry.lock",
25
- "!poetry.toml",
26
- "!components.py",
27
- "!requirements.txt",
28
- "!README.md",
29
- "!metadata.yaml",
30
- "!build_customization.py",
31
- "!source_*",
32
- "!destination_*",
33
- ]
34
- )
35
-
36
- ###########################
37
- # PYTHON CONNECTOR IMAGE ##
38
- ###########################
39
-
40
- PYTHON_CONNECTOR_DOCKERFILE_TEMPLATE = r"""
41
- # syntax=docker/dockerfile:1
42
- # check=skip=all
43
- ARG BASE_IMAGE
44
-
45
- FROM ${BASE_IMAGE} AS builder
46
- ARG BASE_IMAGE
47
- ARG CONNECTOR_SNAKE_NAME
48
- ARG CONNECTOR_KEBAB_NAME
49
- ARG EXTRA_PREREQS_SCRIPT=""
50
-
51
- WORKDIR /airbyte/integration_code
52
-
53
- COPY . ./
54
-
55
- # Conditionally copy and execute the extra build script if provided
56
- RUN if [ -n "${EXTRA_PREREQS_SCRIPT}" ]; then \
57
- cp ${EXTRA_PREREQS_SCRIPT} ./extra_prereqs_script && \
58
- ./extra_prereqs_script; \
59
- fi
60
-
61
- # TODO: Pre-install uv on the base image to speed up the build.
62
- # (uv is still faster even with the extra step.)
63
- RUN pip install --no-cache-dir uv
64
- RUN python -m uv pip install --no-cache-dir .
65
-
66
- FROM ${BASE_IMAGE}
67
- ARG CONNECTOR_SNAKE_NAME
68
- ARG CONNECTOR_KEBAB_NAME
69
- ARG BASE_IMAGE
70
-
71
- WORKDIR /airbyte/integration_code
72
-
73
- COPY --from=builder /usr/local /usr/local
74
- COPY --chmod=755 <<EOT /entrypoint.sh
75
- #!/usr/bin/env bash
76
- set -e
77
-
78
- ${CONNECTOR_KEBAB_NAME} "\$\@"
79
- EOT
80
-
81
- ENV AIRBYTE_ENTRYPOINT="/entrypoint.sh"
82
- ENTRYPOINT ["/entrypoint.sh"]
83
- """
84
-
85
- ##################################
86
- # MANIFEST-ONLY CONNECTOR IMAGE ##
87
- ##################################
88
-
89
- MANIFEST_ONLY_DOCKERFILE_TEMPLATE = """
90
- ARG BASE_IMAGE
91
- ARG CONNECTOR_SNAKE_NAME
92
- ARG CONNECTOR_KEBAB_NAME
93
-
94
- FROM ${BASE_IMAGE}
95
-
96
- WORKDIR /airbyte/integration_code
97
-
98
- COPY . ./
99
-
100
- ENV AIRBYTE_ENTRYPOINT="python ./main.py"
101
- ENTRYPOINT ["python", "./main.py"]
102
- """
103
-
104
- #########################
105
- # JAVA CONNECTOR IMAGE ##
106
- #########################
107
-
108
- JAVA_CONNECTOR_DOCKERFILE_TEMPLATE = """
109
- # Java connector Dockerfile for Airbyte
110
-
111
- # Build arguments
112
- ARG BASE_IMAGE
113
-
114
- # Base image - using Amazon Corretto (Amazon's distribution of OpenJDK)
115
- FROM ${BASE_IMAGE}
116
- ARG CONNECTOR_KEBAB_NAME
117
-
118
- # Set permissions for downloaded files
119
- RUN chmod +x /airbyte/base.sh /airbyte/javabase.sh && \
120
- chown airbyte:airbyte /airbyte/base.sh /airbyte/javabase.sh /airbyte/dd-java-agent.jar
121
-
122
- ENV AIRBYTE_ENTRYPOINT="/airbyte/base.sh"
123
- ENV APPLICATION="${CONNECTOR_KEBAB_NAME}"
124
-
125
- # Add the connector TAR file and extract it
126
- COPY ./build/distributions/${CONNECTOR_KEBAB_NAME}.tar /tmp/${CONNECTOR_KEBAB_NAME}.tar
127
- RUN tar xf /tmp/${CONNECTOR_KEBAB_NAME}.tar --strip-components=1 -C /airbyte && \
128
- rm -rf /tmp/${CONNECTOR_KEBAB_NAME}.tar && \
129
- chown -R airbyte:airbyte /airbyte
130
-
131
- # Set the non-root user
132
- USER airbyte
133
-
134
- # Set entrypoint
135
- ENTRYPOINT ["/airbyte/base.sh"]
136
- """