hotglue-singer-sdk 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hotglue_singer_sdk/__init__.py +34 -0
- hotglue_singer_sdk/authenticators.py +554 -0
- hotglue_singer_sdk/cli/__init__.py +1 -0
- hotglue_singer_sdk/cli/common_options.py +37 -0
- hotglue_singer_sdk/configuration/__init__.py +1 -0
- hotglue_singer_sdk/configuration/_dict_config.py +101 -0
- hotglue_singer_sdk/exceptions.py +52 -0
- hotglue_singer_sdk/helpers/__init__.py +1 -0
- hotglue_singer_sdk/helpers/_catalog.py +122 -0
- hotglue_singer_sdk/helpers/_classproperty.py +18 -0
- hotglue_singer_sdk/helpers/_compat.py +15 -0
- hotglue_singer_sdk/helpers/_flattening.py +374 -0
- hotglue_singer_sdk/helpers/_schema.py +100 -0
- hotglue_singer_sdk/helpers/_secrets.py +41 -0
- hotglue_singer_sdk/helpers/_simpleeval.py +678 -0
- hotglue_singer_sdk/helpers/_singer.py +280 -0
- hotglue_singer_sdk/helpers/_state.py +282 -0
- hotglue_singer_sdk/helpers/_typing.py +231 -0
- hotglue_singer_sdk/helpers/_util.py +27 -0
- hotglue_singer_sdk/helpers/capabilities.py +240 -0
- hotglue_singer_sdk/helpers/jsonpath.py +39 -0
- hotglue_singer_sdk/io_base.py +134 -0
- hotglue_singer_sdk/mapper.py +691 -0
- hotglue_singer_sdk/mapper_base.py +156 -0
- hotglue_singer_sdk/plugin_base.py +415 -0
- hotglue_singer_sdk/py.typed +0 -0
- hotglue_singer_sdk/sinks/__init__.py +14 -0
- hotglue_singer_sdk/sinks/batch.py +90 -0
- hotglue_singer_sdk/sinks/core.py +412 -0
- hotglue_singer_sdk/sinks/record.py +66 -0
- hotglue_singer_sdk/sinks/sql.py +299 -0
- hotglue_singer_sdk/streams/__init__.py +14 -0
- hotglue_singer_sdk/streams/core.py +1294 -0
- hotglue_singer_sdk/streams/graphql.py +74 -0
- hotglue_singer_sdk/streams/rest.py +611 -0
- hotglue_singer_sdk/streams/sql.py +1023 -0
- hotglue_singer_sdk/tap_base.py +580 -0
- hotglue_singer_sdk/target_base.py +554 -0
- hotglue_singer_sdk/target_sdk/__init__.py +0 -0
- hotglue_singer_sdk/target_sdk/auth.py +124 -0
- hotglue_singer_sdk/target_sdk/client.py +286 -0
- hotglue_singer_sdk/target_sdk/common.py +13 -0
- hotglue_singer_sdk/target_sdk/lambda.py +121 -0
- hotglue_singer_sdk/target_sdk/rest.py +108 -0
- hotglue_singer_sdk/target_sdk/sinks.py +16 -0
- hotglue_singer_sdk/target_sdk/target.py +570 -0
- hotglue_singer_sdk/target_sdk/target_base.py +627 -0
- hotglue_singer_sdk/testing.py +198 -0
- hotglue_singer_sdk/typing.py +603 -0
- hotglue_singer_sdk-1.0.2.dist-info/METADATA +53 -0
- hotglue_singer_sdk-1.0.2.dist-info/RECORD +53 -0
- hotglue_singer_sdk-1.0.2.dist-info/WHEEL +4 -0
- hotglue_singer_sdk-1.0.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Abstract base class for stream mapper plugins."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from io import FileIO
|
|
5
|
+
from typing import Callable, Iterable, List, Tuple
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import singer
|
|
9
|
+
|
|
10
|
+
from hotglue_singer_sdk.cli import common_options
|
|
11
|
+
from hotglue_singer_sdk.configuration._dict_config import merge_config_sources
|
|
12
|
+
from hotglue_singer_sdk.helpers._classproperty import classproperty
|
|
13
|
+
from hotglue_singer_sdk.helpers.capabilities import CapabilitiesEnum, PluginCapabilities
|
|
14
|
+
from hotglue_singer_sdk.io_base import SingerReader
|
|
15
|
+
from hotglue_singer_sdk.plugin_base import PluginBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InlineMapper(PluginBase, SingerReader, metaclass=abc.ABCMeta):
|
|
19
|
+
"""Abstract base class for inline mappers."""
|
|
20
|
+
|
|
21
|
+
@classproperty
|
|
22
|
+
def _env_prefix(cls) -> str:
|
|
23
|
+
return f"{cls.name.upper().replace('-', '_')}_"
|
|
24
|
+
|
|
25
|
+
@classproperty
|
|
26
|
+
def capabilities(self) -> List[CapabilitiesEnum]:
|
|
27
|
+
"""Get capabilities.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of plugin capabilities.
|
|
31
|
+
"""
|
|
32
|
+
return [
|
|
33
|
+
PluginCapabilities.STREAM_MAPS,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _write_messages(messages: Iterable[singer.Message]) -> None:
|
|
38
|
+
for message in messages:
|
|
39
|
+
singer.write_message(message)
|
|
40
|
+
|
|
41
|
+
def _process_schema_message(self, message_dict: dict) -> None:
|
|
42
|
+
self._write_messages(self.map_schema_message(message_dict))
|
|
43
|
+
|
|
44
|
+
def _process_record_message(self, message_dict: dict) -> None:
|
|
45
|
+
self._write_messages(self.map_record_message(message_dict))
|
|
46
|
+
|
|
47
|
+
def _process_state_message(self, message_dict: dict) -> None:
|
|
48
|
+
self._write_messages(self.map_state_message(message_dict))
|
|
49
|
+
|
|
50
|
+
def _process_activate_version_message(self, message_dict: dict) -> None:
|
|
51
|
+
self._write_messages(self.map_activate_version_message(message_dict))
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def map_schema_message(self, message_dict: dict) -> Iterable[singer.Message]:
|
|
55
|
+
"""Map a schema message to zero or more new messages.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message_dict: A SCHEMA message JSON dictionary.
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@abc.abstractmethod
|
|
63
|
+
def map_record_message(self, message_dict: dict) -> Iterable[singer.Message]:
|
|
64
|
+
"""Map a record message to zero or more new messages.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
message_dict: A RECORD message JSON dictionary.
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
@abc.abstractmethod
|
|
72
|
+
def map_state_message(self, message_dict: dict) -> Iterable[singer.Message]:
|
|
73
|
+
"""Map a state message to zero or more new messages.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
message_dict: A STATE message JSON dictionary.
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@abc.abstractmethod
|
|
81
|
+
def map_activate_version_message(
|
|
82
|
+
self,
|
|
83
|
+
message_dict: dict,
|
|
84
|
+
) -> Iterable[singer.Message]:
|
|
85
|
+
"""Map a version message to zero or more new messages.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
message_dict: An ACTIVATE_VERSION message JSON dictionary.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@classproperty
|
|
93
|
+
def cli(cls) -> Callable:
|
|
94
|
+
"""Execute standard CLI handler for inline mappers.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A callable CLI object.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@common_options.PLUGIN_VERSION
|
|
101
|
+
@common_options.PLUGIN_ABOUT
|
|
102
|
+
@common_options.PLUGIN_ABOUT_FORMAT
|
|
103
|
+
@common_options.PLUGIN_CONFIG
|
|
104
|
+
@common_options.PLUGIN_FILE_INPUT
|
|
105
|
+
@click.command(
|
|
106
|
+
help="Execute the Singer mapper.",
|
|
107
|
+
context_settings={"help_option_names": ["--help"]},
|
|
108
|
+
)
|
|
109
|
+
def cli(
|
|
110
|
+
version: bool = False,
|
|
111
|
+
about: bool = False,
|
|
112
|
+
config: Tuple[str, ...] = (),
|
|
113
|
+
format: str = None,
|
|
114
|
+
file_input: FileIO = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Handle command line execution.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
version: Display the package version.
|
|
120
|
+
about: Display package metadata and settings.
|
|
121
|
+
format: Specify output style for `--about`.
|
|
122
|
+
config: Configuration file location or 'ENV' to use environment
|
|
123
|
+
variables. Accepts multiple inputs as a tuple.
|
|
124
|
+
file_input: Specify a path to an input file to read messages from.
|
|
125
|
+
Defaults to standard in if unspecified.
|
|
126
|
+
"""
|
|
127
|
+
if version:
|
|
128
|
+
cls.print_version()
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if not about:
|
|
132
|
+
cls.print_version(print_fn=cls.logger.info)
|
|
133
|
+
|
|
134
|
+
validate_config: bool = True
|
|
135
|
+
if about:
|
|
136
|
+
validate_config = False
|
|
137
|
+
|
|
138
|
+
cls.print_version(print_fn=cls.logger.info)
|
|
139
|
+
|
|
140
|
+
config_dict = merge_config_sources(
|
|
141
|
+
config,
|
|
142
|
+
cls.config_jsonschema,
|
|
143
|
+
cls._env_prefix,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
mapper = cls( # type: ignore # Ignore 'type not callable'
|
|
147
|
+
config=config_dict,
|
|
148
|
+
validate_config=validate_config,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if about:
|
|
152
|
+
mapper.print_about(format)
|
|
153
|
+
else:
|
|
154
|
+
mapper.listen(file_input)
|
|
155
|
+
|
|
156
|
+
return cli
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Shared parent class for Tap, Target (future), and Transform (future)."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from pathlib import PurePath
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
Dict,
|
|
14
|
+
List,
|
|
15
|
+
Mapping,
|
|
16
|
+
Optional,
|
|
17
|
+
Tuple,
|
|
18
|
+
Type,
|
|
19
|
+
Union,
|
|
20
|
+
cast,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
from jsonschema import Draft4Validator, SchemaError, ValidationError
|
|
25
|
+
|
|
26
|
+
from hotglue_singer_sdk.configuration._dict_config import parse_environment_config
|
|
27
|
+
from hotglue_singer_sdk.exceptions import ConfigValidationError
|
|
28
|
+
from hotglue_singer_sdk.helpers._classproperty import classproperty
|
|
29
|
+
from hotglue_singer_sdk.helpers._compat import metadata
|
|
30
|
+
from hotglue_singer_sdk.helpers._secrets import SecretString, is_common_secret_key
|
|
31
|
+
from hotglue_singer_sdk.helpers._util import read_json_file
|
|
32
|
+
from hotglue_singer_sdk.helpers.capabilities import (
|
|
33
|
+
FLATTENING_CONFIG,
|
|
34
|
+
STREAM_MAPS_CONFIG,
|
|
35
|
+
CapabilitiesEnum,
|
|
36
|
+
PluginCapabilities,
|
|
37
|
+
AlertingLevel,
|
|
38
|
+
)
|
|
39
|
+
from hotglue_singer_sdk.mapper import PluginMapper
|
|
40
|
+
from hotglue_singer_sdk.typing import extend_validator_with_defaults
|
|
41
|
+
|
|
42
|
+
SDK_PACKAGE_NAME = "hotglue_singer_sdk"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
JSONSchemaValidator = extend_validator_with_defaults(Draft4Validator)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PluginBase(metaclass=abc.ABCMeta):
|
|
49
|
+
"""Abstract base class for taps."""
|
|
50
|
+
|
|
51
|
+
name: str # The executable name of the tap or target plugin.
|
|
52
|
+
alerting_level: AlertingLevel = AlertingLevel.NONE
|
|
53
|
+
|
|
54
|
+
config_jsonschema: dict = {}
|
|
55
|
+
# A JSON Schema object defining the config options that this tap will accept.
|
|
56
|
+
|
|
57
|
+
_config: dict
|
|
58
|
+
|
|
59
|
+
@classproperty
|
|
60
|
+
def logger(cls) -> logging.Logger:
|
|
61
|
+
"""Get logger.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Plugin logger.
|
|
65
|
+
"""
|
|
66
|
+
# Get the level from <PLUGIN_NAME>_LOGLEVEL or LOGLEVEL environment variables
|
|
67
|
+
LOGLEVEL = (
|
|
68
|
+
os.environ.get(f"{cls.name.upper()}_LOGLEVEL")
|
|
69
|
+
or os.environ.get("LOGLEVEL")
|
|
70
|
+
or "INFO"
|
|
71
|
+
).upper()
|
|
72
|
+
|
|
73
|
+
assert (
|
|
74
|
+
LOGLEVEL in logging._levelToName.values()
|
|
75
|
+
), f"Invalid LOGLEVEL configuration: {LOGLEVEL}"
|
|
76
|
+
logger = logging.getLogger(cls.name)
|
|
77
|
+
logger.setLevel(LOGLEVEL)
|
|
78
|
+
return logger
|
|
79
|
+
|
|
80
|
+
# Constructor
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
config: Optional[Union[dict, PurePath, str, List[Union[PurePath, str]]]] = None,
|
|
85
|
+
parse_env_config: bool = False,
|
|
86
|
+
validate_config: bool = True,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Create the tap or target.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
config: May be one or more paths, either as str or PurePath objects, or
|
|
92
|
+
it can be a predetermined config dict.
|
|
93
|
+
parse_env_config: True to parse settings from env vars.
|
|
94
|
+
validate_config: True to require validation of config settings.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If config is not a dict or path string.
|
|
98
|
+
"""
|
|
99
|
+
if not config:
|
|
100
|
+
config_dict = {}
|
|
101
|
+
elif isinstance(config, str) or isinstance(config, PurePath):
|
|
102
|
+
config_dict = read_json_file(config)
|
|
103
|
+
elif isinstance(config, list):
|
|
104
|
+
config_dict = {}
|
|
105
|
+
for config_path in config:
|
|
106
|
+
# Read each config file sequentially. Settings from files later in the
|
|
107
|
+
# list will override those of earlier ones.
|
|
108
|
+
config_dict.update(read_json_file(config_path))
|
|
109
|
+
elif isinstance(config, dict):
|
|
110
|
+
config_dict = config
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Error parsing config of type '{type(config).__name__}'.")
|
|
113
|
+
if parse_env_config:
|
|
114
|
+
self.logger.info("Parsing env var for settings config...")
|
|
115
|
+
config_dict.update(self._env_var_config)
|
|
116
|
+
else:
|
|
117
|
+
self.logger.info("Skipping parse of env var settings...")
|
|
118
|
+
for k, v in config_dict.items():
|
|
119
|
+
if self._is_secret_config(k):
|
|
120
|
+
config_dict[k] = SecretString(v)
|
|
121
|
+
self._config = config_dict
|
|
122
|
+
self._validate_config(raise_errors=validate_config)
|
|
123
|
+
self.mapper: PluginMapper
|
|
124
|
+
|
|
125
|
+
@classproperty
|
|
126
|
+
def capabilities(self) -> List[CapabilitiesEnum]:
|
|
127
|
+
"""Get capabilities.
|
|
128
|
+
|
|
129
|
+
Developers may override this property in oder to add or remove
|
|
130
|
+
advertised capabilities for this plugin.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A list of plugin capabilities.
|
|
134
|
+
"""
|
|
135
|
+
return [
|
|
136
|
+
PluginCapabilities.STREAM_MAPS,
|
|
137
|
+
PluginCapabilities.FLATTENING,
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
@classproperty
|
|
141
|
+
def _env_var_config(cls) -> Dict[str, Any]:
|
|
142
|
+
"""Return any config specified in environment variables.
|
|
143
|
+
|
|
144
|
+
Variables must match the convention "<PLUGIN_NAME>_<SETTING_NAME>",
|
|
145
|
+
all uppercase with dashes converted to underscores.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary of configuration parsed from the environment.
|
|
149
|
+
"""
|
|
150
|
+
plugin_env_prefix = f"{cls.name.upper().replace('-', '_')}_"
|
|
151
|
+
config_jsonschema = cls.config_jsonschema
|
|
152
|
+
cls.append_builtin_config(config_jsonschema)
|
|
153
|
+
|
|
154
|
+
return parse_environment_config(config_jsonschema, plugin_env_prefix)
|
|
155
|
+
|
|
156
|
+
# Core plugin metadata:
|
|
157
|
+
|
|
158
|
+
@classproperty
|
|
159
|
+
def plugin_version(cls) -> str:
|
|
160
|
+
"""Get version.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The package version number.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
version = metadata.version(cls.name)
|
|
167
|
+
except metadata.PackageNotFoundError:
|
|
168
|
+
version = "[could not be detected]"
|
|
169
|
+
return version
|
|
170
|
+
|
|
171
|
+
@classproperty
|
|
172
|
+
def sdk_version(cls) -> str:
|
|
173
|
+
"""Return the package version number.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Meltano SDK version number.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
version = metadata.version(SDK_PACKAGE_NAME)
|
|
180
|
+
except metadata.PackageNotFoundError:
|
|
181
|
+
version = "[could not be detected]"
|
|
182
|
+
return version
|
|
183
|
+
|
|
184
|
+
# Abstract methods:
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def state(self) -> dict:
|
|
188
|
+
"""Get state.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
NotImplementedError: If the derived plugin doesn't override this method.
|
|
192
|
+
"""
|
|
193
|
+
raise NotImplementedError()
|
|
194
|
+
|
|
195
|
+
# Core plugin config:
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def config(self) -> Mapping[str, Any]:
|
|
199
|
+
"""Get config.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A frozen (read-only) config dictionary map.
|
|
203
|
+
"""
|
|
204
|
+
return cast(Dict, MappingProxyType(self._config))
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _is_secret_config(config_key: str) -> bool:
|
|
208
|
+
"""Check if config key is secret.
|
|
209
|
+
|
|
210
|
+
This prevents accidental printing to logs.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
config_key: Configuration key name to match against common secret names.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if a config value should be treated as a secret.
|
|
217
|
+
"""
|
|
218
|
+
return is_common_secret_key(config_key)
|
|
219
|
+
|
|
220
|
+
def _validate_config(
|
|
221
|
+
self, raise_errors: bool = True, warnings_as_errors: bool = False
|
|
222
|
+
) -> Tuple[List[str], List[str]]:
|
|
223
|
+
"""Validate configuration input against the plugin configuration JSON schema.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
raise_errors: Flag to throw an exception if any validation errors are found.
|
|
227
|
+
warnings_as_errors: Flag to throw an exception if any warnings were emitted.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
A tuple of configuration validation warnings and errors.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ConfigValidationError: If raise_errors is True and validation fails.
|
|
234
|
+
"""
|
|
235
|
+
warnings: List[str] = []
|
|
236
|
+
errors: List[str] = []
|
|
237
|
+
log_fn = self.logger.info
|
|
238
|
+
config_jsonschema = self.config_jsonschema
|
|
239
|
+
if config_jsonschema:
|
|
240
|
+
self.append_builtin_config(config_jsonschema)
|
|
241
|
+
try:
|
|
242
|
+
self.logger.debug(
|
|
243
|
+
f"Validating config using jsonschema: {config_jsonschema}"
|
|
244
|
+
)
|
|
245
|
+
validator = JSONSchemaValidator(config_jsonschema)
|
|
246
|
+
validator.validate(self._config)
|
|
247
|
+
except (ValidationError, SchemaError) as ex:
|
|
248
|
+
errors.append(str(ex.message))
|
|
249
|
+
if errors:
|
|
250
|
+
summary = (
|
|
251
|
+
f"Config validation failed: {f'; '.join(errors)}\n"
|
|
252
|
+
f"JSONSchema was: {config_jsonschema}"
|
|
253
|
+
)
|
|
254
|
+
if raise_errors:
|
|
255
|
+
raise ConfigValidationError(summary)
|
|
256
|
+
|
|
257
|
+
log_fn = self.logger.warning
|
|
258
|
+
else:
|
|
259
|
+
summary = f"Config validation passed with {len(warnings)} warnings."
|
|
260
|
+
for warning in warnings:
|
|
261
|
+
summary += f"\n{warning}"
|
|
262
|
+
if warnings_as_errors and raise_errors and warnings:
|
|
263
|
+
raise ConfigValidationError(
|
|
264
|
+
f"One or more warnings ocurred during validation: {warnings}"
|
|
265
|
+
)
|
|
266
|
+
log_fn(summary)
|
|
267
|
+
return warnings, errors
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def print_version(
|
|
271
|
+
cls: Type["PluginBase"],
|
|
272
|
+
print_fn: Callable[[Any], None] = print,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Print help text for the tap.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
print_fn: A function to use to display the plugin version.
|
|
278
|
+
Defaults to :function:`print`.
|
|
279
|
+
"""
|
|
280
|
+
print_fn(f"{cls.name} v{cls.plugin_version}, Meltano SDK v{cls.sdk_version}")
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def _get_about_info(cls: Type["PluginBase"]) -> Dict[str, Any]:
|
|
284
|
+
"""Returns capabilities and other tap metadata.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A dictionary containing the relevant 'about' information.
|
|
288
|
+
"""
|
|
289
|
+
info: Dict[str, Any] = OrderedDict({})
|
|
290
|
+
info["name"] = cls.name
|
|
291
|
+
info["description"] = cls.__doc__
|
|
292
|
+
info["version"] = cls.plugin_version
|
|
293
|
+
info["sdk_version"] = cls.sdk_version
|
|
294
|
+
info["capabilities"] = cls.capabilities
|
|
295
|
+
info["alerting_level"] = cls.alerting_level.value
|
|
296
|
+
|
|
297
|
+
config_jsonschema = cls.config_jsonschema
|
|
298
|
+
cls.append_builtin_config(config_jsonschema)
|
|
299
|
+
info["settings"] = config_jsonschema
|
|
300
|
+
return info
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def append_builtin_config(cls: Type["PluginBase"], config_jsonschema: dict) -> None:
|
|
304
|
+
"""Appends built-in config to `config_jsonschema` if not already set.
|
|
305
|
+
|
|
306
|
+
To customize or disable this behavior, developers may either override this class
|
|
307
|
+
method or override the `capabilities` property to disabled any unwanted
|
|
308
|
+
built-in capabilities.
|
|
309
|
+
|
|
310
|
+
For all except very advanced use cases, we recommend leaving these
|
|
311
|
+
implementations "as-is", since this provides the most choice to users and is
|
|
312
|
+
the most "future proof" in terms of taking advantage of built-in capabilities
|
|
313
|
+
which may be added in the future.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
config_jsonschema: [description]
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def _merge_missing(source_jsonschema: dict, target_jsonschema: dict) -> None:
|
|
320
|
+
# Append any missing properties in the target with those from source.
|
|
321
|
+
for k, v in source_jsonschema["properties"].items():
|
|
322
|
+
if k not in target_jsonschema["properties"]:
|
|
323
|
+
target_jsonschema["properties"][k] = v
|
|
324
|
+
|
|
325
|
+
capabilities = cls.capabilities
|
|
326
|
+
if PluginCapabilities.STREAM_MAPS in capabilities:
|
|
327
|
+
_merge_missing(STREAM_MAPS_CONFIG, config_jsonschema)
|
|
328
|
+
|
|
329
|
+
if PluginCapabilities.FLATTENING in capabilities:
|
|
330
|
+
_merge_missing(FLATTENING_CONFIG, config_jsonschema)
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def print_about(cls: Type["PluginBase"], format: Optional[str] = None) -> None:
|
|
334
|
+
"""Print capabilities and other tap metadata.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
format: Render option for the plugin information.
|
|
338
|
+
"""
|
|
339
|
+
info = cls._get_about_info()
|
|
340
|
+
|
|
341
|
+
if format == "json":
|
|
342
|
+
print(json.dumps(info, indent=2, default=str))
|
|
343
|
+
|
|
344
|
+
elif format == "markdown":
|
|
345
|
+
max_setting_len = cast(
|
|
346
|
+
int, max(len(k) for k in info["settings"]["properties"].keys())
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Set table base for markdown
|
|
350
|
+
table_base = (
|
|
351
|
+
f"| {'Setting':{max_setting_len}}| Required | Default | Description |\n"
|
|
352
|
+
f"|:{'-' * max_setting_len}|:--------:|:-------:|:------------|\n"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Empty list for string parts
|
|
356
|
+
md_list = []
|
|
357
|
+
# Get required settings for table
|
|
358
|
+
required_settings = info["settings"].get("required", [])
|
|
359
|
+
|
|
360
|
+
# Iterate over Dict to set md
|
|
361
|
+
md_list.append(
|
|
362
|
+
f"# `{info['name']}`\n\n"
|
|
363
|
+
f"{info['description']}\n\n"
|
|
364
|
+
f"Built with the [Meltano SDK](https://sdk.meltano.com) for "
|
|
365
|
+
"Singer Taps and Targets.\n\n"
|
|
366
|
+
)
|
|
367
|
+
for key, value in info.items():
|
|
368
|
+
|
|
369
|
+
if key == "capabilities":
|
|
370
|
+
capabilities = f"## {key.title()}\n\n"
|
|
371
|
+
capabilities += "\n".join([f"* `{v}`" for v in value])
|
|
372
|
+
capabilities += "\n\n"
|
|
373
|
+
md_list.append(capabilities)
|
|
374
|
+
|
|
375
|
+
if key == "settings":
|
|
376
|
+
setting = f"## {key.title()}\n\n"
|
|
377
|
+
for k, v in info["settings"].get("properties", {}).items():
|
|
378
|
+
md_description = v.get("description", "").replace("\n", "<BR/>")
|
|
379
|
+
table_base += (
|
|
380
|
+
f"| {k}{' ' * (max_setting_len - len(k))}"
|
|
381
|
+
f"| {'True' if k in required_settings else 'False':8} | "
|
|
382
|
+
f"{v.get('default', 'None'):7} | "
|
|
383
|
+
f"{md_description:11} |\n"
|
|
384
|
+
)
|
|
385
|
+
setting += table_base
|
|
386
|
+
setting += (
|
|
387
|
+
"\n"
|
|
388
|
+
+ "\n".join(
|
|
389
|
+
[
|
|
390
|
+
"A full list of supported settings and capabilities "
|
|
391
|
+
f"is available by running: `{info['name']} --about`"
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
+ "\n"
|
|
395
|
+
)
|
|
396
|
+
md_list.append(setting)
|
|
397
|
+
|
|
398
|
+
print("".join(md_list))
|
|
399
|
+
else:
|
|
400
|
+
formatted = "\n".join([f"{k.title()}: {v}" for k, v in info.items()])
|
|
401
|
+
print(formatted)
|
|
402
|
+
|
|
403
|
+
@classproperty
|
|
404
|
+
def cli(cls) -> Callable:
|
|
405
|
+
"""Handle command line execution.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
A callable CLI object.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
@click.command()
|
|
412
|
+
def cli() -> None:
|
|
413
|
+
pass
|
|
414
|
+
|
|
415
|
+
return cli
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Sink classes for targets."""
|
|
2
|
+
|
|
3
|
+
from hotglue_singer_sdk.sinks.batch import BatchSink
|
|
4
|
+
from hotglue_singer_sdk.sinks.core import Sink
|
|
5
|
+
from hotglue_singer_sdk.sinks.record import RecordSink
|
|
6
|
+
from hotglue_singer_sdk.sinks.sql import SQLConnector, SQLSink
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BatchSink",
|
|
10
|
+
"RecordSink",
|
|
11
|
+
"Sink",
|
|
12
|
+
"SQLSink",
|
|
13
|
+
"SQLConnector",
|
|
14
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Sink classes load data to a target."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import datetime
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from hotglue_singer_sdk.sinks.core import Sink
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BatchSink(Sink):
|
|
11
|
+
"""Base class for batched record writers."""
|
|
12
|
+
|
|
13
|
+
def _get_context(self, record: dict) -> dict:
|
|
14
|
+
"""Return a batch context. If no batch is active, return a new batch context.
|
|
15
|
+
|
|
16
|
+
The SDK-generated context will contain `batch_id` (GUID string) and
|
|
17
|
+
`batch_start_time` (datetime).
|
|
18
|
+
|
|
19
|
+
NOTE: Future versions of the SDK may expand the available context attributes.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
record: Individual record in the stream.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
TODO
|
|
26
|
+
"""
|
|
27
|
+
if self._pending_batch is None:
|
|
28
|
+
new_context = {
|
|
29
|
+
"batch_id": str(uuid.uuid4()),
|
|
30
|
+
"batch_start_time": datetime.datetime.now(),
|
|
31
|
+
}
|
|
32
|
+
self.start_batch(new_context)
|
|
33
|
+
self._pending_batch = new_context
|
|
34
|
+
|
|
35
|
+
return self._pending_batch
|
|
36
|
+
|
|
37
|
+
def start_batch(self, context: dict) -> None:
|
|
38
|
+
"""Start a new batch with the given context.
|
|
39
|
+
|
|
40
|
+
The SDK-generated context will contain `batch_id` (GUID string) and
|
|
41
|
+
`batch_start_time` (datetime).
|
|
42
|
+
|
|
43
|
+
Developers may optionally override this method to add custom markers to the
|
|
44
|
+
`context` dict and/or to initialize batch resources - such as initializing a
|
|
45
|
+
local temp file to hold batch records before uploading.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
context: Stream partition or context dictionary.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def process_record(self, record: dict, context: dict) -> None:
|
|
53
|
+
"""Load the latest record from the stream.
|
|
54
|
+
|
|
55
|
+
Developers may either load to the `context` dict for staging (the
|
|
56
|
+
default behavior for Batch types), or permanently write out to the target.
|
|
57
|
+
|
|
58
|
+
If this method is not overridden, the default implementation will create a
|
|
59
|
+
`context["records"]` list and append all records for processing during
|
|
60
|
+
:meth:`~hotglue_singer_sdk.BatchSink.process_batch()`.
|
|
61
|
+
|
|
62
|
+
If duplicates are merged, these can be tracked via
|
|
63
|
+
:meth:`~hotglue_singer_sdk.Sink.tally_duplicate_merged()`.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
record: Individual record in the stream.
|
|
67
|
+
context: Stream partition or context dictionary.
|
|
68
|
+
"""
|
|
69
|
+
if "records" not in context:
|
|
70
|
+
context["records"] = []
|
|
71
|
+
|
|
72
|
+
context["records"].append(record)
|
|
73
|
+
|
|
74
|
+
@abc.abstractmethod
|
|
75
|
+
def process_batch(self, context: dict) -> None:
|
|
76
|
+
"""Process a batch with the given batch context.
|
|
77
|
+
|
|
78
|
+
This method must be overridden.
|
|
79
|
+
|
|
80
|
+
If :meth:`~hotglue_singer_sdk.BatchSink.process_record()` is not overridden,
|
|
81
|
+
the `context["records"]` list will contain all records from the given batch
|
|
82
|
+
context.
|
|
83
|
+
|
|
84
|
+
If duplicates are merged, these can be tracked via
|
|
85
|
+
:meth:`~hotglue_singer_sdk.Sink.tally_duplicate_merged()`.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
context: Stream partition or context dictionary.
|
|
89
|
+
"""
|
|
90
|
+
pass
|