Add Performance Max Product Listing Group Tree

Stay organized with collections Save and categorize content based on your preferences.

Java

// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ads.googleads.examples.shoppingads;

import com.beust.jcommander.Parameter;
import com.google.ads.googleads.examples.utils.ArgumentNames;
import com.google.ads.googleads.examples.utils.CodeSampleParams;
import com.google.ads.googleads.lib.GoogleAdsClient;
import com.google.ads.googleads.v11.enums.ListingGroupFilterProductConditionEnum.ListingGroupFilterProductCondition;
import com.google.ads.googleads.v11.enums.ListingGroupFilterTypeEnum.ListingGroupFilterType;
import com.google.ads.googleads.v11.enums.ListingGroupFilterVerticalEnum.ListingGroupFilterVertical;
import com.google.ads.googleads.v11.errors.GoogleAdsError;
import com.google.ads.googleads.v11.errors.GoogleAdsException;
import com.google.ads.googleads.v11.resources.AssetGroupListingGroupFilter;
import com.google.ads.googleads.v11.resources.ListingGroupFilterDimension;
import com.google.ads.googleads.v11.resources.ListingGroupFilterDimension.ProductBrand;
import com.google.ads.googleads.v11.resources.ListingGroupFilterDimension.ProductCondition;
import com.google.ads.googleads.v11.services.AssetGroupListingGroupFilterOperation;
import com.google.ads.googleads.v11.services.GoogleAdsRow;
import com.google.ads.googleads.v11.services.GoogleAdsServiceClient;
import com.google.ads.googleads.v11.services.GoogleAdsServiceClient.SearchPagedResponse;
import com.google.ads.googleads.v11.services.MutateGoogleAdsRequest;
import com.google.ads.googleads.v11.services.MutateGoogleAdsResponse;
import com.google.ads.googleads.v11.services.MutateOperation;
import com.google.ads.googleads.v11.services.MutateOperationResponse;
import com.google.ads.googleads.v11.services.MutateOperationResponse.ResponseCase;
import com.google.ads.googleads.v11.services.SearchGoogleAdsRequest;
import com.google.ads.googleads.v11.utils.ResourceNames;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.LongStream;
import org.apache.commons.lang3.StringUtils;

/**
 * This example shows how to add product partitions to a Performance Max retail campaign.
 *
 * <p>For Performance Max campaigns, product partitions are represented using the
 * AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a
 * hierarchy that creates a product partition tree.
 *
 * <p>For more information about Performance Max retail campaigns, see the {@link
 * AddPerformanceMaxRetailCampaign} example.
 */
public class AddPerformanceMaxProductListingGroupTree {

  private final int TEMPORARY_ID_LISTING_GROUP_ROOT = -1;

  private static final int PAGE_SIZE = 10_000;

  private static class AddPerformanceMaxProductListingGroupTreeParams extends CodeSampleParams {

    @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true)
    private Long customerId;

    @Parameter(names = ArgumentNames.ASSET_GROUP_ID, required = true)
    private Long assetGroupId;

    @Parameter(names = ArgumentNames.REPLACE_EXISTING_TREE, required = true, arity = 1)
    private Boolean replaceExistingTree;
  }

  public static void main(String[] args) throws Exception {
    AddPerformanceMaxProductListingGroupTreeParams params =
        new AddPerformanceMaxProductListingGroupTreeParams();
    if (!params.parseArguments(args)) {
      // Either pass the required parameters for this example on the command line, or insert them
      // into the code here. See the parameter class definition above for descriptions.
      params.customerId = Long.parseLong("INSERT_CUSTOMER_ID_HERE");
      params.assetGroupId = Long.parseLong("INSERT_AD_GROUP_ID_HERE");
      // Optional: To replace the existing listing group tree from the asset group set this
      // parameter to true.
      // If the current AssetGroup already has a tree of ListingGroupFilters, attempting to add a
      // new set of ListingGroupFilters including a root filter will result in an
      // 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error. Setting this option to true
      // will remove the existing tree and prevent this error.
      params.replaceExistingTree = Boolean.parseBoolean("INSERT_REPLACE_EXISTING_TREE_HERE");
    }

    GoogleAdsClient googleAdsClient = null;
    try {
      googleAdsClient = GoogleAdsClient.newBuilder().fromPropertiesFile().build();
    } catch (FileNotFoundException fnfe) {
      System.err.printf(
          "Failed to load GoogleAdsClient configuration from file. Exception: %s%n", fnfe);
      System.exit(1);
    } catch (IOException ioe) {
      System.err.printf("Failed to create GoogleAdsClient. Exception: %s%n", ioe);
      System.exit(1);
    }

    try {
      new AddPerformanceMaxProductListingGroupTree()
          .runExample(
              googleAdsClient, params.customerId, params.assetGroupId, params.replaceExistingTree);
    } catch (GoogleAdsException gae) {
      // GoogleAdsException is the base class for most exceptions thrown by an API request.
      // Instances of this exception have a message and a GoogleAdsFailure that contains a
      // collection of GoogleAdsErrors that indicate the underlying causes of the
      // GoogleAdsException.
      System.err.printf(
          "Request ID %s failed due to GoogleAdsException. Underlying errors:%n",
          gae.getRequestId());
      int i = 0;
      for (GoogleAdsError googleAdsError : gae.getGoogleAdsFailure().getErrorsList()) {
        System.err.printf("  Error %d: %s%n", i++, googleAdsError);
      }
      System.exit(1);
    }
  }

  /**
   * A factory that creates MutateOperations for removing an existing tree of
   * AssetGroupListingGroupFilters.
   *
   * <p>AssetGroupListingGroupFilters must be removed in a specific order: all of the children of a
   * filter must be removed before the filter itself, otherwise the API will return an error.
   *
   * <p>This object is intended to be used with an array of MutateOperations to perform a series of
   * related updates to an AssetGroup.
   */
  private static class AssetGroupListingGroupFilterRemoveOperationFactory {
    private String rootResourceName = "";
    private final Map<String, Set<String>> parentsToChildren = new HashMap<>();

    private AssetGroupListingGroupFilterRemoveOperationFactory(
        List<AssetGroupListingGroupFilter> resources) throws Exception {
      if (resources.isEmpty()) {
        throw new Exception("No listing group filters to remove");
      }

      for (AssetGroupListingGroupFilter filter : resources) {
        if (filter.getParentListingGroupFilter().isEmpty() && !this.rootResourceName.isEmpty()) {
          // A node with no parent is the root node, but there can only be a single root node.
          throw new IllegalStateException("More than one root node");
        } else if (filter.getParentListingGroupFilter().isEmpty()) {
          // Sets the root node.
          this.rootResourceName = filter.getResourceName();
        } else {
          // Adds an entry to the parentsToChildren map for each non-root node.
          String parentResourceName = filter.getParentListingGroupFilter();
          // Checks to see if a sibling in this group has already been visited, and fetches or
          // creates a new set as required.
          Set<String> siblings =
              this.parentsToChildren.computeIfAbsent(parentResourceName, p -> new HashSet<>());
          siblings.add(filter.getResourceName());
        }
      }
    }

    /**
     * Creates a list of MutateOperations that remove all of the resources in the tree originally
     * used to create this factory object.
     */
    private List<MutateOperation> removeAll() {
      return removeDescendantsAndFilter(rootResourceName);
    }

    /**
     * Creates a list of MutateOperations that remove all the descendants of the specified
     * AssetGroupListingGroupFilter resource name. The order of removal is post-order, where all the
     * children (and their children, recursively) are removed first. Then, the node itself is
     * removed.
     */
    private List<MutateOperation> removeDescendantsAndFilter(String resourceName) {
      List<MutateOperation> operations = new ArrayList<>();

      if (this.parentsToChildren.containsKey(resourceName)) {
        Set<String> children = parentsToChildren.get(resourceName);
        for (String child : children) {
          // Recursively adds operations to the return value that remove each of the child nodes of
          // the current node from the tree.
          operations.addAll(removeDescendantsAndFilter(child));
        }
      }

      // Creates and adds an operation to the return value that will remove the current node from
      // the tree.
      AssetGroupListingGroupFilterOperation operation =
          AssetGroupListingGroupFilterOperation.newBuilder().setRemove(resourceName).build();
      operations.add(
          MutateOperation.newBuilder().setAssetGroupListingGroupFilterOperation(operation).build());
      return operations;
    }
  }

  /**
   * A factory that creates MutateOperations wrapping AssetGroupListingGroupFilterMutateOperations
   * for a specific customerId and assetGroupId.
   *
   * <p>This object is intended to be used with an array of MutateOperations to perform an atomic
   * update to an AssetGroup.
   */
  private static class AssetGroupListingGroupFilterCreateOperationFactory {
    private final long customerId;
    private final long assetGroupId;
    private final long rootListingGroupId;
    private static Iterator<Long> idGenerator;

    private AssetGroupListingGroupFilterCreateOperationFactory(
        long customerId, long assetGroupId, long rootListingGroupId) {
      this.customerId = customerId;
      this.assetGroupId = assetGroupId;
      this.rootListingGroupId = rootListingGroupId;
      // Generates a new temporary ID to be used for a resource name in a MutateOperation. See
      // https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names
      // for details about temporary IDs.
      idGenerator = LongStream.iterate(rootListingGroupId - 1, prev -> prev - 1).iterator();
    }

    private Long nextId() {
      return idGenerator.next();
    }

    /**
     * Creates a MutateOperation that creates a root AssetGroupListingGroupFilter for the factory's
     * AssetGroup.
     *
     * <p>The root node or partition is the default, which is displayed as "All Products".
     */
    private MutateOperation createRoot() {
      AssetGroupListingGroupFilter listingGroupFilter =
          AssetGroupListingGroupFilter.newBuilder()
              .setResourceName(
                  ResourceNames.assetGroupListingGroupFilter(
                      customerId, assetGroupId, rootListingGroupId))
              .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId))
              // Since this is the root node, do not set the ParentListingGroupFilter. For all other
              // nodes, this would refer to the parent listing group filter resource name.
              // .setParentListingGroupFilter("PARENT_FILTER_NAME")
              //
              // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must be
              // SUBDIVISION because it will have child partitions under it.
              .setType(ListingGroupFilterType.SUBDIVISION)
              // Specifies that this is in the shopping vertical because it is a Performance Max
              // campaign for retail.
              .setVertical(ListingGroupFilterVertical.SHOPPING)
              // Note the case_value is not set because it should be undefined for the root node.
              .build();
      AssetGroupListingGroupFilterOperation operation =
          AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build();
      return MutateOperation.newBuilder()
          .setAssetGroupListingGroupFilterOperation(operation)
          .build();
    }

    /**
     * Creates a MutateOperation that creates an intermediate AssetGroupListingGroupFilter for the
     * factory's AssetGroup.
     *
     * <p>Use this method if the filter will have child filters. Otherwise, use the {@link
     * #createUnit(long, long, ListingGroupFilterDimension), createUnit} method.
     *
     * @param parent the ID of the parent AssetGroupListingGroupFilter.
     * @param id the ID of AssetGroupListingGroupFilter that will be created.
     * @param dimension the dimension to associate with the AssetGroupListingGroupFilter.
     */
    private MutateOperation createSubdivision(
        long parent, long id, ListingGroupFilterDimension dimension) {
      AssetGroupListingGroupFilter listingGroupFilter =
          AssetGroupListingGroupFilter.newBuilder()
              .setResourceName(
                  ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, id))
              .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId))
              .setParentListingGroupFilter(
                  ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, parent))
              // Uses the SUBDIVISION type to indicate that the AssetGroupListingGroupFilter
              // will have children.
              .setType(ListingGroupFilterType.SUBDIVISION)
              // Specifies that this is in the shopping vertical because it is a Performance Max
              // campaign for retail.
              .setVertical(ListingGroupFilterVertical.SHOPPING)
              .setCaseValue(dimension)
              .build();
      AssetGroupListingGroupFilterOperation filterOperation =
          AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build();
      return MutateOperation.newBuilder()
          .setAssetGroupListingGroupFilterOperation(filterOperation)
          .build();
    }

    /**
     * Creates a MutateOperation that creates a child AssetGroupListingGroupFilter for the factory's
     * AssetGroup.
     *
     * <p>Use this method if the filter won't have child filters. Otherwise, use the {@link
     * #createSubdivision(long, long, ListingGroupFilterDimension), createSubdivision} method.
     *
     * @param parent the ID of the parent AssetGroupListingGroupFilter.
     * @param id the ID of AssetGroupListingGroupFilter that will be created.
     * @param dimension the dimension to associate with the AssetGroupListingGroupFilter.
     */
    private MutateOperation createUnit(
        long parent, long id, ListingGroupFilterDimension dimension) {
      AssetGroupListingGroupFilter listingGroupFilter =
          AssetGroupListingGroupFilter.newBuilder()
              .setResourceName(
                  ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, id))
              .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId))
              .setParentListingGroupFilter(
                  ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, parent))
              // Uses the UNIT_INCLUDED type to indicate that the AssetGroupListingGroupFilter
              // won't have children.
              .setType(ListingGroupFilterType.UNIT_INCLUDED)
              // Specifies that this is in the shopping vertical because it is a Performance Max
              // campaign for retail.
              .setVertical(ListingGroupFilterVertical.SHOPPING)
              .setCaseValue(dimension)
              .build();
      AssetGroupListingGroupFilterOperation filterOperation =
          AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build();
      return MutateOperation.newBuilder()
          .setAssetGroupListingGroupFilterOperation(filterOperation)
          .build();
    }
  }

  /**
   * Runs the example.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param assetGroupId the asset group id for the Performance Max campaign.
   * @param replaceExistingTree option to remove existing product tree from the passed in asset
   *     group.
   * @throws GoogleAdsException if an API request failed with one or more service errors.
   */
  private void runExample(
      GoogleAdsClient googleAdsClient,
      long customerId,
      long assetGroupId,
      boolean replaceExistingTree)
      throws Exception {
    String assetGroupResourceName = ResourceNames.assetGroup(customerId, assetGroupId);

    List<MutateOperation> operations = new ArrayList<>();

    if (replaceExistingTree) {
      List<AssetGroupListingGroupFilter> existingListingGroupFilters =
          getAllExistingListingGroupFilterAssetsInAssetGroup(
              googleAdsClient, customerId, assetGroupResourceName);

      if (!existingListingGroupFilters.isEmpty()) {
        // A special factory object that ensures the creation of remove operations in the
        // correct order (child listing group filters must be removed before their parents).
        AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory =
            new AssetGroupListingGroupFilterRemoveOperationFactory(existingListingGroupFilters);

        operations.addAll(removeOperationFactory.removeAll());
      }
    }

    // Uses a factory to create all the MutateOperations that manipulate a specific
    // AssetGroup for a specific customer. The operations returned by the factory's methods
    // are used to construct a new tree of filters. These filters can have parent-child
    // relationships, and also include a special root that includes all children.
    //
    // When creating these filters, temporary IDs are used to create the hierarchy between
    // each of the nodes in the tree, beginning with the root listing group filter.
    //
    // The factory created below is specific to a customerId and assetGroupId.
    AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory =
        new AssetGroupListingGroupFilterCreateOperationFactory(
            customerId, assetGroupId, TEMPORARY_ID_LISTING_GROUP_ROOT);

    // Creates the operation to add the root node of the tree.
    operations.add(createOperationFactory.createRoot());

    // Creates an operation to add a leaf node for new products.
    ListingGroupFilterDimension newProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductCondition(
                ProductCondition.newBuilder()
                    .setCondition(ListingGroupFilterProductCondition.NEW)
                    .build())
            .build();
    operations.add(
        createOperationFactory.createUnit(
            TEMPORARY_ID_LISTING_GROUP_ROOT,
            createOperationFactory.nextId(),
            newProductDimension));

    // Creates an operation to add a leaf node for used products.
    ListingGroupFilterDimension usedProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductCondition(
                ProductCondition.newBuilder()
                    .setCondition(ListingGroupFilterProductCondition.USED)
                    .build())
            .build();
    operations.add(
        createOperationFactory.createUnit(
            TEMPORARY_ID_LISTING_GROUP_ROOT,
            createOperationFactory.nextId(),
            usedProductDimension));

    // This represents the ID of the "other" category in the ProductCondition subdivision. This ID
    // is saved because the node with this ID will be further partitioned, and this ID will serve as
    // the parent ID for subsequent child nodes of the "other" category.
    long otherSubdivisionId = createOperationFactory.nextId();

    // Creates an operation to add a subdivision node for other products in the ProductCondition
    // subdivision.
    ListingGroupFilterDimension otherProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductCondition(ProductCondition.newBuilder().build())
            .build();
    operations.add(
        // Calls createSubdivision because this listing group will have children.
        createOperationFactory.createSubdivision(
            TEMPORARY_ID_LISTING_GROUP_ROOT, otherSubdivisionId, otherProductDimension));

    // Creates an operation to add a leaf node for products with the brand "CoolBrand".
    ListingGroupFilterDimension coolBrandProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductBrand(ProductBrand.newBuilder().setValue("CoolBrand").build())
            .build();
    operations.add(
        createOperationFactory.createUnit(
            otherSubdivisionId,
            createOperationFactory.nextId(),
            coolBrandProductDimension));

    // Creates an operation to add a leaf node for products with the brand "CheapBrand".
    ListingGroupFilterDimension cheapBrandProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductBrand(ProductBrand.newBuilder().setValue("CheapBrand").build())
            .build();
    operations.add(
        createOperationFactory.createUnit(
            otherSubdivisionId,
            createOperationFactory.nextId(),
            cheapBrandProductDimension));

    // Creates an operation to add a leaf node for other products in the ProductBrand subdivision.
    ListingGroupFilterDimension otherBrandProductDimension =
        ListingGroupFilterDimension.newBuilder()
            .setProductBrand(ProductBrand.newBuilder().build())
            .build();
    operations.add(
        createOperationFactory.createUnit(
            otherSubdivisionId,
            createOperationFactory.nextId(),
            otherBrandProductDimension));

    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      MutateGoogleAdsRequest request =
          MutateGoogleAdsRequest.newBuilder()
              .setCustomerId(Long.toString(customerId))
              .addAllMutateOperations(operations)
              .build();
      MutateGoogleAdsResponse response = googleAdsServiceClient.mutate(request);
      printResponseDetails(request, response);
    }
  }

  /**
   * Fetches all of the listing group filters in an asset group.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param assetGroupResourceName the resource name of the asset group.
   */
  private List<AssetGroupListingGroupFilter> getAllExistingListingGroupFilterAssetsInAssetGroup(
      GoogleAdsClient googleAdsClient, long customerId, String assetGroupResourceName) {
    String query =
        "SELECT "
            + "asset_group_listing_group_filter.resource_name, "
            + "asset_group_listing_group_filter.parent_listing_group_filter "
            + "FROM asset_group_listing_group_filter "
            + "WHERE "
            + "asset_group_listing_group_filter.asset_group = '"
            + assetGroupResourceName
            + "'";
    SearchGoogleAdsRequest request =
        SearchGoogleAdsRequest.newBuilder()
            .setCustomerId(Long.toString(customerId))
            .setPageSize(PAGE_SIZE)
            .setQuery(query)
            .build();

    List<AssetGroupListingGroupFilter> resources = new ArrayList<>();
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      SearchPagedResponse searchPagedResponse = googleAdsServiceClient.search(request);
      for (GoogleAdsRow googleAdsRow : searchPagedResponse.iterateAll()) {
        resources.add(googleAdsRow.getAssetGroupListingGroupFilter());
      }
    }
    return resources;
  }

  /**
   * Prints the details of a MutateGoogleAdsResponse.
   *
   * @param request a MutateGoogleAdsRequest instance.
   * @param response a MutateGoogleAdsResponse instance.
   */
  private void printResponseDetails(
      MutateGoogleAdsRequest request, MutateGoogleAdsResponse response) {
    // Parse the Mutate response to print details about the entities that were removed and/or
    // created in the request.
    for (int i = 0; i < response.getMutateOperationResponsesCount(); i++) {
      MutateOperation operationRequest = request.getMutateOperations(i);
      MutateOperationResponse operationResponse = response.getMutateOperationResponses(i);

      if (operationResponse.getResponseCase()
          != ResponseCase.ASSET_GROUP_LISTING_GROUP_FILTER_RESULT) {
        String entityName = operationResponse.getResponseCase().toString();
        // Trim the substring "_RESULT" from the end of the entity name.
        entityName = entityName.substring(0, entityName.lastIndexOf("_RESULT"));
        System.out.printf("Unsupported entity type: %s%n", entityName);
      }

      String resourceName =
          operationResponse.getAssetGroupListingGroupFilterResult().getResourceName();
      AssetGroupListingGroupFilterOperation assetOperation =
          operationRequest.getAssetGroupListingGroupFilterOperation();

      String operationType =
          StringUtils.capitalize(assetOperation.getOperationCase().toString().toLowerCase());
      System.out.printf(
          "%sd a(n) AssetGroupListingGroupFilter with resource name: '%s'%n",
          operationType, resourceName);
    }
  }
}

      

