airbyte_cdk.connector

  1#
  2# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
  3#
  4
  5
  6import json
  7import logging
  8import os
  9import pkgutil
 10from abc import ABC, abstractmethod
 11from typing import Any, Generic, Mapping, MutableMapping, Optional, Protocol, TypeVar
 12
 13import yaml
 14
 15from airbyte_cdk.models import (
 16    AirbyteConnectionStatus,
 17    ConnectorSpecification,
 18    ConnectorSpecificationSerializer,
 19    Status,
 20)
 21
 22
 23def load_optional_package_file(package: str, filename: str) -> Optional[bytes]:
 24    """Gets a resource from a package, returning None if it does not exist"""
 25    try:
 26        return pkgutil.get_data(package, filename)
 27    except FileNotFoundError:
 28        return None
 29
 30
 31TConfig = TypeVar("TConfig", bound=Mapping[str, Any])
 32
 33CHECK_SECTION_NOT_IMPLEMENTED = (
 34    "The connector specification declares sections but the connector does not implement "
 35    "a section check (section '{section}')."
 36)
 37
 38
 39class BaseConnector(ABC, Generic[TConfig]):
 40    # configure whether the `check_config_against_spec_or_exit()` needs to be called
 41    check_config_against_spec: bool = True
 42
 43    @abstractmethod
 44    def configure(self, config: Mapping[str, Any], temp_dir: str) -> TConfig:
 45        """
 46        Persist config in temporary directory to run the Source job
 47        """
 48
 49    @staticmethod
 50    def read_config(config_path: str) -> MutableMapping[str, Any]:
 51        config = BaseConnector._read_json_file(config_path)
 52        if isinstance(config, MutableMapping):
 53            return config
 54        else:
 55            raise ValueError(
 56                f"The content of {config_path} is not an object and therefore is not a valid config. Please ensure the file represent a config."
 57            )
 58
 59    @staticmethod
 60    def _read_json_file(file_path: str) -> Any:
 61        with open(file_path, "r") as file:
 62            contents = file.read()
 63
 64        try:
 65            return json.loads(contents)
 66        except json.JSONDecodeError as error:
 67            raise ValueError(
 68                f"Could not read json file {file_path}: {error}. Please ensure that it is a valid JSON."
 69            )
 70
 71    @staticmethod
 72    def write_config(config: TConfig, config_path: str) -> None:
 73        with open(config_path, "w") as fh:
 74            fh.write(json.dumps(config))
 75
 76    def spec(self, logger: logging.Logger) -> ConnectorSpecification:
 77        """
 78        Returns the spec for this integration. The spec is a JSON-Schema object describing the required configurations (e.g: username and password)
 79        required to run this integration. By default, this will be loaded from a "spec.yaml" or a "spec.json" in the package root.
 80        """
 81
 82        package = self.__class__.__module__.split(".")[0]
 83
 84        yaml_spec = load_optional_package_file(package, "spec.yaml")
 85        json_spec = load_optional_package_file(package, "spec.json")
 86
 87        if yaml_spec and json_spec:
 88            raise RuntimeError(
 89                "Found multiple spec files in the package. Only one of spec.yaml or spec.json should be provided."
 90            )
 91
 92        if yaml_spec:
 93            spec_obj = yaml.load(yaml_spec, Loader=yaml.SafeLoader)
 94        elif json_spec:
 95            try:
 96                spec_obj = json.loads(json_spec)
 97            except json.JSONDecodeError as error:
 98                raise ValueError(
 99                    f"Could not read json spec file: {error}. Please ensure that it is a valid JSON."
100                )
101        else:
102            raise FileNotFoundError("Unable to find spec.yaml or spec.json in the package.")
103
104        return ConnectorSpecificationSerializer.load(spec_obj)
105
106    @abstractmethod
107    def check(self, logger: logging.Logger, config: TConfig) -> AirbyteConnectionStatus:
108        """
109        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
110        to the Stripe API.
111        """
112
113    def check_section(
114        self, logger: logging.Logger, section: str, config: TConfig
115    ) -> AirbyteConnectionStatus:
116        """
117        Checks the configuration up to `section` only, for the stepped setup form
118        (see `airbyte_cdk.sources.utils.check_section`). The entrypoint calls this instead of `check` when the config
119        carries a `check-section` key naming a declared section other than the last one; `config` then only holds
120        the fields of that section and of the sections before it.
121
122        Override it in a connector whose specification declares sections.
123        """
124        return AirbyteConnectionStatus(
125            status=Status.FAILED, message=CHECK_SECTION_NOT_IMPLEMENTED.format(section=section)
126        )
127
128
129class _WriteConfigProtocol(Protocol):
130    @staticmethod
131    def write_config(config: Mapping[str, Any], config_path: str) -> None: ...
132
133
134class DefaultConnectorMixin:
135    # can be overridden to change an input config
136    def configure(
137        self: _WriteConfigProtocol, config: Mapping[str, Any], temp_dir: str
138    ) -> Mapping[str, Any]:
139        config_path = os.path.join(temp_dir, "config.json")
140        self.write_config(config, config_path)
141        return config
142
143
144class Connector(DefaultConnectorMixin, BaseConnector[Mapping[str, Any]], ABC): ...
def load_optional_package_file(package: str, filename: str) -> Optional[bytes]:
24def load_optional_package_file(package: str, filename: str) -> Optional[bytes]:
25    """Gets a resource from a package, returning None if it does not exist"""
26    try:
27        return pkgutil.get_data(package, filename)
28    except FileNotFoundError:
29        return None

