Skip to content

Commit 47d7c16

Browse files
authored
Fix: Auto-use manifests when declared in the connector registry (#347)
1 parent c5e4c20 commit 47d7c16

File tree

11 files changed

+319
-167
lines changed

11 files changed

+319
-167
lines changed

airbyte/_executors/declarative.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from __future__ import annotations
55

6-
import json
76
import warnings
87
from pathlib import Path
98
from typing import IO, TYPE_CHECKING, cast
@@ -63,13 +62,11 @@ def __init__(
6362
self._validate_manifest(self._manifest_dict)
6463
self.declarative_source = ManifestDeclarativeSource(source_config=self._manifest_dict)
6564

66-
# TODO: Consider adding version detection
67-
# https://github.com/airbytehq/airbyte/issues/318
68-
self.reported_version: str | None = None
65+
self.reported_version: str | None = self._manifest_dict.get("version", None)
6966

7067
def _validate_manifest(self, manifest_dict: dict) -> None:
7168
"""Validate the manifest."""
72-
manifest_text = json.dumps(manifest_dict)
69+
manifest_text = yaml.safe_dump(manifest_dict)
7370
if "class_name:" in manifest_text:
7471
raise exc.AirbyteConnectorInstallationError(
7572
message=(

airbyte/_executors/util.py

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
22
from __future__ import annotations
33

4-
import shutil
5-
import sys
64
import tempfile
75
from pathlib import Path
8-
from typing import TYPE_CHECKING, cast
6+
from typing import TYPE_CHECKING, Literal, cast
97

108
import requests
119
import yaml
@@ -17,8 +15,9 @@
1715
from airbyte._executors.docker import DockerExecutor
1816
from airbyte._executors.local import PathExecutor
1917
from airbyte._executors.python import VenvExecutor
18+
from airbyte._util.meta import which
2019
from airbyte._util.telemetry import EventState, log_install_state # Non-public API
21-
from airbyte.sources.registry import ConnectorMetadata, get_connector_metadata
20+
from airbyte.sources.registry import ConnectorMetadata, InstallType, get_connector_metadata
2221

2322

2423
if TYPE_CHECKING:
@@ -77,33 +76,71 @@ def _try_get_source_manifest(source_name: str, manifest_url: str | None) -> dict
7776
return result_1
7877

7978

80-
def get_connector_executor( # noqa: PLR0912, PLR0913, PLR0915 # Too complex
79+
def _get_local_executor(
80+
name: str,
81+
local_executable: Path | str | Literal[True],
82+
version: str | None,
83+
) -> Executor:
84+
"""Get a local executor for a connector."""
85+
if version:
86+
raise exc.PyAirbyteInputError(
87+
message="Param 'version' is not supported when 'local_executable' is set."
88+
)
89+
90+
if local_executable is True:
91+
# Use the default executable name for the connector
92+
local_executable = name
93+
94+
if isinstance(local_executable, str):
95+
if "/" in local_executable or "\\" in local_executable:
96+
# Assume this is a path
97+
local_executable = Path(local_executable).absolute()
98+
else:
99+
which_executable: Path | None = which(local_executable)
100+
if not which_executable:
101+
raise exc.AirbyteConnectorExecutableNotFoundError(
102+
connector_name=name,
103+
context={
104+
"executable": name,
105+
"working_directory": Path.cwd().absolute(),
106+
},
107+
) from FileNotFoundError(name)
108+
local_executable = Path(which_executable).absolute()
109+
110+
# `local_executable` is now a Path object
111+
112+
print(f"Using local `{name}` executable: {local_executable!s}")
113+
return PathExecutor(
114+
name=name,
115+
path=local_executable,
116+
)
117+
118+
119+
def get_connector_executor( # noqa: PLR0912, PLR0913 # Too complex
81120
name: str,
82121
*,
83122
version: str | None = None,
84123
pip_url: str | None = None,
85124
local_executable: Path | str | None = None,
86-
docker_image: bool | str | None = False,
125+
docker_image: bool | str | None = None,
87126
use_host_network: bool = False,
88-
source_manifest: bool | dict | Path | str = False,
127+
source_manifest: bool | dict | Path | str | None = None,
89128
install_if_missing: bool = True,
90129
install_root: Path | None = None,
91130
) -> Executor:
92131
"""This factory function creates an executor for a connector.
93132
94133
For documentation of each arg, see the function `airbyte.sources.util.get_source()`.
95134
"""
96-
if (
97-
sum(
98-
[
99-
bool(local_executable),
100-
bool(docker_image),
101-
bool(pip_url),
102-
bool(source_manifest),
103-
]
104-
)
105-
> 1
106-
):
135+
install_method_count = sum(
136+
[
137+
bool(local_executable),
138+
bool(docker_image),
139+
bool(pip_url),
140+
bool(source_manifest),
141+
]
142+
)
143+
if install_method_count > 1:
107144
raise exc.PyAirbyteInputError(
108145
message=(
109146
"You can only specify one of the settings: 'local_executable', 'docker_image', "
@@ -116,40 +153,37 @@ def get_connector_executor( # noqa: PLR0912, PLR0913, PLR0915 # Too complex
116153
"source_manifest": source_manifest,
117154
},
118155
)
156+
metadata: ConnectorMetadata | None = None
157+
try:
158+
metadata = get_connector_metadata(name)
159+
except exc.AirbyteConnectorNotRegisteredError as ex:
160+
if install_method_count == 0:
161+
# User has not specified how to install the connector, and it is not registered.
162+
# Fail the install.
163+
log_install_state(name, state=EventState.FAILED, exception=ex)
164+
raise
119165

120-
if local_executable:
121-
if version:
122-
raise exc.PyAirbyteInputError(
123-
message="Param 'version' is not supported when 'local_executable' is set."
124-
)
166+
if install_method_count == 0:
167+
# User has not specified how to install the connector.
168+
# Prefer local executable if found, then manifests, then python, then docker, depending upon
169+
# how the connector is declared in the connector registry.
170+
if which(name):
171+
local_executable = name
172+
elif metadata and metadata.install_types:
173+
match metadata.default_install_type:
174+
case InstallType.YAML:
175+
source_manifest = True
176+
case InstallType.PYTHON:
177+
pip_url = metadata.pypi_package_name
178+
case _:
179+
docker_image = True
125180

126-
if isinstance(local_executable, str):
127-
if "/" in local_executable or "\\" in local_executable:
128-
# Assume this is a path
129-
local_executable = Path(local_executable).absolute()
130-
else:
131-
which_executable: str | None = None
132-
which_executable = shutil.which(local_executable)
133-
if not which_executable and sys.platform == "win32":
134-
# Try with the .exe extension
135-
local_executable = f"{local_executable}.exe"
136-
which_executable = shutil.which(local_executable)
137-
138-
if which_executable is None:
139-
raise exc.AirbyteConnectorExecutableNotFoundError(
140-
connector_name=name,
141-
context={
142-
"executable": local_executable,
143-
"working_directory": Path.cwd().absolute(),
144-
},
145-
) from FileNotFoundError(local_executable)
146-
local_executable = Path(which_executable).absolute()
147-
148-
print(f"Using local `{name}` executable: {local_executable!s}")
149-
return PathExecutor(
150-
name=name,
151-
path=local_executable,
152-
)
181+
if local_executable:
182+
return _get_local_executor(
183+
name=name,
184+
local_executable=local_executable,
185+
version=version,
186+
)
153187

154188
if docker_image:
155189
if docker_image is True:
@@ -215,15 +249,6 @@ def get_connector_executor( # noqa: PLR0912, PLR0913, PLR0915 # Too complex
215249

216250
# else: we are installing a connector in a Python virtual environment:
217251

218-
metadata: ConnectorMetadata | None = None
219-
try:
220-
metadata = get_connector_metadata(name)
221-
except exc.AirbyteConnectorNotRegisteredError as ex:
222-
if not pip_url:
223-
log_install_state(name, state=EventState.FAILED, exception=ex)
224-
# We don't have a pip url or registry entry, so we can't install the connector
225-
raise
226-
227252
try:
228253
executor = VenvExecutor(
229254
name=name,

airbyte/_util/meta.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import os
10+
import shutil
1011
import sys
1112
from contextlib import suppress
1213
from functools import lru_cache
@@ -147,3 +148,22 @@ def get_os() -> str:
147148
return f"Google Colab ({get_colab_release_version()})"
148149

149150
return f"{system()}"
151+
152+
153+
@lru_cache
154+
def which(executable_name: str) -> Path | None:
155+
"""Return the path to an executable which would be run if the given name were called.
156+
157+
This function is a cross-platform wrapper for the `shutil.which()` function.
158+
"""
159+
which_executable: str | None = None
160+
which_executable = shutil.which(executable_name)
161+
if not which_executable and is_windows():
162+
# Try with the .exe extension
163+
which_executable = shutil.which(f"{executable_name}.exe")
164+
165+
return Path(which_executable) if which_executable else None
166+
167+
168+
def is_docker_installed() -> bool:
169+
return bool(which("docker"))

airbyte/exceptions.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151

5252

5353
NEW_ISSUE_URL = "https://github.com/airbytehq/airbyte/issues/new/choose"
54-
DOCS_URL = "https://airbytehq.github.io/PyAirbyte/airbyte.html"
54+
DOCS_URL_BASE = "https://airbytehq.github.io/PyAirbyte"
55+
DOCS_URL = f"{DOCS_URL_BASE}/airbyte.html"
5556

5657

5758
# Base error class
@@ -246,7 +247,12 @@ class AirbyteConnectorNotRegisteredError(AirbyteConnectorRegistryError):
246247
"""Connector not found in registry."""
247248

248249
connector_name: str | None = None
249-
guidance = "Please double check the connector name."
250+
guidance = (
251+
"Please double check the connector name. "
252+
"Alternatively, you can provide an explicit connector install method to `get_source()`: "
253+
"`pip_url`, `local_executable`, `docker_image`, or `source_manifest`."
254+
)
255+
help_url = DOCS_URL_BASE + "/airbyte/sources/util.html#get_source"
250256

251257

252258
@dataclass

0 commit comments

Comments
 (0)