C#

// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using CommandLine;
using Google.Ads.Gax.Examples;
using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.Util;
using Google.Ads.GoogleAds.V11.Common;
using Google.Ads.GoogleAds.V11.Enums;
using Google.Ads.GoogleAds.V11.Errors;
using Google.Ads.GoogleAds.V11.Resources;
using Google.Ads.GoogleAds.V11.Services;
using Google.Api.Gax;
using Google.Protobuf.Collections;
using Google.Protobuf;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Threading;
using static Google.Ads.GoogleAds.V11.Enums.AdGroupAdStatusEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AdGroupCriterionStatusEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AdGroupStatusEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AdGroupTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ConversionActionCategoryEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ConversionOriginEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AdvertisingChannelSubTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AdvertisingChannelTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AssetFieldTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AssetGroupStatusEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ListingGroupFilterTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ListingGroupFilterVerticalEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ListingGroupFilterProductConditionEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.AssetTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.BiddingStrategyTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.BudgetDeliveryMethodEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.BudgetTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.CampaignStatusEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.CriterionTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ListingGroupTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.MediaTypeEnum.Types;
using static Google.Ads.GoogleAds.V11.Enums.ProductConditionEnum.Types;
using static Google.Ads.GoogleAds.V11.Resources.Campaign.Types;
using static Google.Ads.GoogleAds.V11.Resources.ListingGroupFilterDimension.Types;
using static Google.Ads.GoogleAds.V11.Services.SmartCampaignSuggestionInfo.Types;

namespace Google.Ads.GoogleAds.Examples.V11
{
    /// <summary>
    /// This example shows how to add product partitions to a Performance Max retail campaign.
    ///
    /// For Performance Max campaigns, product partitions are represented using the
    /// AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a
    /// hierarchy that creates a product partition tree.
    ///
    /// For more information about Performance Max retail campaigns, see the
    /// <see cref="AddPerformanceMaxRetailCampaign"/> example.
    /// </summary>
    public class AddPerformanceMaxProductListingGroupTree : ExampleBase
    {
        /// <summary>
        /// Command line options for running the <see
        /// cref="AddPerformanceMaxProductListingGroupTree"/>
        /// example.
        /// </summary>
        public class Options : OptionsBase
        {
            /// <summary>
            /// The Google Ads customer ID.
            /// </summary>
            [Option("customerId", Required = true, HelpText =
                "The Google Ads customer ID.")]
            public long CustomerId { get; set; }

            /// <summary>
            /// The Asset Group ID.
            /// </summary>
            [Option("assetGroupId", Required = true, HelpText =
                "The Asset Group ID.")]
            public long AssetGroupId { get; set; }

            /// <summary>
            /// An option to remove the listing group tree from the asset group when this example is
            /// run.
            /// </summary>
            [Option("replaceExistingTree", Required = false, HelpText =
                "An option that removes the existing listing group tree from the asset group.")]
            public bool ReplaceExistingTree { get; set; }
        }

        /// <summary>
        /// Main method, to run this code example as a standalone application.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        public static void Main(string[] args)
        {
            Options options = ExampleUtilities.ParseCommandLine<Options>(args);

            AddPerformanceMaxProductListingGroupTree codeExample =
                new AddPerformanceMaxProductListingGroupTree();

            Console.WriteLine(codeExample.Description);

            codeExample.Run(
                new GoogleAdsClient(),
                options.CustomerId,
                options.AssetGroupId,
                options.ReplaceExistingTree
            );
        }

        /// <summary>
        /// Returns a description about the code example.
        /// </summary>
        public override string Description =>
            "This example shows how to add a product listing group tree to  a " +
            "Performance Max retail campaign.";

        /// <summary>
        /// A factory that creates MutateOperations for removing an existing tree of
        /// AssetGroupListingGroupFilters.
        ///
        /// AssetGroupListingGroupFilters must be removed in a specific order: all of the children
        /// of a filter must be removed before the filter itself, otherwise the API will return an
        /// error.
        ///
        /// This object is intended to be used with an array of MutateOperations to perform a series
        /// of related updates to an AssetGroup.
        /// </summary>
        private class AssetGroupListingGroupFilterRemoveOperationFactory {
            private string rootResourceName;
            private Dictionary<string, AssetGroupListingGroupFilter> resources;
            private Dictionary<string, HashSet<string>> parentsToChildren;

            public AssetGroupListingGroupFilterRemoveOperationFactory(
                List<AssetGroupListingGroupFilter> resources)
            {
                if (resources.Count == 0) {
                    throw new InvalidOperationException("No listing group filters to remove");
                }

                this.resources = new Dictionary<string, AssetGroupListingGroupFilter>();
                this.parentsToChildren = new Dictionary<string, HashSet<string>>();

                foreach (AssetGroupListingGroupFilter filter in resources)
                {
                    this.resources[filter.ResourceName] = filter;

                    // When the node has no parent, it means it's the root node, which is treated
                    // differently.
                    if (string.IsNullOrEmpty(filter.ParentListingGroupFilter))
                    {
                        if (!string.IsNullOrEmpty(this.rootResourceName))
                        {
                            throw new InvalidOperationException("More than one root node");
                        }

                        this.rootResourceName = filter.ResourceName;
                        continue;
                    }

                    string parentResourceName = filter.ParentListingGroupFilter;

                    HashSet<string> siblings;

                    // Check to see if we've already visited a sibling in this group, and fetch or
                    // create a new set as required.
                    if (this.parentsToChildren.ContainsKey(parentResourceName))
                    {
                        siblings = this.parentsToChildren[parentResourceName];
                    } else {
                        siblings = new HashSet<string>();
                    }

                    siblings.Add(filter.ResourceName);
                    this.parentsToChildren[parentResourceName] = siblings;
                }
            }

            /// <summary>
            /// Creates a list of MutateOperations that remove all of the resources in the tree
            /// originally used to create this factory object.
            /// </summary>
            /// <returns>A list of MutateOperations</returns>
            public List<MutateOperation> RemoveAll()
            {
                return this.RemoveDescendentsAndFilter(this.rootResourceName);
            }

            /// <summary>
            /// Creates a list of MutateOperations that remove all the descendents of the specified
            /// AssetGroupListingGroupFilter resource name. The order of removal is post-order,
            /// where all the children (and their children, recursively) are removed first. Then,
            /// the node itself is removed.
            /// </summary>
            /// <returns>A list of MutateOperations</returns>
            public List<MutateOperation> RemoveDescendentsAndFilter(string resourceName)
            {
                List<MutateOperation> operations =  new List<MutateOperation>();

                if (this.parentsToChildren.ContainsKey(resourceName))
                {
                    HashSet<string> children = this.parentsToChildren[resourceName];

                    foreach (string child in children)
                    {
                        operations.AddRange(this.RemoveDescendentsAndFilter(child));
                    }
                }

                AssetGroupListingGroupFilterOperation operation =
                    new AssetGroupListingGroupFilterOperation()
                    {
                        Remove = resourceName
                    };

                operations.Add(
                    new MutateOperation()
                    {
                        AssetGroupListingGroupFilterOperation = operation
                    }
                );

                return operations;
            }
        }

        private const int TEMPORARY_ID_LISTING_GROUP_ROOT = -1;

        /// <summary>
        /// A factory that creates MutateOperations wrapping
        /// AssetGroupListingGroupFilterMutateOperations for a specific customerId and
        /// assetGroupId.
        ///
        /// This object is intended to be used with an array of MutateOperations to perform an
        /// atomic update to an AssetGroup.
        /// </summary>
        private class AssetGroupListingGroupFilterCreateOperationFactory {
            private long customerId;
            private long assetGroupId;
            private long rootListingGroupId;
            private long nextId;

            public AssetGroupListingGroupFilterCreateOperationFactory(
                long customerId,
                long assetGroupId,
                long rootListingGroupId)
            {
                this.customerId = customerId;
                this.assetGroupId = assetGroupId;
                this.rootListingGroupId = rootListingGroupId;
                this.nextId = this.rootListingGroupId - 1;
            }

            /// <summary>
            /// Returns a new temporary ID to be used for a resource name in a MutateOperation. See
            /// https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names
            /// for details about temporary IDs.
            /// </summary>
            /// <returns>A new temporary ID.</returns>
            public long NextId()
            {
                long i = nextId;
                Interlocked.Decrement(ref nextId);
                return i;
            }

            /// <summary>
            /// Creates a MutateOperation that creates a root AssetGroupListingGroupFilter for the
            /// factory's AssetGroup.
            ///
            /// The root node or partition is the default, which is displayed as "All Products".
            /// </summary>
            /// <returns>A MutateOperation</returns>
            public MutateOperation CreateRoot()
            {
                AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter()
                {
                    ResourceName = ResourceNames.AssetGroupListingGroupFilter(
                        this.customerId,
                        this.assetGroupId,
                        this.rootListingGroupId
                    ),

                    AssetGroup = ResourceNames.AssetGroup(
                        this.customerId,
                        this.assetGroupId
                    ),

                    // Since this is the root node, do not set the ParentListingGroupFilter. For all
                    // other nodes, this would refer to the parent listing group filter resource
                    // name.
                    // ParentListingGroupFilter = "<PARENT FILTER NAME>"

                    // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must
                    // be Subdivision because we add child partitions under it.
                    Type = ListingGroupFilterType.Subdivision,

                    // Because this is a Performance Max campaign for retail, we need to specify
                    // that this is in the shopping vertical.
                    Vertical = ListingGroupFilterVertical.Shopping
                };

                AssetGroupListingGroupFilterOperation operation =
                    new AssetGroupListingGroupFilterOperation()
                    {
                        Create = listingGroupFilter
                    };

                return new MutateOperation()
                {
                    AssetGroupListingGroupFilterOperation = operation
                };
            }

            /// <summary>
            /// Creates a MutateOperation that creates a intermediate AssetGroupListingGroupFilter
            /// for the factory's AssetGroup.
            ///
            /// Use this method if the filter will have child filters. Otherwise, use the
            /// CreateUnit method.
            /// </summary>
            /// <param name="parent">The ID of the parent AssetGroupListingGroupFilter.</param>
            /// <param name="id">The ID of AssetGroupListingGroupFilter that will be
            /// created.</param>
            /// <param name="dimension">The dimension to associate with the
            /// AssetGroupListingGroupFilter.</param>
            /// <returns>A MutateOperation</returns>
            public MutateOperation CreateSubdivision(
                long parent,
                long id,
                ListingGroupFilterDimension dimension)
            {
                AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter()
                {
                    ResourceName = ResourceNames.AssetGroupListingGroupFilter(
                        this.customerId,
                        this.assetGroupId,
                        id
                    ),

                    AssetGroup = ResourceNames.AssetGroup(
                        this.customerId,
                        this.assetGroupId
                    ),

                    ParentListingGroupFilter = ResourceNames.AssetGroupListingGroupFilter(
                        this.customerId,
                        this.assetGroupId,
                        parent
                    ),

                    // We must use the Subdivision type to indicate that the
                    // AssetGroupListingGroupFilter will have children.
                    Type = ListingGroupFilterType.Subdivision,

                    // Because this is a Performance Max campaign for retail, we need to specify
                    // that this is in the shopping vertical.
                    Vertical = ListingGroupFilterVertical.Shopping,

                    CaseValue = dimension
                };

                AssetGroupListingGroupFilterOperation filterOperation =
                    new AssetGroupListingGroupFilterOperation()
                    {
                        Create = listingGroupFilter
                    };

                return new MutateOperation()
                {
                    AssetGroupListingGroupFilterOperation = filterOperation
                };
            }

            /// <summary>
            /// Creates a MutateOperation that creates a child AssetGroupListingGroupFilter
            /// for the factory's AssetGroup.
            ///
            /// Use this method if the filter won't have child filters. Otherwise, use the
            /// CreateSubdivision method.
            /// </summary>
            /// <param name="parent">The ID of the parent AssetGroupListingGroupFilter.</param>
            /// <param name="id">The ID of AssetGroupListingGroupFilter that will be
            /// created.</param>
            /// <param name="dimension">The dimension to associate with the
            /// AssetGroupListingGroupFilter.</param>
            /// <returns>A MutateOperation</returns>
            public MutateOperation CreateUnit(
                long parent,
                long id,
                ListingGroupFilterDimension dimension)
            {
                AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter()
                {
                    ResourceName = ResourceNames.AssetGroupListingGroupFilter(
                        this.customerId,
                        this.assetGroupId,
                        id
                    ),

                    AssetGroup = ResourceNames.AssetGroup(
                        this.customerId,
                        this.assetGroupId
                    ),

                    ParentListingGroupFilter = ResourceNames.AssetGroupListingGroupFilter(
                        this.customerId,
                        this.assetGroupId,
                        parent
                    ),

                    // We must use the UnitIncluded type to indicate that the
                    // AssetGroupListingGroupFilter won't have children.
                    Type = ListingGroupFilterType.UnitIncluded,

                    // Because this is a Performance Max campaign for retail, we need to specify
                    // that this is in the shopping vertical.
                    Vertical = ListingGroupFilterVertical.Shopping,

                    CaseValue = dimension
                };

                AssetGroupListingGroupFilterOperation filterOperation =
                    new AssetGroupListingGroupFilterOperation()
                    {
                        Create = listingGroupFilter
                    };

                return new MutateOperation()
                {
                    AssetGroupListingGroupFilterOperation = filterOperation
                };
            }
        }

        /// <summary>
        /// Runs the code example.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <param name="assetGroupId">The asset group id for the Performance Max campaign.</param>
        /// <param name="replaceExistingTree">Option to remove existing product tree
        /// from the passed in asset group.</param>
        public void Run(
            GoogleAdsClient client,
            long customerId,
            long assetGroupId,
            bool replaceExistingTree)
        {
            GoogleAdsServiceClient googleAdsServiceClient =
                client.GetService(Services.V11.GoogleAdsService);

            string assetGroupResourceName = ResourceNames.AssetGroup(customerId, assetGroupId);

            // We use a factory to create all the MutateOperations that manipulate a specific
            // AssetGroup for a specific customer. The operations returned by the factory's methods
            // are used to optionally remove all AssetGroupListingGroupFilters from the tree, and
            // then to construct a new tree of filters. These filters can have a parent-child
            // relationship, and also include a special root that includes all children.
            //
            // When creating these filters, we use temporary IDs to create the hierarchy between
            // the root listing group filter, and the subdivisions and leave nodes beneath that.
            //
            // The factory specific to a customerId and assetGroupId is created below.
            AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory =
                new AssetGroupListingGroupFilterCreateOperationFactory(
                    customerId,
                    assetGroupId,
                    TEMPORARY_ID_LISTING_GROUP_ROOT
                );

            MutateGoogleAdsRequest request = new MutateGoogleAdsRequest
            {
                CustomerId = customerId.ToString()
            };

            if (replaceExistingTree)
            {
                List<AssetGroupListingGroupFilter> existingListingGroupFilters =
                    GetAllExistingListingGroupFilterAssetsInAssetGroup(
                        client,
                        customerId,
                        assetGroupResourceName
                    );

                if (existingListingGroupFilters.Count > 0) {
                    // A special factory object that ensures the creation of remove operations in the
                    // correct order (child listing group filters must be removed before their parents).
                    AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory =
                        new AssetGroupListingGroupFilterRemoveOperationFactory(
                            existingListingGroupFilters
                        );

                    request.MutateOperations.AddRange(removeOperationFactory.RemoveAll());
                }
            }

            request.MutateOperations.Add(createOperationFactory.CreateRoot());

            request.MutateOperations.Add(
                createOperationFactory.CreateUnit(
                    TEMPORARY_ID_LISTING_GROUP_ROOT,
                    createOperationFactory.NextId(),
                    new ListingGroupFilterDimension()
                    {
                        ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition()
                        {
                            Condition = ListingGroupFilterProductCondition.New
                        }
                    }
                )
            );

            request.MutateOperations.Add(
                createOperationFactory.CreateUnit(
                    TEMPORARY_ID_LISTING_GROUP_ROOT,
                    createOperationFactory.NextId(),
                    new ListingGroupFilterDimension()
                    {
                        ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition()
                        {
                            Condition = ListingGroupFilterProductCondition.Used
                        }
                    }
                )
            );

            // We save this ID because create child nodes underneath it.
            long subdivisionIdConditionOther = createOperationFactory.NextId();

            request.MutateOperations.Add(
                // We're calling CreateSubdivision because this listing group will have children.
                createOperationFactory.CreateSubdivision(
                    TEMPORARY_ID_LISTING_GROUP_ROOT,
                    subdivisionIdConditionOther,
                    new ListingGroupFilterDimension()
                    {
                        // All sibling nodes must have the same dimension type. We use an empty
                        // ProductCondition to indicate that this is an "Other" partition.
                        ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition()
                    }
                )
            );

            request.MutateOperations.Add(
                createOperationFactory.CreateUnit(
                    subdivisionIdConditionOther,
                    createOperationFactory.NextId(),
                    new ListingGroupFilterDimension()
                    {
                        ProductBrand = new ProductBrand()
                        {
                            Value = "CoolBrand"
                        }
                    }
                )
            );

            request.MutateOperations.Add(
                createOperationFactory.CreateUnit(
                    subdivisionIdConditionOther,
                    createOperationFactory.NextId(),
                    new ListingGroupFilterDimension()
                    {
                        ProductBrand = new ProductBrand()
                        {
                            Value = "CheapBrand"
                        }
                    }
                )
            );

            request.MutateOperations.Add(
                createOperationFactory.CreateUnit(
                    subdivisionIdConditionOther,
                    createOperationFactory.NextId(),
                    new ListingGroupFilterDimension()
                    {
                        ProductBrand = new ProductBrand()
                    }
                )
            );

            MutateGoogleAdsResponse response = googleAdsServiceClient.Mutate(request);

            PrintResponseDetails(request, response);
        }

        /// <summary>
        /// Fetches all of the listing group filters in an asset group.
        /// </summary>
        /// <param name="client">The Google Ads Client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <param name="assetGroupResourceName">The resource name of the asset group.</param>
        /// <returns>A list of asset group listing filter resources.</returns>
        private List<AssetGroupListingGroupFilter>
            GetAllExistingListingGroupFilterAssetsInAssetGroup(
                GoogleAdsClient client,
                long customerId,
                string assetGroupResourceName)
        {
            List<AssetGroupListingGroupFilter> resources =  new List<AssetGroupListingGroupFilter>();

            // Get the GoogleAdsService.
            GoogleAdsServiceClient googleAdsService = client.GetService(
                Services.V11.GoogleAdsService);

            SearchGoogleAdsRequest request = new SearchGoogleAdsRequest()
            {
                CustomerId = customerId.ToString(),
                PageSize = 10000,
                Query =
                    $@"
                    SELECT
                        asset_group_listing_group_filter.resource_name,
                        asset_group_listing_group_filter.parent_listing_group_filter
                    FROM asset_group_listing_group_filter
                    WHERE
                        asset_group_listing_group_filter.asset_group = '{assetGroupResourceName}'
                    "
            };

            // The below enumerable will automatically iterate through the pages of the search
            // request. The limit to the number of listing group filters permitted in a Performance
            // Max campaign can be found here:
            // https://developers.google.com/google-ads/api/docs/best-practices/system-limits
            PagedEnumerable<SearchGoogleAdsResponse, GoogleAdsRow> searchPagedResponse =
                googleAdsService.Search(request);

            foreach (GoogleAdsRow row in searchPagedResponse)
            {
                resources.Add(row.AssetGroupListingGroupFilter);
            }

            return resources;
        }

