From 9087d61d36988928a2333c8c2efe2bc33c9c626b Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:29:42 -0400 Subject: [PATCH 1/7] add option for oauth auth --- tap_linkedin_ads/__main__.py | 7 ++++++ tap_linkedin_ads/client.py | 42 ++++++++++++++++++++++-------------- tap_linkedin_ads/tap.py | 23 +++++++++++++++++++- 3 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 tap_linkedin_ads/__main__.py diff --git a/tap_linkedin_ads/__main__.py b/tap_linkedin_ads/__main__.py new file mode 100644 index 0000000..0a03042 --- /dev/null +++ b/tap_linkedin_ads/__main__.py @@ -0,0 +1,7 @@ +"""LinkedInAds entry point.""" + +from __future__ import annotations + +from tap_linkedin_ads.tap import TapLinkedInAds + +TapLinkedInAds.cli() diff --git a/tap_linkedin_ads/client.py b/tap_linkedin_ads/client.py index 51b8d4d..e2ac47c 100644 --- a/tap_linkedin_ads/client.py +++ b/tap_linkedin_ads/client.py @@ -7,31 +7,47 @@ from datetime import datetime, timezone from pathlib import Path -from singer_sdk.authenticators import BearerTokenAuthenticator +import requests +from singer_sdk.authenticators import BearerTokenAuthenticator, OAuthAuthenticator, SingletonMeta from singer_sdk.streams import RESTStream -if t.TYPE_CHECKING: - import requests - SCHEMAS_DIR = Path(__file__).parent / Path("./schemas") UTC = timezone.utc +_Auth = t.Callable[[requests.PreparedRequest], requests.PreparedRequest] + + +class LinkedInAdsOAuthAuthenticator(OAuthAuthenticator, metaclass=SingletonMeta): + """Authenticator class for LinkedInAds.""" + + @property + def oauth_request_body(self): + return { + "grant_type": "refresh_token", + "client_id": self.config["oauth_credentials"]["client_id"], + "client_secret": self.config["oauth_credentials"]["client_secret"], + "refresh_token": self.config["oauth_credentials"]["refresh_token"], + } + class LinkedInAdsStream(RESTStream): """LinkedInAds stream class.""" records_jsonpath = "$[*]" # Or override `parse_response`. - next_page_token_jsonpath = ( - "$.paging.start" # Or override `get_next_page_token`. # noqa: S105 - ) + next_page_token_jsonpath = "$.paging.start" # Or override `get_next_page_token`. # noqa: S105 @property - def authenticator(self) -> BearerTokenAuthenticator: + def authenticator(self) -> _Auth: """Return a new authenticator object. Returns: An authenticator instance. """ + if "oauth_credentials" in self.config: + return LinkedInAdsOAuthAuthenticator( + self, + auth_endpoint="https://www.linkedin.com/oauth/v2/accessToken", + ) return BearerTokenAuthenticator.create_for_stream( self, token=self.config["access_token"], @@ -143,11 +159,7 @@ def parse_response( columns["run_schedule_start"] = datetime.fromtimestamp( # noqa: DTZ006 int(schedule_column) / 1000, ).isoformat() - yield from ( - resp_json["elements"] - if resp_json.get("elements") is not None - else [columns] - ) + yield from (resp_json["elements"] if resp_json.get("elements") is not None else [columns]) def _to_id_column( self, @@ -161,9 +173,7 @@ def _to_id_column( def _add_datetime_columns(self, columns): # noqa: ANN202, ANN001 created_time = columns.get("changeAuditStamps").get("created").get("time") - last_modified_time = ( - columns.get("changeAuditStamps").get("lastModified").get("time") - ) + last_modified_time = columns.get("changeAuditStamps").get("lastModified").get("time") columns["created_time"] = datetime.fromtimestamp( int(created_time) / 1000, tz=UTC, diff --git a/tap_linkedin_ads/tap.py b/tap_linkedin_ads/tap.py index b95f040..477c90a 100644 --- a/tap_linkedin_ads/tap.py +++ b/tap_linkedin_ads/tap.py @@ -25,9 +25,30 @@ class TapLinkedInAds(Tap): th.Property( "access_token", th.StringType, - required=True, description="The token to authenticate against the API service", ), + # OAuth + th.Property( + "oauth_credentials", + th.ObjectType( + th.Property( + "refresh_token", + th.StringType, + description="LinkedIn Ads Refresh Token", + ), + th.Property( + "client_id", + th.StringType, + description="LinkedIn Ads Client ID", + ), + th.Property( + "client_secret", + th.StringType, + description="LinkedIn Ads Client Secret", + ), + ), + description="LinkedIn Ads OAuth Credentials", + ), th.Property( "start_date", th.DateTimeType, From deee27d2a0fe2a340a2ec132bad1e1bbdb41e9dc Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:30:00 -0400 Subject: [PATCH 2/7] remove index error suppression on required config key --- tap_linkedin_ads/streams.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tap_linkedin_ads/streams.py b/tap_linkedin_ads/streams.py index 5e5b914..3de76c4 100644 --- a/tap_linkedin_ads/streams.py +++ b/tap_linkedin_ads/streams.py @@ -1434,8 +1434,7 @@ def post_process(self, row: dict, context: dict | None = None) -> dict | None: "%Y-%m-%d", ).astimezone(UTC) - with contextlib.suppress(IndexError): - row["creative_id"] = self.config["creative"] + row["creative_id"] = self.config["creative"] viral_registrations = row.pop("viralRegistrations", None) if viral_registrations: From 16b9d24a098c02614ac62d330c5c9500ffc80fcd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:30:44 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes --- tap_linkedin_ads/client.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tap_linkedin_ads/client.py b/tap_linkedin_ads/client.py index e2ac47c..4bbf8ed 100644 --- a/tap_linkedin_ads/client.py +++ b/tap_linkedin_ads/client.py @@ -8,7 +8,11 @@ from pathlib import Path import requests -from singer_sdk.authenticators import BearerTokenAuthenticator, OAuthAuthenticator, SingletonMeta +from singer_sdk.authenticators import ( + BearerTokenAuthenticator, + OAuthAuthenticator, + SingletonMeta, +) from singer_sdk.streams import RESTStream SCHEMAS_DIR = Path(__file__).parent / Path("./schemas") @@ -34,7 +38,9 @@ class LinkedInAdsStream(RESTStream): """LinkedInAds stream class.""" records_jsonpath = "$[*]" # Or override `parse_response`. - next_page_token_jsonpath = "$.paging.start" # Or override `get_next_page_token`. # noqa: S105 + next_page_token_jsonpath = ( + "$.paging.start" # Or override `get_next_page_token`. # noqa: S105 + ) @property def authenticator(self) -> _Auth: @@ -159,7 +165,11 @@ def parse_response( columns["run_schedule_start"] = datetime.fromtimestamp( # noqa: DTZ006 int(schedule_column) / 1000, ).isoformat() - yield from (resp_json["elements"] if resp_json.get("elements") is not None else [columns]) + yield from ( + resp_json["elements"] + if resp_json.get("elements") is not None + else [columns] + ) def _to_id_column( self, @@ -173,7 +183,9 @@ def _to_id_column( def _add_datetime_columns(self, columns): # noqa: ANN202, ANN001 created_time = columns.get("changeAuditStamps").get("created").get("time") - last_modified_time = columns.get("changeAuditStamps").get("lastModified").get("time") + last_modified_time = ( + columns.get("changeAuditStamps").get("lastModified").get("time") + ) columns["created_time"] = datetime.fromtimestamp( int(created_time) / 1000, tz=UTC, From 49786a67deb172063924bcf576dd84c9eb27f9ad Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:38:37 -0400 Subject: [PATCH 4/7] Update tap_linkedin_ads/client.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Ramírez Mondragón <16805946+edgarrmondragon@users.noreply.github.com> --- tap_linkedin_ads/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tap_linkedin_ads/client.py b/tap_linkedin_ads/client.py index 4bbf8ed..739c6cc 100644 --- a/tap_linkedin_ads/client.py +++ b/tap_linkedin_ads/client.py @@ -25,7 +25,7 @@ class LinkedInAdsOAuthAuthenticator(OAuthAuthenticator, metaclass=SingletonMeta) """Authenticator class for LinkedInAds.""" @property - def oauth_request_body(self): + def oauth_request_body(self) -> dict[str, t.Any]: return { "grant_type": "refresh_token", "client_id": self.config["oauth_credentials"]["client_id"], From 46428ad3db862a172ddeec39cdaefef82e00d667 Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:38:42 -0400 Subject: [PATCH 5/7] Update tap_linkedin_ads/tap.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Ramírez Mondragón <16805946+edgarrmondragon@users.noreply.github.com> --- tap_linkedin_ads/tap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tap_linkedin_ads/tap.py b/tap_linkedin_ads/tap.py index 477c90a..52d4c65 100644 --- a/tap_linkedin_ads/tap.py +++ b/tap_linkedin_ads/tap.py @@ -44,6 +44,7 @@ class TapLinkedInAds(Tap): th.Property( "client_secret", th.StringType, + secret=True, description="LinkedIn Ads Client Secret", ), ), From fe24779f889881200a5fb4589ea278c5712e121c Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:38:48 -0400 Subject: [PATCH 6/7] Update tap_linkedin_ads/tap.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Ramírez Mondragón <16805946+edgarrmondragon@users.noreply.github.com> --- tap_linkedin_ads/tap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tap_linkedin_ads/tap.py b/tap_linkedin_ads/tap.py index 52d4c65..ea8e946 100644 --- a/tap_linkedin_ads/tap.py +++ b/tap_linkedin_ads/tap.py @@ -34,6 +34,7 @@ class TapLinkedInAds(Tap): th.Property( "refresh_token", th.StringType, + secret=True, description="LinkedIn Ads Refresh Token", ), th.Property( From 28f2b05b9599d39226310df4459a8ac0276b55d2 Mon Sep 17 00:00:00 2001 From: Pat Nadolny Date: Fri, 18 Oct 2024 16:38:52 -0400 Subject: [PATCH 7/7] Update tap_linkedin_ads/tap.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Ramírez Mondragón <16805946+edgarrmondragon@users.noreply.github.com> --- tap_linkedin_ads/tap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tap_linkedin_ads/tap.py b/tap_linkedin_ads/tap.py index ea8e946..35918ed 100644 --- a/tap_linkedin_ads/tap.py +++ b/tap_linkedin_ads/tap.py @@ -25,6 +25,7 @@ class TapLinkedInAds(Tap): th.Property( "access_token", th.StringType, + secret=True, description="The token to authenticate against the API service", ), # OAuth