diff --git a/notebooks/data_partners_partner_link_python_sdk.ipynb b/notebooks/data_partners_partner_link_python_sdk.ipynb new file mode 100644 index 0000000..7f1c81b --- /dev/null +++ b/notebooks/data_partners_partner_link_python_sdk.ipynb @@ -0,0 +1,670 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "h2sCjFoFUEYZ" + }, + "source": [ + "Project: /data-manager/api/_project.yaml\n", + "Book: /data-manager/api/_book.yaml\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "ZL8ttX6NRUE0" + }, + "outputs": [], + "source": [ + "# @markdown #### Copyright 2026 Google LLC\n", + "# @markdown ##### Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1OuJ0O8peS9M" + }, + "source": [ + "# Data Partners: Partner Link Workflow with the Python SDK" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IXVJckuaRCA6" + }, + "source": [ + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + " Run in Google Colab\n", + " \n", + " \n", + " \n", + " View source on GitHub\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8sNBI9emRRsk" + }, + "source": [ + "## Objective\n", + "This notebook provides a complete workflow for the Data Manager API: linking to a partner, creating a user list, ingesting data, and checking the status on the data ingestion.\n", + "\n", + "**Key Goals:**\n", + "1. **Verify Google Cloud Configuration:** Ensure the Data Manager API and necessary scopes (for OAuth 2.0 or Service Accounts) are enabled.\n", + "2. **Prepare for App Verification (Milestone 2):** Provide a foundation for the app verification process. This is required when using OAuth workflows with the sensitive Data Manager API scope, applicable if you are making the partner linking experience available within your own app or platform.\n", + "3. **Facilitate API Explorer Testing:** For Steps 3 through 7, the notebook prints the required parameters and request payloads to replicate the calls in the Google API Explorer.\n", + "\n", + "---\n", + "\n", + "## The Two Personas\n", + "In a typical integration where partner linking happens within your platform's UI, two distinct roles interact with the API:\n", + "* **The Advertiser:** The Google Ads account owner who authorizes the connection via your UI (**Uses OAuth**).\n", + "* **The Data Partner (You):** The platform working in the background to retrieve the link, create audiences, and push data (**Uses a Service Account**).\n", + "\n", + "## Choose Your Execution Path\n", + "Select the flow that matches your testing needs:\n", + "\n", + "### Path A: Data Partner - Simplified Testing (Single Persona)\n", + "*Best for testing basic API mechanics or if you aren't hosting the linking experience in your UI.*\n", + "1. Ensure your authentication user (Service Account or OAuth) is an **ADMIN** on the linked Google Ads account.\n", + "2. Select your preferred authentication method in **Step 1**.\n", + "3. Run **Steps 1 through 6** sequentially.\n", + "\n", + "### Path B: Data Partner - Simulating the In-Platform Experience (Dual Persona)\n", + "*Mimics the exact flow required for App Verification, where advertisers link to a partner within your UI.*\n", + "\n", + "**Phase 1: Act as the Advertiser**\n", + "1. In **Step 1**, set `authentication_method` to **OAuth Client Credentials**.\n", + "2. Run **Steps 1 through 3** to consent and authorize the partner link.\n", + "\n", + "**Phase 2: Act as the Data Partner**\n", + "3. Return to **Step 1** and change `authentication_method` to **Service Account**.\n", + "4. Run **Steps 1 and 2** to re-authenticate as your backend.\n", + "5. Skip Step 3, and resume from **Step 4** to retrieve the link and ingest data.\n", + "\n", + "### Path C: Advertiser Flow\n", + "*Best for testing purely from the advertiser's perspective.*\n", + "1. Run **Steps 1 and 2**.\n", + "2. **Skip Step 3**.\n", + "3. Run **Steps 4 through 6**.\n", + "4. **Skip Step 7**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BmwaKSZPRZ3p" + }, + "source": [ + "---\n", + "## Instructions\n", + "\n", + "1. **Make a copy of this Colab:**\n", + " * Go to `File -> Save a copy in Drive`.\n", + "\n", + "2. **Choose your Authentication Method in the Setup cell below.**\n", + "\n", + "3. **If using OAuth Client Credentials:**\n", + " * Click the key icon in the left-hand sidebar.\n", + " * Create the following secrets and add your values:\n", + " * `CLIENT_ID`\n", + " * `CLIENT_SECRET`\n", + " * `REFRESH_TOKEN`\n", + " * Ensure \"Notebook access\" is enabled for each secret.\n", + "\n", + "4. **If using a Service Account:**\n", + " * Fill in the `service_account_email` and `google_cloud_project` fields in the Setup cell." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-wtv73t1egvl" + }, + "source": [ + "### Step 0. Install Packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "collapsed": true, + "id": "j9kMcKunX3dd" + }, + "outputs": [], + "source": [ + "!pip install --upgrade google-ads-datamanager google-auth-oauthlib" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "81MXNTkJezi9" + }, + "source": [ + "### Step 1. Setup and Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "J-sCuWtPZgcW" + }, + "outputs": [], + "source": [ + "import datetime\n", + "from google.colab import userdata\n", + "from google.ads import datamanager_v1\n", + "from google.oauth2.credentials import Credentials\n", + "import google.auth\n", + "from google.protobuf.json_format import MessageToJson\n", + "\n", + "persona = \"Advertiser\" # @param [\"Data Partner\", \"Advertiser\"]\n", + "authentication_method = \"Service Account\" # @param [\"OAuth Client Credentials\", \"Service Account\"]\n", + "\n", + "# @markdown ### Account IDs\n", + "# @markdown **DATA PARTNERS:** Provide the advertiser and data partner account IDs below.\n", + "# @markdown - **[required]** `linked_customer_id`: The Google Ads advertiser account.\n", + "# @markdown - **[required]** `login_customer_id`: Your Data Partner account.\n", + "# @markdown - (*optional*) `operating_account_id`: A specific sub-account to operate on. Leave blank if not using.\n", + "\n", + "# @markdown **ADVERTISERS:**\n", + "# @markdown - `linked_customer_id`: Leave blank.\n", + "# @markdown - `login_customer_id`: Leave blank.\n", + "# @markdown - **[required]** `operating_account_id`: Your advertiser account\n", + "\n", + "# @markdown **Enter Ids**\n", + "linked_customer_id = \"\" # @param {type:\"string\"}\n", + "login_customer_id = \"\" # @param {type:\"string\"}\n", + "operating_account_id = \"\" # @param {type:\"string\"}\n", + "\n", + "# @markdown ### Service Account Details (If applicable)\n", + "service_account_email = \"\" #@param {type:\"string\"}\n", + "google_cloud_project = \"\" #@param {type:\"string\"}\n", + "\n", + "target_account_id = operating_account_id or linked_customer_id\n", + "\n", + "GOOGLE_ADS = \"GOOGLE_ADS\"\n", + "DATA_PARTNER = \"DATA_PARTNER\"\n", + "CONSENT_GRANTED = \"CONSENT_GRANTED\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Kv3wnXRDfBgY" + }, + "source": [ + "### Step 2. Initialize Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u5vhuBjIY1G8" + }, + "outputs": [], + "source": [ + "# Create a wrapper to hold the three separate service clients\n", + "class DataManagerSDK:\n", + " def __init__(self, creds):\n", + " # There is no \"DataManagerClient\". You must use these three:\n", + " self.link_service = datamanager_v1.PartnerLinkServiceClient(credentials=creds)\n", + " self.user_list_service = datamanager_v1.UserListServiceClient(credentials=creds)\n", + " self.ingestion_service = datamanager_v1.IngestionServiceClient(credentials=creds)\n", + "\n", + "def initialize_client():\n", + " if authentication_method == \"OAuth Client Credentials\":\n", + " print(\"2. Authenticating with OAuth...\")\n", + " creds = Credentials(\n", + " token=None,\n", + " refresh_token=userdata.get('REFRESH_TOKEN'),\n", + " client_id=userdata.get('CLIENT_ID'),\n", + " client_secret=userdata.get('CLIENT_SECRET'),\n", + " token_uri=\"https://oauth2.googleapis.com/token\",\n", + " scopes=[\"https://www.googleapis.com/auth/datamanager\"]\n", + " )\n", + " else:\n", + " print(\"2. Authenticating with Service Account...\")\n", + " auth_command = (\n", + " f\"gcloud auth application-default login \"\n", + " f\"--impersonate-service-account={service_account_email} \"\n", + " f\"--scopes=https://www.googleapis.com/auth/datamanager,https://www.googleapis.com/auth/cloud-platform\"\n", + " )\n", + " get_ipython().system(auth_command)\n", + " creds, project = google.auth.default()\n", + "\n", + " return DataManagerSDK(creds)\n", + "\n", + "# Initialize\n", + "sdk = initialize_client()\n", + "print(\"SDK Services Initialized Successfully\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "--2LPM7_fKZM" + }, + "source": [ + "### Step 3. Create Partner Link" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V8Fk5jI4aCjO" + }, + "outputs": [], + "source": [ + "print(f\"3. Creating link for Ads {linked_customer_id}..\\n\")\n", + "\n", + "parent_ads = f\"accountTypes/GOOGLE_ADS/accounts/{linked_customer_id}\"\n", + "\n", + "partner_link_data = datamanager_v1.PartnerLink(\n", + " owning_account=datamanager_v1.ProductAccount(\n", + " account_id=linked_customer_id,\n", + " product=GOOGLE_ADS\n", + " ),\n", + " partner_account=datamanager_v1.ProductAccount(\n", + " account_id=login_customer_id,\n", + " product=DATA_PARTNER\n", + " )\n", + ")\n", + "\n", + "try:\n", + " link_res = sdk.link_service.create_partner_link(\n", + " parent=parent_ads,\n", + " partner_link=partner_link_data\n", + " )\n", + " print(f\"Link Created: {link_res.partner_link_id}\")\n", + " product_link_id = link_res.partner_link_id\n", + "except Exception as e:\n", + " print(f\"Link creation failed or exists: {e}\")\n", + " print(\"\\nSearching for existing link...\")\n", + " search_res = sdk.link_service.search_partner_links(parent=f\"accountTypes/DATA_PARTNER/accounts/{login_customer_id}\")\n", + " # The search result is an iterator\n", + " for link in search_res:\n", + " if link.owning_account.account_id == linked_customer_id:\n", + " product_link_id = link.partner_link_id\n", + " print(f\"Found existing Link ID: {product_link_id}\")\n", + " break\n", + "\n", + "print(\"\\nAPIx link: https://developers.google.com/data-manager/api/reference/rest/v1/accountTypes.accounts.partnerLinks/create?apix=true\")\n", + "print(f\"\\nparent: {parent_ads}\")\n", + "print(\"\\n--- Request JSON Payload ---\")\n", + "print(MessageToJson(partner_link_data._pb))\n", + "print(\"----------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "89jsXS8jfQSd" + }, + "source": [ + "### Step 4. Create User List" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UBpVcR_yzHoQ" + }, + "outputs": [], + "source": [ + "print(f\"4. Creating User List in {target_account_id}...\\n\")\n", + "\n", + "parent_userlist = f\"accountTypes/GOOGLE_ADS/accounts/{target_account_id}\"\n", + "\n", + "user_list_data = datamanager_v1.UserList(\n", + " display_name=f\"Python SDK Audience - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\",\n", + " ingested_user_list_info=datamanager_v1.IngestedUserListInfo(\n", + " upload_key_types=[\"CONTACT_ID\"]\n", + " )\n", + ")\n", + "\n", + "# Start with the default arguments needed for both personas\n", + "call_kwargs = {\n", + " \"parent\": parent_userlist,\n", + " \"user_list\": user_list_data\n", + "}\n", + "\n", + "# Only build and attach metadata if the persona is a Data Partner\n", + "if persona == \"Data Partner\":\n", + " login_account = f\"accountTypes/DATA_PARTNER/accounts/{login_customer_id}\"\n", + " linked_account = f\"accountTypes/GOOGLE_ADS/accounts/{linked_customer_id}\"\n", + "\n", + " call_kwargs[\"metadata\"] =[\n", + " (\"login-account\", login_account),\n", + " (\"linked-account\", linked_account)\n", + " ]\n", + "\n", + "try:\n", + " ulist_res = sdk.user_list_service.create_user_list(**call_kwargs)\n", + "\n", + " destination_id = ulist_res.id\n", + "\n", + " print(f\"User List Created!\")\n", + " print(f\"Destination ID: {destination_id}\")\n", + " print(f\"Resource Name: {ulist_res.name}\")\n", + " print(f\"Display Name: {ulist_res.display_name}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Failed to create User List: {e}\")\n", + "\n", + "print(\"\\nAPIx link: https://developers.google.com/data-manager/api/reference/rest/v1/accountTypes.accounts.userLists/create?apix=true\")\n", + "print(f\"\\nparent: {parent_userlist}\")\n", + "\n", + "if persona == \"Data Partner\":\n", + " print(f\"login-account: {login_account}\")\n", + " print(f\"linked-account: {linked_account}\")\n", + "\n", + "print(\"\\n--- Request JSON Payload ---\")\n", + "print(MessageToJson(user_list_data._pb))\n", + "print(\"----------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CrmA_yg0fUad" + }, + "source": [ + "### Step 5. Ingest Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iTYvfXYnM-v-" + }, + "outputs": [], + "source": [ + "print(f\"5. Ingesting sample data to User List {destination_id}...\")\n", + "\n", + "# Build the Destination object explicitly based on the persona\n", + "if persona == \"Data Partner\":\n", + " destination_obj = datamanager_v1.Destination(\n", + " login_account=datamanager_v1.ProductAccount(\n", + " product=DATA_PARTNER,\n", + " account_id=login_customer_id\n", + " ),\n", + " operating_account=datamanager_v1.ProductAccount(\n", + " product=GOOGLE_ADS,\n", + " account_id=target_account_id\n", + " ),\n", + " linked_account=datamanager_v1.ProductAccount(\n", + " product=GOOGLE_ADS,\n", + " account_id=linked_customer_id\n", + " ),\n", + " product_destination_id=str(destination_id)\n", + " )\n", + "elif persona == \"Advertiser\":\n", + " # Advertiser Persona\n", + " destination_obj = datamanager_v1.Destination(\n", + " login_account=datamanager_v1.ProductAccount(\n", + " product=GOOGLE_ADS,\n", + " account_id=target_account_id # Advertiser login is their Google Ads account\n", + " ),\n", + " operating_account=datamanager_v1.ProductAccount(\n", + " product=GOOGLE_ADS,\n", + " account_id=target_account_id\n", + " ),\n", + " # Note: linked_account is omitted for Advertisers\n", + " product_destination_id=str(destination_id)\n", + " )\n", + "else:\n", + " raise ValueError(\n", + " f\"Invalid persona: {persona}.\"\n", + " f\"Expected 'Data Partner' or 'Advertiser'.\"\n", + " )\n", + "\n", + "# Add the destination_obj directly to the payload\n", + "ingest_payload = datamanager_v1.IngestAudienceMembersRequest(\n", + " consent=datamanager_v1.Consent(\n", + " ad_user_data=CONSENT_GRANTED,\n", + " ad_personalization=CONSENT_GRANTED\n", + " ),\n", + " encoding=datamanager_v1.Encoding.HEX,\n", + " terms_of_service=datamanager_v1.TermsOfService(\n", + " customer_match_terms_of_service_status=\"ACCEPTED\"\n", + " ),\n", + " validate_only=False,\n", + " audience_members=[\n", + " datamanager_v1.AudienceMember(\n", + " user_data=datamanager_v1.UserData(\n", + " user_identifiers=[\n", + "\n", + " # NOTE: email_address values must be hex-encoded SHA-256\n", + " # hashes of the normalized email addresses (lowercase, dots\n", + " # removed from gmail, etc.).\n", + "\n", + " # Do NOT use plain text email addresses here.\n", + " # See Data Manager API documentation for formatting rules:\n", + " # https://developers.google.com/data-manager/api/devguides/concepts/formatting\n", + " datamanager_v1.UserIdentifier(email_address=\"223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4\"),\n", + " datamanager_v1.UserIdentifier(email_address=\"F1FCDE379F31F4D446B76EE8F34860ECA2288ADC6B6D6C0FDC56D9EEE75A2FA5\")\n", + " ]\n", + " )\n", + " )\n", + " ],\n", + " destinations=[destination_obj]\n", + ")\n", + "\n", + "try:\n", + " ingest_res = sdk.ingestion_service.ingest_audience_members(request=ingest_payload)\n", + "\n", + " ingestion_request_id = ingest_res.request_id\n", + " print(f\"Ingestion Submitted!\")\n", + " print(f\"Request ID: {ingestion_request_id}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Failed to ingest data: {e}\")\n", + " ingestion_request_id = None\n", + "\n", + "print(\"\\nAPIx link: https://developers.google.com/data-manager/api/reference/rest/v1/audienceMembers/ingest?apix=true\")\n", + "print(\"\\n--- Request JSON Payload ---\")\n", + "print(MessageToJson(ingest_payload._pb))\n", + "print(\"----------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DcXOh5xJfYdh" + }, + "source": [ + "### Step 6. Check Status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DiEenmMYiIN3" + }, + "outputs": [], + "source": [ + "if ingestion_request_id:\n", + " print(f\"6. Checking status for Request ID: {ingestion_request_id}...\")\n", + "\n", + " status_req = datamanager_v1.RetrieveRequestStatusRequest(\n", + " request_id=ingestion_request_id\n", + " )\n", + "\n", + " try:\n", + " status_res = sdk.ingestion_service.retrieve_request_status(request=status_req)\n", + "\n", + " print(f\"Successfully retrieved status!\")\n", + "\n", + " for dest_status in status_res.request_status_per_destination:\n", + " dest_id = dest_status.destination.product_destination_id\n", + " current_status = dest_status.request_status.name\n", + "\n", + " print(f\"Status for destination {dest_id}: {current_status}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Failed to retrieve status: {e}\")\n", + "\n", + "print(\"\\nAPIx link: https://developers.google.com/data-manager/api/reference/rest/v1/requestStatus/retrieve?apix=true\")\n", + "print(\"\\n--- Request JSON Payload ---\")\n", + "print(MessageToJson(status_req._pb))\n", + "print(\"----------------------------\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gYjD5Fzgfbzu" + }, + "source": [ + "### Step 7. [Optional] Remove Product Link" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7NY8t6Y3jSL-" + }, + "outputs": [], + "source": [ + "if product_link_id:\n", + " print(f\"\\nAttempting to remove product link {product_link_id}...\")\n", + "\n", + " link_resource_name = f\"accountTypes/GOOGLE_ADS/accounts/{linked_customer_id}/partnerLinks/{product_link_id}\"\n", + "\n", + " delete_req = datamanager_v1.DeletePartnerLinkRequest(\n", + " name=link_resource_name\n", + " )\n", + "\n", + " try:\n", + " sdk.link_service.delete_partner_link(request=delete_req)\n", + " print(\"Successfully removed product link.\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Failed to remove product link: {e}\")\n", + "else:\n", + " print(\"\\nNo product_link_id found to delete. Did you run Step 1?\")\n", + "\n", + "print(\"\\nAPIx link: https://developers.google.com/data-manager/api/reference/rest/v1/accountTypes.accounts.partnerLinks/delete?apix=true\")\n", + "print(\"\\n--- Request JSON Payload ---\")\n", + "print(MessageToJson(delete_req._pb))\n", + "print(\"----------------------------\")\n" + ] + } + ], + "metadata": { + "colab": { + "private_outputs": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}