airbyte_cdk.sources.declarative.checks

 1#
 2# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
 3#
 4
 5from typing import Mapping
 6
 7from pydantic.v1 import BaseModel
 8
 9from airbyte_cdk.sources.declarative.checks.check_dynamic_stream import CheckDynamicStream
10from airbyte_cdk.sources.declarative.checks.check_stream import (
11    CheckStream,
12    DynamicStreamCheckConfig,
13)
14from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
15from airbyte_cdk.sources.declarative.models import (
16    CheckDynamicStream as CheckDynamicStreamModel,
17)
18from airbyte_cdk.sources.declarative.models import (
19    CheckStream as CheckStreamModel,
20)
21
22COMPONENTS_CHECKER_TYPE_MAPPING: Mapping[str, type[BaseModel]] = {
23    "CheckStream": CheckStreamModel,
24    "CheckDynamicStream": CheckDynamicStreamModel,
25}
26
27__all__ = ["CheckStream", "CheckDynamicStream", "ConnectionChecker", "DynamicStreamCheckConfig"]
@dataclass
class CheckStream(airbyte_cdk.sources.declarative.checks.ConnectionChecker):
 45@dataclass
 46class CheckStream(ConnectionChecker):
 47    """
 48    Checks the connections by checking availability of one or many streams selected by the developer
 49
 50    Attributes:
 51        stream_name (List[str]): names of streams to check
 52    """
 53
 54    stream_names: List[str]
 55    parameters: InitVar[Mapping[str, Any]]
 56    dynamic_streams_check_configs: Optional[List[DynamicStreamCheckConfig]] = None
 57
 58    def __post_init__(self, parameters: Mapping[str, Any]) -> None:
 59        self._parameters = parameters
 60        if self.dynamic_streams_check_configs is None:
 61            self.dynamic_streams_check_configs = []
 62
 63    def _log_error(self, logger: logging.Logger, action: str, error: Exception) -> Tuple[bool, str]:
 64        """Logs an error and returns a formatted error message."""
 65        error_message = f"Encountered an error while {action}. Error: {error}"
 66        logger.error(error_message + f"Error traceback: \n {traceback.format_exc()}", exc_info=True)
 67        return False, error_message
 68
 69    def check_connection(
 70        self,
 71        source: Source,
 72        logger: logging.Logger,
 73        config: Mapping[str, Any],
 74    ) -> Tuple[bool, Any]:
 75        """Checks the connection to the source and its streams."""
 76        stream_names = self._get_stream_names(config)
 77        try:
 78            streams: List[Union[Stream, AbstractStream]] = self._build_streams(
 79                source, config, stream_names
 80            )
 81            if not streams:
 82                return False, f"No streams to connect to from source {source}"
 83        except Exception as error:
 84            return self._log_error(logger, "discovering streams", error)
 85
 86        stream_name_to_stream = {s.name: s for s in streams}
 87        for stream_name in stream_names:
 88            if stream_name not in stream_name_to_stream:
 89                raise ValueError(
 90                    f"{stream_name} is not part of the catalog. Expected one of {list(stream_name_to_stream.keys())}."
 91                )
 92
 93            stream_availability, message = self._check_stream_availability(
 94                stream_name_to_stream, stream_name, logger
 95            )
 96            if not stream_availability:
 97                return stream_availability, message
 98
 99        should_check_dynamic_streams = (
100            self.dynamic_streams_check_configs
101            and hasattr(source, "resolved_manifest")
102            and hasattr(source, "dynamic_streams")
103        )
104
105        if should_check_dynamic_streams:
106            return self._check_dynamic_streams_availability(source, stream_name_to_stream, logger)
107
108        return True, None
109
110    def _build_streams(
111        self,
112        source: Source,
113        config: Mapping[str, Any],
114        stream_names: List[str],
115    ) -> List[Union[Stream, AbstractStream]]:
116        from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
117            ConcurrentDeclarativeSource,
118        )
119
120        if (
121            isinstance(source, ConcurrentDeclarativeSource)
122            and not self.dynamic_streams_check_configs
123        ):
124            streams = source._streams_by_name(stream_names)
125            if streams is not None:
126                return cast(List[Union[Stream, AbstractStream]], streams)
127
128        return source.streams(config=config)  # type: ignore  # this is a migration step and we expect the declarative CDK to migrate off of ConnectionChecker
129
130    def _get_stream_names(self, config: Mapping[str, Any]) -> List[str]:
131        if (
132            CHECK_STREAM_NAMES_CONFIG_KEY not in config
133            or config[CHECK_STREAM_NAMES_CONFIG_KEY] == []
134        ):
135            return self.stream_names
136        configured_stream_names = config[CHECK_STREAM_NAMES_CONFIG_KEY]
137        if not isinstance(configured_stream_names, list) or not all(
138            isinstance(stream_name, str) for stream_name in configured_stream_names
139        ):
140            raise ValueError(f"{CHECK_STREAM_NAMES_CONFIG_KEY} must be a list of strings.")
141        return configured_stream_names
142
143    def _check_stream_availability(
144        self,
145        stream_name_to_stream: Dict[str, Union[Stream, AbstractStream]],
146        stream_name: str,
147        logger: logging.Logger,
148    ) -> Tuple[bool, Any]:
149        """Checks if streams are available."""
150        try:
151            stream = stream_name_to_stream[stream_name]
152            stream_is_available, reason = evaluate_availability(stream, logger)
153            if not stream_is_available:
154                message = f"Stream {stream_name} is not available: {reason}"
155                logger.warning(message)
156                return stream_is_available, message
157        except Exception as error:
158            return self._log_error(logger, f"checking availability of stream {stream_name}", error)
159        return True, None
160
161    def _check_dynamic_streams_availability(
162        self,
163        source: Source,
164        stream_name_to_stream: Dict[str, Union[Stream, AbstractStream]],
165        logger: logging.Logger,
166    ) -> Tuple[bool, Any]:
167        """Checks the availability of dynamic streams."""
168        dynamic_streams = source.resolved_manifest.get("dynamic_streams", [])  # type: ignore[attr-defined] # The source's resolved_manifest manifest is checked before calling this method
169        dynamic_stream_name_to_dynamic_stream = {
170            ds.get("name", f"dynamic_stream_{i}"): ds for i, ds in enumerate(dynamic_streams)
171        }
172        generated_streams = self._map_generated_streams(source.dynamic_streams)  # type: ignore[attr-defined] # The source's dynamic_streams manifest is checked before calling this method
173
174        for check_config in self.dynamic_streams_check_configs:  # type: ignore[union-attr] # None value for self.dynamic_streams_check_configs handled in __post_init__
175            if check_config.dynamic_stream_name not in dynamic_stream_name_to_dynamic_stream:
176                return (
177                    False,
178                    f"Dynamic stream {check_config.dynamic_stream_name} is not found in manifest.",
179                )
180
181            generated = generated_streams.get(check_config.dynamic_stream_name, [])
182            stream_availability, message = self._check_generated_streams_availability(
183                generated, stream_name_to_stream, logger, check_config.stream_count
184            )
185            if not stream_availability:
186                return stream_availability, message
187
188        return True, None
189
190    def _map_generated_streams(
191        self, dynamic_streams: List[Dict[str, Any]]
192    ) -> Dict[str, List[Dict[str, Any]]]:
193        """Maps dynamic stream names to their corresponding generated streams."""
194        mapped_streams: Dict[str, List[Dict[str, Any]]] = {}
195        for stream in dynamic_streams:
196            mapped_streams.setdefault(stream["dynamic_stream_name"], []).append(stream)
197        return mapped_streams
198
199    def _check_generated_streams_availability(
200        self,
201        generated_streams: List[Dict[str, Any]],
202        stream_name_to_stream: Dict[str, Union[Stream, AbstractStream]],
203        logger: logging.Logger,
204        max_count: Optional[int],
205    ) -> Tuple[bool, Any]:
206        """Checks availability of generated dynamic streams.
207
208        If `max_count` is `None`, all generated streams are checked. Otherwise, the
209        first `max_count` streams are checked (capped at the number of available streams).
210        """
211        streams_to_check = generated_streams if max_count is None else generated_streams[:max_count]
212        for declarative_stream in streams_to_check:
213            stream = stream_name_to_stream[declarative_stream["name"]]
214            try:
215                stream_is_available, reason = evaluate_availability(stream, logger)
216                if not stream_is_available:
217                    message = f"Dynamic Stream {stream.name} is not available: {reason}"
218                    logger.warning(message)
219                    return False, message
220            except Exception as error:
221                return self._log_error(
222                    logger, f"checking availability of dynamic stream {stream.name}", error
223                )
224        return True, None

