88import os
99import pkgutil
1010from abc import ABC , abstractmethod
11- from typing import Any , Generic , Mapping , MutableMapping , Optional , Protocol , TypeVar
11+ from email import message
12+ from io import StringIO
13+ from pathlib import Path
14+ from typing import Any , Generic , Mapping , Optional , Self , TypeVar
1215
1316import yaml
1417
1720 ConnectorSpecification ,
1821 ConnectorSpecificationSerializer ,
1922)
23+ from airbyte_cdk .models .airbyte_protocol import AirbyteMessage , Type
24+ from airbyte_cdk .sources .message .repository import MessageRepository , PassthroughMessageRepository
25+ from airbyte_cdk .test .entrypoint_wrapper import EntrypointOutput
26+ from airbyte_cdk .utils .cli_arg_parse import ConnectorCLIArgs , parse_cli_args
2027
2128
22- def load_optional_package_file (package : str , filename : str ) -> Optional [bytes ]:
29+ def _load_optional_package_file (package : str , filename : str ) -> Optional [bytes ]:
2330 """Gets a resource from a package, returning None if it does not exist"""
2431 try :
2532 return pkgutil .get_data (package , filename )
2633 except FileNotFoundError :
2734 return None
2835
36+ def _write_config (config : Mapping [str , Any ], config_path : str ) -> None :
37+ Path (config_path ).write_text (json .dumps (config ))
38+
2939
3040TConfig = TypeVar ("TConfig" , bound = Mapping [str , Any ])
3141
@@ -35,37 +45,19 @@ class BaseConnector(ABC, Generic[TConfig]):
3545 check_config_against_spec : bool = True
3646
3747 @abstractmethod
38- def configure (self , config : Mapping [str , Any ], temp_dir : str ) -> TConfig :
39- """
40- Persist config in temporary directory to run the Source job
41- """
42-
43- @staticmethod
44- def read_config (config_path : str ) -> MutableMapping [str , Any ]:
45- config = BaseConnector ._read_json_file (config_path )
46- if isinstance (config , MutableMapping ):
47- return config
48- else :
49- raise ValueError (
50- f"The content of { config_path } is not an object and therefore is not a valid config. Please ensure the file represent a config."
51- )
52-
53- @staticmethod
54- def _read_json_file (file_path : str ) -> Any :
55- with open (file_path , "r" ) as file :
56- contents = file .read ()
57-
58- try :
59- return json .loads (contents )
60- except json .JSONDecodeError as error :
61- raise ValueError (
62- f"Could not read json file { file_path } : { error } . Please ensure that it is a valid JSON."
63- )
64-
65- @staticmethod
66- def write_config (config : TConfig , config_path : str ) -> None :
67- with open (config_path , "w" ) as fh :
68- fh .write (json .dumps (config ))
48+ @classmethod
49+ def to_typed_config (
50+ cls ,
51+ config : Mapping [str , Any ],
52+ ) -> TConfig :
53+ """Return a typed config object from a config dictionary."""
54+ ...
55+
56+ @classmethod
57+ def configure (cls , config : Mapping [str , Any ], temp_dir : str ) -> TConfig :
58+ config_path = os .path .join (temp_dir , "config.json" )
59+ _write_config (config , config_path )
60+ return cls .to_typed_config (config )
6961
7062 def spec (self , logger : logging .Logger ) -> ConnectorSpecification :
7163 """
@@ -75,8 +67,8 @@ def spec(self, logger: logging.Logger) -> ConnectorSpecification:
7567
7668 package = self .__class__ .__module__ .split ("." )[0 ]
7769
78- yaml_spec = load_optional_package_file (package , "spec.yaml" )
79- json_spec = load_optional_package_file (package , "spec.json" )
70+ yaml_spec = _load_optional_package_file (package , "spec.yaml" )
71+ json_spec = _load_optional_package_file (package , "spec.json" )
8072
8173 if yaml_spec and json_spec :
8274 raise RuntimeError (
@@ -104,20 +96,61 @@ def check(self, logger: logging.Logger, config: TConfig) -> AirbyteConnectionSta
10496 to the Stripe API.
10597 """
10698
107-
108- class _WriteConfigProtocol (Protocol ):
109- @staticmethod
110- def write_config (config : Mapping [str , Any ], config_path : str ) -> None : ...
111-
112-
113- class DefaultConnectorMixin :
114- # can be overridden to change an input config
115- def configure (
116- self : _WriteConfigProtocol , config : Mapping [str , Any ], temp_dir : str
117- ) -> Mapping [str , Any ]:
118- config_path = os .path .join (temp_dir , "config.json" )
119- self .write_config (config , config_path )
120- return config
121-
122-
123- class Connector (DefaultConnectorMixin , BaseConnector [Mapping [str , Any ]], ABC ): ...
99+ @abstractmethod
100+ @classmethod
101+ def create_with_cli_args (
102+ cls ,
103+ cli_args : ConnectorCLIArgs ,
104+ ) -> Self :
105+ """Return an instance of the connector, using the provided CLI args."""
106+ ...
107+
108+ @classmethod
109+ def launch_with_cli_args (
110+ cls ,
111+ args : list [str ],
112+ * ,
113+ logger : logging .Logger | None = None ,
114+ message_repository : MessageRepository | None = None ,
115+ # TODO: Add support for inputs:
116+ # stdin: StringIO | MessageRepository | None = None,
117+ ) -> None :
118+ """Launches the connector with the provided configuration."""
119+ logger = logger or logging .getLogger (f"airbyte.{ type (cls ).__name__ } " )
120+ message_repository = message_repository or PassthroughMessageRepository ()
121+ parsed_cli_args : ConnectorCLIArgs = parse_cli_args (
122+ args ,
123+ with_read = True if getattr (cls , "read" , False ) else False ,
124+ with_write = True if getattr (cls , "write" , False ) else False ,
125+ with_discover = True if getattr (cls , "discover" , False ) else False ,
126+ )
127+ logger .info (f"Launching connector with args: { parsed_cli_args } " )
128+ verb = parsed_cli_args .command
129+
130+ spec : ConnectorSpecification
131+ if verb == "check" :
132+ config = cls .to_typed_config (parsed_cli_args .get_config_dict ())
133+ connector = cls .create_with_cli_args (parsed_cli_args )
134+ connector .check (logger , config )
135+ elif verb == "spec" :
136+ connector = cls ()
137+ spec = connector .spec (logger )
138+ message_repository .emit_message (
139+ AirbyteMessage (
140+ type = Type .SPEC ,
141+ spec = spec ,
142+ )
143+ )
144+ elif verb == "discover" :
145+ connector = cls ()
146+ spec = connector .spec (logger )
147+ print (json .dumps (spec .to_dict (), indent = 2 ))
148+ elif verb == "read" :
149+ # Implementation for reading data goes here
150+ pass
151+ elif verb == "write" :
152+ # Implementation for writing data goes here
153+ pass
154+ else :
155+ raise ValueError (f"Unknown command: { verb } " )
156+ # Implementation for launching the connector goes here
0 commit comments