        /// <summary>
        /// Prints the details of a MutateGoogleAdsResponse. Parses the "response" oneof field name
        /// and uses it to extract the new entity's name and resource name.
        /// </summary>
        /// <param name="request">A MutateGoogleAdsRequest instance.</param>
        /// <param name="response">A MutateGoogleAdsResponse instance.</param>
        private void PrintResponseDetails(MutateGoogleAdsRequest request, MutateGoogleAdsResponse response)
        {
            // Parse the Mutate response to print details about the entities that were created
            // in the request.
            for (int i = 0; i < response.MutateOperationResponses.Count; i++)
            {
                MutateOperation operationRequest = request.MutateOperations[i];
                MutateOperationResponse operationResponse = response.MutateOperationResponses[i];

                if (operationResponse.ResponseCase != MutateOperationResponse.ResponseOneofCase.AssetGroupListingGroupFilterResult)
                {
                    string entityName = operationResponse.ResponseCase.ToString();
                    // Trim the substring "Result" from the end of the entity name.
                    entityName = entityName.Remove(entityName.Length - 6);

                    Console.WriteLine($"Unsupported entity type: {entityName}");
                }

                string resourceName = operationResponse.AssetGroupListingGroupFilterResult.ResourceName;
                AssetGroupListingGroupFilterOperation assetOperation = operationRequest.AssetGroupListingGroupFilterOperation;

                switch (assetOperation.OperationCase)
                {
                    case AssetGroupListingGroupFilterOperation.OperationOneofCase.Create:
                        Console.WriteLine(
                            $"Created a(n) AssetGroupListingGroupFilter with resource name: '{resourceName}'.");
                        break;

                    case AssetGroupListingGroupFilterOperation.OperationOneofCase.Remove:
                        Console.WriteLine(
                            $"Removed a(n) AssetGroupListingGroupFilter with resource name: '{resourceName}'.");
                        break;

                    default:
                        Console.WriteLine($"Unsupported operation type: {assetOperation.OperationCase.ToString()}");
                        continue;
                }
            }
        }
    }
}

      

PHP

<?php

/**
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Google\Ads\GoogleAds\Examples\ShoppingAds;

require __DIR__ . '/../../vendor/autoload.php';

use GetOpt\GetOpt;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentNames;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentParser;
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
use Google\Ads\GoogleAds\Lib\V11\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V11\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\V11\GoogleAdsException;
use Google\Ads\GoogleAds\Util\V11\ResourceNames;
use Google\Ads\GoogleAds\V11\Enums\ListingGroupFilterProductConditionEnum\ListingGroupFilterProductCondition;
use Google\Ads\GoogleAds\V11\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType;
use Google\Ads\GoogleAds\V11\Enums\ListingGroupFilterVerticalEnum\ListingGroupFilterVertical;
use Google\Ads\GoogleAds\V11\Errors\GoogleAdsError;
use Google\Ads\GoogleAds\V11\Resources\AssetGroupListingGroupFilter;
use Google\Ads\GoogleAds\V11\Resources\ListingGroupFilterDimension;
use Google\Ads\GoogleAds\V11\Resources\ListingGroupFilterDimension\ProductBrand;
use Google\Ads\GoogleAds\V11\Resources\ListingGroupFilterDimension\ProductCondition;
use Google\Ads\GoogleAds\V11\Services\AssetGroupListingGroupFilterOperation;
use Google\Ads\GoogleAds\V11\Services\GoogleAdsRow;
use Google\Ads\GoogleAds\V11\Services\MutateGoogleAdsResponse;
use Google\Ads\GoogleAds\V11\Services\MutateOperation;
use Google\Ads\GoogleAds\V11\Services\MutateOperationResponse;
use Google\ApiCore\ApiException;
use Google\ApiCore\Serializer;

/**
 * This example shows how to add product partitions to a Performance Max retail campaign.
 *
 * For Performance Max campaigns, product partitions are represented using the
 * AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a
 * hierarchy that creates a product partition tree.
 *
 * For more information about Performance Max retail campaigns, see the
 * AddPerformanceMaxRetailCampaign example.
 */
class AddPerformanceMaxProductListingGroupTree
{
    private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE';
    private const ASSET_GROUP_ID = 'INSERT_ASSET_GROUP_ID_HERE';
    // Optional: Removes the existing listing group tree from the asset group or not.
    //
    // If the current asset group already has a tree of listing group filters, and you
    // try to add a new set of listing group filters including a root filter, you'll
    // receive a 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error.
    //
    // Setting this option to true will remove the existing tree and prevent this error.
    private const REPLACE_EXISTING_TREE = false;

    // We specify temporary IDs that are specific to a single mutate request.
    // Temporary IDs are always negative and unique within one mutate request.
    private const LISTING_GROUP_ROOT_TEMPORARY_ID = -1;

    private const PAGE_SIZE = 1000;

    public static function main()
    {
        // Either pass the required parameters for this example on the command line, or insert them
        // into the constants above.
        $options = (new ArgumentParser())->parseCommandArguments([
            ArgumentNames::CUSTOMER_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::ASSET_GROUP_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::REPLACE_EXISTING_TREE => GetOpt::OPTIONAL_ARGUMENT
        ]);

        // Generate a refreshable OAuth2 credential for authentication.
        $oAuth2Credential = (new OAuth2TokenBuilder())->fromFile()->build();

        // Construct a Google Ads client configured from a properties file and the
        // OAuth2 credentials above.
        $googleAdsClient = (new GoogleAdsClientBuilder())->fromFile()
            ->withOAuth2Credential($oAuth2Credential)
            ->build();

        try {
            self::runExample(
                $googleAdsClient,
                $options[ArgumentNames::CUSTOMER_ID] ?: self::CUSTOMER_ID,
                $options[ArgumentNames::ASSET_GROUP_ID] ?: self::ASSET_GROUP_ID,
                filter_var(
                    $options[ArgumentNames::REPLACE_EXISTING_TREE]
                        ?: self::REPLACE_EXISTING_TREE,
                    FILTER_VALIDATE_BOOLEAN
                )
            );
        } catch (GoogleAdsException $googleAdsException) {
            printf(
                "Request with ID '%s' has failed.%sGoogle Ads failure details:%s",
                $googleAdsException->getRequestId(),
                PHP_EOL,
                PHP_EOL
            );
            foreach ($googleAdsException->getGoogleAdsFailure()->getErrors() as $error) {
                /** @var GoogleAdsError $error */
                printf(
                    "\t%s: %s%s",
                    $error->getErrorCode()->getErrorCode(),
                    $error->getMessage(),
                    PHP_EOL
                );
            }
            exit(1);
        } catch (ApiException $apiException) {
            printf(
                "ApiException was thrown with message '%s'.%s",
                $apiException->getMessage(),
                PHP_EOL
            );
            exit(1);
        }
    }

    /**
     * Runs the example.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param int $assetGroupId the asset group ID
     * @param bool $replaceExistingTree true if it should replace the existing listing group
     *     tree on the asset group
     */
    public static function runExample(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        int $assetGroupId,
        bool $replaceExistingTree
    ) {
        // We create all the mutate operations that manipulate a specific asset group for a specific
        // customer. The operations are used to optionally remove all asset group listing group
        // filters from the tree, and then to construct a new tree of filters. These filters can
        // have a parent-child relationship, and also include a special root that includes all
        // children.
        //
        // When creating these filters, we use temporary IDs to create the hierarchy between
        // the root listing group filter, and the subdivisions and leave nodes beneath that.
        $mutateOperations = [];
        if ($replaceExistingTree === true) {
            $existingListingGroupFilters = self::getAllExistingListingGroupFilterAssetsInAssetGroup(
                $googleAdsClient,
                $customerId,
                ResourceNames::forAssetGroup($customerId, $assetGroupId)
            );
            if (count($existingListingGroupFilters) > 0) {
                $mutateOperations = array_merge(
                    $mutateOperations,
                    // Ensures the creation of remove operations in the correct order (child listing
                    // group filters must be removed before their parents).
                    self::createMutateOperationsForRemovingListingGroupFiltersTree(
                        $existingListingGroupFilters
                    )
                );
            }
        }

        $mutateOperations[] = self::createMutateOperationForRoot(
            $customerId,
            $assetGroupId,
            self::LISTING_GROUP_ROOT_TEMPORARY_ID
        );

        // The temporary ID to be used for creating subdivisions and units.
        static $tempId = self::LISTING_GROUP_ROOT_TEMPORARY_ID - 1;

        $mutateOperations[] = self::createMutateOperationForUnit(
            $customerId,
            $assetGroupId,
            $tempId--,
            self::LISTING_GROUP_ROOT_TEMPORARY_ID,
            new ListingGroupFilterDimension([
                'product_condition' => new ProductCondition([
                    'condition' => ListingGroupFilterProductCondition::PBNEW
                ])
            ])
        );

        $mutateOperations[] = self::createMutateOperationForUnit(
            $customerId,
            $assetGroupId,
            $tempId--,
            self::LISTING_GROUP_ROOT_TEMPORARY_ID,
            new ListingGroupFilterDimension([
                'product_condition' => new ProductCondition([
                    'condition' => ListingGroupFilterProductCondition::USED
                ])
            ])
        );

        // We save this ID to create child nodes underneath it.
        $conditionOtherSubdivisionId = $tempId--;

        // We're calling createMutateOperationForSubdivision() because this listing group will
        // have children.
        $mutateOperations[] = self::createMutateOperationForSubdivision(
            $customerId,
            $assetGroupId,
            $conditionOtherSubdivisionId,
            self::LISTING_GROUP_ROOT_TEMPORARY_ID,
            new ListingGroupFilterDimension([
                // All sibling nodes must have the same dimension type. We use an empty
                // ProductCondition to indicate that this is an "Other" partition.
                'product_condition' => new ProductCondition()
            ])
        );

        $mutateOperations[] = self::createMutateOperationForUnit(
            $customerId,
            $assetGroupId,
            $tempId--,
            $conditionOtherSubdivisionId,
            new ListingGroupFilterDimension(
                ['product_brand' => new ProductBrand(['value' => 'CoolBrand'])]
            )
        );

        $mutateOperations[] = self::createMutateOperationForUnit(
            $customerId,
            $assetGroupId,
            $tempId--,
            $conditionOtherSubdivisionId,
            new ListingGroupFilterDimension([
                'product_brand' => new ProductBrand(['value' => 'CheapBrand'])
            ])
        );

        $mutateOperations[] = self::createMutateOperationForUnit(
            $customerId,
            $assetGroupId,
            $tempId--,
            $conditionOtherSubdivisionId,
            // All other product brands.
            new ListingGroupFilterDimension(['product_brand' => new ProductBrand()])
        );

        // Issues a mutate request to create everything and prints its information.
        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();
        $response = $googleAdsServiceClient->mutate(
            $customerId,
            $mutateOperations
        );

        self::printResponseDetails($mutateOperations, $response);
    }

    /**
     * Fetches all of the asset group listing group filters in an asset group.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $assetGroupResourceName the resource name of the asset group
     * @return AssetGroupListingGroupFilter[] the list of asset group listing group filters
     */
    private static function getAllExistingListingGroupFilterAssetsInAssetGroup(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $assetGroupResourceName
    ): array {
        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();
        // Creates a query that retrieves asset group listing group filters.
        // The limit to the number of listing group filters permitted in a Performance
        // Max campaign can be found here:
        // https://developers.google.com/google-ads/api/docs/best-practices/system-limits.
        $query = sprintf(
            'SELECT asset_group_listing_group_filter.resource_name, '
            . 'asset_group_listing_group_filter.parent_listing_group_filter '
            . 'FROM asset_group_listing_group_filter '
            . 'WHERE asset_group_listing_group_filter.asset_group = "%s"',
            $assetGroupResourceName
        );

        // Issues a search request by specifying page size.
        $response =
            $googleAdsServiceClient->search($customerId, $query, ['pageSize' => self::PAGE_SIZE]);

        $assetGroupListingGroupFilters = [];
        // Iterates over all rows in all pages to get an asset group listing group filter.
        foreach ($response->iterateAllElements() as $googleAdsRow) {
            /** @var GoogleAdsRow $googleAdsRow */
            $assetGroupListingGroupFilters[] = $googleAdsRow->getAssetGroupListingGroupFilter();
        }
        return $assetGroupListingGroupFilters;
    }

    /**
     * Creates mutate operations for removing an existing tree of asset group listing group filters.
     *
     * Asset group listing group filters must be removed in a specific order: all of the children
     * of a filter must be removed before the filter itself, otherwise the API will return an
     * error.
     *
     * @param AssetGroupListingGroupFilter[] $assetGroupListingGroupFilters the existing asset
     *     group listing group filters
     * @return MutateOperation[] the list of MutateOperations to remove all listing groups
     */
    private static function createMutateOperationsForRemovingListingGroupFiltersTree(
        array $assetGroupListingGroupFilters
    ): array {
        if (empty($assetGroupListingGroupFilters)) {
            throw new \UnexpectedValueException('No listing group filters to remove');
        }

        $resourceNamesToListingGroupFilters = [];
        $parentsToChildren = [];
        $rootResourceName = null;
        foreach ($assetGroupListingGroupFilters as $assetGroupListingGroupFilter) {
            $resourceNamesToListingGroupFilters[$assetGroupListingGroupFilter->getResourceName()] =
                $assetGroupListingGroupFilter;
            // When the node has no parent, it means it's the root node, which is treated
            // differently.
            if (empty($assetGroupListingGroupFilter->getParentListingGroupFilter())) {
                if (!is_null($rootResourceName)) {
                    throw new \UnexpectedValueException('More than one root node found.');
                }
                $rootResourceName = $assetGroupListingGroupFilter->getResourceName();
                continue;
            }

            $parentResourceName = $assetGroupListingGroupFilter->getParentListingGroupFilter();
            $siblings = [];

            // Checks to see if we've already visited a sibling in this group and fetches it.
            if (array_key_exists($parentResourceName, $parentsToChildren)) {
                $siblings = $parentsToChildren[$parentResourceName];
            }
            $siblings[] = $assetGroupListingGroupFilter->getResourceName();
            $parentsToChildren[$parentResourceName] = $siblings;
        }

        return self::createMutateOperationsForRemovingDescendents(
            $rootResourceName,
            $parentsToChildren
        );
    }