Gets a resource from a package, returning None if it does not exist

CHECK_SECTION_NOT_IMPLEMENTED = "The connector specification declares sections but the connector does not implement a section check (section '{section}')."
class BaseConnector(abc.ABC, typing.Generic[~TConfig]):
 40class BaseConnector(ABC, Generic[TConfig]):
 41    # configure whether the `check_config_against_spec_or_exit()` needs to be called
 42    check_config_against_spec: bool = True
 43
 44    @abstractmethod
 45    def configure(self, config: Mapping[str, Any], temp_dir: str) -> TConfig:
 46        """
 47        Persist config in temporary directory to run the Source job
 48        """
 49
 50    @staticmethod
 51    def read_config(config_path: str) -> MutableMapping[str, Any]:
 52        config = BaseConnector._read_json_file(config_path)
 53        if isinstance(config, MutableMapping):
 54            return config
 55        else:
 56            raise ValueError(
 57                f"The content of {config_path} is not an object and therefore is not a valid config. Please ensure the file represent a config."
 58            )
 59
 60    @staticmethod
 61    def _read_json_file(file_path: str) -> Any:
 62        with open(file_path, "r") as file:
 63            contents = file.read()
 64
 65        try:
 66            return json.loads(contents)
 67        except json.JSONDecodeError as error:
 68            raise ValueError(
 69                f"Could not read json file {file_path}: {error}. Please ensure that it is a valid JSON."
 70            )
 71
 72    @staticmethod
 73    def write_config(config: TConfig, config_path: str) -> None:
 74        with open(config_path, "w") as fh:
 75            fh.write(json.dumps(config))
 76
 77    def spec(self, logger: logging.Logger) -> ConnectorSpecification:
 78        """
 79        Returns the spec for this integration. The spec is a JSON-Schema object describing the required configurations (e.g: username and password)
 80        required to run this integration. By default, this will be loaded from a "spec.yaml" or a "spec.json" in the package root.
 81        """
 82
 83        package = self.__class__.__module__.split(".")[0]
 84
 85        yaml_spec = load_optional_package_file(package, "spec.yaml")
 86        json_spec = load_optional_package_file(package, "spec.json")
 87
 88        if yaml_spec and json_spec:
 89            raise RuntimeError(
 90                "Found multiple spec files in the package. Only one of spec.yaml or spec.json should be provided."
 91            )
 92
 93        if yaml_spec:
 94            spec_obj = yaml.load(yaml_spec, Loader=yaml.SafeLoader)
 95        elif json_spec:
 96            try:
 97                spec_obj = json.loads(json_spec)
 98            except json.JSONDecodeError as error:
 99                raise ValueError(
100                    f"Could not read json spec file: {error}. Please ensure that it is a valid JSON."
101                )
102        else:
103            raise FileNotFoundError("Unable to find spec.yaml or spec.json in the package.")
104
105        return ConnectorSpecificationSerializer.load(spec_obj)
106
107    @abstractmethod
108    def check(self, logger: logging.Logger, config: TConfig) -> AirbyteConnectionStatus:
109        """
110        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
111        to the Stripe API.
112        """
113
114    def check_section(
115        self, logger: logging.Logger, section: str, config: TConfig
116    ) -> AirbyteConnectionStatus:
117        """
118        Checks the configuration up to `section` only, for the stepped setup form
119        (see `airbyte_cdk.sources.utils.check_section`). The entrypoint calls this instead of `check` when the config
120        carries a `check-section` key naming a declared section other than the last one; `config` then only holds
121        the fields of that section and of the sections before it.
122
123        Override it in a connector whose specification declares sections.
124        """
125        return AirbyteConnectionStatus(
126            status=Status.FAILED, message=CHECK_SECTION_NOT_IMPLEMENTED.format(section=section)
127        )

Helper class that provides a standard way to create an ABC using inheritance.

check_config_against_spec: bool = True
@abstractmethod
def configure(self, config: Mapping[str, Any], temp_dir: str) -> ~TConfig:
44    @abstractmethod
45    def configure(self, config: Mapping[str, Any], temp_dir: str) -> TConfig:
46        """
47        Persist config in temporary directory to run the Source job
48        """