Checks the connections by checking availability of one or many streams selected by the developer

Attributes:
  • stream_name (List[str]): names of streams to check
CheckStream( stream_names: List[str], parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]], dynamic_streams_check_configs: Optional[List[DynamicStreamCheckConfig]] = None)
stream_names: List[str]
parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]]
dynamic_streams_check_configs: Optional[List[DynamicStreamCheckConfig]] = None
def check_connection( self, source: airbyte_cdk.Source, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
 69    def check_connection(
 70        self,
 71        source: Source,
 72        logger: logging.Logger,
 73        config: Mapping[str, Any],
 74    ) -> Tuple[bool, Any]:
 75        """Checks the connection to the source and its streams."""
 76        stream_names = self._get_stream_names(config)
 77        try:
 78            streams: List[Union[Stream, AbstractStream]] = self._build_streams(
 79                source, config, stream_names
 80            )
 81            if not streams:
 82                return False, f"No streams to connect to from source {source}"
 83        except Exception as error:
 84            return self._log_error(logger, "discovering streams", error)
 85
 86        stream_name_to_stream = {s.name: s for s in streams}
 87        for stream_name in stream_names:
 88            if stream_name not in stream_name_to_stream:
 89                raise ValueError(
 90                    f"{stream_name} is not part of the catalog. Expected one of {list(stream_name_to_stream.keys())}."
 91                )
 92
 93            stream_availability, message = self._check_stream_availability(
 94                stream_name_to_stream, stream_name, logger
 95            )
 96            if not stream_availability:
 97                return stream_availability, message
 98
 99        should_check_dynamic_streams = (
100            self.dynamic_streams_check_configs
101            and hasattr(source, "resolved_manifest")
102            and hasattr(source, "dynamic_streams")
103        )
104
105        if should_check_dynamic_streams:
106            return self._check_dynamic_streams_availability(source, stream_name_to_stream, logger)
107
108        return True, None

Checks the connection to the source and its streams.

@dataclass
class CheckDynamicStream(airbyte_cdk.sources.declarative.checks.ConnectionChecker):
17@dataclass
18class CheckDynamicStream(ConnectionChecker):
19    """
20    Checks the connections by checking availability of one or many dynamic streams
21
22    Attributes:
23        stream_count (int): numbers of streams to check
24    """
25
26    # TODO: Add field stream_names to check_connection for static streams
27    #  https://github.com/airbytehq/airbyte-python-cdk/pull/293#discussion_r1934933483
28
29    stream_count: int
30    parameters: InitVar[Mapping[str, Any]]
31    use_check_availability: bool = True
32
33    def __post_init__(self, parameters: Mapping[str, Any]) -> None:
34        self._parameters = parameters
35
36    def check_connection(
37        self,
38        source: Source,
39        logger: logging.Logger,
40        config: Mapping[str, Any],
41    ) -> Tuple[bool, Any]:
42        from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
43            ConcurrentDeclarativeSource,
44        )
45
46        streams: Optional[List[Union[Stream, AbstractStream]]] = None
47        if (
48            isinstance(source, ConcurrentDeclarativeSource)
49            and self.use_check_availability
50            and self.stream_count >= 1
51        ):
52            streams = cast(
53                Optional[List[Union[Stream, AbstractStream]]],
54                source._first_streams(self.stream_count),
55            )
56        if streams is None:
57            streams = source.streams(config=config)  # type: ignore  # this is a migration step and we expect the declarative CDK to migrate off of ConnectionChecker
58
59        if len(streams) == 0:
60            return False, f"No streams to connect to from source {source}"
61        if not self.use_check_availability:
62            return True, None
63
64        try:
65            for stream in streams[: min(self.stream_count, len(streams))]:
66                stream_is_available, reason = evaluate_availability(stream, logger)
67                if not stream_is_available:
68                    message = f"Stream {stream.name} is not available: {reason}"
69                    logger.warning(message)
70                    return False, message
71        except Exception as error:
72            error_message = (
73                f"Encountered an error trying to connect to stream {stream.name}. Error: {error}"
74            )
75            logger.error(error_message, exc_info=True)
76            return False, error_message
77
78        return True, None

