hotglue-singer-sdk 1.0.7__py3-none-any.whl → 1.0.8__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/helpers/capabilities.py +3 -0
- hotglue_singer_sdk/plugin_base.py +57 -1
- hotglue_singer_sdk/tap_base.py +53 -5
- {hotglue_singer_sdk-1.0.7.dist-info → hotglue_singer_sdk-1.0.8.dist-info}/METADATA +60 -1
- {hotglue_singer_sdk-1.0.7.dist-info → hotglue_singer_sdk-1.0.8.dist-info}/RECORD +7 -7
- {hotglue_singer_sdk-1.0.7.dist-info → hotglue_singer_sdk-1.0.8.dist-info}/WHEEL +1 -1
- {hotglue_singer_sdk-1.0.7.dist-info → hotglue_singer_sdk-1.0.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -182,6 +182,9 @@ class PluginCapabilities(CapabilitiesEnum):
|
|
|
182
182
|
# Supports raising hotglue exception classes
|
|
183
183
|
HOTGLUE_EXCEPTIONS_CLASSES = "hotglue-exceptions-classes"
|
|
184
184
|
|
|
185
|
+
# Supports updating the access token
|
|
186
|
+
ALLOWS_FETCH_ACCESS_TOKEN = "allows-fetch-access-token"
|
|
187
|
+
|
|
185
188
|
|
|
186
189
|
class TapCapabilities(CapabilitiesEnum):
|
|
187
190
|
"""Tap-specific capabilities."""
|
|
@@ -279,6 +279,19 @@ class PluginBase(metaclass=abc.ABCMeta):
|
|
|
279
279
|
"""
|
|
280
280
|
print_fn(f"{cls.name} v{cls.plugin_version}, Meltano SDK v{cls.sdk_version}")
|
|
281
281
|
|
|
282
|
+
@classmethod
|
|
283
|
+
def confirm_fetch_access_token_support(cls: Type["PluginBase"]) -> bool:
|
|
284
|
+
"""Check if fetch access token support is implemented.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if fetch access token support is implemented, False otherwise.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
cls.access_token_support()
|
|
291
|
+
except NotImplementedError:
|
|
292
|
+
return False
|
|
293
|
+
return True
|
|
294
|
+
|
|
282
295
|
@classmethod
|
|
283
296
|
def _get_about_info(cls: Type["PluginBase"]) -> Dict[str, Any]:
|
|
284
297
|
"""Returns capabilities and other tap metadata.
|
|
@@ -294,6 +307,7 @@ class PluginBase(metaclass=abc.ABCMeta):
|
|
|
294
307
|
info["capabilities"] = cls.capabilities
|
|
295
308
|
info["alerting_level"] = cls.alerting_level.value
|
|
296
309
|
|
|
310
|
+
# add settings to info
|
|
297
311
|
config_jsonschema = cls.config_jsonschema
|
|
298
312
|
cls.append_builtin_config(config_jsonschema)
|
|
299
313
|
info["settings"] = config_jsonschema
|
|
@@ -365,7 +379,6 @@ class PluginBase(metaclass=abc.ABCMeta):
|
|
|
365
379
|
"Singer Taps and Targets.\n\n"
|
|
366
380
|
)
|
|
367
381
|
for key, value in info.items():
|
|
368
|
-
|
|
369
382
|
if key == "capabilities":
|
|
370
383
|
capabilities = f"## {key.title()}\n\n"
|
|
371
384
|
capabilities += "\n".join([f"* `{v}`" for v in value])
|
|
@@ -400,6 +413,49 @@ class PluginBase(metaclass=abc.ABCMeta):
|
|
|
400
413
|
formatted = "\n".join([f"{k.title()}: {v}" for k, v in info.items()])
|
|
401
414
|
print(formatted)
|
|
402
415
|
|
|
416
|
+
@classmethod
|
|
417
|
+
def access_token_support(cls: Type["PluginBase"], connector: Any = None) -> None:
|
|
418
|
+
"""Get access token support.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
A tuple of the authenticator class and the auth endpoint.
|
|
422
|
+
"""
|
|
423
|
+
raise NotImplementedError()
|
|
424
|
+
|
|
425
|
+
@classmethod
|
|
426
|
+
def fetch_access_token(cls: Type["PluginBase"], connector) -> dict:
|
|
427
|
+
"""Fetch access token.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
A tuple of the authenticator class and the auth endpoint.
|
|
431
|
+
"""
|
|
432
|
+
if not cls.confirm_fetch_access_token_support():
|
|
433
|
+
print(json.dumps({"error": "Fetch access token support is not implemented"}, indent=2))
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
authenticator, auth_endpoint = cls.access_token_support(connector)
|
|
437
|
+
# Check if a config file path is available for writing updated tokens
|
|
438
|
+
if connector.config_file is None:
|
|
439
|
+
print(
|
|
440
|
+
json.dumps(
|
|
441
|
+
{
|
|
442
|
+
"error": "The --access-token flag requires a config file path. "
|
|
443
|
+
"Please provide a path to a config file instead of "
|
|
444
|
+
"using --config ENV or omitting the config."
|
|
445
|
+
},
|
|
446
|
+
indent=2,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
cls.update_access_token(authenticator, auth_endpoint, connector)
|
|
453
|
+
access_token = {"access_token": connector.config.get("access_token")}
|
|
454
|
+
print(json.dumps(dict(access_token), indent=2, default=str))
|
|
455
|
+
return access_token
|
|
456
|
+
except Exception as ex:
|
|
457
|
+
print(json.dumps({"error": str(ex)}, indent=2))
|
|
458
|
+
|
|
403
459
|
@classproperty
|
|
404
460
|
def cli(cls) -> Callable:
|
|
405
461
|
"""Handle command line execution.
|
hotglue_singer_sdk/tap_base.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
import json
|
|
5
|
+
import sys
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from pathlib import Path, PurePath
|
|
7
8
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, cast
|
|
@@ -78,7 +79,7 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
78
79
|
self._catalog: Optional[Catalog] = None # Tap's working catalog
|
|
79
80
|
self.config_file = config[0] if config else None
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
def register_streams_from_catalog(self, catalog):
|
|
82
83
|
if isinstance(catalog, Catalog):
|
|
83
84
|
self._input_catalog = catalog
|
|
84
85
|
elif isinstance(catalog, dict):
|
|
@@ -95,7 +96,8 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
95
96
|
|
|
96
97
|
self.mapper.register_raw_streams_from_catalog(self.catalog)
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
|
|
100
|
+
def register_state_from_file(self, state):
|
|
99
101
|
state_dict: dict = {}
|
|
100
102
|
if isinstance(state, dict):
|
|
101
103
|
state_dict = state
|
|
@@ -103,6 +105,7 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
103
105
|
state_dict = read_json_file(state)
|
|
104
106
|
self.load_state(state_dict)
|
|
105
107
|
|
|
108
|
+
|
|
106
109
|
# Class properties
|
|
107
110
|
|
|
108
111
|
@property
|
|
@@ -166,7 +169,7 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
166
169
|
Returns:
|
|
167
170
|
A list of capabilities supported by this tap.
|
|
168
171
|
"""
|
|
169
|
-
|
|
172
|
+
capabilities = [
|
|
170
173
|
TapCapabilities.CATALOG,
|
|
171
174
|
TapCapabilities.STATE,
|
|
172
175
|
TapCapabilities.DISCOVER,
|
|
@@ -175,6 +178,10 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
175
178
|
PluginCapabilities.FLATTENING,
|
|
176
179
|
]
|
|
177
180
|
|
|
181
|
+
if self.confirm_fetch_access_token_support():
|
|
182
|
+
capabilities.append(PluginCapabilities.ALLOWS_FETCH_ACCESS_TOKEN)
|
|
183
|
+
return capabilities
|
|
184
|
+
|
|
178
185
|
# Connection test:
|
|
179
186
|
|
|
180
187
|
@final
|
|
@@ -265,6 +272,34 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
265
272
|
"Please set the '--catalog' command line argument and try again."
|
|
266
273
|
)
|
|
267
274
|
|
|
275
|
+
@classmethod
|
|
276
|
+
def update_access_token(cls, authenticator, auth_endpoint, tap) -> None:
|
|
277
|
+
"""Update the access token.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
None
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
# If the tap has a use_auth_dummy_stream method, use it to create a dummy stream
|
|
284
|
+
# normally used for taps with dynamic catalogs
|
|
285
|
+
class DummyStream:
|
|
286
|
+
def __init__(self, tap):
|
|
287
|
+
self._tap = tap
|
|
288
|
+
self.logger = tap.logger
|
|
289
|
+
self.tap_name = tap.name
|
|
290
|
+
self.config = tap.config
|
|
291
|
+
|
|
292
|
+
stream = DummyStream(tap)
|
|
293
|
+
auth = authenticator(
|
|
294
|
+
stream=stream,
|
|
295
|
+
config_file=tap.config_file,
|
|
296
|
+
auth_endpoint=auth_endpoint,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Update the access token
|
|
300
|
+
if not auth.is_token_valid():
|
|
301
|
+
auth.update_access_token()
|
|
302
|
+
|
|
268
303
|
@final
|
|
269
304
|
def load_streams(self) -> List[Stream]:
|
|
270
305
|
"""Load streams from discovery and initialize DAG.
|
|
@@ -385,8 +420,6 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
385
420
|
for stream in self.streams.values():
|
|
386
421
|
stream.log_sync_costs()
|
|
387
422
|
|
|
388
|
-
# Command Line Execution
|
|
389
|
-
|
|
390
423
|
@classproperty
|
|
391
424
|
def cli(cls) -> Callable:
|
|
392
425
|
"""Execute standard CLI handler for taps.
|
|
@@ -425,6 +458,12 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
425
458
|
help="Use a bookmarks file for incremental replication.",
|
|
426
459
|
type=click.Path(),
|
|
427
460
|
)
|
|
461
|
+
@click.option(
|
|
462
|
+
"--access-token",
|
|
463
|
+
"access_token",
|
|
464
|
+
is_flag=True,
|
|
465
|
+
help="Refresh the OAuth access token and update the config file.",
|
|
466
|
+
)
|
|
428
467
|
@click.command(
|
|
429
468
|
help="Execute the Singer tap.",
|
|
430
469
|
context_settings={"help_option_names": ["--help"]},
|
|
@@ -438,6 +477,7 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
438
477
|
state: str = None,
|
|
439
478
|
catalog: str = None,
|
|
440
479
|
format: str = None,
|
|
480
|
+
access_token: bool = False,
|
|
441
481
|
) -> None:
|
|
442
482
|
"""Handle command line execution.
|
|
443
483
|
|
|
@@ -451,6 +491,7 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
451
491
|
variables. Accepts multiple inputs as a tuple.
|
|
452
492
|
catalog: Use a Singer catalog file with the tap.",
|
|
453
493
|
state: Use a bookmarks file for incremental replication.
|
|
494
|
+
access_token: Refresh the OAuth access token and update the config.
|
|
454
495
|
|
|
455
496
|
Raises:
|
|
456
497
|
FileNotFoundError: If the config file does not exist.
|
|
@@ -495,7 +536,12 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
495
536
|
validate_config=validate_config,
|
|
496
537
|
)
|
|
497
538
|
|
|
539
|
+
if access_token:
|
|
540
|
+
return cls.fetch_access_token(connector=tap)
|
|
541
|
+
|
|
498
542
|
if discover:
|
|
543
|
+
tap.register_streams_from_catalog(catalog)
|
|
544
|
+
tap.register_state_from_file(state)
|
|
499
545
|
tap.run_discovery()
|
|
500
546
|
if test == CliTestOptionValue.All.value:
|
|
501
547
|
tap.run_connection_test()
|
|
@@ -504,6 +550,8 @@ class Tap(PluginBase, metaclass=abc.ABCMeta):
|
|
|
504
550
|
elif test == CliTestOptionValue.Schema.value:
|
|
505
551
|
tap.write_schemas()
|
|
506
552
|
else:
|
|
553
|
+
tap.register_streams_from_catalog(catalog)
|
|
554
|
+
tap.register_state_from_file(state)
|
|
507
555
|
tap.sync_all()
|
|
508
556
|
|
|
509
557
|
return cli
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hotglue-singer-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: A framework for building Singer taps and targets
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -51,3 +51,62 @@ This is a fork of Melanto's SingerSDK for special use in [hotglue](https://hotgl
|
|
|
51
51
|
Taps and targets built on the SDK are automatically compliant with the
|
|
52
52
|
[Singer Spec](https://hub.meltano.com/singer/spec), the
|
|
53
53
|
de-facto open source standard for extract and load pipelines.
|
|
54
|
+
|
|
55
|
+
## OAuth Access Token Support
|
|
56
|
+
|
|
57
|
+
Taps can implement the `--access-token` CLI flag to refresh OAuth access tokens without running the tap directly.
|
|
58
|
+
|
|
59
|
+
### Implementing Access Token Support
|
|
60
|
+
|
|
61
|
+
To enable this feature in your tap, override the `access_token_support` class method to return a tuple of `(authenticator_class, auth_endpoint)`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from hotglue_singer_sdk import Tap
|
|
65
|
+
from my_tap.auth import MyOAuthAuthenticator
|
|
66
|
+
|
|
67
|
+
class MyTap(Tap):
|
|
68
|
+
name = "tap-myservice"
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def access_token_support(cls, connector=None):
|
|
72
|
+
"""Return the authenticator class and auth endpoint for token refresh.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
A tuple of (authenticator_class, auth_endpoint).
|
|
76
|
+
"""
|
|
77
|
+
default_url = "https://api.myservice.com/oauth/token"
|
|
78
|
+
# ommit if token url is not dynamic
|
|
79
|
+
dynamic_url = connector.config.get("auth_url")
|
|
80
|
+
url = dynamic_url or default_url
|
|
81
|
+
return (MyOAuthAuthenticator, url)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Authenticator Requirements
|
|
85
|
+
|
|
86
|
+
The authenticator class must implement the following methods:
|
|
87
|
+
|
|
88
|
+
- `is_token_valid()` - Returns `True` if the current access token is still valid
|
|
89
|
+
- `update_access_token()` - Refreshes the access token and updates the config file
|
|
90
|
+
|
|
91
|
+
The authenticator will be instantiated with these parameters:
|
|
92
|
+
- `stream` - A dummy stream object with `logger`, `tap_name`, and `config` attributes
|
|
93
|
+
- `config_file` - Path to the config file for writing updated tokens
|
|
94
|
+
- `auth_endpoint` - The OAuth token endpoint URL
|
|
95
|
+
|
|
96
|
+
### Usage
|
|
97
|
+
|
|
98
|
+
Once implemented, users can refresh the access token using:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
tap-myservice --config config.json --access-token
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This will output the new access token as JSON:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"access_token": "new_token_value"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Note:** The `--access-token` flag requires a config file path. It will not work with `--config ENV` or when omitting the config.
|
|
@@ -17,12 +17,12 @@ hotglue_singer_sdk/helpers/_singer.py,sha256=UgSQW4cGX43a7uxCRp76bdI2C0Zbhd8mfVH
|
|
|
17
17
|
hotglue_singer_sdk/helpers/_state.py,sha256=ptPeelu0BO-myScjhFXW0QEIqKhLB1fELWHEOte-HM4,9668
|
|
18
18
|
hotglue_singer_sdk/helpers/_typing.py,sha256=vKYNXqiie6niSeSQQPkNSv4dxjud4LGYR-SccvZC8dg,8418
|
|
19
19
|
hotglue_singer_sdk/helpers/_util.py,sha256=jGQABMvaKuxFVet4b9FQcWQZnOIRADVXFFzVg311B3A,846
|
|
20
|
-
hotglue_singer_sdk/helpers/capabilities.py,sha256=
|
|
20
|
+
hotglue_singer_sdk/helpers/capabilities.py,sha256=p2m2FjBlyXM_Zbhxfbu9OWQeqNjcWA8vGpn1UL2S9sU,6733
|
|
21
21
|
hotglue_singer_sdk/helpers/jsonpath.py,sha256=oe15S0tE8Pk67Wocg1QdrHmTfDvf1Ay9rOjulqqSU8I,995
|
|
22
22
|
hotglue_singer_sdk/io_base.py,sha256=tWRaB_IlZuI8gnO1xSLuT4Cd0KKj34Ii2TXm2I6TdA4,4018
|
|
23
23
|
hotglue_singer_sdk/mapper.py,sha256=wrk-MoAS0kcF24z29zR4PD_ryRb0MRyxYTCXHNf61SA,23874
|
|
24
24
|
hotglue_singer_sdk/mapper_base.py,sha256=7kPC6yu8c0XXp46bPiP6FX8I2n5Nx-kOoUKCsbREsog,4968
|
|
25
|
-
hotglue_singer_sdk/plugin_base.py,sha256=
|
|
25
|
+
hotglue_singer_sdk/plugin_base.py,sha256=CLpSsTy4t_VZdnHJ4kj-CEGjTVkx9CztaY-Zw9XvqUY,16337
|
|
26
26
|
hotglue_singer_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
hotglue_singer_sdk/sinks/__init__.py,sha256=UtX5EbfE1PKzAW9jptfytGkzpLlTr3a9SgrSCnm_SMs,348
|
|
28
28
|
hotglue_singer_sdk/sinks/batch.py,sha256=a19tgNDMb85c3Bo7bcrcOzPRy1VYKRYuVWZEg9fBUJU,3005
|
|
@@ -34,7 +34,7 @@ hotglue_singer_sdk/streams/core.py,sha256=q1P6ecJ7-Lix1BltLNSvnGlKvUlsnhAW9ReeB1
|
|
|
34
34
|
hotglue_singer_sdk/streams/graphql.py,sha256=HGzJhogpja37pupMsqiXZd6YxV_aZw6LckBjk1R68k8,2615
|
|
35
35
|
hotglue_singer_sdk/streams/rest.py,sha256=FpbPK90-hCCMh1BY0xKvnAg5vpiav3Ls1f2QREUb55k,21420
|
|
36
36
|
hotglue_singer_sdk/streams/sql.py,sha256=UZSpXTRZNIH3C_D5R7-NO3z__QizgJtIHxg1iL8o5wE,34994
|
|
37
|
-
hotglue_singer_sdk/tap_base.py,sha256=
|
|
37
|
+
hotglue_singer_sdk/tap_base.py,sha256=DCqNZT5d24HgZHrk89aFG035zNg8trOB4751aPliZDE,21312
|
|
38
38
|
hotglue_singer_sdk/target_base.py,sha256=QbBVXr6x5G3bgbPwpMG80ttL0TMOK8bjJCbJsa4AOFw,19130
|
|
39
39
|
hotglue_singer_sdk/target_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
40
|
hotglue_singer_sdk/target_sdk/auth.py,sha256=jU3yL5mtlf8p2VWsfoR_TaMjp8_APpQNPtPgHRIUIBc,3925
|
|
@@ -47,7 +47,7 @@ hotglue_singer_sdk/target_sdk/target.py,sha256=uVqmXmyIO-h3M8VCiZ1McPXczZj7EU0PO
|
|
|
47
47
|
hotglue_singer_sdk/target_sdk/target_base.py,sha256=LyQQndYGlzu5LcIj-MMcmcjdGAmAYVgvdNAxvCgFpvk,22022
|
|
48
48
|
hotglue_singer_sdk/testing.py,sha256=BifsP9X83pgKdfynI5CComchoRWEHpe81h-UfOwK_G0,5860
|
|
49
49
|
hotglue_singer_sdk/typing.py,sha256=jTGFhON9uBZe9e0vHH6-6rjeD2YrpzolPiYigIuo7zU,16186
|
|
50
|
-
hotglue_singer_sdk-1.0.
|
|
51
|
-
hotglue_singer_sdk-1.0.
|
|
52
|
-
hotglue_singer_sdk-1.0.
|
|
53
|
-
hotglue_singer_sdk-1.0.
|
|
50
|
+
hotglue_singer_sdk-1.0.8.dist-info/METADATA,sha256=SMmNAlvVRyElWcR-3bHBGYauQCZXJ-EaPG6CnjiJbGA,4228
|
|
51
|
+
hotglue_singer_sdk-1.0.8.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
52
|
+
hotglue_singer_sdk-1.0.8.dist-info/licenses/LICENSE,sha256=BGsDEGu628ZSlSfJzr3RshF0_KTW-E1Z--XnqjioYWg,11337
|
|
53
|
+
hotglue_singer_sdk-1.0.8.dist-info/RECORD,,
|
|
File without changes
|