hotglue-singer-sdk 1.0.4__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.
@@ -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.
@@ -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
- # Process input catalog
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
- # Process state
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
- return [
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
@@ -74,6 +74,11 @@ class HotglueBaseSink(Rest):
74
74
 
75
75
  # remove failed records from the previous state so retrigger retries those records
76
76
  if self.previous_state:
77
+ if not self.previous_state.get("bookmarks"):
78
+ self.previous_state["bookmarks"] = {}
79
+ if not self.previous_state.get("summary"):
80
+ self.previous_state["summary"] = {}
81
+
77
82
  for stream in self.previous_state["bookmarks"]:
78
83
  self.previous_state["bookmarks"][stream] = [record for record in self.previous_state["bookmarks"][stream] if record.get("success") != False]
79
84
  for stream in self.previous_state["summary"]:
@@ -89,7 +94,7 @@ class HotglueBaseSink(Rest):
89
94
 
90
95
  # if previous state exists, add the hashes to the processed_hashes
91
96
  if self.previous_state:
92
- self.processed_hashes.extend([record["hash"] for record in self.previous_state["bookmarks"][self.name] if record.get("hash")])
97
+ self.processed_hashes.extend([record["hash"] for record in self.previous_state.get("bookmarks", {}).get(self.name, []) if record.get("hash")])
93
98
 
94
99
  # get the full target state
95
100
  target_state = self._target._latest_state
@@ -174,6 +179,14 @@ class HotglueSink(HotglueBaseSink, RecordSink):
174
179
  def preprocess_record(self, record: dict, context: dict) -> dict:
175
180
  raise NotImplementedError()
176
181
 
182
+
183
+ def _get_error_classification_metadata(self, error: Exception) -> dict:
184
+ if isinstance(error, (InvalidCredentialsError)):
185
+ return {"hg_error_class": InvalidCredentialsError.__name__}
186
+ elif isinstance(error, (InvalidPayloadError)):
187
+ return {"hg_error_class": InvalidPayloadError.__name__}
188
+ return {}
189
+
177
190
  def process_record(self, record: dict, context: dict) -> None:
178
191
  """Process the record."""
179
192
  if not self.latest_state:
@@ -197,8 +210,7 @@ class HotglueSink(HotglueBaseSink, RecordSink):
197
210
  success = False
198
211
  self.logger.exception(f"Preprocess record error {str(e)}")
199
212
  state_updates['error'] = str(e)
200
- if isinstance(e, (InvalidCredentialsError, InvalidPayloadError)):
201
- state_updates['hg_error_class'] = e.__class__.__name__
213
+ state_updates.update(self._get_error_classification_metadata(e))
202
214
 
203
215
  if success is not False:
204
216
 
@@ -226,8 +238,8 @@ class HotglueSink(HotglueBaseSink, RecordSink):
226
238
  except Exception as e:
227
239
  self.logger.exception(f"Upsert record error {str(e)}")
228
240
  state_updates['error'] = str(e)
229
- if isinstance(e, (InvalidCredentialsError, InvalidPayloadError)):
230
- state_updates['hg_error_class'] = e.__class__.__name__
241
+ success = False
242
+ state_updates.update(self._get_error_classification_metadata(e))
231
243
 
232
244
 
233
245
  if success:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotglue-singer-sdk
3
- Version: 1.0.4
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=b3hwA6liuEwY3CbOalvjdOP9cxkCO6_r2CqO95EapHA,6631
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=bBy8jF3c1XU3RWsYI_eTwIl-jJJnzNk_5_8E-6mbiuA,14258
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,11 +34,11 @@ 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=7lRZt40GzsMOeMlpoivxaG1Jjs5yB8JeR66JZhrLqeU,19644
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
41
- hotglue_singer_sdk/target_sdk/client.py,sha256=9SZL-rLvFlAmZyStuMzjjwXnPauvJ8RxtCNg25kXeqY,10529
41
+ hotglue_singer_sdk/target_sdk/client.py,sha256=fVQVFHtmKn4W2KOALI9XIreX6IY1ji2JeI5lwEiI-jY,10996
42
42
  hotglue_singer_sdk/target_sdk/common.py,sha256=l2ldX1qXxbzvjeaicu0If0CUMV_bmX2OV5NSFMol_fM,333
43
43
  hotglue_singer_sdk/target_sdk/lambda.py,sha256=LyYFS39r0zmCBbUEEwcueLOWI2L2YiKlfob8-dU9jMs,3685
44
44
  hotglue_singer_sdk/target_sdk/rest.py,sha256=3PUOx7pWg7DfbMonu2wiiQqU5mcN49B-_1mvQVgL4Gc,3607
@@ -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.4.dist-info/METADATA,sha256=Pscfl0Ojzj1ST8rmlNRSV1SmanH9URKmuUWq-0Bl95M,2376
51
- hotglue_singer_sdk-1.0.4.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
52
- hotglue_singer_sdk-1.0.4.dist-info/licenses/LICENSE,sha256=BGsDEGu628ZSlSfJzr3RshF0_KTW-E1Z--XnqjioYWg,11337
53
- hotglue_singer_sdk-1.0.4.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.3.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any