From 36965ad6eb1d71cf5a78b4c194879e331cbc90ac Mon Sep 17 00:00:00 2001 From: Hadrian de Oliveira Date: Tue, 13 Aug 2024 12:57:46 -0300 Subject: [PATCH 1/3] feat: add oauth authenticator --- tap_hubspot/client.py | 25 +++++++++++++++++++++---- tap_hubspot/tap.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tap_hubspot/client.py b/tap_hubspot/client.py index 29a636c..0f8edcd 100644 --- a/tap_hubspot/client.py +++ b/tap_hubspot/client.py @@ -5,10 +5,10 @@ import backoff import pytz import requests -from typing import Generator +from typing import Generator, Union from singer_sdk import typing as th from singer_sdk._singerlib.utils import strptime_to_utc -from singer_sdk.authenticators import BearerTokenAuthenticator +from singer_sdk.authenticators import BearerTokenAuthenticator, OAuthAuthenticator from singer_sdk.exceptions import RetriableAPIError from singer_sdk.helpers.jsonpath import extract_jsonpath from singer_sdk.streams import RESTStream @@ -28,6 +28,21 @@ ] MAX_PROPERTIES_LEN = 15000 +class HubSpotOAuthAuthenticator(OAuthAuthenticator): + def __init__(self, stream: RESTStream) -> None: + super().__init__( + auth_endpoint="https://api.hubapi.com/oauth/v1/token", + stream=stream, + ) + + @property + def oauth_request_body(self) -> dict: + return { + "client_id": self.config["client_id"], + "client_secret": self.config.get("client_secret"), + "refresh_token": self.config.get("refresh_token"), + "grant_type": "refresh_token", + } class HubspotStream(RESTStream): """Hubspot stream class.""" @@ -48,8 +63,10 @@ def schema_filepath(self) -> Path: return SCHEMAS_DIR / f"{self.name}.json" @property - def authenticator(self) -> BearerTokenAuthenticator: + def authenticator(self) -> Union[BearerTokenAuthenticator, OAuthAuthenticator]: """Return a new authenticator object.""" + if self.config.get("auth_type") == "oauth2": + return HubspotOAuthAuthenticator(self) return BearerTokenAuthenticator.create_for_stream( self, token=self.config.get("access_token"), @@ -299,4 +316,4 @@ def post_process( for subkey in keys_to_remove: row[key].pop(subkey) - return row \ No newline at end of file + return row diff --git a/tap_hubspot/tap.py b/tap_hubspot/tap.py index 3235d55..3eca34f 100644 --- a/tap_hubspot/tap.py +++ b/tap_hubspot/tap.py @@ -101,12 +101,48 @@ class TapHubspot(Tap): required=True, description="PRIVATE Access Token for Hubspot API", ), + th.Property( + "auth_type", + th.OneOf(th.Constant("access_token"), th.Constant("oauth2")), + required=False, + description="PRIVATE Auth type for Hubspot API", + ), + th.Property( + "client_id", + th.StringType, + required=False, + description="PRIVATE Client ID for Hubspot API OAuth", + ), + th.Property( + "client_secret", + th.StringType, + required=False, + description="PRIVATE Client Secret for Hubspot API OAuth", + ), + th.Property( + "expiry_date", + th.StringType, + required=False, + description="PRIVATE Access Token expiry date for Hubspot API OAuth", + ), + th.Property( + "refresh_token", + th.StringType, + required=False, + description="PRIVATE Refresh Token for Hubspot API OAuth", + ), th.Property( "start_date", th.DateTimeType, required=True, description="The earliest record date to sync", ), + th.Property( + "token_uri", + th.StringType, + required=False, + description="PRIVATE Token URI for Hubspot API OAuth", + ), ).to_dict() def discover_streams(self) -> List[Stream]: From 8d9a0824659fd13242c14c35583877ce358cee5b Mon Sep 17 00:00:00 2001 From: Hadrian de Oliveira Date: Wed, 14 Aug 2024 08:14:50 -0300 Subject: [PATCH 2/3] feat: extract auth class; remove auth type --- tap_hubspot/auth.py | 19 +++++++++++++++++++ tap_hubspot/client.py | 21 ++++----------------- tap_hubspot/tap.py | 12 +++--------- 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 tap_hubspot/auth.py diff --git a/tap_hubspot/auth.py b/tap_hubspot/auth.py new file mode 100644 index 0000000..35b72a5 --- /dev/null +++ b/tap_hubspot/auth.py @@ -0,0 +1,19 @@ +from singer_sdk.authenticators import OAuthAuthenticator +from singer_sdk.streams import RESTStream + + +class HubSpotOAuthAuthenticator(OAuthAuthenticator): + def __init__(self, stream: RESTStream) -> None: + super().__init__( + auth_endpoint="https://api.hubapi.com/oauth/v1/token", + stream=stream, + ) + + @property + def oauth_request_body(self) -> dict: + return { + "client_id": self.config["client_id"], + "client_secret": self.config.get("client_secret"), + "grant_type": "refresh_token", + "refresh_token": self.config.get("refresh_token"), + } diff --git a/tap_hubspot/client.py b/tap_hubspot/client.py index 0f8edcd..1da8256 100644 --- a/tap_hubspot/client.py +++ b/tap_hubspot/client.py @@ -13,6 +13,8 @@ from singer_sdk.helpers.jsonpath import extract_jsonpath from singer_sdk.streams import RESTStream from singer_sdk.exceptions import FatalAPIError, RetriableAPIError +from tap_hubspot.auth import HubSpotOAuthAuthenticator + SCHEMAS_DIR = Path(__file__).parent / Path("./schemas") HUBSPOT_OBJECTS = [ @@ -28,21 +30,6 @@ ] MAX_PROPERTIES_LEN = 15000 -class HubSpotOAuthAuthenticator(OAuthAuthenticator): - def __init__(self, stream: RESTStream) -> None: - super().__init__( - auth_endpoint="https://api.hubapi.com/oauth/v1/token", - stream=stream, - ) - - @property - def oauth_request_body(self) -> dict: - return { - "client_id": self.config["client_id"], - "client_secret": self.config.get("client_secret"), - "refresh_token": self.config.get("refresh_token"), - "grant_type": "refresh_token", - } class HubspotStream(RESTStream): """Hubspot stream class.""" @@ -65,8 +52,8 @@ def schema_filepath(self) -> Path: @property def authenticator(self) -> Union[BearerTokenAuthenticator, OAuthAuthenticator]: """Return a new authenticator object.""" - if self.config.get("auth_type") == "oauth2": - return HubspotOAuthAuthenticator(self) + if "refresh_token" in self.config: + return HubSpotOAuthAuthenticator(self) return BearerTokenAuthenticator.create_for_stream( self, token=self.config.get("access_token"), diff --git a/tap_hubspot/tap.py b/tap_hubspot/tap.py index 3eca34f..2727aa1 100644 --- a/tap_hubspot/tap.py +++ b/tap_hubspot/tap.py @@ -101,12 +101,6 @@ class TapHubspot(Tap): required=True, description="PRIVATE Access Token for Hubspot API", ), - th.Property( - "auth_type", - th.OneOf(th.Constant("access_token"), th.Constant("oauth2")), - required=False, - description="PRIVATE Auth type for Hubspot API", - ), th.Property( "client_id", th.StringType, @@ -120,10 +114,10 @@ class TapHubspot(Tap): description="PRIVATE Client Secret for Hubspot API OAuth", ), th.Property( - "expiry_date", - th.StringType, + "expires_in", + th.IntegerType, required=False, - description="PRIVATE Access Token expiry date for Hubspot API OAuth", + description="PRIVATE Expiration time for Hubspot API OAuth", ), th.Property( "refresh_token", From 7d6cbe32c530d590e4823bdecd6b08fd3b3bde4a Mon Sep 17 00:00:00 2001 From: Hadrian de Oliveira Date: Thu, 15 Aug 2024 15:07:47 -0300 Subject: [PATCH 3/3] fix: remove unused inputs; add credentials type guard --- tap_hubspot/auth.py | 19 +++++++++++++++---- tap_hubspot/client.py | 4 ++-- tap_hubspot/tap.py | 23 +++++++---------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/tap_hubspot/auth.py b/tap_hubspot/auth.py index 35b72a5..68789c4 100644 --- a/tap_hubspot/auth.py +++ b/tap_hubspot/auth.py @@ -1,8 +1,9 @@ -from singer_sdk.authenticators import OAuthAuthenticator +from singer_sdk.authenticators import OAuthAuthenticator, SingletonMeta from singer_sdk.streams import RESTStream +from typing import Any, Mapping, TypedDict, TypeGuard -class HubSpotOAuthAuthenticator(OAuthAuthenticator): +class HubSpotOAuthAuthenticator(OAuthAuthenticator, metaclass=SingletonMeta): def __init__(self, stream: RESTStream) -> None: super().__init__( auth_endpoint="https://api.hubapi.com/oauth/v1/token", @@ -13,7 +14,17 @@ def __init__(self, stream: RESTStream) -> None: def oauth_request_body(self) -> dict: return { "client_id": self.config["client_id"], - "client_secret": self.config.get("client_secret"), + "client_secret": self.config["client_secret"], "grant_type": "refresh_token", - "refresh_token": self.config.get("refresh_token"), + "refresh_token": self.config["refresh_token"], } + + +class OAuthCredentials(TypedDict): + client_id: str + client_secret: str + refresh_token: str + + +def is_oauth_credentials(value: Mapping[str, Any]) -> TypeGuard[OAuthCredentials]: + return "refresh_token" in value diff --git a/tap_hubspot/client.py b/tap_hubspot/client.py index 1da8256..4cc3efa 100644 --- a/tap_hubspot/client.py +++ b/tap_hubspot/client.py @@ -13,7 +13,7 @@ from singer_sdk.helpers.jsonpath import extract_jsonpath from singer_sdk.streams import RESTStream from singer_sdk.exceptions import FatalAPIError, RetriableAPIError -from tap_hubspot.auth import HubSpotOAuthAuthenticator +from tap_hubspot.auth import HubSpotOAuthAuthenticator, is_oauth_credentials SCHEMAS_DIR = Path(__file__).parent / Path("./schemas") @@ -52,7 +52,7 @@ def schema_filepath(self) -> Path: @property def authenticator(self) -> Union[BearerTokenAuthenticator, OAuthAuthenticator]: """Return a new authenticator object.""" - if "refresh_token" in self.config: + if is_oauth_credentials(self.config): return HubSpotOAuthAuthenticator(self) return BearerTokenAuthenticator.create_for_stream( self, diff --git a/tap_hubspot/tap.py b/tap_hubspot/tap.py index 2727aa1..b7517f0 100644 --- a/tap_hubspot/tap.py +++ b/tap_hubspot/tap.py @@ -99,31 +99,28 @@ class TapHubspot(Tap): "access_token", th.StringType, required=True, - description="PRIVATE Access Token for Hubspot API", + secret=True, + description="Access Token for Hubspot API", ), th.Property( "client_id", th.StringType, required=False, - description="PRIVATE Client ID for Hubspot API OAuth", + description="Client ID for Hubspot API OAuth", ), th.Property( "client_secret", th.StringType, required=False, - description="PRIVATE Client Secret for Hubspot API OAuth", - ), - th.Property( - "expires_in", - th.IntegerType, - required=False, - description="PRIVATE Expiration time for Hubspot API OAuth", + secret=True, + description="Client Secret for Hubspot API OAuth", ), th.Property( "refresh_token", th.StringType, required=False, - description="PRIVATE Refresh Token for Hubspot API OAuth", + secret=True, + description="Refresh Token for Hubspot API OAuth", ), th.Property( "start_date", @@ -131,12 +128,6 @@ class TapHubspot(Tap): required=True, description="The earliest record date to sync", ), - th.Property( - "token_uri", - th.StringType, - required=False, - description="PRIVATE Token URI for Hubspot API OAuth", - ), ).to_dict() def discover_streams(self) -> List[Stream]: