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): ...
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
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.
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
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 )
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.
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.
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.
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
145class Connector(DefaultConnectorMixin, BaseConnector[Mapping[str, Any]], ABC): ...
Helper class that provides a standard way to create an ABC using inheritance.