Checks the connections by checking availability of one or many dynamic streams

Attributes:
  • stream_count (int): numbers of streams to check
CheckDynamicStream( stream_count: int, parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]], use_check_availability: bool = True)
stream_count: int
parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]]
use_check_availability: bool = True
def check_connection( self, source: airbyte_cdk.Source, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
36    def check_connection(
37        self,
38        source: Source,
39        logger: logging.Logger,
40        config: Mapping[str, Any],
41    ) -> Tuple[bool, Any]:
42        from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
43            ConcurrentDeclarativeSource,
44        )
45
46        streams: Optional[List[Union[Stream, AbstractStream]]] = None
47        if (
48            isinstance(source, ConcurrentDeclarativeSource)
49            and self.use_check_availability
50            and self.stream_count >= 1
51        ):
52            streams = cast(
53                Optional[List[Union[Stream, AbstractStream]]],
54                source._first_streams(self.stream_count),
55            )
56        if streams is None:
57            streams = source.streams(config=config)  # type: ignore  # this is a migration step and we expect the declarative CDK to migrate off of ConnectionChecker
58
59        if len(streams) == 0:
60            return False, f"No streams to connect to from source {source}"
61        if not self.use_check_availability:
62            return True, None
63
64        try:
65            for stream in streams[: min(self.stream_count, len(streams))]:
66                stream_is_available, reason = evaluate_availability(stream, logger)
67                if not stream_is_available:
68                    message = f"Stream {stream.name} is not available: {reason}"
69                    logger.warning(message)
70                    return False, message
71        except Exception as error:
72            error_message = (
73                f"Encountered an error trying to connect to stream {stream.name}. Error: {error}"
74            )
75            logger.error(error_message, exc_info=True)
76            return False, error_message
77
78        return True, None