Persist config in temporary directory to run the Source job

@staticmethod
def read_config(config_path: str) -> MutableMapping[str, Any]:
50    @staticmethod
51    def read_config(config_path: str) -> MutableMapping[str, Any]:
52        config = BaseConnector._read_json_file(config_path)
53        if isinstance(config, MutableMapping):
54            return config
55        else:
56            raise ValueError(
57                f"The content of {config_path} is not an object and therefore is not a valid config. Please ensure the file represent a config."
58            )
@staticmethod
def write_config(config: ~TConfig, config_path: str) -> None:
72    @staticmethod
73    def write_config(config: TConfig, config_path: str) -> None:
74        with open(config_path, "w") as fh:
75            fh.write(json.dumps(config))
def spec( self, logger: logging.Logger) -> airbyte_cdk.ConnectorSpecification:
 77    def spec(self, logger: logging.Logger) -> ConnectorSpecification:
 78        """
 79        Returns the spec for this integration. The spec is a JSON-Schema object describing the required configurations (e.g: username and password)
 80        required to run this integration. By default, this will be loaded from a "spec.yaml" or a "spec.json" in the package root.
 81        """
 82
 83        package = self.__class__.__module__.split(".")[0]
 84
 85        yaml_spec = load_optional_package_file(package, "spec.yaml")
 86        json_spec = load_optional_package_file(package, "spec.json")
 87
 88        if yaml_spec and json_spec:
 89            raise RuntimeError(
 90                "Found multiple spec files in the package. Only one of spec.yaml or spec.json should be provided."
 91            )
 92
 93        if yaml_spec:
 94            spec_obj = yaml.load(yaml_spec, Loader=yaml.SafeLoader)
 95        elif json_spec:
 96            try:
 97                spec_obj = json.loads(json_spec)
 98            except json.JSONDecodeError as error:
 99                raise ValueError(
100                    f"Could not read json spec file: {error}. Please ensure that it is a valid JSON."
101                )
102        else:
103            raise FileNotFoundError("Unable to find spec.yaml or spec.json in the package.")
104
105        return ConnectorSpecificationSerializer.load(spec_obj)

Returns the spec for this integration. The spec is a JSON-Schema object describing the required configurations (e.g: username and password) required to run this integration. By default, this will be loaded from a "spec.yaml" or a "spec.json" in the package root.

@abstractmethod
def check( self, logger: logging.Logger, config: ~TConfig) -> airbyte_protocol_dataclasses.models.airbyte_protocol.AirbyteConnectionStatus:
107    @abstractmethod
108    def check(self, logger: logging.Logger, config: TConfig) -> AirbyteConnectionStatus:
109        """
110        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
111        to the Stripe API.
112        """

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.

def check_section( self, logger: logging.Logger, section: str, config: ~TConfig) -> airbyte_protocol_dataclasses.models.airbyte_protocol.AirbyteConnectionStatus:
114    def check_section(
115        self, logger: logging.Logger, section: str, config: TConfig
116    ) -> AirbyteConnectionStatus:
117        """
118        Checks the configuration up to `section` only, for the stepped setup form
119        (see `airbyte_cdk.sources.utils.check_section`). The entrypoint calls this instead of `check` when the config
120        carries a `check-section` key naming a declared section other than the last one; `config` then only holds
121        the fields of that section and of the sections before it.
122
123        Override it in a connector whose specification declares sections.
124        """
125        return AirbyteConnectionStatus(
126            status=Status.FAILED, message=CHECK_SECTION_NOT_IMPLEMENTED.format(section=section)
127        )

Checks the configuration up to section only, for the stepped setup form (see airbyte_cdk.sources.utils.check_section). The entrypoint calls this instead of check when the config carries a check-section key naming a declared section other than the last one; config then only holds the fields of that section and of the sections before it.

Override it in a connector whose specification declares sections.

class DefaultConnectorMixin:
135class DefaultConnectorMixin:
136    # can be overridden to change an input config
137    def configure(
138        self: _WriteConfigProtocol, config: Mapping[str, Any], temp_dir: str
139    ) -> Mapping[str, Any]:
140        config_path = os.path.join(temp_dir, "config.json")
141        self.write_config(config, config_path)
142        return config
def configure( self: airbyte_cdk.connector._WriteConfigProtocol, config: Mapping[str, Any], temp_dir: str) -> Mapping[str, Any]:
137    def configure(
138        self: _WriteConfigProtocol, config: Mapping[str, Any], temp_dir: str
139    ) -> Mapping[str, Any]:
140        config_path = os.path.join(temp_dir, "config.json")
141        self.write_config(config, config_path)
142        return config
145class Connector(DefaultConnectorMixin, BaseConnector[Mapping[str, Any]], ABC): ...

Helper class that provides a standard way to create an ABC using inheritance.