    /**
     * Creates a list of mutate operations that remove all the descendents of the specified
     * asset group listing group filter's resource name. The order of removal is post-order,
     * where all the children (and their children, recursively) are removed first. Then,
     * the node itself is removed.
     *
     * @param string $assetGroupListingGroupFilterResourceName the resource name of the root of
     *     listing group tree
     * @param array $parentsToChildren the map from parent resource names to children resource
     *     names
     * @return MutateOperation[] the list of MutateOperations to remove all listing groups
     */
    private static function createMutateOperationsForRemovingDescendents(
        string $assetGroupListingGroupFilterResourceName,
        array $parentsToChildren
    ): array {
        $operations = [];
        if (array_key_exists($assetGroupListingGroupFilterResourceName, $parentsToChildren)) {
            foreach ($parentsToChildren[$assetGroupListingGroupFilterResourceName] as $child) {
                $operations = array_merge(
                    $operations,
                    self::createMutateOperationsForRemovingDescendents($child, $parentsToChildren)
                );
            }
        }

        $operations[] = new MutateOperation([
            'asset_group_listing_group_filter_operation'
                => new AssetGroupListingGroupFilterOperation([
                    'remove' => $assetGroupListingGroupFilterResourceName
                ])
        ]);
        return $operations;
    }

    /**
     * Creates a mutate operation that creates a root asset group listing group filter for the
     * factory's asset group.
     *
     * The root node or partition is the default, which is displayed as "All Products".
     *
     * @param int $customerId the customer ID
     * @param int $assetGroupId the asset group ID
     * @param int $rootListingGroupId the root listing group ID
     * @return MutateOperation the mutate operation for creating the root
     */
    private static function createMutateOperationForRoot(
        int $customerId,
        int $assetGroupId,
        int $rootListingGroupId
    ): MutateOperation {
        $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([
            'resource_name' => ResourceNames::forAssetGroupListingGroupFilter(
                $customerId,
                $assetGroupId,
                $rootListingGroupId
            ),
            'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId),
            // Since this is the root node, do not set the 'parent_listing_group_filter' field. For
            // all other nodes, this would refer to the parent listing group filter resource
            // name.

            // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must
            // be SUBDIVISION because we add child partitions under it.
            'type' => ListingGroupFilterType::SUBDIVISION,
            // Because this is a Performance Max campaign for retail, we need to specify
            // that this is in the shopping vertical.
            'vertical' => ListingGroupFilterVertical::SHOPPING
        ]);

        return new MutateOperation([
            'asset_group_listing_group_filter_operation'
                => new AssetGroupListingGroupFilterOperation([
                    'create' => $assetGroupListingGroupFilter
                ])
        ]);
    }

    /**
     * Creates a mutate operation that creates a intermediate asset group listing group filter.
     *
     * @param int $customerId the customer ID
     * @param int $assetGroupId the asset group ID
     * @param int $assetGroupListingGroupFilterId the ID of the asset group listing group filter to
     *     be created
     * @param int $parentId the ID of the parent of asset group listing group filter to be created
     * @param ListingGroupFilterDimension $listingGroupFilterDimension the listing group
     *     filter dimension to associate with the asset group listing group filter
     * @return MutateOperation the mutate operation for creating a subdivision
     */
    private static function createMutateOperationForSubdivision(
        int $customerId,
        int $assetGroupId,
        int $assetGroupListingGroupFilterId,
        int $parentId,
        ListingGroupFilterDimension $listingGroupFilterDimension
    ): MutateOperation {
        $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([
            'resource_name' => ResourceNames::forAssetGroupListingGroupFilter(
                $customerId,
                $assetGroupId,
                $assetGroupListingGroupFilterId
            ),
            'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId),
            // Sets the type as a SUBDIVISION, which will allow the node to be the parent of
            // another sub-tree.
            'type' => ListingGroupFilterType::SUBDIVISION,
            // Because this is a Performance Max campaign for retail, we need to specify
            // that this is in the shopping vertical.
            'vertical' => ListingGroupFilterVertical::SHOPPING,
            'parent_listing_group_filter' => ResourceNames::forAssetGroupListingGroupFilter(
                $customerId,
                $assetGroupId,
                $parentId
            ),
            // Case values contain the listing dimension used for the node.
            'case_value' => $listingGroupFilterDimension
        ]);

        return new MutateOperation([
            'asset_group_listing_group_filter_operation'
                => new AssetGroupListingGroupFilterOperation([
                    'create' => $assetGroupListingGroupFilter
                ])
        ]);
    }

    /**
     * Creates a mutate operation that creates a child asset group listing group filter (unit
     * node).
     *
     * Use this method if the filter won't have child filters. Otherwise, use
     * createMutateOperationForSubdivision().
     *
     * @param int $customerId the customer ID
     * @param int $assetGroupId the asset group ID
     * @param int $assetGroupListingGroupFilterId the ID of the asset group listing group filter to
     *     be created
     * @param int $parentId the ID of the parent of asset group listing group filter to be
     *      created
     * @param ListingGroupFilterDimension $listingGroupFilterDimension the listing group
     *     filter dimension to associate with the asset group listing group filter
     * @return MutateOperation the mutate operation for creating a unit
     */
    private static function createMutateOperationForUnit(
        int $customerId,
        int $assetGroupId,
        int $assetGroupListingGroupFilterId,
        string $parentId,
        ListingGroupFilterDimension $listingGroupFilterDimension
    ): MutateOperation {
        $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([
            'resource_name' => ResourceNames::forAssetGroupListingGroupFilter(
                $customerId,
                $assetGroupId,
                $assetGroupListingGroupFilterId
            ),
            'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId),
            'parent_listing_group_filter' => ResourceNames::forAssetGroupListingGroupFilter(
                $customerId,
                $assetGroupId,
                $parentId
            ),
            // Sets the type as a UNIT_INCLUDED to indicate that this asset group listing group
            // filter won't have children.
            'type' => ListingGroupFilterType::UNIT_INCLUDED,
            // Because this is a Performance Max campaign for retail, we need to specify
            // that this is in the shopping vertical.
            'vertical' => ListingGroupFilterVertical::SHOPPING,
            'case_value' => $listingGroupFilterDimension
        ]);

        return new MutateOperation([
            'asset_group_listing_group_filter_operation'
                => new AssetGroupListingGroupFilterOperation([
                    'create' => $assetGroupListingGroupFilter
                ])
        ]);
    }

    /**
     * Prints the details of a mutate google ads response. Parses the "response" oneof field name
     * and uses it to extract the new entity's name and resource name.
     *
     * @param MutateOperation[] $mutateOperations the submitted mutate operations
     * @param MutateGoogleAdsResponse $mutateGoogleAdsResponse the mutate Google Ads response
     */
    private static function printResponseDetails(
        array $mutateOperations,
        MutateGoogleAdsResponse $mutateGoogleAdsResponse
    ): void {
        foreach (
            $mutateGoogleAdsResponse->getMutateOperationResponses() as $i => $operationResponse
        ) {
            /** @var MutateOperationResponse $operationResponse */
            if (
                $operationResponse->getResponse()
                    !== 'asset_group_listing_group_filter_result'
            ) {
                // Trims the substring "_result" from the end of the entity name.
                printf(
                    "Unsupported entity type: %s.%s",
                    substr($operationResponse->getResponse(), 0, -strlen('_result')),
                    PHP_EOL
                );
                continue;
            }

            $operation = $mutateOperations[$i]->getAssetGroupListingGroupFilterOperation();
            $getter = Serializer::getGetter($operationResponse->getResponse());
            switch ($operation->getOperation()) {
                case 'create':
                    printf(
                        "Created an asset group listing group filter with resource name: "
                         . " '%s'.%s",
                        $operationResponse->$getter()->getResourceName(),
                        PHP_EOL
                    );
                    break;
                case 'remove':
                    printf(
                        "Removed an asset group listing group filter with resource name: "
                        . " '%s'.%s",
                        $operationResponse->$getter()->getResourceName(),
                        PHP_EOL
                    );
                    break;
                default:
                    printf(
                        "Unsupported operation type: '%s'.%s",
                        $operation->getOperation(),
                        PHP_EOL
                    );
            }
        }
    }
}

AddPerformanceMaxProductListingGroupTree::main();

      

Python

#!/usr/bin/env python
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shows how to add product partitions to a Performance Max retail campaign.

For Performance Max campaigns, product partitions are represented using the
AssetGroupListingGroupFilter resource. This resource can be combined with
itself to form a hierarchy that creates a product partition tree.