Tests if the input configuration can be used to successfully connect to the integration e.g: if a provided Stripe API token can be used to connect to the Stripe API.

Parameters
  • source: source
  • logger: source logger
  • config: The user-provided configuration as specified by the source's spec. This usually contains information required to check connection e.g. tokens, secrets and keys etc.
Returns

A tuple of (boolean, error). If boolean is true, then the connection check is successful and we can connect to the underlying data source using the provided configuration. Otherwise, the input config cannot be used to connect to the underlying data source, and the "error" object should describe what went wrong. The error object will be cast to string to display the problem to the user.

class ConnectionChecker(abc.ABC):
13class ConnectionChecker(ABC):
14    """
15    Abstract base class for checking a connection
16    """
17
18    @abstractmethod
19    def check_connection(
20        self,
21        source: Source,
22        logger: logging.Logger,
23        config: Mapping[str, Any],
24    ) -> Tuple[bool, Any]:
25        """
26        Tests if the input configuration can be used to successfully connect to the integration e.g: if a provided Stripe API token can be used to connect
27        to the Stripe API.
28
29        :param source: source
30        :param logger: source logger
31        :param config: The user-provided configuration as specified by the source's spec.
32          This usually contains information required to check connection e.g. tokens, secrets and keys etc.
33        :return: A tuple of (boolean, error). If boolean is true, then the connection check is successful
34          and we can connect to the underlying data source using the provided configuration.
35          Otherwise, the input config cannot be used to connect to the underlying data source,
36          and the "error" object should describe what went wrong.
37          The error object will be cast to string to display the problem to the user.
38        """
39        pass

Abstract base class for checking a connection

@abstractmethod
def check_connection( self, source: airbyte_cdk.Source, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
18    @abstractmethod
19    def check_connection(
20        self,
21        source: Source,
22        logger: logging.Logger,
23        config: Mapping[str, Any],
24    ) -> Tuple[bool, Any]:
25        """
26        Tests if the input configuration can be used to successfully connect to the integration e.g: if a provided Stripe API token can be used to connect
27        to the Stripe API.
28
29        :param source: source
30        :param logger: source logger
31        :param config: The user-provided configuration as specified by the source's spec.
32          This usually contains information required to check connection e.g. tokens, secrets and keys etc.
33        :return: A tuple of (boolean, error). If boolean is true, then the connection check is successful
34          and we can connect to the underlying data source using the provided configuration.
35          Otherwise, the input config cannot be used to connect to the underlying data source,
36          and the "error" object should describe what went wrong.
37          The error object will be cast to string to display the problem to the user.
38        """
39        pass

Tests if the input configuration can be used to successfully connect to the integration e.g: if a provided Stripe API token can be used to connect to the Stripe API.

Parameters
  • source: source
  • logger: source logger
  • config: The user-provided configuration as specified by the source's spec. This usually contains information required to check connection e.g. tokens, secrets and keys etc.
Returns

A tuple of (boolean, error). If boolean is true, then the connection check is successful and we can connect to the underlying data source using the provided configuration. Otherwise, the input config cannot be used to connect to the underlying data source, and the "error" object should describe what went wrong. The error object will be cast to string to display the problem to the user.

@dataclass(frozen=True)
class DynamicStreamCheckConfig:
35@dataclass(frozen=True)
36class DynamicStreamCheckConfig:
37    """Defines the configuration for dynamic stream during connection checking. This class specifies
38    what dynamic streams  in the stream template should be updated with value, supporting dynamic interpolation
39    and type enforcement."""
40
41    dynamic_stream_name: str
42    stream_count: Optional[int] = None

Defines the configuration for dynamic stream during connection checking. This class specifies what dynamic streams in the stream template should be updated with value, supporting dynamic interpolation and type enforcement.

DynamicStreamCheckConfig(dynamic_stream_name: str, stream_count: Optional[int] = None)
dynamic_stream_name: str
stream_count: Optional[int] = None