diff --git a/.env.template b/.env.template index 49fe67b..506f6f3 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,6 @@ TAP_LINKEDIN_ADS_ACCESS_TOKEN='' TAP_LINKEDIN_ADS_ACCOUNTS='' TAP_LINKEDIN_ADS_CAMPAIGN='' +TAP_LINKEDIN_ADS_CAMPAIGN_GROUP='' +TAP_LINKEDIN_ADS_CREATIVE='' TAP_LINKEDIN_ADS_OWNER='' diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index 3c2d4dc..7bddcbe 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -42,5 +42,7 @@ jobs: TAP_LINKEDIN_ADS_ACCOUNTS: ${{ secrets.accounts }} TAP_LINKEDIN_ADS_OWNER: ${{ secrets.owner }} TAP_LINKEDIN_ADS_CAMPAIGN: ${{ secrets.campaign }} + TAP_LINKEDIN_ADS_CAMPAIGN_GROUP: ${{ secrets.campaign_group }} + TAP_LINKEDIN_ADS_CREATIVE: ${{ secrets.creative }} run: | poetry run pytest --capture=no diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/README.md b/README.md index 07454d1..8613e88 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,21 @@ Built with the [Meltano Singer SDK](https://sdk.meltano.com). ## Settings -| Setting | Required | Default | Description | -|:--------------------|:--------:|:-------:|:------------| -| access_token | True | None | The token to authenticate against the API service | -| start_date | True | None | The earliest record date to sync | -| end_date | False | 2023-05-09 02:04:18.151589 | The latest record date to sync | -| user_agent | False | tap-linkedin-ads | API ID | -| api_version | False | 202211 | LinkedInAds API Version | -| accounts | True | None | LinkedInAds Account ID | -| campaign | True | None | LinkedInAds Campaign ID | -| owner | True | None | LinkedInAds Owner ID | -| stream_maps | False | None | Config object for stream maps capability. For more information check out [Stream Maps](https://sdk.meltano.com/en/latest/stream_maps.html). | -| stream_map_config | False | None | User-defined config values to be used within map expressions. | -| flattening_enabled | False | None | 'True' to enable schema flattening and automatically expand nested properties. | -| flattening_max_depth| False | None | The max depth to flatten schemas. | +| Setting | Required | Default | Description | +|:---------------------|:--------:|:--------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------| +| access_token | True | None | The token to authenticate against the API service | +| start_date | True | None | The earliest record date to sync | +| end_date | False | 2023-05-09 02:04:18.151589 | The latest record date to sync | +| user_agent | False | tap-linkedin-ads | API ID | +| accounts | True | None | LinkedInAds Account ID | +| campaign | True | None | LinkedInAds Campaign ID | +| creative | True | None | LinkedInAds Creative ID | +| campaign_group | True | None | LinkedInAds Campaign Group ID | +| owner | True | None | LinkedInAds Owner ID | +| stream_maps | False | None | Config object for stream maps capability. For more information check out [Stream Maps](https://sdk.meltano.com/en/latest/stream_maps.html). | +| stream_map_config | False | None | User-defined config values to be used within map expressions. | +| flattening_enabled | False | None | 'True' to enable schema flattening and automatically expand nested properties. | +| flattening_max_depth | False | None | The max depth to flatten schemas. | A full list of supported settings and capabilities is available by running: `tap-linkedin-ads --about` diff --git a/meltano.yml b/meltano.yml index 6562d4f..1326207 100644 --- a/meltano.yml +++ b/meltano.yml @@ -17,10 +17,10 @@ plugins: - name: accounts - name: user_agent value: 'meltano' - - name: linkedin_version - value: '202211' - name: campaign - name: owner + - name: campaign_group + - name: creative - name: start_date value: '2023-01-01T00:00:00Z' - name: end_date diff --git a/tap_linkedin_ads/client.py b/tap_linkedin_ads/client.py index 00d44ee..dd5ce09 100644 --- a/tap_linkedin_ads/client.py +++ b/tap_linkedin_ads/client.py @@ -19,9 +19,7 @@ class LinkedInAdsStream(RESTStream): """LinkedInAds stream class.""" - url_base = "https://api.linkedin.com/rest/" - - records_jsonpath = "$.elements[*]" # Or override `parse_response`. + records_jsonpath = "$[*]" # Or override `parse_response`. next_page_token_jsonpath = ( "$.paging.start" # Or override `get_next_page_token`. # noqa: S105 ) @@ -48,7 +46,7 @@ def http_headers(self) -> dict: headers = {} if "user_agent" in self.config: headers["User-Agent"] = self.config["user_agent"] - headers["LinkedIn-Version"] = self.config["api_version"] + headers["LinkedIn-Version"] = "202305" headers["Content-Type"] = "application/json" headers["X-Restli-Protocol-Version"] = "1.0.0" @@ -63,15 +61,19 @@ def get_next_page_token( # If pagination is required, return a token which can be used to get the # next page. If this is the final page, return "None" to end the # pagination loop. - resp_json = response.json() if previous_token is None: previous_token = 0 elements = resp_json.get("elements") - if len(elements) == 0 or len(elements) == previous_token + 1: - return None + if elements is not None: + if len(elements) == 0 or len(elements) == previous_token + 1: + return None + else: + page = resp_json + if len(page) == 0 or len(page) == previous_token + 1: + return None return previous_token + 1 @@ -111,13 +113,14 @@ def parse_response( # noqa: PLR0912 Each record from the source. """ resp_json = response.json() - - if isinstance(resp_json, list): - results = resp_json - elif resp_json.get("elements") is not None: + if resp_json.get("elements") is not None: results = resp_json["elements"] try: columns = results[0] + except: # noqa: E722 + columns = results + pass + try: created_time = ( columns.get("changeAuditStamps").get("created").get("time") ) @@ -132,37 +135,54 @@ def parse_response( # noqa: PLR0912 int(last_modified_time) / 1000, tz=UTC, ).isoformat() - try: - account_column = columns.get("account") - account_id = int(account_column.split(":")[3]) - columns["account_id"] = account_id - except: # noqa: E722, S110 - pass - try: - campaign_column = columns.get("campaignGroup") - campaign = int(campaign_column.split(":")[3]) - columns["campaign_group_id"] = campaign - except: # noqa: E722, S110 - pass - try: - user_column = columns.get("user") - user = user_column.split(":")[3] - columns["user_person_id"] = user - except: # noqa: E722, S110 - pass - try: - schedule_column = columns.get("runSchedule").get("start") - columns[ - "run_schedule_start" - ] = datetime.fromtimestamp( # noqa: DTZ006 - int(schedule_column) / 1000, - ).isoformat() - except: # noqa: E722, S110 - pass - results = [columns] except: # noqa: E722, S110 pass else: results = resp_json + try: + columns = results + created_time = ( + columns.get("changeAuditStamps").get("created").get("time") + ) + last_modified_time = ( + columns.get("changeAuditStamps").get("lastModified").get("time") + ) + columns["created_time"] = datetime.fromtimestamp( + int(created_time) / 1000, + tz=UTC, + ).isoformat() + columns["last_modified_time"] = datetime.fromtimestamp( + int(last_modified_time) / 1000, + tz=UTC, + ).isoformat() + except: # noqa: E722 + columns = results + pass + + try: + account_column = columns.get("account") + account_id = int(account_column.split(":")[3]) + columns["account_id"] = account_id + except: # noqa: E722, S110 + pass + try: + campaign_column = columns.get("campaignGroup") + campaign = int(campaign_column.split(":")[3]) + columns["campaign_group_id"] = campaign + except: # noqa: E722, S110 + pass + try: + schedule_column = columns.get("runSchedule").get("start") + columns["run_schedule_start"] = datetime.fromtimestamp( # noqa: DTZ006 + int(schedule_column) / 1000, + ).isoformat() + except: # noqa: E722, S110 + pass + + results = ( + resp_json["elements"] + if resp_json.get("elements") is not None + else [columns] + ) yield from results diff --git a/tap_linkedin_ads/streams.py b/tap_linkedin_ads/streams.py index d6d6372..6bcdb0d 100644 --- a/tap_linkedin_ads/streams.py +++ b/tap_linkedin_ads/streams.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import typing as t from datetime import datetime, timezone from pathlib import Path @@ -118,6 +119,10 @@ class Accounts(LinkedInAdsStream): ), ).to_dict() + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + def get_url_params( self, context: dict | None, # noqa: ARG002 @@ -165,7 +170,7 @@ class AdAnalyticsByCampaignInit(LinkedInAdsStream): path = "adAnalytics" schema = PropertiesList( - Property("campaign_id", IntegerType), + Property("campaign_id", StringType), Property("documentCompletions", IntegerType), Property("documentFirstQuartileCompletions", IntegerType), Property("clicks", IntegerType), @@ -238,7 +243,6 @@ class AdAnalyticsByCampaignInit(LinkedInAdsStream): Property("oneClickLeads", IntegerType), Property("opens", IntegerType), Property("otherEngagements", IntegerType), - Property("pivotValue", StringType), Property("sends", IntegerType), Property("shares", IntegerType), Property("textUrlClicks", IntegerType), @@ -280,7 +284,7 @@ def adanalyticscolumns(self) -> list[str]: return [ "viralLandingPageClicks,viralExternalWebsitePostClickConversions,externalWebsiteConversions,viralVideoFirstQuartileCompletions,leadGenerationMailContactInfoShares,clicks,viralClicks,shares,viralFullScreenPlays,videoMidpointCompletions,viralCardClicks,viralExternalWebsitePostViewConversions,viralTotalEngagements,viralCompanyPageClicks,actionClicks,viralShares,videoCompletions,comments,externalWebsitePostViewConversions,dateRange", "costInUsd,landingPageClicks,oneClickLeadFormOpens,talentLeads,sends,viralOneClickLeadFormOpens,conversionValueInLocalCurrency,viralFollows,otherEngagements,viralVideoCompletions,cardImpressions,leadGenerationMailInterestedClicks,opens,totalEngagements,videoViews,viralImpressions,viralVideoViews,commentLikes,viralDocumentThirdQuartileCompletions,viralLikes", - "adUnitClicks,videoThirdQuartileCompletions,cardClicks,likes,viralComments,viralVideoMidpointCompletions,viralVideoThirdQuartileCompletions,oneClickLeads,fullScreenPlays,viralCardImpressions,follows,videoStarts,videoFirstQuartileCompletions,textUrlClicks,pivotValue,reactions,viralReactions,externalWebsitePostClickConversions,viralOtherEngagements,costInLocalCurrency", + "adUnitClicks,videoThirdQuartileCompletions,cardClicks,likes,viralComments,viralVideoMidpointCompletions,viralVideoThirdQuartileCompletions,oneClickLeads,fullScreenPlays,viralCardImpressions,follows,videoStarts,videoFirstQuartileCompletions,textUrlClicks,reactions,viralReactions,externalWebsitePostClickConversions,viralOtherEngagements,costInLocalCurrency", "viralVideoStarts,viralRegistrations,viralJobApplyClicks,viralJobApplications,jobApplications,jobApplyClicks,viralExternalWebsiteConversions,postViewRegistrations,companyPageClicks,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,approximateUniqueImpressions,viralDownloadClicks,impressions", ] @@ -327,7 +331,7 @@ def get_url_params( def post_process(self, row: dict, context: dict | None = None) -> dict | None: # This function extracts day, month, and year from date rannge column - # These values are aprsed with datetime function and the date is added to the day column + # These values are parsed with datetime function and the date is added to the day column date_range = row.get("dateRange", {}) start_date = date_range.get("start", {}) @@ -341,16 +345,15 @@ def post_process(self, row: dict, context: dict | None = None) -> dict | None: "%Y-%m-%d", ).astimezone(UTC) - pivot_value = row.get("pivotValue", "") - - try: - campaign_column = int(pivot_value.split(":")[3]) - row["campaign_id"] = campaign_column - except IndexError: - pass + with contextlib.suppress(IndexError): + row["campaign_id"] = self.config["campaign"] return super().post_process(row, context) + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AdAnalyticsByCampaign(AdAnalyticsByCampaignInit): name = "ad_analytics_by_campaign" @@ -448,6 +451,10 @@ def merge_dicts(self, *dict_args: dict) -> dict: result.update(dictionary) return result + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AdAnalyticsByCampaignSecond(AdAnalyticsByCampaignInit): name = "adanalyticsbycampaign_second" @@ -492,6 +499,10 @@ def get_url_params( return params + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AdAnalyticsByCampaignThird(AdAnalyticsByCampaignInit): name = "adanalyticsbycampaign_third" @@ -535,6 +546,10 @@ def get_url_params( params["campaigns[0]"] = "urn:li:sponsoredCampaign:" + self.config["campaign"] return params + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class VideoAds(LinkedInAdsStream): """https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads/advertising-targeting/create-and-manage-video#finders.""" @@ -612,6 +627,33 @@ def get_url_params( return params + def post_process(self, row: dict, context: dict | None = None) -> dict | None: + # This function extracts day, month, and year from date rannge column + # These values are parse with datetime function and the date is added to the day column + try: + created_time = ( + row.get("changeAuditStamps", {}).get("created", {}).get("time") + ) + last_modified_time = ( + row.get("changeAuditStamps", {}).get("lastModified", {}).get("time") + ) + row["created_time"] = datetime.fromtimestamp( + int(created_time) / 1000, + tz=UTC, + ).isoformat() + row["last_modified_time"] = datetime.fromtimestamp( + int(last_modified_time) / 1000, + tz=UTC, + ).isoformat() + except: # noqa: E722, S110 + pass + + return super().post_process(row, context) + + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AccountUsers(LinkedInAdsStream): """https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-account-users#find-ad-account-users-by-accounts.""" @@ -699,6 +741,39 @@ def get_url_params( return params + def post_process(self, row: dict, context: dict | None = None) -> dict | None: + # This function extracts day, month, and year from date rannge column + # These values are parsed with datetime function and the date is added to the day column + try: + account_user = row.get("user", {}) + user = account_user.split(":")[3] + row["user_person_id"] = user + except: # noqa: E722, S110 + pass + try: + created_time = ( + row.get("changeAuditStamps", {}).get("created", {}).get("time") + ) + last_modified_time = ( + row.get("changeAuditStamps", {}).get("lastModified", {}).get("time") + ) + row["created_time"] = datetime.fromtimestamp( + int(created_time) / 1000, + tz=UTC, + ).isoformat() + row["last_modified_time"] = datetime.fromtimestamp( + int(last_modified_time) / 1000, + tz=UTC, + ).isoformat() + except: # noqa: E722, S110 + pass + + return super().post_process(row, context) + + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class CampaignGroups(LinkedInAdsStream): """https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-campaign-groups#search-for-campaign-groups.""" @@ -716,7 +791,7 @@ class CampaignGroups(LinkedInAdsStream): replication_keys = ["last_modified_time"] replication_method = "incremental" primary_keys = ["last_modified_time", "id", "status"] - path = "adCampaignGroups" + path = "" PropertiesList = th.PropertiesList Property = th.Property @@ -775,6 +850,13 @@ class CampaignGroups(LinkedInAdsStream): schema = jsonschema + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/adAccounts/{}/adCampaignGroups/{}".format( + self.config["accounts"], + self.config["campaign_group"], + ) + def get_url_params( self, context: dict | None, # noqa: ARG002 @@ -796,10 +878,6 @@ def get_url_params( params["sort"] = "asc" params["order_by"] = self.replication_key - params["q"] = "search" - params["sort.field"] = "ID" - params["sort.order"] = "ASCENDING" - return params @@ -819,9 +897,10 @@ class Campaigns(LinkedInAdsStream): replication_keys = ["last_modified_time"] replication_method = "incremental" primary_keys = ["last_modified_time", "id", "status"] - path = "adCampaigns" + path = "" schema = PropertiesList( + Property("storyDeliveryEnabled", BooleanType), Property( "targeting", ObjectType( @@ -1066,6 +1145,13 @@ class Campaigns(LinkedInAdsStream): Property("run_schedule_end", StringType), ).to_dict() + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/adAccounts/{}/adCampaigns/{}".format( + self.config["accounts"], + self.config["campaign"], + ) + def get_url_params( self, context: dict | None, # noqa: ARG002 @@ -1087,15 +1173,11 @@ def get_url_params( params["sort"] = "asc" params["order_by"] = self.replication_key - params["q"] = "search" - params["sort.field"] = "ID" - params["sort.order"] = "ASCENDING" - return params class Creatives(LinkedInAdsStream): - """https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-creatives?view=li-lms-2023-01&tabs=http#search-for-creatives.""" + """https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-creatives?view=li-lms-2023-05&tabs=http%2Chttp-update-a-creative#search-for-creatives.""" """ columns: columns which will be added to fields parameter in api @@ -1103,46 +1185,56 @@ class Creatives(LinkedInAdsStream): path: path which will be added to api url in client.py schema: instream schema primary_keys = primary keys for the table - replication_keys = datetime keys for replication + replication_keys = datetime keys for replication. """ name = "creatives" - replication_keys = ["last_modified_time"] + replication_keys = ["lastModifiedAt"] replication_method = "incremental" - primary_keys = ["last_modified_time", "id"] - path = "creatives" + primary_keys = ["lastModifiedAt", "id"] + path = "" schema = PropertiesList( Property("account", StringType), Property("account_id", IntegerType), Property("campaign", StringType), - Property("campaign_id", IntegerType), + Property("campaign_id", StringType), Property( "content", ObjectType( - Property("reference", StringType), Property( - "text_ad", + "spotlight", ObjectType( - Property("headline", StringType), + Property("showMemberProfilePhoto", BooleanType), + Property("organizationName", StringType), + Property("landingPage", StringType), Property("description", StringType), - Property("landing_page", StringType), + Property("logo", StringType), + Property("headline", StringType), + Property("callToAction", StringType), additional_properties=False, ), ), ), ), - Property("created_at", StringType), - Property("created_by", StringType), - Property("last_modified_at", StringType), - Property("last_modified_by", StringType), + Property("createdAt", IntegerType), + Property("createdBy", StringType), + Property("lastModifiedAt", IntegerType), + Property("lastModifiedBy", StringType), Property("id", StringType), - Property("intended_status", StringType), - Property("is_serving", BooleanType), - Property("is_test", BooleanType), - Property("serving_hold_reasons", th.ArrayType(Property("items", StringType))), + Property("intendedStatus", StringType), + Property("isServing", BooleanType), + Property("isTest", BooleanType), + Property("servingHoldReasons", th.ArrayType(Property("items", StringType))), ).to_dict() + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/adAccounts/{}/creatives/urn%3Ali%3AsponsoredCreative%3A{}".format( + self.config["accounts"], + self.config["creative"], + ) + def get_url_params( self, context: dict | None, # noqa: ARG002 @@ -1164,12 +1256,6 @@ def get_url_params( params["sort"] = "asc" params["order_by"] = self.replication_key - # TODO(edgarrmondragon): Resolve issue with parentheses in campaigns parameter being - # encoded by rest.py - # https://github.com/meltano/sdk/issues/1666 - params["campaigns"] = "urn:li:sponsoredCampaign:" + self.config["campaign"] - params["q"] = "criteria" - return params @@ -1195,7 +1281,7 @@ class AdAnalyticsByCreativeInit(LinkedInAdsStream): Property("landingPageClicks", IntegerType), Property("reactions", IntegerType), Property("adUnitClicks", IntegerType), - Property("creative_id", IntegerType), + Property("creative_id", StringType), Property("documentCompletions", IntegerType), Property("documentFirstQuartileCompletions", IntegerType), Property("clicks", IntegerType), @@ -1267,7 +1353,6 @@ class AdAnalyticsByCreativeInit(LinkedInAdsStream): Property("oneClickLeads", IntegerType), Property("opens", IntegerType), Property("otherEngagements", IntegerType), - Property("pivotValue", StringType), Property("sends", IntegerType), Property("shares", IntegerType), Property("textUrlClicks", IntegerType), @@ -1309,7 +1394,7 @@ def adanalyticscolumns(self) -> list[str]: return [ "viralLandingPageClicks,viralExternalWebsitePostClickConversions,externalWebsiteConversions,viralVideoFirstQuartileCompletions,leadGenerationMailContactInfoShares,clicks,viralClicks,shares,viralFullScreenPlays,videoMidpointCompletions,viralCardClicks,viralExternalWebsitePostViewConversions,viralTotalEngagements,viralCompanyPageClicks,actionClicks,viralShares,videoCompletions,comments,externalWebsitePostViewConversions,dateRange", "costInUsd,landingPageClicks,oneClickLeadFormOpens,talentLeads,sends,viralOneClickLeadFormOpens,conversionValueInLocalCurrency,viralFollows,otherEngagements,viralVideoCompletions,cardImpressions,leadGenerationMailInterestedClicks,opens,totalEngagements,videoViews,viralImpressions,viralVideoViews,commentLikes,viralDocumentThirdQuartileCompletions,viralLikes", - "adUnitClicks,videoThirdQuartileCompletions,cardClicks,likes,viralComments,viralVideoMidpointCompletions,viralVideoThirdQuartileCompletions,oneClickLeads,fullScreenPlays,viralCardImpressions,follows,videoStarts,videoFirstQuartileCompletions,textUrlClicks,pivotValue,reactions,viralReactions,externalWebsitePostClickConversions,viralOtherEngagements,costInLocalCurrency", + "adUnitClicks,videoThirdQuartileCompletions,cardClicks,likes,viralComments,viralVideoMidpointCompletions,viralVideoThirdQuartileCompletions,oneClickLeads,fullScreenPlays,viralCardImpressions,follows,videoStarts,videoFirstQuartileCompletions,textUrlClicks,reactions,viralReactions,externalWebsitePostClickConversions,viralOtherEngagements,costInLocalCurrency", "viralVideoStarts,viralRegistrations,viralJobApplyClicks,viralJobApplications,jobApplications,jobApplyClicks,viralExternalWebsiteConversions,postViewRegistrations,companyPageClicks,documentCompletions,documentFirstQuartileCompletions,documentMidpointCompletions,documentThirdQuartileCompletions,downloadClicks,viralDocumentCompletions,viralDocumentFirstQuartileCompletions,viralDocumentMidpointCompletions,approximateUniqueImpressions,viralDownloadClicks,impressions", ] @@ -1354,9 +1439,13 @@ def get_url_params( return params + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + def post_process(self, row: dict, context: dict | None = None) -> dict | None: # This function extracts day, month, and year from date rannge column - # These values are aprsed with datetime function and the date is added to the day column + # These values are parsed with datetime function and the date is added to the day column date_range = row.get("dateRange", {}) start_date = date_range.get("start", {}) @@ -1370,13 +1459,8 @@ def post_process(self, row: dict, context: dict | None = None) -> dict | None: "%Y-%m-%d", ).astimezone(UTC) - pivot_value = row.get("pivotValue", "") - - try: - creative_column = int(pivot_value.split(":")[3]) - row["creative_id"] = creative_column - except IndexError: - pass + with contextlib.suppress(IndexError): + row["creative_id"] = self.config["creative"] viral_registrations = row.pop("viralRegistrations", None) if viral_registrations: @@ -1481,6 +1565,10 @@ def merge_dicts(self, *dict_args: dict) -> dict: result.update(dictionary) return result + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AdAnalyticsByCreativeSecond(AdAnalyticsByCreativeInit): name = "adanalyticsbycreative_second" @@ -1525,6 +1613,10 @@ def get_url_params( return params + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" + class AdAnalyticsByCreativeThird(AdAnalyticsByCreativeInit): name = "adanalyticsbycreative_third" @@ -1568,3 +1660,7 @@ def get_url_params( params["campaigns[0]"] = "urn:li:sponsoredCampaign:" + self.config["campaign"] return params + + @property + def url_base(self) -> str: + return "https://api.linkedin.com/rest/" diff --git a/tap_linkedin_ads/tap.py b/tap_linkedin_ads/tap.py index 841d37a..b95f040 100644 --- a/tap_linkedin_ads/tap.py +++ b/tap_linkedin_ads/tap.py @@ -47,12 +47,6 @@ class TapLinkedInAds(Tap): default="tap-linkedin-ads ", description="API ID", ), - th.Property( - "api_version", - th.StringType, - default="202211", - description="LinkedInAds API Version", - ), th.Property( "accounts", th.StringType, @@ -71,6 +65,18 @@ class TapLinkedInAds(Tap): required=True, description="LinkedInAds Owner ID", ), + th.Property( + "campaign_group", + th.StringType, + required=True, + description="LinkedInAds Campaign Group ID. Used for the campaign_group stream", + ), + th.Property( + "creative", + th.StringType, + required=True, + description="LinkedInAds Creative ID. Used for the creative stream", + ), ).to_dict() def discover_streams(self) -> list[LinkedInAdsStream]: @@ -83,7 +89,7 @@ def discover_streams(self) -> list[LinkedInAdsStream]: streams.Accounts(self), streams.VideoAds(self), streams.AccountUsers(self), - # streams.Creatives(self), # noqa: ERA001 + streams.Creatives(self), streams.Campaigns(self), streams.CampaignGroups(self), streams.AdAnalyticsByCampaign(self),