For more information about Performance Max retail campaigns, see the
shopping_ads/add_performance_max_retail_campaign.py example.
"""


import argparse
import sys

from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException


# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.
#
# See https://developers.google.com/google-ads/api/docs/mutating/best-practices
# for further details.
#
# These temporary IDs are fixed because they are used in multiple places.
_TEMPORARY_ID_LISTING_GROUP_ROOT = -1

# Sets the page size for paged search queries.
_PAGE_SIZE = 10000


class AssetGroupListingGroupFilterRemoveOperationFactory:
    def __init__(self, client, listing_group_filters):
        """Factory class for creating sorted list of MutateOperations.

        The operations remove the given tree of AssetGroupListingGroupFilters,
        When removing these listing group filters, the remove operations must be
        sent in a specific order that removes leaf nodes before removing parent
        nodes.

        Args:
            client: an initialized GoogleAdsClient instance.
            listing_group_filters: a list of AssetGroupListingGroupFilters.
        """
        if len(listing_group_filters) < 1:
            raise ValueError("No listing group filters to remove.")

        self.client = client
        self.root_resource_name = None
        self.parents_to_children = {}

        # Process the given list of listing group filters to identify the root
        # node and any parent to child edges in the tree.
        for listing_group_filter in listing_group_filters:
            resource_name = listing_group_filter.resource_name
            parent_resource_name = (
                listing_group_filter.parent_listing_group_filter
            )

            # When the node has no parent, it means it's the root node.
            if not parent_resource_name:
                if self.root_resource_name:
                    # Check if another root node has already been detected and
                    # raise an error if so, as only one root node can exist for
                    # a given tree.
                    raise ValueError("More than one listing group parent node.")
                else:
                    self.root_resource_name = resource_name
            else:
                # Check if we've already visited a sibling in this group, and
                # either update it or create a new branch accordingly.
                if parent_resource_name in self.parents_to_children:
                    # If we've visitied a sibling already, add this resource
                    # name to the existing list.
                    self.parents_to_children[parent_resource_name].append(
                        resource_name
                    )
                else:
                    # If we haven't visited any siblings, then create a new list
                    # for this parent node and add this resource name to it.
                    self.parents_to_children[parent_resource_name] = [
                        resource_name
                    ]

    def remove_all(self):
        """Creates a list of MutateOperations for the listing group filter tree.

        Returns:
            A list of MutateOperations that remove each specified
            AssetGroupListingGroupFilter in the tree passed in when this
            class was initialized.
        """
        return self.remove_descendants_and_filter(self.root_resource_name)

    def remove_descendants_and_filter(self, resource_name):
        """Builds a post-order sorted list of MutateOperations.

        Creates a list of MutateOperations that remove all the descendents of
        the specified AssetGroupListingGroupFilter resource name. The order of
        removal is post-order, where all the children (and their children,
        recursively) are removed first. Then, the root node itself is removed.

        Args:
            resource_name: an AssetGroupListingGroupFilter resource name.

        Returns:
            a sorted list of MutateOperations.
        """
        operations = []

        # Check if resource name is a parent.
        if resource_name in self.parents_to_children:
            # If this resource name is a parent, call this method recursively
            # on each of its children.
            for child in self.parents_to_children[resource_name]:
                operations.extend(self.remove_descendants_and_filter(child))

        mutate_operation = self.client.get_type("MutateOperation")
        mutate_operation.asset_group_listing_group_filter_operation.remove = (
            resource_name
        )
        operations.append(mutate_operation)

        return operations


class AssetGroupListingGroupFilterCreateOperationFactory:
    def __init__(self, client, customer_id, asset_group_id, root_listing_id):
        """A factory class for creating MutateOperations.

        These operations create new AssetGroupListingGroupFilterMutateOperation
        instances using the given customer ID and asset group ID.

        Args:
            client: an initialized GoogleAdsClient instance.
            customer_id: a client customer ID.
            asset_group_id: the asset group id for the Performance Max campaign.
            root_listing_id: a temporary ID to use as the listing group root.
        """
        self.client = client
        self.customer_id = customer_id
        self.asset_group_id = asset_group_id
        self.root_listing_id = root_listing_id
        self.next_temp_id = self.root_listing_id - 1

    def next_id(self):
        """Returns the next temporary ID for use in a sequence.

        The temporary IDs are used in the list of MutateOperations in order to
        refer to objects in the request that aren't in the API yet. For more
        details see:
        https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names

        Returns:
            A new temporary ID.
        """
        self.next_temp_id -= 1
        return self.next_temp_id

    def create_root(self):
        """Creates a MutateOperation to add a root AssetGroupListingGroupFilter.

        Returns:
            A MutateOperation for a new AssetGroupListingGroupFilter.
        """
        googleads_service = self.client.get_service("GoogleAdsService")

        mutate_operation = self.client.get_type("MutateOperation")
        asset_group_listing_group_filter = (
            mutate_operation.asset_group_listing_group_filter_operation.create
        )

        asset_group_listing_group_filter.resource_name = googleads_service.asset_group_listing_group_filter_path(
            self.customer_id, self.asset_group_id, self.root_listing_id
        )
        asset_group_listing_group_filter.asset_group = googleads_service.asset_group_path(
            self.customer_id, self.asset_group_id
        )
        # Since this is the root node, do not set the
        # parent_listing_group_filter field. For all other nodes, this would
        # refer to the parent listing group filter resource name.
        # asset_group_listing_group_filter.parent_listing_group_filter = "<PARENT FILTER NAME>"

        # Unlike the add_performance_max_retail_campaign example, the type for
        # the root node here must be Subsivision because we add child
        # partitions under it.
        asset_group_listing_group_filter.type_ = (
            self.client.enums.ListingGroupFilterTypeEnum.SUBDIVISION
        )

        # Because this is a Performance Max campaign for retail, we need to
        # specify that this is a shopping vertical.
        asset_group_listing_group_filter.vertical = (
            self.client.enums.ListingGroupFilterVerticalEnum.SHOPPING
        )

        return mutate_operation

    def create_subdivision(self, parent_id, temporary_id, dimension):
        """Creates a MutateOperation to add an AssetGroupListingGroupFilter.

        Use this method if the filter will have child filters. Otherwise use
        the create_unit method.

        Args:
            parent_id: the ID of the parent AssetGroupListingGroupFilter.
            temporary_id: a temporary ID for the operation being created.
            dimension: The dimension to associate with this new
                AssetGroupListingGroupFilter.

        Returns:
            a MutateOperation for a new AssetGroupListingGroupFilter
        """
        googleads_service = self.client.get_service("GoogleAdsService")

        mutate_operation = self.client.get_type("MutateOperation")
        asset_group_listing_group_filter = (
            mutate_operation.asset_group_listing_group_filter_operation.create
        )

        asset_group_listing_group_filter.resource_name = googleads_service.asset_group_listing_group_filter_path(
            self.customer_id, self.asset_group_id, temporary_id
        )
        asset_group_listing_group_filter.asset_group = googleads_service.asset_group_path(
            self.customer_id, self.asset_group_id
        )
        asset_group_listing_group_filter.parent_listing_group_filter = googleads_service.asset_group_listing_group_filter_path(
            self.customer_id, self.asset_group_id, parent_id
        )
        asset_group_listing_group_filter.type_ = (
            self.client.enums.ListingGroupFilterTypeEnum.SUBDIVISION
        )
        asset_group_listing_group_filter.vertical = (
            self.client.enums.ListingGroupFilterVerticalEnum.SHOPPING
        )
        asset_group_listing_group_filter.case_value = dimension

        return mutate_operation

    def create_unit(self, parent_id, temporary_id, dimension):
        """Creates a MutateOperation to add an AssetGroupListingGroupFilter.

        Use this method if the filter will not have child filters. Otherwise use
        the create_subdivision method.

        Args:
            parent_id: the ID of the parent AssetGroupListingGroupFilter.
            dimension: The dimension to associate with this new
                AssetGroupListingGroupFilter.

        Returns:
            a MutateOperation for a new AssetGroupListingGroupFilter
        """
        googleads_service = self.client.get_service("GoogleAdsService")

        mutate_operation = self.client.get_type("MutateOperation")
        asset_group_listing_group_filter = (
            mutate_operation.asset_group_listing_group_filter_operation.create
        )

        asset_group_listing_group_filter.resource_name = googleads_service.asset_group_listing_group_filter_path(
            self.customer_id, self.asset_group_id, temporary_id
        )
        asset_group_listing_group_filter.asset_group = googleads_service.asset_group_path(
            self.customer_id, self.asset_group_id
        )
        asset_group_listing_group_filter.parent_listing_group_filter = googleads_service.asset_group_listing_group_filter_path(
            self.customer_id, self.asset_group_id, parent_id
        )
        # We must use the UnitIncluded type to indicate that the
        # AssetGroupListingGroupFilter won't have children.
        asset_group_listing_group_filter.type_ = (
            self.client.enums.ListingGroupFilterTypeEnum.UNIT_INCLUDED
        )
        # Because this is a Performance Max campaign for retail, we need to
        # specify that this is in the shopping vertical.
        asset_group_listing_group_filter.vertical = (
            self.client.enums.ListingGroupFilterVerticalEnum.SHOPPING
        )
        asset_group_listing_group_filter.case_value = dimension

        return mutate_operation


def main(client, customer_id, asset_group_id, replace_existing_tree):
    """The main method that creates all necessary entities for the example.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        asset_group_id: the asset group id for the Performance Max campaign.
        replace_existing_tree: option to remove existing product tree from the
            passed in asset group.
    """
    googleads_service = client.get_service("GoogleAdsService")
    asset_group_resource_name = googleads_service.asset_group_path(
        customer_id, asset_group_id
    )
    operations = []

    if replace_existing_tree:
        # Retrieve a list of existing AssetGroupListingGroupFilters
        existing_listing_group_filters = get_all_existing_listing_group_filter_assets_in_asset_group(
            client, customer_id, asset_group_resource_name
        )

        # If present, create MutateOperations to remove each
        # AssetGroupListingGroupFilter and add them to the list of operations.
        if existing_listing_group_filters:
            remove_operation_factory = AssetGroupListingGroupFilterRemoveOperationFactory(
                client, existing_listing_group_filters
            )
            operations.extend(remove_operation_factory.remove_all())

    create_operation_factory = AssetGroupListingGroupFilterCreateOperationFactory(
        client, customer_id, asset_group_id, _TEMPORARY_ID_LISTING_GROUP_ROOT,
    )

    operations.append(create_operation_factory.create_root())

    new_dimension = client.get_type("ListingGroupFilterDimension")
    new_dimension.product_condition.condition = (
        client.enums.ListingGroupFilterProductConditionEnum.NEW
    )
    operations.append(
        create_operation_factory.create_unit(
            _TEMPORARY_ID_LISTING_GROUP_ROOT,
            create_operation_factory.next_id(),
            new_dimension,
        )
    )

    used_dimension = client.get_type("ListingGroupFilterDimension")
    used_dimension.product_condition.condition = (
        client.enums.ListingGroupFilterProductConditionEnum.USED
    )
    operations.append(
        create_operation_factory.create_unit(
            _TEMPORARY_ID_LISTING_GROUP_ROOT,
            create_operation_factory.next_id(),
            used_dimension,
        )
    )

    # We save this ID because create child nodes underneath it.
    subdivision_id_condition_other = create_operation_factory.next_id()

    # All sibling nodes must have the same dimension type. We use an empty
    # product_condition to indicate that this is an "Other" partition.
    other_dimension = client.get_type("ListingGroupFilterDimension")
    # This triggers the presence of the product_condition field without
    # specifying any field values. This is important in order to tell the API
    # that this is an "other" node.
    other_dimension.product_condition._pb.SetInParent()
    # We're calling create_subdivision because this listing group will have
    # children.
    operations.append(
        create_operation_factory.create_subdivision(
            _TEMPORARY_ID_LISTING_GROUP_ROOT,
            subdivision_id_condition_other,
            other_dimension,
        )
    )

    cool_dimension = client.get_type("ListingGroupFilterDimension")
    cool_dimension.product_brand.value = "CoolBrand"
    operations.append(
        create_operation_factory.create_unit(
            subdivision_id_condition_other,
            create_operation_factory.next_id(),
            cool_dimension,
        )
    )

    cheap_dimension = client.get_type("ListingGroupFilterDimension")
    cheap_dimension.product_brand.value = "CheapBrand"
    operations.append(
        create_operation_factory.create_unit(
            subdivision_id_condition_other,
            create_operation_factory.next_id(),
            cheap_dimension,
        )
    )

    empty_dimension = client.get_type("ListingGroupFilterDimension")
    # This triggers the presence of the product_brand field without specifying
    # any field values. This is important in order to tell the API
    # that this is an "other" node.
    empty_dimension.product_brand._pb.SetInParent()
    operations.append(
        create_operation_factory.create_unit(
            subdivision_id_condition_other,
            create_operation_factory.next_id(),
            empty_dimension,
        )
    )

    response = googleads_service.mutate(
        customer_id=customer_id, mutate_operations=operations
    )

    print_response_details(operations, response)


def get_all_existing_listing_group_filter_assets_in_asset_group(
    client, customer_id, asset_group_resource_name
):
    """Fetches all of the listing group filters in an asset group.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        asset_group_resource_name: the asset group resource name for the
            Performance Max campaign.

    Returns:
        a list of AssetGroupListingGroupFilters.
    """
    query = f"""
        SELECT
          asset_group_listing_group_filter.resource_name,
          asset_group_listing_group_filter.parent_listing_group_filter
        FROM asset_group_listing_group_filter
        WHERE asset_group_listing_group_filter.asset_group = '{asset_group_resource_name}'"""

    request = client.get_type("SearchGoogleAdsRequest")
    request.customer_id = customer_id
    request.query = query
    request.page_size = _PAGE_SIZE

    googleads_service = client.get_service("GoogleAdsService")
    response = googleads_service.search(request=request)

    return [result.asset_group_listing_group_filter for result in response]


def print_response_details(mutate_operations, response):
    """Prints the details of the GoogleAdsService.Mutate request.

    This uses the original list of mutate operations to map the operation
    result to what was sent. It can be assumed that the initial set of
    operations and the list returned in the response are in the same order.

    Args:
        mutate_operations: a list of MutateOperation instances.
        response: a GoogleAdsMutateResponse instance.
    """
    # Parse the Mutate response to print details about the entities that were
    # created in the request.
    for i, result in enumerate(response.mutate_operation_responses):
        requested = mutate_operations[i]
        resource_name = (
            result.asset_group_listing_group_filter_result.resource_name
        )

        # Check the operation type for the requested operation in order to
        # log whether it was a remove or a create request.
        if "remove" in requested.asset_group_listing_group_filter_operation:
            print(
                "Removed an AssetGroupListingGroupFilter with resource name: "
                f"'{resource_name}'."
            )
        elif "create" in requested.asset_group_listing_group_filter_operation:
            print(
                "Created an AssetGroupListingGroupFilter with resource name: "
                f"'{resource_name}'."
            )
        else:
            print("An unknown operation was returned.")


if __name__ == "__main__":
    # GoogleAdsClient will read the google-ads.yaml configuration file in the
    # home directory if none is specified.
    googleads_client = GoogleAdsClient.load_from_storage(version="v11")

    parser = argparse.ArgumentParser(
        description=(
            "Adds product partitions to a Performance Max retail campaign."
        )
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID.",
    )
    parser.add_argument(
        "-a",
        "--asset_group_id",
        type=int,
        required=True,
        help="The asset group id for the Performance Max campaign.",
    )
    parser.add_argument(
        "-r",
        "--replace_existing_tree",
        action="store_true",
        help=(
            "Whether or not to replace the existing product partition tree. "
            "If the current AssetGroup already has a tree of "
            "ListingGroupFilters, attempting to add a new set of "
            "ListingGroupFilters including a root filter will result in an "
            "ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS error. "
            "Setting this option to true will remove the existing tree and "
            "prevent this error."
        ),
    )

    args = parser.parse_args()

    try:
        main(
            googleads_client,
            args.customer_id,
            args.asset_group_id,
            args.replace_existing_tree,
        )
    except GoogleAdsException as ex:
        print(
            f'Request with ID "{ex.request_id}" failed with status '
            f'"{ex.error.code().name}" and includes the following errors:'
        )
        for error in ex.failure.errors:
            print(f'Error with message "{error.message}".')
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print(f"\t\tOn field: {field_path_element.field_name}")
        sys.exit(1)

      

Ruby

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This example shows how to add product partitions to a Performance Max retail
# campaign.
#
# For Performance Max campaigns, product partitions are represented using the
# AssetGroupListingGroupFilter resource. This resource can be combined with
# itself to form a hierarchy that creates a product partition tree.
#
# For more information about Performance Max retail campaigns, see the
# add_performance_max_retail_campaign.rb example.

require 'optparse'
require 'google/ads/google_ads'

# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.
#
# See https://developers.google.com/google-ads/api/docs/mutating/best-practices
# for further details.
TEMPORARY_ID_LISTING_GROUP_ROOT = "-1"

def add_performance_max_product_listing_group_tree(
    customer_id,
    asset_group_id,
    replace_existing_tree)
  # GoogleAdsClient will read a config file from
  # ENV['HOME']/google_ads_config.rb when called without parameters
  client = Google::Ads::GoogleAds::GoogleAdsClient.new

  asset_group_resource_name = client.path.asset_group(
    customer_id,
    asset_group_id,
  )

  # We use a factory to create all the MutateOperations that manipulate a
  # specific AssetGroup for a specific customer. The operations returned by the
  # factory's methods are used to optionally remove all
  # AssetGroupListingGroupFilters from the tree, and then to construct a new
  # tree of filters. These filters can have a parent-child relationship, and
  # also include a special root that includes all children.
  #
  # When creating these filters, we use temporary IDs to create the hierarchy
  # between the root listing group filter, and the subdivisions and leave nodes
  # beneath that.
  #
  # The factory specific to a customerId and assetGroupId is created below.
  create_operation_factory = AssetGroupListingGroupFilterCreateOperationFactory.new(
    customer_id,
    asset_group_id,
    TEMPORARY_ID_LISTING_GROUP_ROOT,
  )

  operations = []

  if replace_existing_tree
    existing_listing_group_filters = get_existing_listing_group_filters_in_asset_group(
      client,
      customer_id,
      asset_group_resource_name,
    )

    if existing_listing_group_filters.length > 0
      # A special factory object that ensures the creation of remove operations
      # in the correct order (child listing group filters must be removed
      # before their parents).
      remove_operation_factory = AssetGroupListingGroupFilterRemoveOperationFactory.new(
        existing_listing_group_filters
      )

      operations += remove_operation_factory.remove_all(client)
    end
  end

  operations << create_operation_factory.create_root(client)

  operations << create_operation_factory.create_unit(
    client,
    TEMPORARY_ID_LISTING_GROUP_ROOT,
    create_operation_factory.next_id,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_condition = client.resource.product_condition do |condition|
        condition.condition = :NEW
      end
    end,
  )
  operations << create_operation_factory.create_unit(
    client,
    TEMPORARY_ID_LISTING_GROUP_ROOT,
    create_operation_factory.next_id,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_condition = client.resource.product_condition do |condition|
        condition.condition = :USED
      end
    end,
  )

  # We save this ID because we create child nodes underneath it.
  subdivision_id_condition_other = create_operation_factory.next_id

  operations << create_operation_factory.create_subdivision(
    client,
    TEMPORARY_ID_LISTING_GROUP_ROOT,
    subdivision_id_condition_other,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_condition = client.resource.product_condition do |condition|
        # All sibling nodes must have the same dimension type. We use an empty
        # ProductCondition to indicate that this is an "Other" partition.
      end
    end,
  )

  operations << create_operation_factory.create_unit(
    client,
    subdivision_id_condition_other,
    create_operation_factory.next_id,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_brand = client.resource.product_brand do |brand|
        brand.value = 'CoolBrand'
      end
    end,
  )
  operations << create_operation_factory.create_unit(
    client,
    subdivision_id_condition_other,
    create_operation_factory.next_id,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_brand = client.resource.product_brand do |brand|
        brand.value = 'CheapBrand'
      end
    end,
  )
  operations << create_operation_factory.create_unit(
    client,
    subdivision_id_condition_other,
    create_operation_factory.next_id,
    client.resource.listing_group_filter_dimension do |dimension|
      dimension.product_brand = client.resource.product_brand do |brand|
      end
    end,
  )

  response = client.service.google_ads.mutate(
    customer_id: customer_id,
    mutate_operations: operations,
  )

  print_response_details(operations, response)
end

# Fetches all of the listing group filters in an asset group.
def get_existing_listing_group_filters_in_asset_group(client, customer_id, asset_group_resource_name)
  query = <<~QUERY
    SELECT
        asset_group_listing_group_filter.resource_name,
        asset_group_listing_group_filter.parent_listing_group_filter
    FROM asset_group_listing_group_filter
    WHERE
        asset_group_listing_group_filter.asset_group = '#{asset_group_resource_name}'
  QUERY
  response = client.service.google_ads.search(
    customer_id: customer_id,
    page_size: 10000,
    query: query,
  )

  response.map { |row| row.asset_group_listing_group_filter }
end

def print_response_details(operations, response)
  response.mutate_operation_responses.each_with_index do |row, i|
    resource_name = row.asset_group_listing_group_filter_result.resource_name
    operation_type = operations[i].asset_group_listing_group_filter_operation.operation
    case operation_type
    when :create
      puts "Created AssetGroupListingGroupFilter with resource name '#{resource_name}'."
    when :remove
      puts "Removed AssetGroupListingGroupFilter with resource name '#{resource_name}'."
    else
      puts "Unsupported operation type #{operation_type}."
    end
  end
end

# A factory that creates MutateOperations for removing an existing tree of
# AssetGroupListingGroupFilters.
#
# AssetGroupListingGroupFilters must be removed in a specific order: all of the
# children of a filter must be removed before the filter itself, otherwise the
# API will return an error.
#
# This object is intended to be used with an array of MutateOperations to
# perform a series of related updates to an AssetGroup.
class AssetGroupListingGroupFilterRemoveOperationFactory
  def initialize(resources)
    raise "No listing group filters to remove." if resources.size == 0

    # By default, each node only knows about its parents.
    # However, to remove children first, we need to have a mapping
    # of parents to children, so we build that here.
    @parents_to_children = {}

    resources.each do |filter|
      parent_resource_name = filter.parent_listing_group_filter

      if parent_resource_name.nil? || parent_resource_name.empty?
        if !@root_resource_name.nil?
          raise "More than one root node."
        end

        @root_resource_name = filter.resource_name
        next
      end

      siblings = if @parents_to_children.has_key?(parent_resource_name)
                   @parents_to_children[parent_resource_name]
                 else
                   Set.new
                 end
      siblings.add(filter.resource_name)
      @parents_to_children[parent_resource_name] = siblings
    end
  end

  # Creates a list of MutateOperations that remove all of the resources in the
  # tree originally used to create this factory object.
  def remove_all(client)
    remove_descendents_and_filter(client, @root_resource_name)
  end

  # Creates a list of MutateOperations that remove all the descendents of the
  # specified AssetGroupListingGroupFilter resource name. The order of removal
  # is post-order, where all the children (and their children, recursively) are
  # removed first. Then, the node itself is removed.
  def remove_descendents_and_filter(client, resource_name)
    operations = []

    if @parents_to_children.has_key?(resource_name)
      @parents_to_children[resource_name].each do |child|
        operations += remove_descendents_and_filter(client, child)
      end
    end

    operations << client.operation.mutate do |m|
      m.asset_group_listing_group_filter_operation =
        client.operation.remove_resource.asset_group_listing_group_filter(resource_name)
    end

    operations
  end
end

# A factory that creates MutateOperations wrapping
# AssetGroupListingGroupFilterMutateOperations for a specific customerId and
# assetGroupId.
#
# This object is intended to be used with an array of MutateOperations to
# perform an atomic update to an AssetGroup.
class AssetGroupListingGroupFilterCreateOperationFactory
  def initialize(customer_id, asset_group_id, root_listing_group_id)
    @customer_id = customer_id
    @asset_group_id = asset_group_id
    @root_listing_group_id = root_listing_group_id.to_i
    @next_id = @root_listing_group_id - 1
  end

  # Returns a new temporary ID to be used for a resource name in a
  # MutateOperation. See
  # https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names
  # for details about temporary IDs.
  def next_id
    @next_id -= 1
  end

  # Creates a MutateOperation that creates a root AssetGroupListingGroupFilter
  # for the factory's AssetGroup.
  #
  # The root node or partition is the default, which is displayed as "All
  # Products".
  def create_root(client)
    operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf|
      lgf.resource_name = client.path.asset_group_listing_group_filter(
        @customer_id,
        @asset_group_id,
        @root_listing_group_id,
      )
      lgf.asset_group = client.path.asset_group(
        @customer_id,
        @asset_group_id,
      )

      # Since this is the root node, do not set the ParentListingGroupFilter.
      # For all other nodes, this would refer to the parent listing group
      # filter resource name.
      # lgf.parent_listing_group_filter = "<PARENT FILTER NAME>"

      # Unlike AddPerformanceMaxRetailCampaign, the type for the root node here
      # must be SUBDIVISION  because we add child partitions under it.
      lgf.type = :SUBDIVISION

      # Because this is a Performance Max campaign for retail, we need to
      # specify that this is in the SHOPPING vertical.
      lgf.vertical = :SHOPPING
    end

    client.operation.mutate do |m|
      m.asset_group_listing_group_filter_operation = operation
    end
  end

  # Creates a MutateOperation that creates a intermediate
  # AssetGroupListingGroupFilter for the factory's AssetGroup.
  #
  # Use this method if the filter will have child filters. Otherwise, use the
  # create_unit method.
  def create_subdivision(client, parent, id, dimension)
    operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf|
      lgf.resource_name = client.path.asset_group_listing_group_filter(
        @customer_id,
        @asset_group_id,
        id,
      )
      lgf.asset_group = client.path.asset_group(
        @customer_id,
        @asset_group_id,
      )
      lgf.parent_listing_group_filter = client.path.asset_group_listing_group_filter(
        @customer_id,
        @asset_group_id,
        parent,
      )

      # We must use the SUBDIVISION type to indicate that the
      # AssetGroupListingGroupFilter will have children.
      lgf.type = :SUBDIVISION

      # Because this is a Performance Max campaign for retail, we need to
      # specify that this is in the SHOPPING vertical.
      lgf.vertical = :SHOPPING

      lgf.case_value = dimension
    end

    client.operation.mutate do |m|
      m.asset_group_listing_group_filter_operation = operation
    end
  end

  # Creates a MutateOperation that creates a child AssetGroupListingGroupFilter
  # for the factory's AssetGroup.
  #
  # Use this method if the filter won't have child filters. Otherwise, use the
  # create_subdivision method.
  def create_unit(client, parent, id, dimension)
    operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf|
      lgf.resource_name = client.path.asset_group_listing_group_filter(
        @customer_id,
        @asset_group_id,
        id,
      )
      lgf.asset_group = client.path.asset_group(
        @customer_id,
        @asset_group_id,
      )
      lgf.parent_listing_group_filter = client.path.asset_group_listing_group_filter(
        @customer_id,
        @asset_group_id,
        parent,
      )

      # We must use the UNIT_INCLUDED type to indicate that the
      # AssetGroupListingGroupFilter won't have children.
      lgf.type = :UNIT_INCLUDED

      # Because this is a Performance Max campaign for retail, we need to
      # specify that this is in the SHOPPING vertical.
      lgf.vertical = :SHOPPING

      lgf.case_value = dimension
    end

    client.operation.mutate do |m|
      m.asset_group_listing_group_filter_operation = operation
    end
  end
end

if __FILE__ == $0
    options = {}

    # The following parameter(s) should be provided to run the example. You can
    # either specify these by changing the INSERT_XXX_ID_HERE values below, or on
    # the command line.
    #
    # Parameters passed on the command line will override any parameters set in
    # code.
    #
    # Running the example with -h will print the command line usage.
    options[:customer_id] = 'INSERT_CUSTOMER_ID_HERE'
    options[:asset_group_id] = 'INSERT_ASSET_GROUP_ID_HERE'
    options[:replace_existing_tree] = false

    OptionParser.new do |opts|
      opts.banner = sprintf('Usage: %s [options]', File.basename(__FILE__))

      opts.separator ''
      opts.separator 'Options:'

      opts.on('-C', '--customer-id CUSTOMER-ID', String, 'Customer ID') do |v|
        options[:customer_id] = v
      end

      opts.on('-g', '--asset-group-id ASSET-GROUP-ID', String, 'Asset Group ID') do |v|
        options[:asset_group_id] = v
      end

      opts.on('-r', '--replace-existing-tree REPLACE-EXISTING-TREE',
              String, 'Replace existing tree?') do |v|
        options[:replace_existing_tree] = true
      end

      opts.separator ''
      opts.separator 'Help:'

      opts.on_tail('-h', '--help', 'Show this message') do
        puts opts
        exit
      end
    end.parse!

    begin
      add_performance_max_product_listing_group_tree(
        options.fetch(:customer_id).tr("-", ""),
        options.fetch(:asset_group_id),
        options.fetch(:replace_existing_tree),
      )
    rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e
      e.failure.errors.each do |error|
        STDERR.printf("Error with message: %s\n", error.message)
        if error.location
          error.location.field_path_elements.each do |field_path_element|
            STDERR.printf("\tOn field: %s\n", field_path_element.field_name)
          end
        end
        error.error_code.to_h.each do |k, v|
          next if v == :UNSPECIFIED
          STDERR.printf("\tType: %s\n\tCode: %s\n", k, v)
        end
      end
      raise
    end
  end

      

Perl

#!/usr/bin/perl -w
#
# Copyright 2022, Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This example shows how to add product partitions to a Performance Max retail campaign.
#
# For Performance Max campaigns, product partitions are represented using the
# AssetGroupListingGroupFilter resource. This resource can be combined with itself
# to form a hierarchy that creates a product partition tree.
#
# For more information about Performance Max retail campaigns, see the
# add_performance_max_retail_campaign example.

use strict;
use warnings;
use utf8;

use FindBin qw($Bin);
use lib "$Bin/../../lib";
use Google::Ads::GoogleAds::Client;
use Google::Ads::GoogleAds::Utils::GoogleAdsHelper;
use Google::Ads::GoogleAds::V11::Resources::AssetGroupListingGroupFilter;
use Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension;
use Google::Ads::GoogleAds::V11::Resources::ProductCondition;
use Google::Ads::GoogleAds::V11::Resources::ProductBrand;
use Google::Ads::GoogleAds::V11::Enums::ListingGroupFilterTypeEnum
  qw(SUBDIVISION UNIT_INCLUDED);
use Google::Ads::GoogleAds::V11::Enums::ListingGroupFilterVerticalEnum
  qw(SHOPPING);
use Google::Ads::GoogleAds::V11::Enums::ListingGroupFilterProductConditionEnum
  qw(NEW USED);
use Google::Ads::GoogleAds::V11::Services::GoogleAdsService::MutateOperation;
use
  Google::Ads::GoogleAds::V11::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation;
use Google::Ads::GoogleAds::V11::Utils::ResourceNames;

use Getopt::Long qw(:config auto_help);
use Pod::Usage;
use Cwd qw(abs_path);

# The following parameter(s) should be provided to run the example. You can
# either specify these by changing the INSERT_XXX_ID_HERE values below, or on
# the command line.
#
# Parameters passed on the command line will override any parameters set in
# code.
#
# Running the example with -h will print the command line usage.
my $customer_id    = "INSERT_CUSTOMER_ID_HERE";
my $asset_group_id = "INSERT_ASSET_GROUP_ID_HERE";
# Optional: Removes the existing listing group tree from the asset group or not.
#
# If the current asset group already has a tree of listing group filters, and you
# try to add a new set of listing group filters including a root filter, you'll
# receive a 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error.
#
# Setting this option to a defined value will remove the existing tree and prevent
# this error.
my $replace_existing_tree = undef;

# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.
use constant LISTING_GROUP_ROOT_TEMPORARY_ID => -1;

use constant PAGE_SIZE => 1000;

sub add_performance_max_product_listing_group_tree {
  my ($api_client, $customer_id, $asset_group_id, $replace_existing_tree) = @_;

  # We create all the mutate operations that manipulate a specific asset group for
  # a specific customer. The operations are used to optionally remove all asset
  # group listing group filters from the tree, and then to construct a new tree
  # of filters. These filters can have a parent-child relationship, and also include
  # a special root that includes all children.
  #
  # When creating these filters, we use temporary IDs to create the hierarchy between
  # the root listing group filter, and the subdivisions and leave nodes beneath that.
  my $mutate_operations = [];
  if (defined $replace_existing_tree) {
    my $existing_listing_group_filters =
      get_all_existing_listing_group_filter_assets_in_asset_group(
      $api_client,
      $customer_id,
      Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group(
        $customer_id, $asset_group_id
      ));

    if (scalar @$existing_listing_group_filters > 0) {
      push @$mutate_operations,
        # Ensure the creation of remove operations in the correct order (child
        # listing group filters must be removed before their parents).
        @{
        create_mutate_operations_for_removing_listing_group_filters_tree(
          $existing_listing_group_filters)};
    }
  }

  push @$mutate_operations,
    create_mutate_operation_for_root($customer_id, $asset_group_id,
    LISTING_GROUP_ROOT_TEMPORARY_ID);

  # The temporary ID to be used for creating subdivisions and units.
  my $temp_id = LISTING_GROUP_ROOT_TEMPORARY_ID - 1;

  push @$mutate_operations,
    create_mutate_operation_for_unit(
    $customer_id,
    $asset_group_id,
    $temp_id--,
    LISTING_GROUP_ROOT_TEMPORARY_ID,
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        productCondition =>
          Google::Ads::GoogleAds::V11::Resources::ProductCondition->new({
            condition => NEW
          })}));

  push @$mutate_operations,
    create_mutate_operation_for_unit(
    $customer_id,
    $asset_group_id,
    $temp_id--,
    LISTING_GROUP_ROOT_TEMPORARY_ID,
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        productCondition =>
          Google::Ads::GoogleAds::V11::Resources::ProductCondition->new({
            condition => USED
          })}));

  # We save this ID to create child nodes underneath it.
  my $condition_other_subdivision_id = $temp_id--;

  # We're calling create_mutate_operation_for_subdivision() because this listing
  # group will have children.
  push @$mutate_operations, create_mutate_operation_for_subdivision(
    $customer_id,
    $asset_group_id,
    $condition_other_subdivision_id,
    LISTING_GROUP_ROOT_TEMPORARY_ID,
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        # All sibling nodes must have the same dimension type. We use an empty
        # ProductCondition to indicate that this is an "Other" partition.
        productCondition =>
          Google::Ads::GoogleAds::V11::Resources::ProductCondition->new({})}));

  push @$mutate_operations,
    create_mutate_operation_for_unit(
    $customer_id,
    $asset_group_id,
    $temp_id--,
    $condition_other_subdivision_id,
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        productBrand =>
          Google::Ads::GoogleAds::V11::Resources::ProductBrand->new({
            value => "CoolBrand"
          })}));

  push @$mutate_operations,
    create_mutate_operation_for_unit(
    $customer_id,
    $asset_group_id,
    $temp_id--,
    $condition_other_subdivision_id,
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        productBrand =>
          Google::Ads::GoogleAds::V11::Resources::ProductBrand->new({
            value => "CheapBrand"
          })}));

  push @$mutate_operations, create_mutate_operation_for_unit(
    $customer_id,
    $asset_group_id,
    $temp_id--,
    $condition_other_subdivision_id,
    # All other product brands.
    Google::Ads::GoogleAds::V11::Resources::ListingGroupFilterDimension->new({
        productBrand =>
          Google::Ads::GoogleAds::V11::Resources::ProductBrand->new({})}));

  # Issue a mutate request to create everything and print its information.
  my $response = $api_client->GoogleAdsService()->mutate({
    customerId       => $customer_id,
    mutateOperations => $mutate_operations
  });

  print_response_details($mutate_operations, $response);

  return 1;
}

# Fetches all of the asset group listing group filters in an asset group.
sub get_all_existing_listing_group_filter_assets_in_asset_group {
  my ($api_client, $customer_id, $asset_group_resource_name) = @_;

  # Create a query that retrieves asset group listing group filters.
  # The limit to the number of listing group filters permitted in a Performance
  # Max campaign can be found here:
  # https://developers.google.com/google-ads/api/docs/best-practices/system-limits.
  my $query =
    sprintf "SELECT asset_group_listing_group_filter.resource_name, " .
    "asset_group_listing_group_filter.parent_listing_group_filter " .
    "FROM asset_group_listing_group_filter " .
    "WHERE asset_group_listing_group_filter.asset_group = '%s'",
    $asset_group_resource_name;

  # Issue a search request by specifying page size.
  my $response = $api_client->GoogleAdsService()->search({
    customerId => $customer_id,
    query      => $query,
    pageSize   => PAGE_SIZE
  });

  my $asset_group_listing_group_filters = [];
  # Iterate over all rows in all pages to get an asset group listing group filter.
  foreach my $google_ads_row (@{$response->{results}}) {
    push @$asset_group_listing_group_filters,
      $google_ads_row->{assetGroupListingGroupFilter};
  }

  return $asset_group_listing_group_filters;
}

# Creates mutate operations for removing an existing tree of asset group listing
# group filters.
#
# Asset group listing group filters must be removed in a specific order: all of
# the children of a filter must be removed before the filter itself, otherwise
# the API will return an error.
sub create_mutate_operations_for_removing_listing_group_filters_tree {
  my ($asset_group_listing_group_filters) = @_;
  if (scalar @$asset_group_listing_group_filters == 0) {
    die "No listing group filters to remove.";
  }

  my $resource_names_to_listing_group_filters = {};
  my $parents_to_children                     = {};
  my $root_resource_name                      = undef;
  foreach
    my $asset_group_listing_group_filter (@$asset_group_listing_group_filters)
  {
    $resource_names_to_listing_group_filters->
      {$asset_group_listing_group_filter->{resourceName}} =
      $asset_group_listing_group_filter;
    # When the node has no parent, it means it's the root node, which is treated
    # differently.
    if (!defined $asset_group_listing_group_filter->{parentListingGroupFilter})
    {
      if (defined $root_resource_name) {
        die "More than one root node found.";
      }
      $root_resource_name = $asset_group_listing_group_filter->{resourceName};
      next;
    }

    my $parent_resource_name =
      $asset_group_listing_group_filter->{parentListingGroupFilter};
    my $siblings = [];

    # Check to see if we've already visited a sibling in this group and fetch it.
    if (exists $parents_to_children->{$parent_resource_name}) {
      $siblings = $parents_to_children->{$parent_resource_name};
    }
    push @$siblings, $asset_group_listing_group_filter->{resourceName};
    $parents_to_children->{$parent_resource_name} = $siblings;
  }

  return create_mutate_operations_for_removing_descendents($root_resource_name,
    $parents_to_children);
}

# Creates a list of mutate operations that remove all the descendents of the
# specified asset group listing group filter's resource name. The order of removal
# is post-order, where all the children (and their children, recursively) are
# removed first. Then, the node itself is removed.
sub create_mutate_operations_for_removing_descendents {
  my ($asset_group_listing_group_filter_resource_name, $parents_to_children) =
    @_;

  my $operations = [];
  if (
    exists $parents_to_children->
    {$asset_group_listing_group_filter_resource_name})
  {
    foreach my $child (
      @{$parents_to_children->{$asset_group_listing_group_filter_resource_name}}
      )
    {
      push @$operations,
        @{
        create_mutate_operations_for_removing_descendents($child,
          $parents_to_children)};
    }
  }

  push @$operations,
    Google::Ads::GoogleAds::V11::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupListingGroupFilterOperation =>
        Google::Ads::GoogleAds::V11::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation
        ->new({
          remove => $asset_group_listing_group_filter_resource_name
        })});

  return $operations;
}

# Creates a mutate operation that creates a root asset group listing group filter
# for the factory's asset group.
#
# The root node or partition is the default, which is displayed as "All Products".
sub create_mutate_operation_for_root {
  my ($customer_id, $asset_group_id, $root_listing_group_id) = @_;

  my $asset_group_listing_group_filter =
    Google::Ads::GoogleAds::V11::Resources::AssetGroupListingGroupFilter->new({
      resourceName =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group_listing_group_filter(
        $customer_id, $asset_group_id, $root_listing_group_id
        ),
      assetGroup =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group(
        $customer_id, $asset_group_id
        ),
      # Since this is the root node, do not set the 'parentListingGroupFilter' field.
      # For all other nodes, this would refer to the parent listing group filter
      # resource name.

      # Unlike add_performance_max_retail_campaign, the type for the root node
      # here must be SUBDIVISION because we add child partitions under it.
      type => SUBDIVISION,
      # Because this is a Performance Max campaign for retail, we need to specify
      # that this is in the shopping vertical.
      vertical => SHOPPING
    });

  return
    Google::Ads::GoogleAds::V11::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupListingGroupFilterOperation =>
        Google::Ads::GoogleAds::V11::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation
        ->new({
          create => $asset_group_listing_group_filter
        })});
}

# Creates a mutate operation that creates a intermediate asset group listing group filter.
sub create_mutate_operation_for_subdivision {
  my ($customer_id, $asset_group_id, $asset_group_listing_group_filter_id,
    $parent_id, $listing_group_filter_dimension)
    = @_;

  my $asset_group_listing_group_filter =
    Google::Ads::GoogleAds::V11::Resources::AssetGroupListingGroupFilter->new({
      resourceName =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group_listing_group_filter(
        $customer_id, $asset_group_id, $asset_group_listing_group_filter_id
        ),
      assetGroup =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group(
        $customer_id, $asset_group_id
        ),
      # Set the type as a SUBDIVISION, which will allow the node to be the parent
      # of another sub-tree.
      type => SUBDIVISION,
      # Because this is a Performance Max campaign for retail, we need to specify
      # that this is in the shopping vertical.
      vertical                 => SHOPPING,
      parentListingGroupFilter =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group_listing_group_filter(
        $customer_id, $asset_group_id, $parent_id
        ),
      # Case values contain the listing dimension used for the node.
      caseValue => $listing_group_filter_dimension
    });

  return
    Google::Ads::GoogleAds::V11::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupListingGroupFilterOperation =>
        Google::Ads::GoogleAds::V11::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation
        ->new({
          create => $asset_group_listing_group_filter
        })});
}

# Creates a mutate operation that creates a child asset group listing group filter
# (unit node).
#
# Use this method if the filter won't have child filters. Otherwise, use
# create_mutate_operation_for_subdivision().
sub create_mutate_operation_for_unit {
  my ($customer_id, $asset_group_id, $asset_group_listing_group_filter_id,
    $parent_id, $listing_group_filter_dimension)
    = @_;

  my $asset_group_listing_group_filter =
    Google::Ads::GoogleAds::V11::Resources::AssetGroupListingGroupFilter->new({
      resourceName =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group_listing_group_filter(
        $customer_id, $asset_group_id, $asset_group_listing_group_filter_id
        ),
      assetGroup =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group(
        $customer_id, $asset_group_id
        ),
      parentListingGroupFilter =>
        Google::Ads::GoogleAds::V11::Utils::ResourceNames::asset_group_listing_group_filter(
        $customer_id, $asset_group_id, $parent_id
        ),
      # Set the type as a UNIT_INCLUDED to indicate that this asset group listing
      # group filter won't have children.
      type => UNIT_INCLUDED,
      # Because this is a Performance Max campaign for retail, we need to specify
      # that this is in the shopping vertical.
      vertical  => SHOPPING,
      caseValue => $listing_group_filter_dimension
    });

  return
    Google::Ads::GoogleAds::V11::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupListingGroupFilterOperation =>
        Google::Ads::GoogleAds::V11::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation
        ->new({
          create => $asset_group_listing_group_filter
        })});
}

# Prints the details of a mutate google ads response. Parses the "response" oneof
# field name and uses it to extract the new entity's name and resource name.
sub print_response_details {
  my ($mutate_operations, $mutate_google_ads_response) = @_;

  while (my ($i, $operation_response) =
    each @{$mutate_google_ads_response->{mutateOperationResponses}})
  {
    if (!exists $operation_response->{assetGroupListingGroupFilterResult}) {
      # Trim the substring "Result" from the end of the entity name.
      my $result_type = [keys %$operation_response]->[0];
      printf "Unsupported entity type: %s.\n", $result_type =~ s/Result$//r;
      next;
    }

    my $operation =
      $mutate_operations->[$i]{assetGroupListingGroupFilterOperation};
    if (exists $operation->{create}) {
      printf "Created an asset group listing group filter with resource name: "
        . "'%s'.\n",
        $operation_response->{assetGroupListingGroupFilterResult}{resourceName};
    } elsif (exists $operation->{remove}) {
      printf "Removed an asset group listing group filter with resource name: "
        . "'%s'.\n",
        $operation_response->{assetGroupListingGroupFilterResult}{resourceName};
    } else {
      printf
        "Unsupported operation type: '%s'.\n",
        [keys %$operation]->[0];
    }
  }
}

# Don't run the example if the file is being included.
if (abs_path($0) ne abs_path(__FILE__)) {
  return 1;
}

# Get Google Ads Client, credentials will be read from ~/googleads.properties.
my $api_client = Google::Ads::GoogleAds::Client->new();

# By default examples are set to die on any server returned fault.
$api_client->set_die_on_faults(1);

# Parameters passed on the command line will override any parameters set in code.
GetOptions(
  "customer_id=s"           => \$customer_id,
  "asset_group_id=i"        => \$asset_group_id,
  "replace_existing_tree=s" => \$replace_existing_tree
);

# Print the help message if the parameters are not initialized in the code nor
# in the command line.
pod2usage(2) if not check_params($customer_id, $asset_group_id);

# Call the example.
add_performance_max_product_listing_group_tree(
  $api_client,     $customer_id =~ s/-//gr,
  $asset_group_id, $replace_existing_tree
);

=pod

=head1 NAME

add_performance_max_product_listing_group_tree

=head1 DESCRIPTION

This example shows how to add product partitions to a Performance Max retail campaign.

For Performance Max campaigns, product partitions are represented using the
AssetGroupListingGroupFilter resource. This resource can be combined with itself
to form a hierarchy that creates a product partition tree.

For more information about Performance Max retail campaigns, see the
add_performance_max_retail_campaign example.

=head1 SYNOPSIS

add_performance_max_product_listing_group_tree.pl [options]

    -help                       Show the help message.
    -customer_id                The Google Ads customer ID.
    -asset_group_id             The asset group ID.
    -replace_existing_tree      [optional] Whether it should replace the existing
                                listing group tree on an asset group.

=cut