diff --git a/ChangeLog b/ChangeLog index a33b0b0ef..71e92b2fd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +* 35.0.0 +- Google Ads API v26_1 release. +- Remove support for Python 3.9. +- Update `YouTubeVideoUploadService` in v26 to use `ResumableUploadSession`, + add `async_rest` optional extra for async resumable uploads, and update the + `upload_video` example. +- Update minimum version requirements for `google-api-core`, `protobuf`, + `proto-plus`, and `googleapis-common-protos`. + * 34.0.0 - Google Ads API v26 release. diff --git a/Dockerfile b/Dockerfile index 1b5eaa2f9..749ec7801 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:24.04 ARG DEBIAN_FRONTEND=noninteractive -ARG PYTHON_VERSIONS="3.14 3.13 3.12 3.11 3.10 3.9" +ARG PYTHON_VERSIONS="3.14 3.13 3.12 3.11 3.10" ENV TZ=Etc/GMT ENV PATH="$PATH:/root/.local/bin" diff --git a/README.rst b/README.rst index 98f7afc62..7253baa45 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ Build Status Requirements ------------ -* Python 3.9+ +* Python 3.10+ Installation ------------ diff --git a/examples/advanced_operations/upload_video.py b/examples/advanced_operations/upload_video.py index 86dbf017c..38d2a6b25 100644 --- a/examples/advanced_operations/upload_video.py +++ b/examples/advanced_operations/upload_video.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright 2026s Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,41 +16,28 @@ import argparse import itertools -import os import sys -import logging -from typing import Iterator, Iterable, List, MutableSequence +from typing import Iterator -import google.auth -from google.auth.credentials import Credentials -from google.auth import impersonated_credentials +from google.api_core.exceptions import GoogleAPICallError +from google.api_core.resumable_transfer import ResumableUploadSession from google.ads.googleads.client import GoogleAdsClient from google.ads.googleads.errors import GoogleAdsException -from google.ads.googleads.v24.services.services.google_ads_service import ( +from google.ads.googleads.v26.services.services.google_ads_service import ( GoogleAdsServiceClient, ) -from google.ads.googleads.v24.services.types.google_ads_service import ( +from google.ads.googleads.v26.services.types.google_ads_service import ( SearchGoogleAdsStreamResponse, - GoogleAdsRow, ) - -from google.ads.googleads.v24.services.services.you_tube_video_upload_service.client import ( +from google.ads.googleads.v26.services.services.you_tube_video_upload_service.client import ( YouTubeVideoUploadServiceClient, ) -from google.ads.googleads.v24.services.types import youtube_video_upload_service -from google.ads.googleads.v24.services.types.youtube_video_upload_service import ( +from google.ads.googleads.v26.services.types.youtube_video_upload_service import ( CreateYouTubeVideoUploadRequest, CreateYouTubeVideoUploadResponse, - UpdateYouTubeVideoUploadRequest, - UpdateYouTubeVideoUploadResponse, - RemoveYouTubeVideoUploadRequest, - RemoveYouTubeVideoUploadResponse, ) -from google.protobuf import field_mask_pb2 -from google.ads.googleads.v24.resources.types import youtube_video_upload - def main( client: GoogleAdsClient, customer_id: str, video_file_path: str @@ -68,8 +55,8 @@ def main( "YouTubeVideoUploadService" ) - create_upload_request: CreateYouTubeVideoUploadRequest = ( - youtube_video_upload_service.CreateYouTubeVideoUploadRequest() + create_upload_request: CreateYouTubeVideoUploadRequest = client.get_type( + "CreateYouTubeVideoUploadRequest" ) create_upload_request.customer_id = customer_id create_upload_request.you_tube_video_upload.video_title = "Test Video" @@ -80,14 +67,13 @@ def main( client.enums.YouTubeVideoPrivacyEnum.UNLISTED ) + upload_session: ResumableUploadSession = ( + yt_service.create_you_tube_video_upload(request=create_upload_request) + ) video_upload_resource_name: str with open(video_file_path, "rb") as stream: - response: CreateYouTubeVideoUploadResponse = ( - yt_service.create_you_tube_video_upload( - stream=stream, - request=create_upload_request, - retry=None, - ) + response: CreateYouTubeVideoUploadResponse = upload_session.upload( + stream ) video_upload_resource_name = response.resource_name print(f"Created YouTube video upload: {video_upload_resource_name}") @@ -108,17 +94,19 @@ def main( customer_id=customer_id, query=query ) - for row in itertools.chain.from_iterable(batch.results for batch in stream): - video = row.you_tube_video_upload + for row in itertools.chain.from_iterable( + batch.results for batch in stream + ): print( - f"Video with ID {row.you_tube_video_upload.video_id} was found in state {row.you_tube_video_upload.state}." + f"Video with ID {row.you_tube_video_upload.video_id} was found in " + f"state {row.you_tube_video_upload.state}." ) # [END upload_video_3] if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Lists all campaigns for specified customer." + description="Uploads a video to YouTube using Google Ads API." ) # The following argument(s) should be provided to run the example. parser.add_argument( @@ -140,7 +128,7 @@ def main( # GoogleAdsClient will read the google-ads.yaml configuration file in the # home directory if none is specified. googleads_client: GoogleAdsClient = GoogleAdsClient.load_from_storage( - version="v24" + version="v26" ) try: main( @@ -153,17 +141,14 @@ def main( f'Request with ID "{ex.request_id}" failed with status ' f'"{ex.error.code().name}" and includes the following errors:' ) - if hasattr(ex.error, "_response"): - raw_res = getattr(ex.error, "_response") - print(f"\n--- Full Response (Reflected from Proxy) ---") - print(f"Status Code: {raw_res.status_code}") - print(f"Headers: {raw_res.headers}") - print(f"Body: {raw_res.text}") - print(f"-------------------------------------------\n") - for error in ex.failure.errors: print(f'\tError with message "{error.message}".') if hasattr(error, "location") and 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) + except GoogleAPICallError as ex: + print(f"Resumable upload failed: {ex}") + if getattr(ex, "upload_url", None): + print(f"\tResumable upload URL: {ex.upload_url}") + sys.exit(1) diff --git a/google/ads/googleads/__init__.py b/google/ads/googleads/__init__.py index 6612925cd..242278325 100644 --- a/google/ads/googleads/__init__.py +++ b/google/ads/googleads/__init__.py @@ -19,7 +19,7 @@ import google.ads.googleads.errors import google.ads.googleads.util -VERSION = "34.0.0" +VERSION = "35.0.0" # Checks if the current runtime is Python 3.10. if sys.version_info.major == 3 and sys.version_info.minor <= 10: diff --git a/google/ads/googleads/client.py b/google/ads/googleads/client.py index 10fcf5736..24b88afa1 100644 --- a/google/ads/googleads/client.py +++ b/google/ads/googleads/client.py @@ -14,6 +14,7 @@ """A client and common configurations for the Google Ads API.""" from importlib import import_module, metadata +import inspect import logging.config from google.api_core.gapic_v1.client_info import ClientInfo @@ -26,6 +27,7 @@ from google.ads.googleads import config, oauth2, util from google.ads.googleads.interceptors import ( + metadata_interceptor, MetadataInterceptor, AsyncUnaryUnaryMetadataInterceptor, AsyncUnaryStreamMetadataInterceptor, @@ -38,7 +40,7 @@ ) from types import ModuleType -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Sequence, Tuple, Union _logger = logging.getLogger(__name__) @@ -72,6 +74,81 @@ _GRPC_CHANNEL_OPTIONS.append((unary_stream_single_threading_option, 1)) +def _augment_rest_metadata( + client: "GoogleAdsClient", + metadata_items: Sequence[Tuple[str, Union[str, bytes]]], +) -> Sequence[Tuple[str, Union[str, bytes]]]: + """Appends Google Ads headers to outgoing REST request metadata.""" + updated = list(metadata_items) if metadata_items else [] + if client.developer_token: + updated.append(("developer-token", client.developer_token)) + if client.login_customer_id: + updated.append(("login-customer-id", client.login_customer_id)) + if client.linked_customer_id: + updated.append(("linked-customer-id", client.linked_customer_id)) + + for i, (key, value) in enumerate(updated): + if key == "x-goog-api-client" and isinstance(value, str): + if client._ads_assistant: + value += f" gaada/{client._ads_assistant}" + if ( + metadata_interceptor._PROTOBUF_VERSION + and "pb" not in value + ): + value += ( + f" pb/{metadata_interceptor._PROTOBUF_VERSION}" + f"{metadata_interceptor._PB_IMPL_HEADER}" + ) + updated[i] = (key, value) + break + + return tuple(updated) + + +class _LazyRestTransport: + """Lazily initializes a synchronous REST transport on first access.""" + + def __init__(self, factory: Callable[[], Any]) -> None: + self._factory = factory + self._transport: Any = None + + def _get(self) -> Any: + if self._transport is None: + self._transport = self._factory() + return self._transport + + def __getattr__(self, name: str) -> Any: + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + return getattr(self._get(), name) + + def close(self) -> None: + if self._transport is not None: + self._transport.close() + + +class _AsyncLazyRestTransport: + """Lazily initializes an asynchronous REST transport on first access.""" + + def __init__(self, factory: Callable[[], Any]) -> None: + self._factory = factory + self._transport: Any = None + + def _get(self) -> Any: + if self._transport is None: + self._transport = self._factory() + return self._transport + + def __getattr__(self, name: str) -> Any: + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + return getattr(self._get(), name) + + async def close(self) -> None: + if self._transport is not None: + await self._transport.close() + + class _EnumGetter: """An intermediate getter for retrieving enums from service clients. @@ -426,6 +503,15 @@ def get_service( else service_client_class.DEFAULT_ENDPOINT ) + is_legacy_resumable: bool = ( + name == "YouTubeVideoUploadService" + and "developer_token" + in inspect.signature(service_client_class.__init__).parameters + ) + is_resumable: bool = ( + name == "YouTubeVideoUploadService" and not is_legacy_resumable + ) + if is_async: # In async requests, separate UnaryUnary and UnaryStream # interceptors need to be added to the channel. @@ -460,13 +546,26 @@ def get_service( interceptors=interceptors, ) + if is_resumable: + service_transport = ( + self._configure_async_resumable_upload_transport( + name=name, + services_path=services_path, + snaked=snaked, + endpoint=endpoint, + service_transport_class=service_transport_class, + channel=channel, + ) + ) + return service_client_class(transport=service_transport) + service_transport: Any = service_transport_class( channel=channel, client_info=_CLIENT_INFO ) - if name == "YouTubeVideoUploadService": - # YouTubeVideoUploadService uses REST, so we cannot pass the credentials inside the gRPC - # channel; we need to pass them explicitly. + if is_legacy_resumable: + # Legacy YouTubeVideoUploadService uses a prototype REST + # transport that accepts credentials and Ads headers directly. return service_client_class( transport=service_transport, credentials=self.credentials, @@ -501,13 +600,24 @@ def get_service( channel: grpc.Channel = grpc.intercept_channel(channel, *interceptors) + if is_resumable: + service_transport = self._configure_resumable_upload_transport( + name=name, + services_path=services_path, + snaked=snaked, + endpoint=endpoint, + service_transport_class=service_transport_class, + channel=channel, + ) + return service_client_class(transport=service_transport) + service_transport: Any = service_transport_class( channel=channel, client_info=_CLIENT_INFO ) - if name == "YouTubeVideoUploadService": - # YouTubeVideoUploadService uses REST, so we cannot pass the credentials inside the gRPC - # channel; we need to pass them explicitly. + if is_legacy_resumable: + # Legacy YouTubeVideoUploadService uses a prototype REST + # transport that accepts credentials and Ads headers directly. return service_client_class( transport=service_transport, credentials=self.credentials, @@ -518,6 +628,110 @@ def get_service( return service_client_class(transport=service_transport) + def _configure_resumable_upload_transport( + self, + name: str, + services_path: str, + snaked: str, + endpoint: str, + service_transport_class: Any, + channel: grpc.Channel, + ) -> Any: + """Configures a hybrid gRPC/REST transport for resumable uploads.""" + service_transport = service_transport_class( + channel=lambda *args, **kwargs: channel, + credentials=self.credentials, + host=endpoint, + client_info=_CLIENT_INFO, + ) + ads_client = self + + def _make_rest_transport() -> Any: + rest_mod = import_module( + f"{services_path}.{snaked}.transports.rest" + ) + base_interceptor = getattr(rest_mod, f"{name}RestInterceptor") + rest_transport_cls = getattr(rest_mod, f"{name}RestTransport") + + class _AdsRestInterceptor(base_interceptor): + def pre_create_you_tube_video_upload( + self, request: Any, metadata: Any + ) -> Any: + return request, _augment_rest_metadata( + ads_client, metadata + ) + + rest_transport = rest_transport_cls( + host=endpoint, + credentials=ads_client.credentials, + client_info=_CLIENT_INFO, + interceptor=_AdsRestInterceptor(), + ) + if ads_client.http_proxy: + rest_transport._session.proxies = { + "http": ads_client.http_proxy, + "https": ads_client.http_proxy, + } + return rest_transport + + service_transport._rest_transport = _LazyRestTransport( + _make_rest_transport + ) + return service_transport + + def _configure_async_resumable_upload_transport( + self, + name: str, + services_path: str, + snaked: str, + endpoint: str, + service_transport_class: Any, + channel: grpc.aio.Channel, + ) -> Any: + """Configures an async hybrid gRPC/REST resumable upload transport.""" + service_transport = service_transport_class( + channel=lambda *args, **kwargs: channel, + credentials=self.credentials, + host=endpoint, + client_info=_CLIENT_INFO, + ) + grpc_asyncio_mod = import_module( + f"{services_path}.{snaked}.transports.grpc_asyncio" + ) + if getattr(grpc_asyncio_mod, "HAS_ASYNC_REST", False): + ads_client = self + + def _make_async_rest_transport() -> Any: + rest_asyncio_mod = import_module( + f"{services_path}.{snaked}.transports.rest_asyncio" + ) + base_interceptor = getattr( + rest_asyncio_mod, f"Async{name}RestInterceptor" + ) + rest_transport_cls = getattr( + rest_asyncio_mod, f"Async{name}RestTransport" + ) + + class _AdsAsyncRestInterceptor(base_interceptor): + async def pre_create_you_tube_video_upload( + self, request: Any, metadata: Any + ) -> Any: + return request, _augment_rest_metadata( + ads_client, metadata + ) + + return rest_transport_cls( + host=endpoint, + credentials=ads_client.credentials, + client_info=_CLIENT_INFO, + interceptor=_AdsAsyncRestInterceptor(), + ) + + service_transport._rest_transport = _AsyncLazyRestTransport( + _make_async_rest_transport + ) + return service_transport + def get_type( self, name: str, version: str = _DEFAULT_VERSION ) -> Union[ProtoPlusMessageType, ProtobufMessageType]: diff --git a/google/ads/googleads/v26/common/__init__.py b/google/ads/googleads/v26/common/__init__.py index 69035e734..3fbc70c64 100644 --- a/google/ads/googleads/v26/common/__init__.py +++ b/google/ads/googleads/v26/common/__init__.py @@ -36,6 +36,7 @@ "google.ads.googleads.v26.types.audience_insights_attribute", "google.ads.googleads.v26.types.audiences", "google.ads.googleads.v26.types.bidding", + "google.ads.googleads.v26.types.budget_network_common", "google.ads.googleads.v26.types.campaign_goal_settings", "google.ads.googleads.v26.types.campaign_reservation_quote", "google.ads.googleads.v26.types.click_location", @@ -147,6 +148,8 @@ from .types.asset_types import DynamicLocalAsset from .types.asset_types import DynamicRealEstateAsset from .types.asset_types import DynamicTravelAsset +from .types.asset_types import ExternalPartnerId +from .types.asset_types import ExternalRedeemInfo from .types.asset_types import FacebookMessengerBusinessMessageInfo from .types.asset_types import HotelCalloutAsset from .types.asset_types import HotelPropertyAsset @@ -227,6 +230,11 @@ from .types.bidding import TargetImpressionShare from .types.bidding import TargetRoas from .types.bidding import TargetSpend +from .types.budget_network_common import BudgetCapConfig +from .types.budget_network_common import BudgetNetworkEligibility +from .types.budget_network_common import BudgetNetworkSetting +from .types.budget_network_common import BudgetNetworkSettings +from .types.budget_network_common import ChannelPriorityConfig from .types.campaign_goal_settings import CampaignGoalSettings from .types.campaign_reservation_quote import CampaignReservationQuote from .types.click_location import ClickLocation @@ -354,6 +362,7 @@ from .types.local_services import LocalServicesPhoneNumber from .types.metric_goal import MetricGoal from .types.metrics import Metrics +from .types.metrics import ReachMetricInfo from .types.metrics import SearchVolumeRange from .types.offline_user_data import CustomerMatchUserListMetadata from .types.offline_user_data import EventAttribute @@ -509,6 +518,10 @@ "BrandInfo", "BrandListInfo", "BudgetCampaignAssociationStatus", + "BudgetCapConfig", + "BudgetNetworkEligibility", + "BudgetNetworkSetting", + "BudgetNetworkSettings", "BudgetSimulationPoint", "BudgetSimulationPointList", "BusinessMessageAsset", @@ -534,6 +547,7 @@ "ChainFilter", "ChainLocationGroup", "ChainSet", + "ChannelPriorityConfig", "ClickLocation", "CombinedAudienceInfo", "Commission", @@ -589,6 +603,8 @@ "ExpandedDynamicSearchAdInfo", "ExpandedTextAdInfo", "ExtendedDemographicInfo", + "ExternalPartnerId", + "ExternalRedeemInfo", "FacebookMessengerBusinessMessageInfo", "FinalAppUrl", "FixedCpm", @@ -712,6 +728,7 @@ "PromotionBarcodeInfo", "PromotionQrCodeInfo", "ProximityInfo", + "ReachMetricInfo", "RealTimeBiddingSetting", "ResponsiveDisplayAdControlSpec", "ResponsiveDisplayAdInfo", diff --git a/google/ads/googleads/v26/common/types/__init__.py b/google/ads/googleads/v26/common/types/__init__.py index 172ea0ae5..fdda8710b 100644 --- a/google/ads/googleads/v26/common/types/__init__.py +++ b/google/ads/googleads/v26/common/types/__init__.py @@ -97,6 +97,8 @@ DynamicLocalAsset, DynamicRealEstateAsset, DynamicTravelAsset, + ExternalPartnerId, + ExternalRedeemInfo, FacebookMessengerBusinessMessageInfo, HotelCalloutAsset, HotelPropertyAsset, @@ -184,6 +186,13 @@ TargetRoas, TargetSpend, ) +from .budget_network_common import ( + BudgetCapConfig, + BudgetNetworkEligibility, + BudgetNetworkSetting, + BudgetNetworkSettings, + ChannelPriorityConfig, +) from .campaign_goal_settings import ( CampaignGoalSettings, ) @@ -348,6 +357,7 @@ ) from .metrics import ( Metrics, + ReachMetricInfo, SearchVolumeRange, ) from .offline_user_data import ( @@ -531,6 +541,8 @@ "DynamicLocalAsset", "DynamicRealEstateAsset", "DynamicTravelAsset", + "ExternalPartnerId", + "ExternalRedeemInfo", "FacebookMessengerBusinessMessageInfo", "HotelCalloutAsset", "HotelPropertyAsset", @@ -609,6 +621,11 @@ "TargetImpressionShare", "TargetRoas", "TargetSpend", + "BudgetCapConfig", + "BudgetNetworkEligibility", + "BudgetNetworkSetting", + "BudgetNetworkSettings", + "ChannelPriorityConfig", "CampaignGoalSettings", "CampaignReservationQuote", "ClickLocation", @@ -732,6 +749,7 @@ "LocalServicesPhoneNumber", "MetricGoal", "Metrics", + "ReachMetricInfo", "SearchVolumeRange", "CustomerMatchUserListMetadata", "EventAttribute", diff --git a/google/ads/googleads/v26/common/types/asset_types.py b/google/ads/googleads/v26/common/types/asset_types.py index 1fcf1f7fb..d6ad63462 100644 --- a/google/ads/googleads/v26/common/types/asset_types.py +++ b/google/ads/googleads/v26/common/types/asset_types.py @@ -43,6 +43,8 @@ ) from google.ads.googleads.v26.enums.types import mime_type as gage_mime_type from google.ads.googleads.v26.enums.types import mobile_app_vendor +from google.ads.googleads.v26.enums.types import offline_promotion_code_source +from google.ads.googleads.v26.enums.types import offline_promotion_code_type from google.ads.googleads.v26.enums.types import price_extension_price_qualifier from google.ads.googleads.v26.enums.types import price_extension_price_unit from google.ads.googleads.v26.enums.types import price_extension_type @@ -70,6 +72,8 @@ "WebhookDelivery", "BookOnGoogleAsset", "PromotionAsset", + "ExternalRedeemInfo", + "ExternalPartnerId", "PromotionBarcodeInfo", "PromotionQrCodeInfo", "CalloutAsset", @@ -649,6 +653,10 @@ class PromotionAsset(proto.Message): terms_and_conditions_uri (str): URI to the terms and conditions of the promotion. + external_redeem_info (google.ads.googleads.v26.common.types.ExternalRedeemInfo): + Immutable. Info related to external promotion + redemption. This is only set for promotions with + external redemption tracking ability. percent_off (int): Percentage off discount in the promotion. 1,000,000 = 100%. Either this or money_amount_off is required. @@ -735,6 +743,11 @@ class PromotionAsset(proto.Message): proto.STRING, number=15, ) + external_redeem_info: "ExternalRedeemInfo" = proto.Field( + proto.MESSAGE, + number=18, + message="ExternalRedeemInfo", + ) percent_off: int = proto.Field( proto.INT64, number=3, @@ -771,6 +784,84 @@ class PromotionAsset(proto.Message): ) +class ExternalRedeemInfo(proto.Message): + r"""Info related to external promotion redemption. + + Attributes: + partner_promotion_id (google.ads.googleads.v26.common.types.ExternalPartnerId): + Immutable. ID of this promotion in the partner's system that + the account is integrated with for redemption tracking. + Exactly one of ``int_value`` or ``string_value`` must be + set; which one is accepted depends on the account's + promotion integration. Required when ``code_source`` is + ``GOOGLE_GENERATED``. + code_type (google.ads.googleads.v26.enums.types.OfflinePromotionCodeTypeEnum.OfflinePromotionCodeType): + Required. Immutable. Code type of the offline + promotion. + code_source (google.ads.googleads.v26.enums.types.OfflinePromotionCodeSourceEnum.OfflinePromotionCodeSource): + Required. Immutable. Code source of the + offline promotion. + """ + + partner_promotion_id: "ExternalPartnerId" = proto.Field( + proto.MESSAGE, + number=1, + message="ExternalPartnerId", + ) + code_type: ( + offline_promotion_code_type.OfflinePromotionCodeTypeEnum.OfflinePromotionCodeType + ) = proto.Field( + proto.ENUM, + number=2, + enum=offline_promotion_code_type.OfflinePromotionCodeTypeEnum.OfflinePromotionCodeType, + ) + code_source: ( + offline_promotion_code_source.OfflinePromotionCodeSourceEnum.OfflinePromotionCodeSource + ) = proto.Field( + proto.ENUM, + number=3, + enum=offline_promotion_code_source.OfflinePromotionCodeSourceEnum.OfflinePromotionCodeSource, + ) + + +class ExternalPartnerId(proto.Message): + r"""The ID of an entity in a partner's system. Partners key their + records by different primitive types, so the ID is carried in a + oneof rather than in a per-partner message. + + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + int_value (int): + The ID when the partner's system keys the + entity by an integer. + + This field is a member of `oneof`_ ``value``. + string_value (str): + The ID when the partner's system keys the + entity by a string. The length of this string + should be between 1 and 50, inclusive. + + This field is a member of `oneof`_ ``value``. + """ + + int_value: int = proto.Field( + proto.INT64, + number=1, + oneof="value", + ) + string_value: str = proto.Field( + proto.STRING, + number=2, + oneof="value", + ) + + class PromotionBarcodeInfo(proto.Message): r"""Barcode info to generate promotion barcode. diff --git a/google/ads/googleads/v26/common/types/budget_network_common.py b/google/ads/googleads/v26/common/types/budget_network_common.py new file mode 100644 index 000000000..994dbdbc0 --- /dev/null +++ b/google/ads/googleads/v26/common/types/budget_network_common.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + +from typing import MutableSequence + +import proto # type: ignore + +from google.ads.googleads.v26.enums.types import ( + advertising_channel_type as gage_advertising_channel_type, +) +from google.ads.googleads.v26.enums.types import ( + budget_network_ineligible_reason, +) +from google.ads.googleads.v26.enums.types import channel_priority_tier +from google.ads.googleads.v26.enums.types import network_channel_type + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.common", + marshal="google.ads.googleads.v26", + manifest={ + "BudgetNetworkSettings", + "BudgetNetworkSetting", + "BudgetCapConfig", + "ChannelPriorityConfig", + "BudgetNetworkEligibility", + }, +) + + +class BudgetNetworkSettings(proto.Message): + r"""Container for budget network settings (waterfall channel + prioritization and budget allocation). + + Attributes: + settings (MutableSequence[google.ads.googleads.v26.common.types.BudgetNetworkSetting]): + List of network allocation settings per + advertising channel type. Each advertising + channel type can appear at most once in this + list. + """ + + settings: MutableSequence["BudgetNetworkSetting"] = proto.RepeatedField( + proto.MESSAGE, + number=1, + message="BudgetNetworkSetting", + ) + + +class BudgetNetworkSetting(proto.Message): + r"""Budget allocation setting for a specific advertising channel type + (e.g., SEARCH, SHOPPING, PERFORMANCE_MAX). + + Attributes: + advertising_channel_type (google.ads.googleads.v26.enums.types.AdvertisingChannelTypeEnum.AdvertisingChannelType): + The advertising channel type this setting applies to (e.g., + SEARCH, SHOPPING, or PERFORMANCE_MAX). + budget_caps (MutableSequence[google.ads.googleads.v26.common.types.BudgetCapConfig]): + Budget caps restricting spend per secondary + network channel. Caps can only be applied to + channels assigned to secondary priority tiers, + and each network channel can appear at most once + in this list. + channel_priorities (MutableSequence[google.ads.googleads.v26.common.types.ChannelPriorityConfig]): + Waterfall priority tiers per network channel. + """ + + advertising_channel_type: ( + gage_advertising_channel_type.AdvertisingChannelTypeEnum.AdvertisingChannelType + ) = proto.Field( + proto.ENUM, + number=1, + enum=gage_advertising_channel_type.AdvertisingChannelTypeEnum.AdvertisingChannelType, + ) + budget_caps: MutableSequence["BudgetCapConfig"] = proto.RepeatedField( + proto.MESSAGE, + number=2, + message="BudgetCapConfig", + ) + channel_priorities: MutableSequence["ChannelPriorityConfig"] = ( + proto.RepeatedField( + proto.MESSAGE, + number=3, + message="ChannelPriorityConfig", + ) + ) + + +class BudgetCapConfig(proto.Message): + r"""Configures a maximum percentage of the total budget that a + network channel can spend. + + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + network_channel (google.ads.googleads.v26.enums.types.NetworkChannelTypeEnum.NetworkChannelType): + The network channel being capped. + cap_percent_millis (int): + The maximum percentage of the total budget + that this network channel can spend, specified + in milli-percent units (for example, 100,000 = + 100%, 50,000 = 50%, and 1,000 = 1%). Valid range + is 0 to 100,000, where 0 represents a complete + 0% opt-out. + + This field is a member of `oneof`_ ``_cap_percent_millis``. + """ + + network_channel: ( + network_channel_type.NetworkChannelTypeEnum.NetworkChannelType + ) = proto.Field( + proto.ENUM, + number=1, + enum=network_channel_type.NetworkChannelTypeEnum.NetworkChannelType, + ) + cap_percent_millis: int = proto.Field( + proto.INT64, + number=2, + optional=True, + ) + + +class ChannelPriorityConfig(proto.Message): + r"""Configures which network channels belong to a priority tier. + + Attributes: + priority_tier (google.ads.googleads.v26.enums.types.ChannelPriorityTierEnum.ChannelPriorityTier): + The priority tier (e.g., TIER1, TIER2). + network_channels (MutableSequence[google.ads.googleads.v26.enums.types.NetworkChannelTypeEnum.NetworkChannelType]): + The network channels assigned to this + priority tier. + """ + + priority_tier: ( + channel_priority_tier.ChannelPriorityTierEnum.ChannelPriorityTier + ) = proto.Field( + proto.ENUM, + number=1, + enum=channel_priority_tier.ChannelPriorityTierEnum.ChannelPriorityTier, + ) + network_channels: MutableSequence[ + network_channel_type.NetworkChannelTypeEnum.NetworkChannelType + ] = proto.RepeatedField( + proto.ENUM, + number=2, + enum=network_channel_type.NetworkChannelTypeEnum.NetworkChannelType, + ) + + +class BudgetNetworkEligibility(proto.Message): + r"""Read-only eligibility status and reasons for budget network + allocation on a CampaignBudget. + + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + eligible (bool): + Output only. True if the budget is eligible + for waterfall and budget cap allocation. + + This field is a member of `oneof`_ ``_eligible``. + ineligible_reasons (MutableSequence[google.ads.googleads.v26.enums.types.BudgetNetworkIneligibleReasonEnum.BudgetNetworkIneligibleReason]): + Output only. Reasons why the budget is + ineligible, if eligible is false. + """ + + eligible: bool = proto.Field( + proto.BOOL, + number=1, + optional=True, + ) + ineligible_reasons: MutableSequence[ + budget_network_ineligible_reason.BudgetNetworkIneligibleReasonEnum.BudgetNetworkIneligibleReason + ] = proto.RepeatedField( + proto.ENUM, + number=2, + enum=budget_network_ineligible_reason.BudgetNetworkIneligibleReasonEnum.BudgetNetworkIneligibleReason, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/common/types/metrics.py b/google/ads/googleads/v26/common/types/metrics.py index 6c5584bdf..1cfa7ea15 100644 --- a/google/ads/googleads/v26/common/types/metrics.py +++ b/google/ads/googleads/v26/common/types/metrics.py @@ -21,6 +21,7 @@ from google.ads.googleads.v26.enums.types import interaction_event_type from google.ads.googleads.v26.enums.types import quality_score_bucket +from google.ads.googleads.v26.enums.types import reach_metric_state __protobuf__ = proto.module( @@ -29,6 +30,7 @@ manifest={ "Metrics", "SearchVolumeRange", + "ReachMetricInfo", }, ) @@ -1814,6 +1816,38 @@ class Metrics(proto.Message): for date ranges of 31 days or less. This field is a member of `oneof`_ ``_unique_users_five_plus``. + unique_users_six_plus (int): + This metric counts the unique individuals who + were shown your video ad six or more times + within the selected date range. This metric + cannot be aggregated, and can only be requested + for date ranges of 31 days or less. + + This field is a member of `oneof`_ ``_unique_users_six_plus``. + unique_users_seven_plus (int): + This metric counts the unique individuals who + were shown your video ad seven or more times + within the selected date range. This metric + cannot be aggregated, and can only be requested + for date ranges of 31 days or less. + + This field is a member of `oneof`_ ``_unique_users_seven_plus``. + unique_users_eight_plus (int): + This metric counts the unique individuals who + were shown your video ad eight or more times + within the selected date range. This metric + cannot be aggregated, and can only be requested + for date ranges of 31 days or less. + + This field is a member of `oneof`_ ``_unique_users_eight_plus``. + unique_users_nine_plus (int): + This metric counts the unique individuals who + were shown your video ad nine or more times + within the selected date range. This metric + cannot be aggregated, and can only be requested + for date ranges of 31 days or less. + + This field is a member of `oneof`_ ``_unique_users_nine_plus``. unique_users_ten_plus (int): This metric counts the unique individuals who were shown your video ad ten or more times @@ -2986,6 +3020,82 @@ class Metrics(proto.Message): impression. This field is a member of `oneof`_ ``_normalized_incremental_searches_per_impression_p90_upper_bound``. + unique_users_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad at + least once within the selected date range. + + This field is a member of `oneof`_ ``_unique_users_info``. + unique_users_two_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad two + or more times within the selected date range. + + This field is a member of `oneof`_ ``_unique_users_two_plus_info``. + unique_users_three_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + three or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_three_plus_info``. + unique_users_four_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + four or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_four_plus_info``. + unique_users_five_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + five or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_five_plus_info``. + unique_users_six_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad six + or more times within the selected date range. + + This field is a member of `oneof`_ ``_unique_users_six_plus_info``. + unique_users_seven_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + seven or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_seven_plus_info``. + unique_users_eight_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + eight or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_eight_plus_info``. + unique_users_nine_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad + nine or more times within the selected date + range. + + This field is a member of `oneof`_ ``_unique_users_nine_plus_info``. + unique_users_ten_plus_info (google.ads.googleads.v26.common.types.ReachMetricInfo): + Additional information (e.g. quality of reach + data) about the metric representing the number + of unique individuals who were shown your ad ten + or more times within the selected date range. + + This field is a member of `oneof`_ ``_unique_users_ten_plus_info``. """ absolute_top_impression_percentage: float = proto.Field( @@ -4070,6 +4180,26 @@ class Metrics(proto.Message): number=396, optional=True, ) + unique_users_six_plus: int = proto.Field( + proto.INT64, + number=556, + optional=True, + ) + unique_users_seven_plus: int = proto.Field( + proto.INT64, + number=557, + optional=True, + ) + unique_users_eight_plus: int = proto.Field( + proto.INT64, + number=558, + optional=True, + ) + unique_users_nine_plus: int = proto.Field( + proto.INT64, + number=559, + optional=True, + ) unique_users_ten_plus: int = proto.Field( proto.INT64, number=397, @@ -4842,6 +4972,66 @@ class Metrics(proto.Message): optional=True, ) ) + unique_users_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=550, + optional=True, + message="ReachMetricInfo", + ) + unique_users_two_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=551, + optional=True, + message="ReachMetricInfo", + ) + unique_users_three_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=552, + optional=True, + message="ReachMetricInfo", + ) + unique_users_four_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=553, + optional=True, + message="ReachMetricInfo", + ) + unique_users_five_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=554, + optional=True, + message="ReachMetricInfo", + ) + unique_users_six_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=560, + optional=True, + message="ReachMetricInfo", + ) + unique_users_seven_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=561, + optional=True, + message="ReachMetricInfo", + ) + unique_users_eight_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=562, + optional=True, + message="ReachMetricInfo", + ) + unique_users_nine_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=563, + optional=True, + message="ReachMetricInfo", + ) + unique_users_ten_plus_info: "ReachMetricInfo" = proto.Field( + proto.MESSAGE, + number=555, + optional=True, + message="ReachMetricInfo", + ) class SearchVolumeRange(proto.Message): @@ -4874,4 +5064,23 @@ class SearchVolumeRange(proto.Message): ) +class ReachMetricInfo(proto.Message): + r"""Container for storing the additional information about ad + reach. + + Attributes: + reach_metric_states (MutableSequence[google.ads.googleads.v26.enums.types.ReachMetricStateEnum.ReachMetricState]): + The state of the reach metric, providing + additional information on reach metrics. + """ + + reach_metric_states: MutableSequence[ + reach_metric_state.ReachMetricStateEnum.ReachMetricState + ] = proto.RepeatedField( + proto.ENUM, + number=1, + enum=reach_metric_state.ReachMetricStateEnum.ReachMetricState, + ) + + __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/common/types/offline_user_data.py b/google/ads/googleads/v26/common/types/offline_user_data.py index e9ca8794e..0a7ee9da0 100644 --- a/google/ads/googleads/v26/common/types/offline_user_data.py +++ b/google/ads/googleads/v26/common/types/offline_user_data.py @@ -134,6 +134,22 @@ class OfflineUserAddressInfo(proto.Message): class UserIdentifier(proto.Message): r"""User identifying information. + Each ``UserIdentifier`` object represents a single identifier using + a mutually exclusive ``oneof``. Only one identifier field + (``hashed_email``, ``hashed_phone_number``, ``mobile_id``, + ``third_party_user_id``, or ``address_info``) may be set per + ``UserIdentifier`` instance. Setting multiple fields on the same + ``UserIdentifier`` clears the previously set field. To provide + multiple identifiers for the same user (for example, both an email + address and a phone number), add separate ``UserIdentifier`` objects + to the ``user_identifiers`` list. + + Before hashing with SHA-256, normalize the raw values. For + ``hashed_email`` and address fields, trim leading and trailing + whitespace and convert all characters to lowercase. Phone numbers + must be formatted in E.164 format (for example, ``+16502531234``) + before hashing. + This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. Setting any member of the oneof automatically clears all other @@ -148,15 +164,20 @@ class UserIdentifier(proto.Message): ConversionAdjustmentUploadService. hashed_email (str): Hashed email address using SHA-256 hash - function after normalization. Accepted for - Customer Match, Store Sales, + function after normalization. Normalize the + email address by trimming leading and trailing + whitespace and converting all characters to + lowercase before hashing with SHA-256. Accepted + for Customer Match, Store Sales, ConversionUploadService, and ConversionAdjustmentUploadService. This field is a member of `oneof`_ ``identifier``. hashed_phone_number (str): - Hashed phone number using SHA-256 hash - function after normalization (E164 standard). + Hashed phone number using SHA-256 hash function after + normalization (E164 standard). Normalize the phone number by + trimming whitespace and formatting in E.164 format (for + example, ``+16502531234``) before hashing with SHA-256. Accepted for Customer Match, Store Sales, ConversionUploadService, and ConversionAdjustmentUploadService. diff --git a/google/ads/googleads/v26/common/types/synthetic_content_info.py b/google/ads/googleads/v26/common/types/synthetic_content_info.py index 219bf8f67..a0d1ac002 100644 --- a/google/ads/googleads/v26/common/types/synthetic_content_info.py +++ b/google/ads/googleads/v26/common/types/synthetic_content_info.py @@ -40,8 +40,9 @@ class SyntheticContentAttestation(proto.Message): Attributes: status (google.ads.googleads.v26.enums.types.SyntheticContentAttestationStatusEnum.SyntheticContentAttestationStatus): - Indicates whether the content is considered - synthetic by this source. + Indicates the attestation status for whether + the content should be labeled as created or + edited with AI by this source. source (google.ads.googleads.v26.enums.types.SyntheticContentSourceEnum.SyntheticContentSource): The source of the synthetic content attestation. diff --git a/google/ads/googleads/v26/enums/__init__.py b/google/ads/googleads/v26/enums/__init__.py index 07606ccb9..64ee4baf3 100644 --- a/google/ads/googleads/v26/enums/__init__.py +++ b/google/ads/googleads/v26/enums/__init__.py @@ -110,6 +110,7 @@ "google.ads.googleads.v26.types.brand_state", "google.ads.googleads.v26.types.budget_campaign_association_status", "google.ads.googleads.v26.types.budget_delivery_method", + "google.ads.googleads.v26.types.budget_network_ineligible_reason", "google.ads.googleads.v26.types.budget_period", "google.ads.googleads.v26.types.budget_status", "google.ads.googleads.v26.types.budget_type", @@ -134,6 +135,7 @@ "google.ads.googleads.v26.types.change_event_resource_type", "google.ads.googleads.v26.types.change_status_operation", "google.ads.googleads.v26.types.change_status_resource_type", + "google.ads.googleads.v26.types.channel_priority_tier", "google.ads.googleads.v26.types.click_type", "google.ads.googleads.v26.types.combined_audience_status", "google.ads.googleads.v26.types.consent_status", @@ -291,10 +293,13 @@ "google.ads.googleads.v26.types.multi_party_auth_review_status", "google.ads.googleads.v26.types.multi_party_auth_review_target_resource", "google.ads.googleads.v26.types.negative_geo_target_type", + "google.ads.googleads.v26.types.network_channel_type", "google.ads.googleads.v26.types.non_skippable_max_duration", "google.ads.googleads.v26.types.non_skippable_min_duration", "google.ads.googleads.v26.types.offline_conversion_diagnostic_status_enum", "google.ads.googleads.v26.types.offline_event_upload_client_enum", + "google.ads.googleads.v26.types.offline_promotion_code_source", + "google.ads.googleads.v26.types.offline_promotion_code_type", "google.ads.googleads.v26.types.offline_user_data_job_failure_reason", "google.ads.googleads.v26.types.offline_user_data_job_match_rate_range", "google.ads.googleads.v26.types.offline_user_data_job_status", @@ -334,6 +339,7 @@ "google.ads.googleads.v26.types.promotion_extension_occasion", "google.ads.googleads.v26.types.proximity_radius_units", "google.ads.googleads.v26.types.quality_score_bucket", + "google.ads.googleads.v26.types.reach_metric_state", "google.ads.googleads.v26.types.reach_plan_age_range", "google.ads.googleads.v26.types.reach_plan_buying_method", "google.ads.googleads.v26.types.reach_plan_conversion_rate_model", @@ -556,6 +562,9 @@ BudgetCampaignAssociationStatusEnum, ) from .types.budget_delivery_method import BudgetDeliveryMethodEnum +from .types.budget_network_ineligible_reason import ( + BudgetNetworkIneligibleReasonEnum, +) from .types.budget_period import BudgetPeriodEnum from .types.budget_status import BudgetStatusEnum from .types.budget_type import BudgetTypeEnum @@ -588,6 +597,7 @@ from .types.change_event_resource_type import ChangeEventResourceTypeEnum from .types.change_status_operation import ChangeStatusOperationEnum from .types.change_status_resource_type import ChangeStatusResourceTypeEnum +from .types.channel_priority_tier import ChannelPriorityTierEnum from .types.click_type import ClickTypeEnum from .types.combined_audience_status import CombinedAudienceStatusEnum from .types.consent_status import ConsentStatusEnum @@ -847,12 +857,15 @@ MultiPartyAuthReviewTargetResourceEnum, ) from .types.negative_geo_target_type import NegativeGeoTargetTypeEnum +from .types.network_channel_type import NetworkChannelTypeEnum from .types.non_skippable_max_duration import NonSkippableMaxDurationEnum from .types.non_skippable_min_duration import NonSkippableMinDurationEnum from .types.offline_conversion_diagnostic_status_enum import ( OfflineConversionDiagnosticStatusEnum, ) from .types.offline_event_upload_client_enum import OfflineEventUploadClientEnum +from .types.offline_promotion_code_source import OfflinePromotionCodeSourceEnum +from .types.offline_promotion_code_type import OfflinePromotionCodeTypeEnum from .types.offline_user_data_job_failure_reason import ( OfflineUserDataJobFailureReasonEnum, ) @@ -916,6 +929,7 @@ from .types.promotion_extension_occasion import PromotionExtensionOccasionEnum from .types.proximity_radius_units import ProximityRadiusUnitsEnum from .types.quality_score_bucket import QualityScoreBucketEnum +from .types.reach_metric_state import ReachMetricStateEnum from .types.reach_plan_age_range import ReachPlanAgeRangeEnum from .types.reach_plan_buying_method import ReachPlanBuyingMethodEnum from .types.reach_plan_conversion_rate_model import ( @@ -1165,6 +1179,7 @@ "BrandStateEnum", "BudgetCampaignAssociationStatusEnum", "BudgetDeliveryMethodEnum", + "BudgetNetworkIneligibleReasonEnum", "BudgetPeriodEnum", "BudgetStatusEnum", "BudgetTypeEnum", @@ -1189,6 +1204,7 @@ "ChangeEventResourceTypeEnum", "ChangeStatusOperationEnum", "ChangeStatusResourceTypeEnum", + "ChannelPriorityTierEnum", "ClickTypeEnum", "CombinedAudienceStatusEnum", "ConsentStatusEnum", @@ -1346,10 +1362,13 @@ "MultiPartyAuthReviewStatusEnum", "MultiPartyAuthReviewTargetResourceEnum", "NegativeGeoTargetTypeEnum", + "NetworkChannelTypeEnum", "NonSkippableMaxDurationEnum", "NonSkippableMinDurationEnum", "OfflineConversionDiagnosticStatusEnum", "OfflineEventUploadClientEnum", + "OfflinePromotionCodeSourceEnum", + "OfflinePromotionCodeTypeEnum", "OfflineUserDataJobFailureReasonEnum", "OfflineUserDataJobMatchRateRangeEnum", "OfflineUserDataJobStatusEnum", @@ -1389,6 +1408,7 @@ "PromotionExtensionOccasionEnum", "ProximityRadiusUnitsEnum", "QualityScoreBucketEnum", + "ReachMetricStateEnum", "ReachPlanAgeRangeEnum", "ReachPlanBuyingMethodEnum", "ReachPlanConversionRateModelEnum", diff --git a/google/ads/googleads/v26/enums/types/__init__.py b/google/ads/googleads/v26/enums/types/__init__.py index 6a239282a..4559bb27b 100644 --- a/google/ads/googleads/v26/enums/types/__init__.py +++ b/google/ads/googleads/v26/enums/types/__init__.py @@ -265,6 +265,9 @@ from .budget_delivery_method import ( BudgetDeliveryMethodEnum, ) +from .budget_network_ineligible_reason import ( + BudgetNetworkIneligibleReasonEnum, +) from .budget_period import ( BudgetPeriodEnum, ) @@ -337,6 +340,9 @@ from .change_status_resource_type import ( ChangeStatusResourceTypeEnum, ) +from .channel_priority_tier import ( + ChannelPriorityTierEnum, +) from .click_type import ( ClickTypeEnum, ) @@ -808,6 +814,9 @@ from .negative_geo_target_type import ( NegativeGeoTargetTypeEnum, ) +from .network_channel_type import ( + NetworkChannelTypeEnum, +) from .non_skippable_max_duration import ( NonSkippableMaxDurationEnum, ) @@ -820,6 +829,12 @@ from .offline_event_upload_client_enum import ( OfflineEventUploadClientEnum, ) +from .offline_promotion_code_source import ( + OfflinePromotionCodeSourceEnum, +) +from .offline_promotion_code_type import ( + OfflinePromotionCodeTypeEnum, +) from .offline_user_data_job_failure_reason import ( OfflineUserDataJobFailureReasonEnum, ) @@ -937,6 +952,9 @@ from .quality_score_bucket import ( QualityScoreBucketEnum, ) +from .reach_metric_state import ( + ReachMetricStateEnum, +) from .reach_plan_age_range import ( ReachPlanAgeRangeEnum, ) @@ -1314,6 +1332,7 @@ "BrandStateEnum", "BudgetCampaignAssociationStatusEnum", "BudgetDeliveryMethodEnum", + "BudgetNetworkIneligibleReasonEnum", "BudgetPeriodEnum", "BudgetStatusEnum", "BudgetTypeEnum", @@ -1338,6 +1357,7 @@ "ChangeEventResourceTypeEnum", "ChangeStatusOperationEnum", "ChangeStatusResourceTypeEnum", + "ChannelPriorityTierEnum", "ClickTypeEnum", "CombinedAudienceStatusEnum", "ConsentStatusEnum", @@ -1495,10 +1515,13 @@ "MultiPartyAuthReviewStatusEnum", "MultiPartyAuthReviewTargetResourceEnum", "NegativeGeoTargetTypeEnum", + "NetworkChannelTypeEnum", "NonSkippableMaxDurationEnum", "NonSkippableMinDurationEnum", "OfflineConversionDiagnosticStatusEnum", "OfflineEventUploadClientEnum", + "OfflinePromotionCodeSourceEnum", + "OfflinePromotionCodeTypeEnum", "OfflineUserDataJobFailureReasonEnum", "OfflineUserDataJobMatchRateRangeEnum", "OfflineUserDataJobStatusEnum", @@ -1538,6 +1561,7 @@ "PromotionExtensionOccasionEnum", "ProximityRadiusUnitsEnum", "QualityScoreBucketEnum", + "ReachMetricStateEnum", "ReachPlanAgeRangeEnum", "ReachPlanBuyingMethodEnum", "ReachPlanConversionRateModelEnum", diff --git a/google/ads/googleads/v26/enums/types/ad_group_type.py b/google/ads/googleads/v26/enums/types/ad_group_type.py index 0dc390f6c..984cdd0c8 100644 --- a/google/ads/googleads/v26/enums/types/ad_group_type.py +++ b/google/ads/googleads/v26/enums/types/ad_group_type.py @@ -59,7 +59,8 @@ class AdGroupType(proto.Enum): campaigns. SHOPPING_SMART_ADS (7): The type for ad groups in Smart Shopping - campaigns. + campaigns. New Smart Shopping campaigns cannot + be created. VIDEO_BUMPER (8): Short unskippable in-stream video ads. VIDEO_TRUE_VIEW_IN_STREAM (9): diff --git a/google/ads/googleads/v26/enums/types/advertising_channel_sub_type.py b/google/ads/googleads/v26/enums/types/advertising_channel_sub_type.py index 2939ef7ba..a6c75ee6a 100644 --- a/google/ads/googleads/v26/enums/types/advertising_channel_sub_type.py +++ b/google/ads/googleads/v26/enums/types/advertising_channel_sub_type.py @@ -49,7 +49,8 @@ class AdvertisingChannelSubType(proto.Enum): DISPLAY_EXPRESS (5): AdWords Express campaigns for display. SHOPPING_SMART_ADS (6): - Smart Shopping campaigns. + Smart Shopping campaigns. New Smart Shopping + campaigns cannot be created. DISPLAY_GMAIL_AD (7): Gmail Ad campaigns. DISPLAY_SMART_CAMPAIGN (8): diff --git a/google/ads/googleads/v26/enums/types/budget_network_ineligible_reason.py b/google/ads/googleads/v26/enums/types/budget_network_ineligible_reason.py new file mode 100644 index 000000000..a37f5da5d --- /dev/null +++ b/google/ads/googleads/v26/enums/types/budget_network_ineligible_reason.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "BudgetNetworkIneligibleReasonEnum", + }, +) + + +class BudgetNetworkIneligibleReasonEnum(proto.Message): + r"""Container for enum describing reasons why a campaign budget + is ineligible for budget network settings. + + """ + + class BudgetNetworkIneligibleReason(proto.Enum): + r"""Reasons why a budget is ineligible for waterfall or budget + cap allocation. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Used for return value only. Represents value + unknown in this version. + UNSUPPORTED_BIDDING_STRATEGY (2): + Associated campaign(s) use an unsupported + bidding strategy. + MIXED_CAMPAIGN_TYPES (3): + Shared budget is associated with campaigns of + mixed advertising channel types. + HAS_ENABLED_TRIALS (4): + Associated campaign(s) have active + experiments/trials. + UNSUPPORTED_BUDGET_PERIOD (5): + Budget uses a custom period. + MIXED_BIDDING_STRATEGIES (6): + Shared budget is associated with campaigns of + mixed bidding strategy types. + INSUFFICIENT_ELIGIBLE_CHANNELS (7): + Budget does not have all required network + channels enabled. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + UNSUPPORTED_BIDDING_STRATEGY = 2 + MIXED_CAMPAIGN_TYPES = 3 + HAS_ENABLED_TRIALS = 4 + UNSUPPORTED_BUDGET_PERIOD = 5 + MIXED_BIDDING_STRATEGIES = 6 + INSUFFICIENT_ELIGIBLE_CHANNELS = 7 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/channel_priority_tier.py b/google/ads/googleads/v26/enums/types/channel_priority_tier.py new file mode 100644 index 000000000..3406d1c07 --- /dev/null +++ b/google/ads/googleads/v26/enums/types/channel_priority_tier.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "ChannelPriorityTierEnum", + }, +) + + +class ChannelPriorityTierEnum(proto.Message): + r"""Container for enum describing the priority tier of a network + channel in waterfall prioritization. + + """ + + class ChannelPriorityTier(proto.Enum): + r"""Priority tiers for channel prioritization. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Used for return value only. Represents value + unknown in this version. + TIER1 (2): + Primary priority tier (allocated first before + lower tiers). + TIER2 (3): + Secondary priority tier (allocated remaining + budget after Tier 1). + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + TIER1 = 2 + TIER2 = 3 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/network_channel_type.py b/google/ads/googleads/v26/enums/types/network_channel_type.py new file mode 100644 index 000000000..ad363ae2a --- /dev/null +++ b/google/ads/googleads/v26/enums/types/network_channel_type.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "NetworkChannelTypeEnum", + }, +) + + +class NetworkChannelTypeEnum(proto.Message): + r"""Container for enum describing the network channel type for + budget allocation. + + """ + + class NetworkChannelType(proto.Enum): + r"""Possible network channel types for waterfall prioritization + and budget caps. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Used for return value only. Represents value + unknown in this version. + OWNED_AND_OPERATED (2): + Google Owned and Operated properties (e.g., + Search, YouTube, Discover). + SEARCH_PARTNER (3): + Search Partner Network. + GOOGLE_DISPLAY (4): + Google Display Network. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + OWNED_AND_OPERATED = 2 + SEARCH_PARTNER = 3 + GOOGLE_DISPLAY = 4 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/offline_promotion_code_source.py b/google/ads/googleads/v26/enums/types/offline_promotion_code_source.py new file mode 100644 index 000000000..606548ef4 --- /dev/null +++ b/google/ads/googleads/v26/enums/types/offline_promotion_code_source.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "OfflinePromotionCodeSourceEnum", + }, +) + + +class OfflinePromotionCodeSourceEnum(proto.Message): + r"""Container for enum describing the code source of an offline + promotion. + + """ + + class OfflinePromotionCodeSource(proto.Enum): + r"""The code source of an offline promotion. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Used for return value only. Represents value + unknown in this version. + CUSTOMER_UPLOADED (2): + The promotion code is uploaded by the + advertiser. + GOOGLE_GENERATED (3): + The promotion code is generated by Google. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + CUSTOMER_UPLOADED = 2 + GOOGLE_GENERATED = 3 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/offline_promotion_code_type.py b/google/ads/googleads/v26/enums/types/offline_promotion_code_type.py new file mode 100644 index 000000000..56aef9345 --- /dev/null +++ b/google/ads/googleads/v26/enums/types/offline_promotion_code_type.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "OfflinePromotionCodeTypeEnum", + }, +) + + +class OfflinePromotionCodeTypeEnum(proto.Message): + r"""Container for enum describing the code type of an offline + promotion. + + """ + + class OfflinePromotionCodeType(proto.Enum): + r"""The code type of an offline promotion. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Used for return value only. Represents value + unknown in this version. + SINGLE_USE_PROMOTION_CODE (2): + A promotion code that can be redeemed only + once. + MULTI_USE_PROMOTION_CODE (3): + A promotion code that can be redeemed + multiple times. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + SINGLE_USE_PROMOTION_CODE = 2 + MULTI_USE_PROMOTION_CODE = 3 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/reach_metric_state.py b/google/ads/googleads/v26/enums/types/reach_metric_state.py new file mode 100644 index 000000000..69a3099aa --- /dev/null +++ b/google/ads/googleads/v26/enums/types/reach_metric_state.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.enums", + marshal="google.ads.googleads.v26", + manifest={ + "ReachMetricStateEnum", + }, +) + + +class ReachMetricStateEnum(proto.Message): + r"""Container for enum with additional information about reach + metrics. + + """ + + class ReachMetricState(proto.Enum): + r"""Enum listing all possible reach metric states. + + Values: + UNSPECIFIED (0): + Not specified. + UNKNOWN (1): + Represents unknown value. + COMPLETE (2): + The corresponding reach metric is complete. + Meaning that there was no suppression due to + privacy, quality or any other reason. + PRIVACY_SUPPRESSION (3): + The reach metrics were suppressed due to + privacy reasons. + QUALITY_SUPPRESSION (4): + The reach metrics were suppressed due to + quality reasons. + LOW_QUALITY (5): + The reach metric is low quality. + DATE_RANGE_BEYOND_RETENTION (6): + The reach metrics are unavailable because the + selected date range is beyond the data retention + period of 3 years. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + COMPLETE = 2 + PRIVACY_SUPPRESSION = 3 + QUALITY_SUPPRESSION = 4 + LOW_QUALITY = 5 + DATE_RANGE_BEYOND_RETENTION = 6 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/recommendation_type.py b/google/ads/googleads/v26/enums/types/recommendation_type.py index 31acd416f..f29f42dbe 100644 --- a/google/ads/googleads/v26/enums/types/recommendation_type.py +++ b/google/ads/googleads/v26/enums/types/recommendation_type.py @@ -234,6 +234,8 @@ class RecommendationType(proto.Enum): RAISE_BUDGET_PERFORMANCE_TOO_LOW (63): Recommendation to raise budget when it is too low for Search campaigns. + OPTIMIZE_FOR_NEW_CUSTOMERS (64): + Recommendation to optimize for new customers. """ UNSPECIFIED = 0 @@ -298,6 +300,7 @@ class RecommendationType(proto.Enum): RAISE_TARGET_CPA_PERFORMANCE_BID_TOO_LOW = 61 LOWER_TARGET_ROAS_PERFORMANCE_BID_TOO_LOW = 62 RAISE_BUDGET_PERFORMANCE_TOO_LOW = 63 + OPTIMIZE_FOR_NEW_CUSTOMERS = 64 __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/enums/types/synthetic_content_attestation_status.py b/google/ads/googleads/v26/enums/types/synthetic_content_attestation_status.py index 6aa1a8512..ee0f09093 100644 --- a/google/ads/googleads/v26/enums/types/synthetic_content_attestation_status.py +++ b/google/ads/googleads/v26/enums/types/synthetic_content_attestation_status.py @@ -44,9 +44,11 @@ class SyntheticContentAttestationStatus(proto.Enum): Used for return values only. Represents value unknown in this version. NOT_SYNTHETIC (2): - The content is attested as not synthetic. + The content is attested as not needing to be + labeled as created or edited with AI. IS_SYNTHETIC (3): - The content is attested as synthetic. + The content is attested as needing to be + labeled as created or edited with AI. """ UNSPECIFIED = 0 diff --git a/google/ads/googleads/v26/errors/__init__.py b/google/ads/googleads/v26/errors/__init__.py index aa1fef71e..b31a4de10 100644 --- a/google/ads/googleads/v26/errors/__init__.py +++ b/google/ads/googleads/v26/errors/__init__.py @@ -63,6 +63,7 @@ "google.ads.googleads.v26.types.bidding_strategy_error", "google.ads.googleads.v26.types.billing_setup_error", "google.ads.googleads.v26.types.brand_guidelines_migration_error", + "google.ads.googleads.v26.types.budget_network_setting_error", "google.ads.googleads.v26.types.business_profile_location_sync_error", "google.ads.googleads.v26.types.campaign_budget_error", "google.ads.googleads.v26.types.campaign_conversion_goal_error", @@ -106,6 +107,7 @@ "google.ads.googleads.v26.types.database_error", "google.ads.googleads.v26.types.date_error", "google.ads.googleads.v26.types.date_range_error", + "google.ads.googleads.v26.types.demand_gen_campaign_error", "google.ads.googleads.v26.types.distinct_error", "google.ads.googleads.v26.types.enum_error", "google.ads.googleads.v26.types.errors", @@ -243,6 +245,7 @@ from .types.brand_guidelines_migration_error import ( BrandGuidelinesMigrationErrorEnum, ) +from .types.budget_network_setting_error import BudgetNetworkSettingErrorEnum from .types.business_profile_location_sync_error import ( BusinessProfileLocationSyncErrorEnum, ) @@ -302,6 +305,7 @@ from .types.database_error import DatabaseErrorEnum from .types.date_error import DateErrorEnum from .types.date_range_error import DateRangeErrorEnum +from .types.demand_gen_campaign_error import DemandGenCampaignErrorEnum from .types.distinct_error import DistinctErrorEnum from .types.enum_error import EnumErrorEnum from .types.errors import BudgetPerDayMinimumErrorDetails @@ -461,6 +465,7 @@ "BiddingStrategyErrorEnum", "BillingSetupErrorEnum", "BrandGuidelinesMigrationErrorEnum", + "BudgetNetworkSettingErrorEnum", "BudgetPerDayMinimumErrorDetails", "BusinessProfileLocationSyncErrorEnum", "CampaignBudgetErrorEnum", @@ -505,6 +510,7 @@ "DatabaseErrorEnum", "DateErrorEnum", "DateRangeErrorEnum", + "DemandGenCampaignErrorEnum", "DistinctErrorEnum", "EnumErrorEnum", "ErrorCode", diff --git a/google/ads/googleads/v26/errors/types/__init__.py b/google/ads/googleads/v26/errors/types/__init__.py index 00279c1f7..d97f67ba0 100644 --- a/google/ads/googleads/v26/errors/types/__init__.py +++ b/google/ads/googleads/v26/errors/types/__init__.py @@ -124,6 +124,9 @@ from .brand_guidelines_migration_error import ( BrandGuidelinesMigrationErrorEnum, ) +from .budget_network_setting_error import ( + BudgetNetworkSettingErrorEnum, +) from .business_profile_location_sync_error import ( BusinessProfileLocationSyncErrorEnum, ) @@ -253,6 +256,9 @@ from .date_range_error import ( DateRangeErrorEnum, ) +from .demand_gen_campaign_error import ( + DemandGenCampaignErrorEnum, +) from .distinct_error import ( DistinctErrorEnum, ) @@ -570,6 +576,7 @@ "BiddingStrategyErrorEnum", "BillingSetupErrorEnum", "BrandGuidelinesMigrationErrorEnum", + "BudgetNetworkSettingErrorEnum", "BusinessProfileLocationSyncErrorEnum", "CampaignBudgetErrorEnum", "CampaignConversionGoalErrorEnum", @@ -613,6 +620,7 @@ "DatabaseErrorEnum", "DateErrorEnum", "DateRangeErrorEnum", + "DemandGenCampaignErrorEnum", "DistinctErrorEnum", "EnumErrorEnum", "BudgetPerDayMinimumErrorDetails", diff --git a/google/ads/googleads/v26/errors/types/asset_error.py b/google/ads/googleads/v26/errors/types/asset_error.py index 5741b6316..4a217d9f9 100644 --- a/google/ads/googleads/v26/errors/types/asset_error.py +++ b/google/ads/googleads/v26/errors/types/asset_error.py @@ -159,6 +159,10 @@ class AssetError(proto.Enum): CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS (46): The customer is not in the allow-list for Business message asset type. + PARTNER_PROMOTION_ID_TYPE_INCOMPATIBLE (47): + The partner promotion ID is set as the oneof variant + (int_value or string_value) that this account's promotion + integration does not accept. """ UNSPECIFIED = 0 @@ -207,6 +211,7 @@ class AssetError(proto.Enum): PROMOTION_QR_CODE_CANNOT_CONTAIN_LINKS = 44 PROMOTION_QR_CODE_INVALID_FORMAT = 45 CUSTOMER_NOT_ON_ALLOWLIST_FOR_MESSAGE_ASSETS = 46 + PARTNER_PROMOTION_ID_TYPE_INCOMPATIBLE = 47 __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/errors/types/budget_network_setting_error.py b/google/ads/googleads/v26/errors/types/budget_network_setting_error.py new file mode 100644 index 000000000..2f81f127c --- /dev/null +++ b/google/ads/googleads/v26/errors/types/budget_network_setting_error.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.errors", + marshal="google.ads.googleads.v26", + manifest={ + "BudgetNetworkSettingErrorEnum", + }, +) + + +class BudgetNetworkSettingErrorEnum(proto.Message): + r"""Container for enum describing possible budget network setting + errors. + + """ + + class BudgetNetworkSettingError(proto.Enum): + r"""Enum describing possible budget network setting errors. + + Values: + UNSPECIFIED (0): + Enum unspecified. + UNKNOWN (1): + The received error code is not known in this + version. + DUPLICATE_CHANNEL_ACROSS_PRIORITY_TIERS (2): + A network channel cannot appear in more than + one priority tier. + CAP_NOT_ALLOWED_FOR_PRIMARY_TIER (3): + Budget caps are not allowed on channels in + the primary priority tier (TIER1). + MISSING_PRIMARY_PRIORITY_CHANNEL (4): + A primary priority tier (TIER1) with at least + one network channel is required. + INVALID_PRIORITY_TIER (5): + The specified priority tier is invalid or + unsupported. + UNSUPPORTED_NETWORK_CHANNEL (6): + The specified network channel is not + supported for the given advertising channel + type. + UNSUPPORTED_ADVERTISING_CHANNEL_TYPE (7): + The specified advertising channel type is not + supported for budget network settings. + CANNOT_BE_SET_ON_FLIGHTED_BUDGET (8): + Budget network settings cannot be set on + custom-period (flighted) budgets. + INCOMPLETE_NETWORK_CHANNELS (9): + All supported network channels for the + advertising channel type must be assigned to a + priority tier. + CUSTOMER_NOT_ALLOWLISTED (10): + The customer is not allowlisted for budget + network settings. + BUDGET_CAPS_UNSUPPORTED_FOR_ADVERTISING_CHANNEL_TYPE (11): + Budget caps are not supported for the + specified advertising channel type. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + DUPLICATE_CHANNEL_ACROSS_PRIORITY_TIERS = 2 + CAP_NOT_ALLOWED_FOR_PRIMARY_TIER = 3 + MISSING_PRIMARY_PRIORITY_CHANNEL = 4 + INVALID_PRIORITY_TIER = 5 + UNSUPPORTED_NETWORK_CHANNEL = 6 + UNSUPPORTED_ADVERTISING_CHANNEL_TYPE = 7 + CANNOT_BE_SET_ON_FLIGHTED_BUDGET = 8 + INCOMPLETE_NETWORK_CHANNELS = 9 + CUSTOMER_NOT_ALLOWLISTED = 10 + BUDGET_CAPS_UNSUPPORTED_FOR_ADVERTISING_CHANNEL_TYPE = 11 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/errors/types/conversion_upload_error.py b/google/ads/googleads/v26/errors/types/conversion_upload_error.py index ff80c697c..feeb874da 100644 --- a/google/ads/googleads/v26/errors/types/conversion_upload_error.py +++ b/google/ads/googleads/v26/errors/types/conversion_upload_error.py @@ -29,14 +29,27 @@ class ConversionUploadErrorEnum(proto.Message): - r"""Container for enum describing possible conversion upload - errors. + r"""Container for enum describing possible conversion upload errors. + + For help diagnosing and resolving these errors, see the `Conversion + upload troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). """ class ConversionUploadError(proto.Enum): r"""Enum describing possible conversion upload errors. + For step-by-step resolution guidance, see the `Conversion upload + troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). + Values: UNSPECIFIED (0): Enum unspecified. diff --git a/google/ads/googleads/v26/errors/types/demand_gen_campaign_error.py b/google/ads/googleads/v26/errors/types/demand_gen_campaign_error.py new file mode 100644 index 000000000..f487ef5d6 --- /dev/null +++ b/google/ads/googleads/v26/errors/types/demand_gen_campaign_error.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.errors", + marshal="google.ads.googleads.v26", + manifest={ + "DemandGenCampaignErrorEnum", + }, +) + + +class DemandGenCampaignErrorEnum(proto.Message): + r"""Container for enum describing possible Demand Gen campaign + errors. + + """ + + class DemandGenCampaignError(proto.Enum): + r"""Enum describing possible Demand Gen campaign errors. + + Values: + UNSPECIFIED (0): + Enum unspecified. + UNKNOWN (1): + The received error code is not known in this + version. + CANNOT_ACTIVATE_MIGRATED_CAMPAIGN (2): + Cannot activate a migrated campaign before + the original campaign is removed. In addition, + only one migrated campaign for each original + campaign can be activated at a time. + """ + + UNSPECIFIED = 0 + UNKNOWN = 1 + CANNOT_ACTIVATE_MIGRATED_CAMPAIGN = 2 + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/errors/types/errors.py b/google/ads/googleads/v26/errors/types/errors.py index 7e95bbc11..d80c4b989 100644 --- a/google/ads/googleads/v26/errors/types/errors.py +++ b/google/ads/googleads/v26/errors/types/errors.py @@ -133,6 +133,9 @@ from google.ads.googleads.v26.errors.types import ( brand_guidelines_migration_error as gage_brand_guidelines_migration_error, ) +from google.ads.googleads.v26.errors.types import ( + budget_network_setting_error as gage_budget_network_setting_error, +) from google.ads.googleads.v26.errors.types import ( business_profile_location_sync_error as gage_business_profile_location_sync_error, ) @@ -260,6 +263,9 @@ from google.ads.googleads.v26.errors.types import ( date_range_error as gage_date_range_error, ) +from google.ads.googleads.v26.errors.types import ( + demand_gen_campaign_error as gage_demand_gen_campaign_error, +) from google.ads.googleads.v26.errors.types import ( distinct_error as gage_distinct_error, ) @@ -1340,6 +1346,16 @@ class ErrorCode(proto.Message): The reasons for the multi party auth review error + This field is a member of `oneof`_ ``error_code``. + budget_network_setting_error (google.ads.googleads.v26.errors.types.BudgetNetworkSettingErrorEnum.BudgetNetworkSettingError): + The reasons for the budget network setting + error. + + This field is a member of `oneof`_ ``error_code``. + demand_gen_campaign_error (google.ads.googleads.v26.errors.types.DemandGenCampaignErrorEnum.DemandGenCampaignError): + The reasons for the Demand Gen campaign + error. + This field is a member of `oneof`_ ``error_code``. """ @@ -2651,6 +2667,22 @@ class ErrorCode(proto.Message): oneof="error_code", enum=gage_multi_party_auth_review_error.MultiPartyAuthReviewErrorEnum.MultiPartyAuthReviewError, ) + budget_network_setting_error: ( + gage_budget_network_setting_error.BudgetNetworkSettingErrorEnum.BudgetNetworkSettingError + ) = proto.Field( + proto.ENUM, + number=201, + oneof="error_code", + enum=gage_budget_network_setting_error.BudgetNetworkSettingErrorEnum.BudgetNetworkSettingError, + ) + demand_gen_campaign_error: ( + gage_demand_gen_campaign_error.DemandGenCampaignErrorEnum.DemandGenCampaignError + ) = proto.Field( + proto.ENUM, + number=202, + oneof="error_code", + enum=gage_demand_gen_campaign_error.DemandGenCampaignErrorEnum.DemandGenCampaignError, + ) class ErrorLocation(proto.Message): diff --git a/google/ads/googleads/v26/gapic_metadata.json b/google/ads/googleads/v26/gapic_metadata.json index c923ebe28..24f2af6c5 100644 --- a/google/ads/googleads/v26/gapic_metadata.json +++ b/google/ads/googleads/v26/gapic_metadata.json @@ -1641,6 +1641,30 @@ } } }, + "CustomerBudgetNetworkSettingService": { + "clients": { + "grpc": { + "libraryClient": "CustomerBudgetNetworkSettingServiceClient", + "rpcs": { + "MutateCustomerBudgetNetworkSetting": { + "methods": [ + "mutate_customer_budget_network_setting" + ] + } + } + }, + "grpc-async": { + "libraryClient": "CustomerBudgetNetworkSettingServiceAsyncClient", + "rpcs": { + "MutateCustomerBudgetNetworkSetting": { + "methods": [ + "mutate_customer_budget_network_setting" + ] + } + } + } + } + }, "CustomerClientLinkService": { "clients": { "grpc": { @@ -2517,6 +2541,30 @@ } } }, + "LocalServicesEligibilityService": { + "clients": { + "grpc": { + "libraryClient": "LocalServicesEligibilityServiceClient", + "rpcs": { + "CheckLocalServicesEligibility": { + "methods": [ + "check_local_services_eligibility" + ] + } + } + }, + "grpc-async": { + "libraryClient": "LocalServicesEligibilityServiceAsyncClient", + "rpcs": { + "CheckLocalServicesEligibility": { + "methods": [ + "check_local_services_eligibility" + ] + } + } + } + } + }, "LocalServicesLeadService": { "clients": { "grpc": { diff --git a/google/ads/googleads/v26/resources/__init__.py b/google/ads/googleads/v26/resources/__init__.py index 5ac052bbb..9e03f354f 100644 --- a/google/ads/googleads/v26/resources/__init__.py +++ b/google/ads/googleads/v26/resources/__init__.py @@ -113,6 +113,7 @@ "google.ads.googleads.v26.types.customer", "google.ads.googleads.v26.types.customer_asset", "google.ads.googleads.v26.types.customer_asset_set", + "google.ads.googleads.v26.types.customer_budget_network_setting", "google.ads.googleads.v26.types.customer_client", "google.ads.googleads.v26.types.customer_client_link", "google.ads.googleads.v26.types.customer_conversion_goal", @@ -340,13 +341,16 @@ from .types.customer import ConversionTrackingSetting from .types.customer import Customer from .types.customer import CustomerAgreementSetting +from .types.customer import ExternalPartnerMerchantId from .types.customer import GranularInsuranceStatus from .types.customer import GranularLicenseStatus from .types.customer import LocalServicesSettings +from .types.customer import RedemptionProviderInfo from .types.customer import RemarketingSetting from .types.customer import VideoCustomer from .types.customer_asset import CustomerAsset from .types.customer_asset_set import CustomerAssetSet +from .types.customer_budget_network_setting import CustomerBudgetNetworkSetting from .types.customer_client import CustomerClient from .types.customer_client_link import CustomerClientLink from .types.customer_conversion_goal import CustomerConversionGoal @@ -655,6 +659,7 @@ "CustomerAgreementSetting", "CustomerAsset", "CustomerAssetSet", + "CustomerBudgetNetworkSetting", "CustomerClient", "CustomerClientLink", "CustomerConversionGoal", @@ -681,6 +686,7 @@ "ExpandedLandingPageView", "Experiment", "ExperimentArm", + "ExternalPartnerMerchantId", "Fellowship", "FinalUrlExpansionAssetView", "GenderView", @@ -771,6 +777,7 @@ "QualifyingQuestion", "Recommendation", "RecommendationSubscription", + "RedemptionProviderInfo", "RemarketingAction", "RemarketingSetting", "Residency", diff --git a/google/ads/googleads/v26/resources/types/__init__.py b/google/ads/googleads/v26/resources/types/__init__.py index 5eba3fd1c..236414b3c 100644 --- a/google/ads/googleads/v26/resources/types/__init__.py +++ b/google/ads/googleads/v26/resources/types/__init__.py @@ -286,9 +286,11 @@ ConversionTrackingSetting, Customer, CustomerAgreementSetting, + ExternalPartnerMerchantId, GranularInsuranceStatus, GranularLicenseStatus, LocalServicesSettings, + RedemptionProviderInfo, RemarketingSetting, VideoCustomer, ) @@ -298,6 +300,9 @@ from .customer_asset_set import ( CustomerAssetSet, ) +from .customer_budget_network_setting import ( + CustomerBudgetNetworkSetting, +) from .customer_client import ( CustomerClient, ) @@ -764,13 +769,16 @@ "ConversionTrackingSetting", "Customer", "CustomerAgreementSetting", + "ExternalPartnerMerchantId", "GranularInsuranceStatus", "GranularLicenseStatus", "LocalServicesSettings", + "RedemptionProviderInfo", "RemarketingSetting", "VideoCustomer", "CustomerAsset", "CustomerAssetSet", + "CustomerBudgetNetworkSetting", "CustomerClient", "CustomerClientLink", "CustomerConversionGoal", diff --git a/google/ads/googleads/v26/resources/types/campaign.py b/google/ads/googleads/v26/resources/types/campaign.py index b8cf4c7af..8dfe0b46c 100644 --- a/google/ads/googleads/v26/resources/types/campaign.py +++ b/google/ads/googleads/v26/resources/types/campaign.py @@ -780,8 +780,6 @@ class ShoppingSetting(proto.Message): over those with lower priorities. This field is required for Shopping campaigns, with values between 0 and 2, inclusive. - This field is optional for Smart Shopping - campaigns, but must be equal to 3 if set. This field is a member of `oneof`_ ``_campaign_priority``. enable_local (bool): @@ -790,9 +788,9 @@ class ShoppingSetting(proto.Message): This field is a member of `oneof`_ ``_enable_local``. use_vehicle_inventory (bool): Immutable. Whether to target Vehicle Listing inventory. This - field is supported only in Smart Shopping Campaigns. For - setting Vehicle Listing inventory in Performance Max - campaigns, use ``listing_type`` instead. + field is no longer supported. Use Performance Max or + Shopping campaigns with ``listing_type`` to target Vehicle + Listing inventory. advertising_partner_ids (MutableSequence[int]): The list of Google Ads accounts IDs of advertising partners cooperating within the diff --git a/google/ads/googleads/v26/resources/types/campaign_budget.py b/google/ads/googleads/v26/resources/types/campaign_budget.py index 26341bacb..2d5802c65 100644 --- a/google/ads/googleads/v26/resources/types/campaign_budget.py +++ b/google/ads/googleads/v26/resources/types/campaign_budget.py @@ -18,6 +18,7 @@ import proto # type: ignore +from google.ads.googleads.v26.common.types import budget_network_common from google.ads.googleads.v26.enums.types import budget_delivery_method from google.ads.googleads.v26.enums.types import budget_period from google.ads.googleads.v26.enums.types import budget_status @@ -198,6 +199,15 @@ class CampaignBudget(proto.Message): with a bidding strategy, campaigns that are added to the campaign budget must also use the aligned bidding strategy. + budget_network_settings (google.ads.googleads.v26.common.types.BudgetNetworkSettings): + Waterfall channel priorities and budget caps for this + budget, at most one entry per advertising channel type. When + unset, the customer-level defaults in + ``CustomerBudgetNetworkSetting`` apply. + budget_network_eligibility (google.ads.googleads.v26.common.types.BudgetNetworkEligibility): + Output only. Whether this budget is eligible + for waterfall channel prioritization and budget + caps, and if not, why. """ resource_name: str = proto.Field( @@ -290,6 +300,20 @@ class CampaignBudget(proto.Message): proto.INT64, number=31, ) + budget_network_settings: budget_network_common.BudgetNetworkSettings = ( + proto.Field( + proto.MESSAGE, + number=32, + message=budget_network_common.BudgetNetworkSettings, + ) + ) + budget_network_eligibility: ( + budget_network_common.BudgetNetworkEligibility + ) = proto.Field( + proto.MESSAGE, + number=33, + message=budget_network_common.BudgetNetworkEligibility, + ) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/resources/types/customer.py b/google/ads/googleads/v26/resources/types/customer.py index 60e41b63f..c48e420f7 100644 --- a/google/ads/googleads/v26/resources/types/customer.py +++ b/google/ads/googleads/v26/resources/types/customer.py @@ -45,6 +45,8 @@ "RemarketingSetting", "CustomerAgreementSetting", "LocalServicesSettings", + "RedemptionProviderInfo", + "ExternalPartnerMerchantId", "GranularLicenseStatus", "GranularInsuranceStatus", "VideoCustomer", @@ -184,8 +186,7 @@ class Customer(proto.Message): Output only. Customer Agreement Setting for a customer. local_services_settings (google.ads.googleads.v26.resources.types.LocalServicesSettings): - Output only. Settings for Local Services - customer. + Settings for Local Services customer. video_brand_safety_suitability (google.ads.googleads.v26.enums.types.BrandSafetySuitabilityEnum.BrandSafetySuitability): Brand Safety setting at the account level. Allows for selecting an inventory type to show @@ -533,6 +534,9 @@ class LocalServicesSettings(proto.Message): granular_insurance_statuses (MutableSequence[google.ads.googleads.v26.resources.types.GranularInsuranceStatus]): Output only. A read-only list of geo vertical level insurance statuses. + redemption_provider_info (google.ads.googleads.v26.resources.types.RedemptionProviderInfo): + Settings for external redemption provider + integration. """ granular_license_statuses: MutableSequence["GranularLicenseStatus"] = ( @@ -549,6 +553,65 @@ class LocalServicesSettings(proto.Message): message="GranularInsuranceStatus", ) ) + redemption_provider_info: "RedemptionProviderInfo" = proto.Field( + proto.MESSAGE, + number=3, + message="RedemptionProviderInfo", + ) + + +class RedemptionProviderInfo(proto.Message): + r"""Information about the external redemption provider for a + Local Services customer. + + Attributes: + external_merchant_id (google.ads.googleads.v26.resources.types.ExternalPartnerMerchantId): + Required. Identifier for a merchant in an + external partner's point-of-sale system. + """ + + external_merchant_id: "ExternalPartnerMerchantId" = proto.Field( + proto.MESSAGE, + number=1, + message="ExternalPartnerMerchantId", + ) + + +class ExternalPartnerMerchantId(proto.Message): + r"""Identifier for a merchant in an external partner's + point-of-sale system. + + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + numeric_id (int): + Numeric merchant identifier in the external + partner's system. + + This field is a member of `oneof`_ ``id``. + alphanumeric_id (str): + Alphanumeric or string merchant identifier in + the external partner's system. The length of + this string must be between 1 and 50, inclusive. + + This field is a member of `oneof`_ ``id``. + """ + + numeric_id: int = proto.Field( + proto.INT64, + number=1, + oneof="id", + ) + alphanumeric_id: str = proto.Field( + proto.STRING, + number=2, + oneof="id", + ) class GranularLicenseStatus(proto.Message): diff --git a/google/ads/googleads/v26/resources/types/customer_budget_network_setting.py b/google/ads/googleads/v26/resources/types/customer_budget_network_setting.py new file mode 100644 index 000000000..4beeb161b --- /dev/null +++ b/google/ads/googleads/v26/resources/types/customer_budget_network_setting.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + +from google.ads.googleads.v26.common.types import budget_network_common + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.resources", + marshal="google.ads.googleads.v26", + manifest={ + "CustomerBudgetNetworkSetting", + }, +) + + +class CustomerBudgetNetworkSetting(proto.Message): + r"""Account-level default waterfall priorities and budget caps. Applies + to every campaign budget whose ``budget_network_settings`` is unset. + + Attributes: + resource_name (str): + Immutable. The resource name of the customer budget network + setting. Customer budget network setting resource names have + the form: + + ``customers/{customer_id}/customerBudgetNetworkSettings`` + budget_network_settings (google.ads.googleads.v26.common.types.BudgetNetworkSettings): + Account default settings, at most one entry + per advertising channel type. + """ + + resource_name: str = proto.Field( + proto.STRING, + number=1, + ) + budget_network_settings: budget_network_common.BudgetNetworkSettings = ( + proto.Field( + proto.MESSAGE, + number=2, + message=budget_network_common.BudgetNetworkSettings, + ) + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/resources/types/recommendation.py b/google/ads/googleads/v26/resources/types/recommendation.py index 803802868..9af1d3d04 100644 --- a/google/ads/googleads/v26/resources/types/recommendation.py +++ b/google/ads/googleads/v26/resources/types/recommendation.py @@ -420,6 +420,11 @@ class Recommendation(proto.Message): Output only. The raise budget for Performance Too Low recommendation. + This field is a member of `oneof`_ ``recommendation``. + optimize_for_new_customers_recommendation (google.ads.googleads.v26.resources.types.Recommendation.OptimizeForNewCustomersRecommendation): + Output only. The optimize for new customers + recommendation. + This field is a member of `oneof`_ ``recommendation``. """ @@ -1861,6 +1866,33 @@ class CampaignSpecificAppGoalRecommendation(proto.Message): number=11, ) + class OptimizeForNewCustomersRecommendation(proto.Message): + r"""Recommendation to optimize for new customers. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + recommended_incremental_conversion_value (float): + Output only. The recommended incremental conversion value, + in the account's local currency, to be added to conversion + values for new customers. This is a standard floating-point + value expressed in units of the account's local currency, + not in micros. + + When applying this recommendation, this value can be + supplied to ``account_default_incremental_conversion_value`` + or ``campaign_override_incremental_conversion_value`` in + [OptimizeForNewCustomersParameters][google.ads.googleads.v26.services.ApplyRecommendationOperation.OptimizeForNewCustomersParameters]. + + This field is a member of `oneof`_ ``_recommended_incremental_conversion_value``. + """ + + recommended_incremental_conversion_value: float = proto.Field( + proto.DOUBLE, + number=1, + optional=True, + ) + resource_name: str = proto.Field( proto.STRING, number=1, @@ -2347,6 +2379,14 @@ class CampaignSpecificAppGoalRecommendation(proto.Message): oneof="recommendation", message=RaiseBudgetPerformanceTooLowRecommendation, ) + optimize_for_new_customers_recommendation: ( + OptimizeForNewCustomersRecommendation + ) = proto.Field( + proto.MESSAGE, + number=75, + oneof="recommendation", + message=OptimizeForNewCustomersRecommendation, + ) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/services/__init__.py b/google/ads/googleads/v26/services/__init__.py index 876de6a1d..9bb67a6f5 100644 --- a/google/ads/googleads/v26/services/__init__.py +++ b/google/ads/googleads/v26/services/__init__.py @@ -460,6 +460,12 @@ MutateCustomerAssetSetsRequest, MutateCustomerAssetSetsResponse, ) +from .types.customer_budget_network_setting_service import ( + CustomerBudgetNetworkSettingOperation, + MutateCustomerBudgetNetworkSettingRequest, + MutateCustomerBudgetNetworkSettingResponse, + MutateCustomerBudgetNetworkSettingResult, +) from .types.customer_client_link_service import ( CustomerClientLinkOperation, MutateCustomerClientLinkRequest, @@ -675,6 +681,13 @@ MutateLabelsRequest, MutateLabelsResponse, ) +from .types.local_services_eligibility_service import ( + CheckLocalServicesEligibilityRequest, + CheckLocalServicesEligibilityResponse, + EligibleCategory, + EligibleGoal, + SuggestionReason, +) from .types.local_services_lead_service import ( AppendLeadConversationRequest, AppendLeadConversationResponse, @@ -1196,6 +1209,10 @@ "MutateCustomerAssetSetResult", "MutateCustomerAssetSetsRequest", "MutateCustomerAssetSetsResponse", + "CustomerBudgetNetworkSettingOperation", + "MutateCustomerBudgetNetworkSettingRequest", + "MutateCustomerBudgetNetworkSettingResponse", + "MutateCustomerBudgetNetworkSettingResult", "CustomerClientLinkOperation", "MutateCustomerClientLinkRequest", "MutateCustomerClientLinkResponse", @@ -1353,6 +1370,11 @@ "MutateLabelResult", "MutateLabelsRequest", "MutateLabelsResponse", + "CheckLocalServicesEligibilityRequest", + "CheckLocalServicesEligibilityResponse", + "EligibleCategory", + "EligibleGoal", + "SuggestionReason", "AppendLeadConversationRequest", "AppendLeadConversationResponse", "Conversation", diff --git a/google/ads/googleads/v26/services/services/batch_job_service/async_client.py b/google/ads/googleads/v26/services/services/batch_job_service/async_client.py index a3c659847..eb7a8c37d 100644 --- a/google/ads/googleads/v26/services/services/batch_job_service/async_client.py +++ b/google/ads/googleads/v26/services/services/batch_job_service/async_client.py @@ -323,6 +323,12 @@ class BatchJobServiceAsyncClient: parse_customer_asset_path = staticmethod( BatchJobServiceClient.parse_customer_asset_path ) + customer_budget_network_setting_path = staticmethod( + BatchJobServiceClient.customer_budget_network_setting_path + ) + parse_customer_budget_network_setting_path = staticmethod( + BatchJobServiceClient.parse_customer_budget_network_setting_path + ) customer_conversion_goal_path = staticmethod( BatchJobServiceClient.customer_conversion_goal_path ) diff --git a/google/ads/googleads/v26/services/services/batch_job_service/client.py b/google/ads/googleads/v26/services/services/batch_job_service/client.py index d8e6736a5..58cdab7b9 100644 --- a/google/ads/googleads/v26/services/services/batch_job_service/client.py +++ b/google/ads/googleads/v26/services/services/batch_job_service/client.py @@ -1173,6 +1173,24 @@ def parse_customer_asset_path(path: str) -> Dict[str, str]: ) return m.groupdict() if m else {} + @staticmethod + def customer_budget_network_setting_path( + customer_id: str, + ) -> str: + """Returns a fully-qualified customer_budget_network_setting string.""" + return "customers/{customer_id}/customerBudgetNetworkSettings".format( + customer_id=customer_id, + ) + + @staticmethod + def parse_customer_budget_network_setting_path(path: str) -> Dict[str, str]: + """Parses a customer_budget_network_setting path into its component segments.""" + m = re.match( + r"^customers/(?P.+?)/customerBudgetNetworkSettings$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def customer_conversion_goal_path( customer_id: str, diff --git a/google/ads/googleads/v26/services/services/campaign_budget_service/async_client.py b/google/ads/googleads/v26/services/services/campaign_budget_service/async_client.py index e7f27729c..9171ad1fe 100644 --- a/google/ads/googleads/v26/services/services/campaign_budget_service/async_client.py +++ b/google/ads/googleads/v26/services/services/campaign_budget_service/async_client.py @@ -336,6 +336,13 @@ async def mutate_campaign_budgets( request (Optional[Union[google.ads.googleads.v26.services.types.MutateCampaignBudgetsRequest, dict]]): The request object. Request message for [CampaignBudgetService.MutateCampaignBudgets][google.ads.googleads.v26.services.CampaignBudgetService.MutateCampaignBudgets]. + + Update operations in ``operations`` require an + ``update_mask`` specifying the fields to modify. Note + that ``explicitly_shared`` defaults to ``true`` if + unspecified. A non-shared budget can be updated to + explicitly shared, but an explicitly shared budget + cannot become non-shared. customer_id (:class:`str`): Required. The ID of the customer whose campaign budgets are being diff --git a/google/ads/googleads/v26/services/services/campaign_budget_service/client.py b/google/ads/googleads/v26/services/services/campaign_budget_service/client.py index ccc7e3408..f1017df46 100644 --- a/google/ads/googleads/v26/services/services/campaign_budget_service/client.py +++ b/google/ads/googleads/v26/services/services/campaign_budget_service/client.py @@ -653,6 +653,13 @@ def mutate_campaign_budgets( request (Union[google.ads.googleads.v26.services.types.MutateCampaignBudgetsRequest, dict]): The request object. Request message for [CampaignBudgetService.MutateCampaignBudgets][google.ads.googleads.v26.services.CampaignBudgetService.MutateCampaignBudgets]. + + Update operations in ``operations`` require an + ``update_mask`` specifying the fields to modify. Note + that ``explicitly_shared`` defaults to ``true`` if + unspecified. A non-shared budget can be updated to + explicitly shared, but an explicitly shared budget + cannot become non-shared. customer_id (str): Required. The ID of the customer whose campaign budgets are being diff --git a/google/ads/googleads/v26/services/services/conversion_upload_service/async_client.py b/google/ads/googleads/v26/services/services/conversion_upload_service/async_client.py index 13a2995dd..5087786d5 100644 --- a/google/ads/googleads/v26/services/services/conversion_upload_service/async_client.py +++ b/google/ads/googleads/v26/services/services/conversion_upload_service/async_client.py @@ -335,6 +335,27 @@ async def upload_click_conversions( request (Optional[Union[google.ads.googleads.v26.services.types.UploadClickConversionsRequest, dict]]): The request object. Request message for [ConversionUploadService.UploadClickConversions][google.ads.googleads.v26.services.ConversionUploadService.UploadClickConversions]. + + Important: Always set ``partial_failure = true``. + Partial failure mode is required for conversion uploads. + When enabled, valid conversions are processed and any + invalid conversions return individual errors in + ``UploadClickConversionsResponse.partial_failure_error``. + Setting ``partial_failure = false`` or omitting the + field is not supported and causes the entire request to + fail. + + When uploading enhanced conversions for leads with + ``user_identifiers``, normalize raw identifier strings + before hashing with SHA-256 (trim leading and trailing + whitespace, convert email addresses to lowercase, and + format phone numbers in E.164 format), and populate only + one identifier field per ``UserIdentifier`` object + (``oneof identifier`` is mutually exclusive). + + For help diagnosing conversion upload issues, see the + conversion upload `troubleshooting + guide `__. customer_id (:class:`str`): Required. The ID of the customer performing the upload. @@ -350,11 +371,12 @@ async def upload_click_conversions( on the ``request`` instance; if ``request`` is provided, this should not be set. partial_failure (:class:`bool`): - Required. If ``true``, successful operations will be - carried out and invalid operations will return errors. - If ``false``, all operations will be carried out in one - transaction if and only if they are all valid. This - should always be set to ``true``. + Required. Must be set to ``true``. Enables partial + failure mode so that valid conversions are processed and + any invalid conversions return individual errors in + ``partial_failure_error``. Setting this field to + ``false`` is not supported and causes the entire request + to fail. See `Best practices for partial failures `__. @@ -476,12 +498,12 @@ async def upload_call_conversions( on the ``request`` instance; if ``request`` is provided, this should not be set. partial_failure (:class:`bool`): - Required. If ``true``, successful operations will be - carried out and invalid operations will return errors. - If ``false``, all operations will be carried out in one - transaction if and only if they are all valid. - - This should always be set to ``true``. + Required. Must be set to ``true``. Enables partial + failure mode so that valid conversions are processed and + any invalid conversions return individual errors in + ``partial_failure_error``. Setting this field to + ``false`` is not supported and causes the entire request + to fail. See `Best practices for partial failures `__. diff --git a/google/ads/googleads/v26/services/services/conversion_upload_service/client.py b/google/ads/googleads/v26/services/services/conversion_upload_service/client.py index 9527f593b..71adecf11 100644 --- a/google/ads/googleads/v26/services/services/conversion_upload_service/client.py +++ b/google/ads/googleads/v26/services/services/conversion_upload_service/client.py @@ -654,6 +654,27 @@ def upload_click_conversions( request (Union[google.ads.googleads.v26.services.types.UploadClickConversionsRequest, dict]): The request object. Request message for [ConversionUploadService.UploadClickConversions][google.ads.googleads.v26.services.ConversionUploadService.UploadClickConversions]. + + Important: Always set ``partial_failure = true``. + Partial failure mode is required for conversion uploads. + When enabled, valid conversions are processed and any + invalid conversions return individual errors in + ``UploadClickConversionsResponse.partial_failure_error``. + Setting ``partial_failure = false`` or omitting the + field is not supported and causes the entire request to + fail. + + When uploading enhanced conversions for leads with + ``user_identifiers``, normalize raw identifier strings + before hashing with SHA-256 (trim leading and trailing + whitespace, convert email addresses to lowercase, and + format phone numbers in E.164 format), and populate only + one identifier field per ``UserIdentifier`` object + (``oneof identifier`` is mutually exclusive). + + For help diagnosing conversion upload issues, see the + conversion upload `troubleshooting + guide `__. customer_id (str): Required. The ID of the customer performing the upload. @@ -669,11 +690,12 @@ def upload_click_conversions( on the ``request`` instance; if ``request`` is provided, this should not be set. partial_failure (bool): - Required. If ``true``, successful operations will be - carried out and invalid operations will return errors. - If ``false``, all operations will be carried out in one - transaction if and only if they are all valid. This - should always be set to ``true``. + Required. Must be set to ``true``. Enables partial + failure mode so that valid conversions are processed and + any invalid conversions return individual errors in + ``partial_failure_error``. Setting this field to + ``false`` is not supported and causes the entire request + to fail. See `Best practices for partial failures `__. @@ -794,12 +816,12 @@ def upload_call_conversions( on the ``request`` instance; if ``request`` is provided, this should not be set. partial_failure (bool): - Required. If ``true``, successful operations will be - carried out and invalid operations will return errors. - If ``false``, all operations will be carried out in one - transaction if and only if they are all valid. - - This should always be set to ``true``. + Required. Must be set to ``true``. Enables partial + failure mode so that valid conversions are processed and + any invalid conversions return individual errors in + ``partial_failure_error``. Setting this field to + ``false`` is not supported and causes the entire request + to fail. See `Best practices for partial failures `__. diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/__init__.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/__init__.py new file mode 100644 index 000000000..2a8d7fed4 --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from .client import CustomerBudgetNetworkSettingServiceClient +from .async_client import CustomerBudgetNetworkSettingServiceAsyncClient + +__all__ = ( + "CustomerBudgetNetworkSettingServiceClient", + "CustomerBudgetNetworkSettingServiceAsyncClient", +) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/async_client.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/async_client.py new file mode 100644 index 000000000..c955b61d2 --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/async_client.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import logging as std_logging +from typing import Callable, Optional, Sequence, Tuple, Union + +from google.ads.googleads.v26 import gapic_version as package_version + +from google.api_core.client_options import ClientOptions +from google.api_core import gapic_v1 +from google.api_core import retry_async as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + + +try: + OptionalRetry = Union[ + retries.AsyncRetry, gapic_v1.method._MethodDefault, None + ] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.AsyncRetry, object, None] # type: ignore + +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) +from .transports.base import ( + CustomerBudgetNetworkSettingServiceTransport, + DEFAULT_CLIENT_INFO, +) +from .client import CustomerBudgetNetworkSettingServiceClient + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class CustomerBudgetNetworkSettingServiceAsyncClient: + """Service to manage customer-level budget network settings.""" + + _client: CustomerBudgetNetworkSettingServiceClient + + # Copy defaults from the synchronous client for use here. + # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. + DEFAULT_ENDPOINT = ( + CustomerBudgetNetworkSettingServiceClient.DEFAULT_ENDPOINT + ) + DEFAULT_MTLS_ENDPOINT = ( + CustomerBudgetNetworkSettingServiceClient.DEFAULT_MTLS_ENDPOINT + ) + _DEFAULT_ENDPOINT_TEMPLATE = ( + CustomerBudgetNetworkSettingServiceClient._DEFAULT_ENDPOINT_TEMPLATE + ) + _DEFAULT_UNIVERSE = ( + CustomerBudgetNetworkSettingServiceClient._DEFAULT_UNIVERSE + ) + + customer_budget_network_setting_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.customer_budget_network_setting_path + ) + parse_customer_budget_network_setting_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_customer_budget_network_setting_path + ) + common_billing_account_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_common_billing_account_path + ) + common_folder_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.common_folder_path + ) + parse_common_folder_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_common_folder_path + ) + common_organization_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_common_organization_path + ) + common_project_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.common_project_path + ) + parse_common_project_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_common_project_path + ) + common_location_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.common_location_path + ) + parse_common_location_path = staticmethod( + CustomerBudgetNetworkSettingServiceClient.parse_common_location_path + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CustomerBudgetNetworkSettingServiceAsyncClient: The constructed client. + """ + sa_info_func = ( + CustomerBudgetNetworkSettingServiceClient.from_service_account_info.__func__ # type: ignore + ) + return sa_info_func( + CustomerBudgetNetworkSettingServiceAsyncClient, + info, + *args, + **kwargs, + ) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CustomerBudgetNetworkSettingServiceAsyncClient: The constructed client. + """ + sa_file_func = ( + CustomerBudgetNetworkSettingServiceClient.from_service_account_file.__func__ # type: ignore + ) + return sa_file_func( + CustomerBudgetNetworkSettingServiceAsyncClient, + filename, + *args, + **kwargs, + ) + + from_service_account_json = from_service_account_file + + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variable is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return CustomerBudgetNetworkSettingServiceClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + + @property + def transport(self) -> CustomerBudgetNetworkSettingServiceTransport: + """Returns the transport used by the client instance. + + Returns: + CustomerBudgetNetworkSettingServiceTransport: The transport used by the client instance. + """ + return self._client.transport + + @property + def api_endpoint(self) -> str: + """Return the API endpoint used by the client instance. + + Returns: + str: The API endpoint used by the client instance. + """ + return self._client._api_endpoint + + @property + def universe_domain(self) -> str: + """Return the universe domain used by the client instance. + + Returns: + str: The universe domain used + by the client instance. + """ + return self._client._universe_domain + + get_transport_class = ( + CustomerBudgetNetworkSettingServiceClient.get_transport_class + ) + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Optional[ + Union[ + str, + CustomerBudgetNetworkSettingServiceTransport, + Callable[..., CustomerBudgetNetworkSettingServiceTransport], + ] + ] = "grpc_asyncio", + client_options: Optional[ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the customer budget network setting service async client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Optional[Union[str,CustomerBudgetNetworkSettingServiceTransport,Callable[..., CustomerBudgetNetworkSettingServiceTransport]]]): + The transport to use, or a Callable that constructs and returns a new transport to use. + If a Callable is given, it will be called with the same set of initialization + arguments as used in the CustomerBudgetNetworkSettingServiceTransport constructor. + If set to None, a transport is chosen automatically. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client. + + 1. The ``api_endpoint`` property can be used to override the + default endpoint provided by the client when ``transport`` is + not explicitly provided. Only if this property is not set and + ``transport`` was not explicitly provided, the endpoint is + determined by the GOOGLE_API_USE_MTLS_ENDPOINT environment + variable, which have one of the following values: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto-switch to the + default mTLS endpoint if client certificate is present; this is + the default value). + + 2. If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide a client certificate for mTLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + 3. The ``universe_domain`` property can be used to override the + default "googleapis.com" universe. Note that ``api_endpoint`` + property still takes precedence; and ``universe_domain`` is + currently not supported for mTLS. + + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client = CustomerBudgetNetworkSettingServiceClient( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ): # pragma: NO COVER + _LOGGER.debug( + "Created client `google.ads.googleads.v26.services.CustomerBudgetNetworkSettingServiceAsyncClient`.", + extra=( + { + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "universeDomain": getattr( + self._client._transport._credentials, + "universe_domain", + "", + ), + "credentialsType": f"{type(self._client._transport._credentials).__module__}.{type(self._client._transport._credentials).__qualname__}", + "credentialsInfo": getattr( + self.transport._credentials, + "get_cred_info", + lambda: None, + )(), + } + if hasattr(self._client._transport, "_credentials") + else { + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "credentialsType": None, + } + ), + ) + + async def mutate_customer_budget_network_setting( + self, + request: Optional[ + Union[ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest, + dict, + ] + ] = None, + *, + customer_id: Optional[str] = None, + operation: Optional[ + customer_budget_network_setting_service.CustomerBudgetNetworkSettingOperation + ] = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Union[float, object] = gapic_v1.method.DEFAULT, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> ( + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse + ): + r"""Creates or updates the customer budget network setting. + + List of thrown errors: `AuthenticationError <>`__ + `AuthorizationError <>`__ `BudgetNetworkSettingError <>`__ + `DatabaseError <>`__ `DistinctError <>`__ `FieldError <>`__ + `FieldMaskError <>`__ `HeaderError <>`__ `InternalError <>`__ + `MutateError <>`__ `NewResourceCreationError <>`__ + `OperationAccessDeniedError <>`__ `QuotaError <>`__ + `RangeError <>`__ `RequestError <>`__ + `ResourceCountLimitExceededError <>`__ `StringLengthError <>`__ + + Args: + request (Optional[Union[google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingRequest, dict]]): + The request object. Request message for + [CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting][google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting]. + customer_id (:class:`str`): + Required. The ID of the customer + whose customer budget network setting is + being modified. + + This corresponds to the ``customer_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + operation (:class:`google.ads.googleads.v26.services.types.CustomerBudgetNetworkSettingOperation`): + Required. The operation to perform on + the customer budget network setting. + + This corresponds to the ``operation`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingResponse: + Response message for customer budget + network setting mutate. + + """ + # Create or coerce a protobuf request object. + # - Quick check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + flattened_params = [customer_id, operation] + has_flattened_params = ( + len([param for param in flattened_params if param is not None]) > 0 + ) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # - Use the request object if provided (there's no risk of modifying the input as + # there are no flattened fields), or create one. + if not isinstance( + request, + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest, + ): + request = customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest( + request + ) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if customer_id is not None: + request.customer_id = customer_id + if operation is not None: + request.operation = operation + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._client._transport._wrapped_methods[ + self._client._transport.mutate_customer_budget_network_setting + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("customer_id", request.customer_id),) + ), + ) + + # Validate the universe domain. + self._client._validate_universe_domain() + + # Send the request. + response = await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # Done; return the response. + return response + + async def __aenter__( + self, + ) -> "CustomerBudgetNetworkSettingServiceAsyncClient": + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.transport.close() + + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +__all__ = ("CustomerBudgetNetworkSettingServiceAsyncClient",) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/client.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/client.py new file mode 100644 index 000000000..d8eb25a3d --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/client.py @@ -0,0 +1,768 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from collections import OrderedDict +from http import HTTPStatus +import json +import logging as std_logging +import os +import re +from typing import Dict, Callable, Optional, Sequence, Tuple, Type, Union, cast +import warnings + +from google.ads.googleads.v26 import gapic_version as package_version + +from google.api_core import client_options as client_options_lib +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.ads.googleads.v26._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, +) +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object, None] # type: ignore + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) +from .transports.base import ( + CustomerBudgetNetworkSettingServiceTransport, + DEFAULT_CLIENT_INFO, +) +from .transports.grpc import CustomerBudgetNetworkSettingServiceGrpcTransport +from .transports.grpc_asyncio import ( + CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport, +) + + +class CustomerBudgetNetworkSettingServiceClientMeta(type): + """Metaclass for the CustomerBudgetNetworkSettingService client. + + This provides class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = ( + OrderedDict() + ) # type: Dict[str, Type[CustomerBudgetNetworkSettingServiceTransport]] + _transport_registry["grpc"] = ( + CustomerBudgetNetworkSettingServiceGrpcTransport + ) + _transport_registry["grpc_asyncio"] = ( + CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport + ) + + def get_transport_class( + cls, + label: Optional[str] = None, + ) -> Type[CustomerBudgetNetworkSettingServiceTransport]: + """Returns an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class CustomerBudgetNetworkSettingServiceClient( + metaclass=CustomerBudgetNetworkSettingServiceClientMeta +): + """Service to manage customer-level budget network settings.""" + + # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. + DEFAULT_ENDPOINT = "googleads.googleapis.com" + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint(DEFAULT_ENDPOINT) + + _DEFAULT_ENDPOINT_TEMPLATE = "googleads.{UNIVERSE_DOMAIN}" + _DEFAULT_UNIVERSE = "googleapis.com" + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CustomerBudgetNetworkSettingServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info( + info + ) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CustomerBudgetNetworkSettingServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file( + filename + ) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> CustomerBudgetNetworkSettingServiceTransport: + """Returns the transport used by the client instance. + + Returns: + CustomerBudgetNetworkSettingServiceTransport: The transport used by the client + instance. + """ + return self._transport + + @staticmethod + def customer_budget_network_setting_path( + customer_id: str, + ) -> str: + """Returns a fully-qualified customer_budget_network_setting string.""" + return "customers/{customer_id}/customerBudgetNetworkSettings".format( + customer_id=customer_id, + ) + + @staticmethod + def parse_customer_budget_network_setting_path(path: str) -> Dict[str, str]: + """Parses a customer_budget_network_setting path into its component segments.""" + m = re.match( + r"^customers/(?P.+?)/customerBudgetNetworkSettings$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def common_billing_account_path( + billing_account: str, + ) -> str: + """Returns a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path( + folder: str, + ) -> str: + """Returns a fully-qualified folder string.""" + return "folders/{folder}".format( + folder=folder, + ) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path( + organization: str, + ) -> str: + """Returns a fully-qualified organization string.""" + return "organizations/{organization}".format( + organization=organization, + ) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path( + project: str, + ) -> str: + """Returns a fully-qualified project string.""" + return "projects/{project}".format( + project=project, + ) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path( + project: str, + location: str, + ) -> str: + """Returns a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, + location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)$", path + ) + return m.groupdict() if m else {} + + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Deprecated. Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variable is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + + warnings.warn( + "get_mtls_endpoint_and_cert_source is deprecated. Use the api_endpoint property instead.", + DeprecationWarning, + ) + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = should_use_client_cert() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert: + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT # type: ignore + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + + @staticmethod + def _get_client_cert_source(provided_cert_source, use_cert_flag): + """Return the client cert source to be used by the client. + + Args: + provided_cert_source (bytes): The client certificate source provided. + use_cert_flag (bool): A flag indicating whether to use the client certificate. + + Returns: + bytes or None: The client cert source to be used by the client. + """ + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + return client_cert_source + + def _validate_universe_domain(self): + """Validates client's and credentials' universe domains are consistent. + + Returns: + bool: True iff the configured universe domain is valid. + + Raises: + ValueError: If the configured universe domain is not valid. + """ + + # NOTE (b/349488459): universe validation is disabled until further notice. + return True + + def _add_cred_info_for_auth_errors( + self, error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [ + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + HTTPStatus.NOT_FOUND, + ]: + return + + cred = self._transport._credentials + + # get_cred_info is only available in google-auth>=2.35.0 + if not hasattr(cred, "get_cred_info"): + return + + # ignore the type check since pypy test fails when get_cred_info + # is not available + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + + @property + def api_endpoint(self) -> str: + """Return the API endpoint used by the client instance. + + Returns: + str: The API endpoint used by the client instance. + """ + return self._api_endpoint + + @property + def universe_domain(self) -> str: + """Return the universe domain used by the client instance. + + Returns: + str: The universe domain used by the client instance. + """ + return self._universe_domain + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Optional[ + Union[ + str, + CustomerBudgetNetworkSettingServiceTransport, + Callable[..., CustomerBudgetNetworkSettingServiceTransport], + ] + ] = None, + client_options: Optional[ + Union[client_options_lib.ClientOptions, dict] + ] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the customer budget network setting service client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Optional[Union[str,CustomerBudgetNetworkSettingServiceTransport,Callable[..., CustomerBudgetNetworkSettingServiceTransport]]]): + The transport to use, or a Callable that constructs and returns a new transport. + If a Callable is given, it will be called with the same set of initialization + arguments as used in the CustomerBudgetNetworkSettingServiceTransport constructor. + If set to None, a transport is chosen automatically. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client. + + 1. The ``api_endpoint`` property can be used to override the + default endpoint provided by the client when ``transport`` is + not explicitly provided. Only if this property is not set and + ``transport`` was not explicitly provided, the endpoint is + determined by the GOOGLE_API_USE_MTLS_ENDPOINT environment + variable, which have one of the following values: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto-switch to the + default mTLS endpoint if client certificate is present; this is + the default value). + + 2. If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide a client certificate for mTLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + 3. The ``universe_domain`` property can be used to override the + default "googleapis.com" universe. Note that the ``api_endpoint`` + property still takes precedence; and ``universe_domain`` is + currently not supported for mTLS. + + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client_options = client_options + if isinstance(self._client_options, dict): + self._client_options = client_options_lib.from_dict( + self._client_options + ) + if self._client_options is None: + self._client_options = client_options_lib.ClientOptions() + self._client_options = cast( + client_options_lib.ClientOptions, self._client_options + ) + + universe_domain_opt = getattr( + self._client_options, "universe_domain", None + ) + + ( + self._use_client_cert, + self._use_mtls_endpoint, + self._universe_domain_env, + ) = read_environment_variables() + self._client_cert_source = ( + CustomerBudgetNetworkSettingServiceClient._get_client_cert_source( + self._client_options.client_cert_source, self._use_client_cert + ) + ) + self._universe_domain = get_universe_domain( + universe_domain_opt, + self._universe_domain_env, + default_universe=CustomerBudgetNetworkSettingServiceClient._DEFAULT_UNIVERSE, + ) + self._api_endpoint: str = "" # updated below, depending on `transport` + + # Initialize the universe domain validation. + self._is_universe_domain_valid = False + + if CLIENT_LOGGING_SUPPORTED: # pragma: NO COVER + # Setup logging. + client_logging.initialize_logging() + + api_key_value = getattr(self._client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + transport_provided = isinstance( + transport, CustomerBudgetNetworkSettingServiceTransport + ) + if transport_provided: + # transport is a CustomerBudgetNetworkSettingServiceTransport instance. + if ( + credentials + or self._client_options.credentials_file + or api_key_value + ): + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if self._client_options.scopes: + raise ValueError( + "When providing a transport instance, provide its scopes " + "directly." + ) + self._transport = cast( + CustomerBudgetNetworkSettingServiceTransport, transport + ) + self._api_endpoint = self._transport.host + + self._api_endpoint = self._api_endpoint or get_api_endpoint( + api_override=self._client_options.api_endpoint, + universe_domain=self._universe_domain, + default_universe=CustomerBudgetNetworkSettingServiceClient._DEFAULT_UNIVERSE, + default_mtls_endpoint=CustomerBudgetNetworkSettingServiceClient.DEFAULT_MTLS_ENDPOINT, + default_endpoint_template=CustomerBudgetNetworkSettingServiceClient._DEFAULT_ENDPOINT_TEMPLATE, + use_mtls=self._use_mtls_endpoint == "always" + or (self._use_mtls_endpoint == "auto" and self._client_cert_source), + ) + + if not transport_provided: + import google.auth._default # type: ignore + + if api_key_value and hasattr( + google.auth._default, "get_api_key_credentials" + ): + credentials = google.auth._default.get_api_key_credentials( + api_key_value + ) + + transport_init: Union[ + Type[CustomerBudgetNetworkSettingServiceTransport], + Callable[..., CustomerBudgetNetworkSettingServiceTransport], + ] = ( + CustomerBudgetNetworkSettingServiceClient.get_transport_class( + transport + ) + if isinstance(transport, str) or transport is None + else cast( + Callable[..., CustomerBudgetNetworkSettingServiceTransport], + transport, + ) + ) + # initialize with the provided callable or the passed in class + self._transport = transport_init( + credentials=credentials, + credentials_file=self._client_options.credentials_file, + host=self._api_endpoint, + scopes=self._client_options.scopes, + client_cert_source_for_mtls=self._client_cert_source, + quota_project_id=self._client_options.quota_project_id, + client_info=client_info, + always_use_jwt_access=True, + api_audience=self._client_options.api_audience, + ) + + if "async" not in str(self._transport): + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ): # pragma: NO COVER + _LOGGER.debug( + "Created client `google.ads.googleads.v26.services.CustomerBudgetNetworkSettingServiceClient`.", + extra=( + { + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "universeDomain": getattr( + self._transport._credentials, + "universe_domain", + "", + ), + "credentialsType": f"{type(self._transport._credentials).__module__}.{type(self._transport._credentials).__qualname__}", + "credentialsInfo": getattr( + self.transport._credentials, + "get_cred_info", + lambda: None, + )(), + } + if hasattr(self._transport, "_credentials") + else { + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "credentialsType": None, + } + ), + ) + + def mutate_customer_budget_network_setting( + self, + request: Optional[ + Union[ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest, + dict, + ] + ] = None, + *, + customer_id: Optional[str] = None, + operation: Optional[ + customer_budget_network_setting_service.CustomerBudgetNetworkSettingOperation + ] = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Union[float, object] = gapic_v1.method.DEFAULT, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> ( + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse + ): + r"""Creates or updates the customer budget network setting. + + List of thrown errors: `AuthenticationError <>`__ + `AuthorizationError <>`__ `BudgetNetworkSettingError <>`__ + `DatabaseError <>`__ `DistinctError <>`__ `FieldError <>`__ + `FieldMaskError <>`__ `HeaderError <>`__ `InternalError <>`__ + `MutateError <>`__ `NewResourceCreationError <>`__ + `OperationAccessDeniedError <>`__ `QuotaError <>`__ + `RangeError <>`__ `RequestError <>`__ + `ResourceCountLimitExceededError <>`__ `StringLengthError <>`__ + + Args: + request (Union[google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingRequest, dict]): + The request object. Request message for + [CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting][google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting]. + customer_id (str): + Required. The ID of the customer + whose customer budget network setting is + being modified. + + This corresponds to the ``customer_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + operation (google.ads.googleads.v26.services.types.CustomerBudgetNetworkSettingOperation): + Required. The operation to perform on + the customer budget network setting. + + This corresponds to the ``operation`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingResponse: + Response message for customer budget + network setting mutate. + + """ + # Create or coerce a protobuf request object. + # - Quick check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + flattened_params = [customer_id, operation] + has_flattened_params = ( + len([param for param in flattened_params if param is not None]) > 0 + ) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # - Use the request object if provided (there's no risk of modifying the input as + # there are no flattened fields), or create one. + if not isinstance( + request, + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest, + ): + request = customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest( + request + ) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if customer_id is not None: + request.customer_id = customer_id + if operation is not None: + request.operation = operation + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[ + self._transport.mutate_customer_budget_network_setting + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("customer_id", request.customer_id),) + ), + ) + + # Validate the universe domain. + self._validate_universe_domain() + + # Send the request. + response = rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # Done; return the response. + return response + + def __enter__(self) -> "CustomerBudgetNetworkSettingServiceClient": + return self + + def __exit__(self, type, value, traceback): + """Releases underlying transport's resources. + + .. warning:: + ONLY use as a context manager if the transport is NOT shared + with other clients! Exiting the with block will CLOSE the transport + and may cause errors in other clients! + """ + self.transport.close() + + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + +__all__ = ("CustomerBudgetNetworkSettingServiceClient",) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/README.rst b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/README.rst new file mode 100644 index 000000000..19c206c63 --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/README.rst @@ -0,0 +1,10 @@ + +transport inheritance structure +_______________________________ + +``CustomerBudgetNetworkSettingServiceTransport`` is the ABC for all transports. + +- public child ``CustomerBudgetNetworkSettingServiceGrpcTransport`` for sync gRPC transport (defined in ``grpc.py``). +- public child ``CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport`` for async gRPC transport (defined in ``grpc_asyncio.py``). +- private child ``_BaseCustomerBudgetNetworkSettingServiceRestTransport`` for base REST transport with inner classes ``_BaseMETHOD`` (defined in ``rest_base.py``). +- public child ``CustomerBudgetNetworkSettingServiceRestTransport`` for sync REST transport with inner classes ``METHOD`` derived from the parent's corresponding ``_BaseMETHOD`` classes (defined in ``rest.py``). diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/__init__.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/__init__.py new file mode 100644 index 000000000..9dcc22329 --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from collections import OrderedDict + +from .base import CustomerBudgetNetworkSettingServiceTransport +from .grpc import CustomerBudgetNetworkSettingServiceGrpcTransport +from .grpc_asyncio import ( + CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport, +) + + +# Compile a registry of transports. +_transport_registry = ( + OrderedDict() +) # type: Dict[str, Type[CustomerBudgetNetworkSettingServiceTransport]] +_transport_registry["grpc"] = CustomerBudgetNetworkSettingServiceGrpcTransport +_transport_registry["grpc_asyncio"] = ( + CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport +) + +__all__ = ( + "CustomerBudgetNetworkSettingServiceTransport", + "CustomerBudgetNetworkSettingServiceGrpcTransport", + "CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport", +) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/base.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/base.py new file mode 100644 index 000000000..c3782b08c --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/base.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import abc +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union + +from google.ads.googleads.v26 import gapic_version as package_version + +import google.auth # type: ignore +import google.api_core +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +class CustomerBudgetNetworkSettingServiceTransport(abc.ABC): + """Abstract transport class for CustomerBudgetNetworkSettingService.""" + + AUTH_SCOPES = ("https://www.googleapis.com/auth/adwords",) + + DEFAULT_HOST: str = "googleads.googleapis.com" + + def __init__( + self, + *, + host: str = DEFAULT_HOST, + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + **kwargs, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A list of scopes. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + """ + + # Save the scopes. + self._scopes = scopes + if not hasattr(self, "_ignore_credentials"): + self._ignore_credentials: bool = False + + # If no credentials are provided, then determine the appropriate + # defaults. + if credentials and credentials_file: + raise core_exceptions.DuplicateCredentialArgs( + "'credentials_file' and 'credentials' are mutually exclusive" + ) + + if credentials_file is not None: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + default_scopes=self.AUTH_SCOPES, + ) + elif credentials is None and not self._ignore_credentials: + credentials, _ = google.auth.default( + scopes=scopes, + quota_project_id=quota_project_id, + default_scopes=self.AUTH_SCOPES, + ) + # Don't apply audience if the credentials file passed from user. + if hasattr(credentials, "with_gdch_audience"): + credentials = credentials.with_gdch_audience( + api_audience if api_audience else host + ) + + # If the credentials are service account credentials, then always try to use self signed JWT. + if ( + always_use_jwt_access + and isinstance(credentials, service_account.Credentials) + and hasattr( + service_account.Credentials, "with_always_use_jwt_access" + ) + ): + credentials = credentials.with_always_use_jwt_access(True) + + # Save the credentials. + self._credentials = credentials + + # Save the hostname. Default to port 443 (HTTPS) if none is specified. + if ":" not in host: + host += ":443" + self._host = host + + self._wrapped_methods: Dict[Callable, Callable] = {} + + @property + def host(self): + return self._host + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.mutate_customer_budget_network_setting: gapic_v1.method.wrap_method( + self.mutate_customer_budget_network_setting, + default_timeout=None, + client_info=client_info, + ), + } + + def close(self): + """Closes resources associated with the transport. + + .. warning:: + Only call this method if the transport is NOT shared + with other clients - this may cause errors in other clients! + """ + raise NotImplementedError() + + @property + def mutate_customer_budget_network_setting( + self, + ) -> Callable[ + [ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest + ], + Union[ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse, + Awaitable[ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse + ], + ], + ]: + raise NotImplementedError() + + @property + def kind(self) -> str: + raise NotImplementedError() + + +__all__ = ("CustomerBudgetNetworkSettingServiceTransport",) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc.py new file mode 100644 index 000000000..8fb49705a --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import logging as std_logging +import pickle +import warnings +from typing import Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import grpc_helpers +from google.api_core import gapic_v1 +import google.auth # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.protobuf.json_format import MessageToJson +import google.protobuf.message + +import grpc # type: ignore +import proto # type: ignore + +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) +from .base import ( + CustomerBudgetNetworkSettingServiceTransport, + DEFAULT_CLIENT_INFO, +) + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class _LoggingClientInterceptor( + grpc.UnaryUnaryClientInterceptor +): # pragma: NO COVER + def intercept_unary_unary(self, continuation, client_call_details, request): + logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ) + if logging_enabled: # pragma: NO COVER + request_metadata = client_call_details.metadata + if isinstance(request, proto.Message): + request_payload = type(request).to_json(request) + elif isinstance(request, google.protobuf.message.Message): + request_payload = MessageToJson(request) + else: + request_payload = ( + f"{type(request).__name__}: {pickle.dumps(request)!r}" + ) + + request_metadata = { + key: ( + value.decode("utf-8") if isinstance(value, bytes) else value + ) + for key, value in request_metadata + } + grpc_request = { + "payload": request_payload, + "requestMethod": "grpc", + "metadata": dict(request_metadata), + } + _LOGGER.debug( + f"Sending request for {client_call_details.method}", + extra={ + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "rpcName": str(client_call_details.method), + "request": grpc_request, + "metadata": grpc_request["metadata"], + }, + ) + response = continuation(client_call_details, request) + if logging_enabled: # pragma: NO COVER + response_metadata = response.trailing_metadata() + # Convert gRPC metadata `` to list of tuples + metadata = ( + dict([(k, str(v)) for k, v in response_metadata]) + if response_metadata + else None + ) + result = response.result() + if isinstance(result, proto.Message): + response_payload = type(result).to_json(result) + elif isinstance(result, google.protobuf.message.Message): + response_payload = MessageToJson(result) + else: + response_payload = ( + f"{type(result).__name__}: {pickle.dumps(result)!r}" + ) + grpc_response = { + "payload": response_payload, + "metadata": metadata, + "status": "OK", + } + _LOGGER.debug( + f"Received response for {client_call_details.method}.", + extra={ + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "rpcName": client_call_details.method, + "response": grpc_response, + "metadata": grpc_response["metadata"], + }, + ) + return response + + +class CustomerBudgetNetworkSettingServiceGrpcTransport( + CustomerBudgetNetworkSettingServiceTransport +): + """gRPC backend transport for CustomerBudgetNetworkSettingService. + + Service to manage customer-level budget network settings. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _stubs: Dict[str, Callable] + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: Optional[ + Union[grpc.Channel, Callable[..., grpc.Channel]] + ] = None, + api_mtls_endpoint: Optional[str] = None, + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + ssl_channel_credentials: Optional[grpc.ChannelCredentials] = None, + client_cert_source_for_mtls: Optional[ + Callable[[], Tuple[bytes, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if a ``channel`` instance is provided. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. + scopes (Optional(Sequence[str])): A list of scopes. This argument is + ignored if a ``channel`` instance is provided. + channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): + A ``Channel`` instance through which to make calls, or a Callable + that constructs and returns one. If set to None, ``self.create_channel`` + is used to create the channel. If a Callable is given, it will be called + with the same arguments as used in ``self.create_channel``. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or application default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for the grpc channel. It is ignored if a ``channel`` instance is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure a mutual TLS channel. It is + ignored if a ``channel`` instance or ``ssl_channel_credentials`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn( + "client_cert_source is deprecated", DeprecationWarning + ) + + if isinstance(channel, grpc.Channel): + # Ignore credentials if a channel was passed. + credentials = None + self._ignore_credentials = True + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + else: + self._ssl_channel_credentials = ( + SslCredentials().ssl_credentials + ) + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + + if not self._grpc_channel: + # initialize with the provided callable or the default channel + channel_init = channel or type(self).create_channel + self._grpc_channel = channel_init( + self._host, + # use the credentials which are saved + credentials=self._credentials, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + self._interceptor = _LoggingClientInterceptor() + self._logged_channel = grpc.intercept_channel( + self._grpc_channel, self._interceptor + ) + + # Wrap messages. This must be done after self._logged_channel exists + self._prep_wrapped_messages(client_info) + + @classmethod + def create_channel( + cls, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> grpc.Channel: + """Create and return a gRPC channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + grpc.Channel: A gRPC channel object. + + Raises: + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + + return grpc_helpers.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + @property + def grpc_channel(self) -> grpc.Channel: + """Return the channel designed to connect to this service.""" + return self._grpc_channel + + @property + def mutate_customer_budget_network_setting( + self, + ) -> Callable[ + [ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest + ], + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse, + ]: + r"""Return a callable for the mutate customer budget network + setting method over gRPC. + + Creates or updates the customer budget network setting. + + List of thrown errors: `AuthenticationError <>`__ + `AuthorizationError <>`__ `BudgetNetworkSettingError <>`__ + `DatabaseError <>`__ `DistinctError <>`__ `FieldError <>`__ + `FieldMaskError <>`__ `HeaderError <>`__ `InternalError <>`__ + `MutateError <>`__ `NewResourceCreationError <>`__ + `OperationAccessDeniedError <>`__ `QuotaError <>`__ + `RangeError <>`__ `RequestError <>`__ + `ResourceCountLimitExceededError <>`__ `StringLengthError <>`__ + + Returns: + Callable[[~.MutateCustomerBudgetNetworkSettingRequest], + ~.MutateCustomerBudgetNetworkSettingResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "mutate_customer_budget_network_setting" not in self._stubs: + self._stubs["mutate_customer_budget_network_setting"] = ( + self._logged_channel.unary_unary( + "/google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService/MutateCustomerBudgetNetworkSetting", + request_serializer=customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest.serialize, + response_deserializer=customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse.deserialize, + ) + ) + return self._stubs["mutate_customer_budget_network_setting"] + + def close(self): + self._logged_channel.close() + + @property + def kind(self) -> str: + return "grpc" + + +__all__ = ("CustomerBudgetNetworkSettingServiceGrpcTransport",) diff --git a/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc_asyncio.py new file mode 100644 index 000000000..ee9c22f83 --- /dev/null +++ b/google/ads/googleads/v26/services/services/customer_budget_network_setting_service/transports/grpc_asyncio.py @@ -0,0 +1,429 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import inspect +import pickle +import logging as std_logging +import warnings +from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import gapic_v1 +from google.api_core import grpc_helpers_async +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.protobuf.json_format import MessageToJson +import google.protobuf.message + +import grpc # type: ignore +import proto # type: ignore +from grpc.experimental import aio # type: ignore + +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) +from .base import ( + CustomerBudgetNetworkSettingServiceTransport, + DEFAULT_CLIENT_INFO, +) + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class _LoggingClientAIOInterceptor( + grpc.aio.UnaryUnaryClientInterceptor +): # pragma: NO COVER + async def intercept_unary_unary( + self, continuation, client_call_details, request + ): + logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ) + if logging_enabled: # pragma: NO COVER + request_metadata = client_call_details.metadata + if isinstance(request, proto.Message): + request_payload = type(request).to_json(request) + elif isinstance(request, google.protobuf.message.Message): + request_payload = MessageToJson(request) + else: + request_payload = ( + f"{type(request).__name__}: {pickle.dumps(request)!r}" + ) + + request_metadata = { + key: ( + value.decode("utf-8") if isinstance(value, bytes) else value + ) + for key, value in request_metadata + } + grpc_request = { + "payload": request_payload, + "requestMethod": "grpc", + "metadata": dict(request_metadata), + } + _LOGGER.debug( + f"Sending request for {client_call_details.method}", + extra={ + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "rpcName": str(client_call_details.method), + "request": grpc_request, + "metadata": grpc_request["metadata"], + }, + ) + response = await continuation(client_call_details, request) + if logging_enabled: # pragma: NO COVER + response_metadata = await response.trailing_metadata() + # Convert gRPC metadata `` to list of tuples + metadata = ( + dict([(k, str(v)) for k, v in response_metadata]) + if response_metadata + else None + ) + result = await response + if isinstance(result, proto.Message): + response_payload = type(result).to_json(result) + elif isinstance(result, google.protobuf.message.Message): + response_payload = MessageToJson(result) + else: + response_payload = ( + f"{type(result).__name__}: {pickle.dumps(result)!r}" + ) + grpc_response = { + "payload": response_payload, + "metadata": metadata, + "status": "OK", + } + _LOGGER.debug( + f"Received response to rpc {client_call_details.method}.", + extra={ + "serviceName": "google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService", + "rpcName": str(client_call_details.method), + "response": grpc_response, + "metadata": grpc_response["metadata"], + }, + ) + return response + + +class CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport( + CustomerBudgetNetworkSettingServiceTransport +): + """gRPC AsyncIO backend transport for CustomerBudgetNetworkSettingService. + + Service to manage customer-level budget network settings. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _grpc_channel: aio.Channel + _stubs: Dict[str, Callable] = {} + + @classmethod + def create_channel( + cls, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> aio.Channel: + """Create and return a gRPC AsyncIO channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + aio.Channel: A gRPC AsyncIO channel object. + """ + + return grpc_helpers_async.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: Optional[ + Union[aio.Channel, Callable[..., aio.Channel]] + ] = None, + api_mtls_endpoint: Optional[str] = None, + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + ssl_channel_credentials: Optional[grpc.ChannelCredentials] = None, + client_cert_source_for_mtls: Optional[ + Callable[[], Tuple[bytes, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if a ``channel`` instance is provided. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + channel (Optional[Union[aio.Channel, Callable[..., aio.Channel]]]): + A ``Channel`` instance through which to make calls, or a Callable + that constructs and returns one. If set to None, ``self.create_channel`` + is used to create the channel. If a Callable is given, it will be called + with the same arguments as used in ``self.create_channel``. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or application default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for the grpc channel. It is ignored if a ``channel`` instance is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure a mutual TLS channel. It is + ignored if a ``channel`` instance or ``ssl_channel_credentials`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn( + "client_cert_source is deprecated", DeprecationWarning + ) + + if isinstance(channel, aio.Channel): + # Ignore credentials if a channel was passed. + credentials = None + self._ignore_credentials = True + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + else: + self._ssl_channel_credentials = ( + SslCredentials().ssl_credentials + ) + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + + if not self._grpc_channel: + # initialize with the provided callable or the default channel + channel_init = channel or type(self).create_channel + self._grpc_channel = channel_init( + self._host, + # use the credentials which are saved + credentials=self._credentials, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + self._interceptor = _LoggingClientAIOInterceptor() + self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + self._logged_channel = self._grpc_channel + self._wrap_with_kind = ( + "kind" + in inspect.signature(gapic_v1.method_async.wrap_method).parameters + ) + # Wrap messages. This must be done after self._logged_channel exists + self._prep_wrapped_messages(client_info) + + @property + def grpc_channel(self) -> aio.Channel: + """Create the channel designed to connect to this service. + + This property caches on the instance; repeated calls return + the same channel. + """ + # Return the channel from cache. + return self._grpc_channel + + @property + def mutate_customer_budget_network_setting( + self, + ) -> Callable[ + [ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest + ], + Awaitable[ + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse + ], + ]: + r"""Return a callable for the mutate customer budget network + setting method over gRPC. + + Creates or updates the customer budget network setting. + + List of thrown errors: `AuthenticationError <>`__ + `AuthorizationError <>`__ `BudgetNetworkSettingError <>`__ + `DatabaseError <>`__ `DistinctError <>`__ `FieldError <>`__ + `FieldMaskError <>`__ `HeaderError <>`__ `InternalError <>`__ + `MutateError <>`__ `NewResourceCreationError <>`__ + `OperationAccessDeniedError <>`__ `QuotaError <>`__ + `RangeError <>`__ `RequestError <>`__ + `ResourceCountLimitExceededError <>`__ `StringLengthError <>`__ + + Returns: + Callable[[~.MutateCustomerBudgetNetworkSettingRequest], + Awaitable[~.MutateCustomerBudgetNetworkSettingResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "mutate_customer_budget_network_setting" not in self._stubs: + self._stubs["mutate_customer_budget_network_setting"] = ( + self._logged_channel.unary_unary( + "/google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService/MutateCustomerBudgetNetworkSetting", + request_serializer=customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingRequest.serialize, + response_deserializer=customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResponse.deserialize, + ) + ) + return self._stubs["mutate_customer_budget_network_setting"] + + def _prep_wrapped_messages(self, client_info): + """Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + self._wrapped_methods = { + self.mutate_customer_budget_network_setting: self._wrap_method( + self.mutate_customer_budget_network_setting, + default_timeout=None, + client_info=client_info, + ), + } + + def _wrap_method(self, func, *args, **kwargs): + if self._wrap_with_kind: # pragma: NO COVER + kwargs["kind"] = self.kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + + def close(self): + return self._logged_channel.close() + + @property + def kind(self) -> str: + return "grpc_asyncio" + + +__all__ = ("CustomerBudgetNetworkSettingServiceGrpcAsyncIOTransport",) diff --git a/google/ads/googleads/v26/services/services/google_ads_service/async_client.py b/google/ads/googleads/v26/services/services/google_ads_service/async_client.py index 97e1d4b82..6586455b6 100644 --- a/google/ads/googleads/v26/services/services/google_ads_service/async_client.py +++ b/google/ads/googleads/v26/services/services/google_ads_service/async_client.py @@ -60,7 +60,24 @@ class GoogleAdsServiceAsyncClient: - """Service to fetch data and metrics across resources.""" + """Service to fetch data and metrics across resources. Use the + interactive `Google Ads Query + Builder `__ to + construct and validate Google Ads Query Language (GAQL) queries. + + Search versus SearchStream: + + - ``Search``: Paginated using ``next_page_token`` with up to 10,000 + rows per page. Caches pages across requests. Best for small result + sets (< 10,000 rows) or user-driven paginated interfaces. + - ``SearchStream``: Delivers a single stream of response batches + with up to 10,000 rows per batch. Lower overhead without request + caching. Best for large result sets (> 10,000 rows) or batch + reporting workloads. + + For more details, see `Streaming versus + paging `__. + """ _client: GoogleAdsServiceClient @@ -551,6 +568,12 @@ class GoogleAdsServiceAsyncClient: parse_customer_asset_set_path = staticmethod( GoogleAdsServiceClient.parse_customer_asset_set_path ) + customer_budget_network_setting_path = staticmethod( + GoogleAdsServiceClient.customer_budget_network_setting_path + ) + parse_customer_budget_network_setting_path = staticmethod( + GoogleAdsServiceClient.parse_customer_budget_network_setting_path + ) customer_client_path = staticmethod( GoogleAdsServiceClient.customer_client_path ) @@ -1379,7 +1402,12 @@ async def search( timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), ) -> pagers.SearchAsyncPager: - r"""Returns all rows that match the search query. + r"""Returns all rows that match the search query. Uses pagination + (``next_page_token``) with up to 10,000 rows per page. For + result sets larger than 10,000 rows, prefer + [GoogleAdsService.SearchStream][google.ads.googleads.v26.services.GoogleAdsService.SearchStream]. + To build and validate queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ @@ -1498,7 +1526,13 @@ def search_stream( ) -> Awaitable[ AsyncIterable[google_ads_service.SearchGoogleAdsStreamResponse] ]: - r"""Returns all rows that match the search stream query. + r"""Returns all rows that match the search stream query. Streams + batches of rows over a single connection without pagination + overhead, making it preferred over + [GoogleAdsService.Search][google.ads.googleads.v26.services.GoogleAdsService.Search] + for large result sets (> 10,000 rows). To build and validate + queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ diff --git a/google/ads/googleads/v26/services/services/google_ads_service/client.py b/google/ads/googleads/v26/services/services/google_ads_service/client.py index 9aa783610..944ccafc4 100644 --- a/google/ads/googleads/v26/services/services/google_ads_service/client.py +++ b/google/ads/googleads/v26/services/services/google_ads_service/client.py @@ -113,7 +113,24 @@ def get_transport_class( class GoogleAdsServiceClient(metaclass=GoogleAdsServiceClientMeta): - """Service to fetch data and metrics across resources.""" + """Service to fetch data and metrics across resources. Use the + interactive `Google Ads Query + Builder `__ to + construct and validate Google Ads Query Language (GAQL) queries. + + Search versus SearchStream: + + - ``Search``: Paginated using ``next_page_token`` with up to 10,000 + rows per page. Caches pages across requests. Best for small result + sets (< 10,000 rows) or user-driven paginated interfaces. + - ``SearchStream``: Delivers a single stream of response batches + with up to 10,000 rows per batch. Lower overhead without request + caching. Best for large result sets (> 10,000 rows) or batch + reporting workloads. + + For more details, see `Streaming versus + paging `__. + """ # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "googleads.googleapis.com" @@ -2079,6 +2096,24 @@ def parse_customer_asset_set_path(path: str) -> Dict[str, str]: ) return m.groupdict() if m else {} + @staticmethod + def customer_budget_network_setting_path( + customer_id: str, + ) -> str: + """Returns a fully-qualified customer_budget_network_setting string.""" + return "customers/{customer_id}/customerBudgetNetworkSettings".format( + customer_id=customer_id, + ) + + @staticmethod + def parse_customer_budget_network_setting_path(path: str) -> Dict[str, str]: + """Parses a customer_budget_network_setting path into its component segments.""" + m = re.match( + r"^customers/(?P.+?)/customerBudgetNetworkSettings$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def customer_client_path( customer_id: str, @@ -4692,7 +4727,12 @@ def search( timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), ) -> pagers.SearchPager: - r"""Returns all rows that match the search query. + r"""Returns all rows that match the search query. Uses pagination + (``next_page_token``) with up to 10,000 rows per page. For + result sets larger than 10,000 rows, prefer + [GoogleAdsService.SearchStream][google.ads.googleads.v26.services.GoogleAdsService.SearchStream]. + To build and validate queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ @@ -4806,7 +4846,13 @@ def search_stream( timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), ) -> Iterable[google_ads_service.SearchGoogleAdsStreamResponse]: - r"""Returns all rows that match the search stream query. + r"""Returns all rows that match the search stream query. Streams + batches of rows over a single connection without pagination + overhead, making it preferred over + [GoogleAdsService.Search][google.ads.googleads.v26.services.GoogleAdsService.Search] + for large result sets (> 10,000 rows). To build and validate + queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ diff --git a/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc.py b/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc.py index 4d45b57a1..1b3a6b481 100644 --- a/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc.py +++ b/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc.py @@ -118,7 +118,23 @@ def intercept_unary_unary(self, continuation, client_call_details, request): class GoogleAdsServiceGrpcTransport(GoogleAdsServiceTransport): """gRPC backend transport for GoogleAdsService. - Service to fetch data and metrics across resources. + Service to fetch data and metrics across resources. Use the + interactive `Google Ads Query + Builder `__ to + construct and validate Google Ads Query Language (GAQL) queries. + + Search versus SearchStream: + + - ``Search``: Paginated using ``next_page_token`` with up to 10,000 + rows per page. Caches pages across requests. Best for small result + sets (< 10,000 rows) or user-driven paginated interfaces. + - ``SearchStream``: Delivers a single stream of response batches + with up to 10,000 rows per batch. Lower overhead without request + caching. Best for large result sets (> 10,000 rows) or batch + reporting workloads. + + For more details, see `Streaming versus + paging `__. This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation @@ -354,7 +370,12 @@ def search( ]: r"""Return a callable for the search method over gRPC. - Returns all rows that match the search query. + Returns all rows that match the search query. Uses pagination + (``next_page_token``) with up to 10,000 rows per page. For + result sets larger than 10,000 rows, prefer + [GoogleAdsService.SearchStream][google.ads.googleads.v26.services.GoogleAdsService.SearchStream]. + To build and validate queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ @@ -389,7 +410,13 @@ def search_stream( ]: r"""Return a callable for the search stream method over gRPC. - Returns all rows that match the search stream query. + Returns all rows that match the search stream query. Streams + batches of rows over a single connection without pagination + overhead, making it preferred over + [GoogleAdsService.Search][google.ads.googleads.v26.services.GoogleAdsService.Search] + for large result sets (> 10,000 rows). To build and validate + queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ diff --git a/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc_asyncio.py index 66ec353e9..81199d6c0 100644 --- a/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc_asyncio.py +++ b/google/ads/googleads/v26/services/services/google_ads_service/transports/grpc_asyncio.py @@ -121,7 +121,23 @@ async def intercept_unary_unary( class GoogleAdsServiceGrpcAsyncIOTransport(GoogleAdsServiceTransport): """gRPC AsyncIO backend transport for GoogleAdsService. - Service to fetch data and metrics across resources. + Service to fetch data and metrics across resources. Use the + interactive `Google Ads Query + Builder `__ to + construct and validate Google Ads Query Language (GAQL) queries. + + Search versus SearchStream: + + - ``Search``: Paginated using ``next_page_token`` with up to 10,000 + rows per page. Caches pages across requests. Best for small result + sets (< 10,000 rows) or user-driven paginated interfaces. + - ``SearchStream``: Delivers a single stream of response batches + with up to 10,000 rows per batch. Lower overhead without request + caching. Best for large result sets (> 10,000 rows) or batch + reporting workloads. + + For more details, see `Streaming versus + paging `__. This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation @@ -360,7 +376,12 @@ def search( ]: r"""Return a callable for the search method over gRPC. - Returns all rows that match the search query. + Returns all rows that match the search query. Uses pagination + (``next_page_token``) with up to 10,000 rows per page. For + result sets larger than 10,000 rows, prefer + [GoogleAdsService.SearchStream][google.ads.googleads.v26.services.GoogleAdsService.SearchStream]. + To build and validate queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ @@ -395,7 +416,13 @@ def search_stream( ]: r"""Return a callable for the search stream method over gRPC. - Returns all rows that match the search stream query. + Returns all rows that match the search stream query. Streams + batches of rows over a single connection without pagination + overhead, making it preferred over + [GoogleAdsService.Search][google.ads.googleads.v26.services.GoogleAdsService.Search] + for large result sets (> 10,000 rows). To build and validate + queries, use the `Google Ads Query + Builder `__. List of thrown errors: `AuthenticationError <>`__ `AuthorizationError <>`__ `ChangeEventError <>`__ diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/__init__.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/__init__.py new file mode 100644 index 000000000..061d43b58 --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from .client import LocalServicesEligibilityServiceClient +from .async_client import LocalServicesEligibilityServiceAsyncClient + +__all__ = ( + "LocalServicesEligibilityServiceClient", + "LocalServicesEligibilityServiceAsyncClient", +) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/async_client.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/async_client.py new file mode 100644 index 000000000..0d70ef536 --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/async_client.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import logging as std_logging +from typing import Callable, Optional, Sequence, Tuple, Union + +from google.ads.googleads.v26 import gapic_version as package_version + +from google.api_core.client_options import ClientOptions +from google.api_core import gapic_v1 +from google.api_core import retry_async as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + + +try: + OptionalRetry = Union[ + retries.AsyncRetry, gapic_v1.method._MethodDefault, None + ] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.AsyncRetry, object, None] # type: ignore + +from google.ads.googleads.v26.services.types import ( + local_services_eligibility_service, +) +from .transports.base import ( + LocalServicesEligibilityServiceTransport, + DEFAULT_CLIENT_INFO, +) +from .client import LocalServicesEligibilityServiceClient + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class LocalServicesEligibilityServiceAsyncClient: + """This service allows eligibility checking and discovery of + category/job type details for Local Services. + """ + + _client: LocalServicesEligibilityServiceClient + + # Copy defaults from the synchronous client for use here. + # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. + DEFAULT_ENDPOINT = LocalServicesEligibilityServiceClient.DEFAULT_ENDPOINT + DEFAULT_MTLS_ENDPOINT = ( + LocalServicesEligibilityServiceClient.DEFAULT_MTLS_ENDPOINT + ) + _DEFAULT_ENDPOINT_TEMPLATE = ( + LocalServicesEligibilityServiceClient._DEFAULT_ENDPOINT_TEMPLATE + ) + _DEFAULT_UNIVERSE = LocalServicesEligibilityServiceClient._DEFAULT_UNIVERSE + + common_billing_account_path = staticmethod( + LocalServicesEligibilityServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + LocalServicesEligibilityServiceClient.parse_common_billing_account_path + ) + common_folder_path = staticmethod( + LocalServicesEligibilityServiceClient.common_folder_path + ) + parse_common_folder_path = staticmethod( + LocalServicesEligibilityServiceClient.parse_common_folder_path + ) + common_organization_path = staticmethod( + LocalServicesEligibilityServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + LocalServicesEligibilityServiceClient.parse_common_organization_path + ) + common_project_path = staticmethod( + LocalServicesEligibilityServiceClient.common_project_path + ) + parse_common_project_path = staticmethod( + LocalServicesEligibilityServiceClient.parse_common_project_path + ) + common_location_path = staticmethod( + LocalServicesEligibilityServiceClient.common_location_path + ) + parse_common_location_path = staticmethod( + LocalServicesEligibilityServiceClient.parse_common_location_path + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + LocalServicesEligibilityServiceAsyncClient: The constructed client. + """ + sa_info_func = ( + LocalServicesEligibilityServiceClient.from_service_account_info.__func__ # type: ignore + ) + return sa_info_func( + LocalServicesEligibilityServiceAsyncClient, info, *args, **kwargs + ) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + LocalServicesEligibilityServiceAsyncClient: The constructed client. + """ + sa_file_func = ( + LocalServicesEligibilityServiceClient.from_service_account_file.__func__ # type: ignore + ) + return sa_file_func( + LocalServicesEligibilityServiceAsyncClient, + filename, + *args, + **kwargs, + ) + + from_service_account_json = from_service_account_file + + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variable is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return LocalServicesEligibilityServiceClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + + @property + def transport(self) -> LocalServicesEligibilityServiceTransport: + """Returns the transport used by the client instance. + + Returns: + LocalServicesEligibilityServiceTransport: The transport used by the client instance. + """ + return self._client.transport + + @property + def api_endpoint(self) -> str: + """Return the API endpoint used by the client instance. + + Returns: + str: The API endpoint used by the client instance. + """ + return self._client._api_endpoint + + @property + def universe_domain(self) -> str: + """Return the universe domain used by the client instance. + + Returns: + str: The universe domain used + by the client instance. + """ + return self._client._universe_domain + + get_transport_class = ( + LocalServicesEligibilityServiceClient.get_transport_class + ) + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Optional[ + Union[ + str, + LocalServicesEligibilityServiceTransport, + Callable[..., LocalServicesEligibilityServiceTransport], + ] + ] = "grpc_asyncio", + client_options: Optional[ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the local services eligibility service async client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Optional[Union[str,LocalServicesEligibilityServiceTransport,Callable[..., LocalServicesEligibilityServiceTransport]]]): + The transport to use, or a Callable that constructs and returns a new transport to use. + If a Callable is given, it will be called with the same set of initialization + arguments as used in the LocalServicesEligibilityServiceTransport constructor. + If set to None, a transport is chosen automatically. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client. + + 1. The ``api_endpoint`` property can be used to override the + default endpoint provided by the client when ``transport`` is + not explicitly provided. Only if this property is not set and + ``transport`` was not explicitly provided, the endpoint is + determined by the GOOGLE_API_USE_MTLS_ENDPOINT environment + variable, which have one of the following values: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto-switch to the + default mTLS endpoint if client certificate is present; this is + the default value). + + 2. If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide a client certificate for mTLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + 3. The ``universe_domain`` property can be used to override the + default "googleapis.com" universe. Note that ``api_endpoint`` + property still takes precedence; and ``universe_domain`` is + currently not supported for mTLS. + + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client = LocalServicesEligibilityServiceClient( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ): # pragma: NO COVER + _LOGGER.debug( + "Created client `google.ads.googleads.v26.services.LocalServicesEligibilityServiceAsyncClient`.", + extra=( + { + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "universeDomain": getattr( + self._client._transport._credentials, + "universe_domain", + "", + ), + "credentialsType": f"{type(self._client._transport._credentials).__module__}.{type(self._client._transport._credentials).__qualname__}", + "credentialsInfo": getattr( + self.transport._credentials, + "get_cred_info", + lambda: None, + )(), + } + if hasattr(self._client._transport, "_credentials") + else { + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "credentialsType": None, + } + ), + ) + + async def check_local_services_eligibility( + self, + request: Optional[ + Union[ + local_services_eligibility_service.CheckLocalServicesEligibilityRequest, + dict, + ] + ] = None, + *, + customer_id: Optional[str] = None, + listing_id: Optional[str] = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Union[float, object] = gapic_v1.method.DEFAULT, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> ( + local_services_eligibility_service.CheckLocalServicesEligibilityResponse + ): + r"""Returns the Local Services eligibility status and + valid categories for a given Customer and GBP Listing + ID. + + Args: + request (Optional[Union[google.ads.googleads.v26.services.types.CheckLocalServicesEligibilityRequest, dict]]): + The request object. Request message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + customer_id (:class:`str`): + Required. The ID of the customer + whose eligibility is being checked. + + This corresponds to the ``customer_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + listing_id (:class:`str`): + Required. The Google Business Profile + listing ID to check eligibility for. + + This corresponds to the ``listing_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + google.ads.googleads.v26.services.types.CheckLocalServicesEligibilityResponse: + Response message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + + """ + # Create or coerce a protobuf request object. + # - Quick check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + flattened_params = [customer_id, listing_id] + has_flattened_params = ( + len([param for param in flattened_params if param is not None]) > 0 + ) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # - Use the request object if provided (there's no risk of modifying the input as + # there are no flattened fields), or create one. + if not isinstance( + request, + local_services_eligibility_service.CheckLocalServicesEligibilityRequest, + ): + request = local_services_eligibility_service.CheckLocalServicesEligibilityRequest( + request + ) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if customer_id is not None: + request.customer_id = customer_id + if listing_id is not None: + request.listing_id = listing_id + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._client._transport._wrapped_methods[ + self._client._transport.check_local_services_eligibility + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("customer_id", request.customer_id),) + ), + ) + + # Validate the universe domain. + self._client._validate_universe_domain() + + # Send the request. + response = await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # Done; return the response. + return response + + async def __aenter__(self) -> "LocalServicesEligibilityServiceAsyncClient": + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.transport.close() + + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +__all__ = ("LocalServicesEligibilityServiceAsyncClient",) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/client.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/client.py new file mode 100644 index 000000000..7cb5610bc --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/client.py @@ -0,0 +1,740 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from collections import OrderedDict +from http import HTTPStatus +import json +import logging as std_logging +import os +import re +from typing import Dict, Callable, Optional, Sequence, Tuple, Type, Union, cast +import warnings + +from google.ads.googleads.v26 import gapic_version as package_version + +from google.api_core import client_options as client_options_lib +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.ads.googleads.v26._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, +) +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object, None] # type: ignore + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + +from google.ads.googleads.v26.services.types import ( + local_services_eligibility_service, +) +from .transports.base import ( + LocalServicesEligibilityServiceTransport, + DEFAULT_CLIENT_INFO, +) +from .transports.grpc import LocalServicesEligibilityServiceGrpcTransport +from .transports.grpc_asyncio import ( + LocalServicesEligibilityServiceGrpcAsyncIOTransport, +) + + +class LocalServicesEligibilityServiceClientMeta(type): + """Metaclass for the LocalServicesEligibilityService client. + + This provides class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = ( + OrderedDict() + ) # type: Dict[str, Type[LocalServicesEligibilityServiceTransport]] + _transport_registry["grpc"] = LocalServicesEligibilityServiceGrpcTransport + _transport_registry["grpc_asyncio"] = ( + LocalServicesEligibilityServiceGrpcAsyncIOTransport + ) + + def get_transport_class( + cls, + label: Optional[str] = None, + ) -> Type[LocalServicesEligibilityServiceTransport]: + """Returns an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class LocalServicesEligibilityServiceClient( + metaclass=LocalServicesEligibilityServiceClientMeta +): + """This service allows eligibility checking and discovery of + category/job type details for Local Services. + """ + + # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. + DEFAULT_ENDPOINT = "googleads.googleapis.com" + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint(DEFAULT_ENDPOINT) + + _DEFAULT_ENDPOINT_TEMPLATE = "googleads.{UNIVERSE_DOMAIN}" + _DEFAULT_UNIVERSE = "googleapis.com" + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + LocalServicesEligibilityServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info( + info + ) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + LocalServicesEligibilityServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file( + filename + ) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> LocalServicesEligibilityServiceTransport: + """Returns the transport used by the client instance. + + Returns: + LocalServicesEligibilityServiceTransport: The transport used by the client + instance. + """ + return self._transport + + @staticmethod + def common_billing_account_path( + billing_account: str, + ) -> str: + """Returns a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path( + folder: str, + ) -> str: + """Returns a fully-qualified folder string.""" + return "folders/{folder}".format( + folder=folder, + ) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path( + organization: str, + ) -> str: + """Returns a fully-qualified organization string.""" + return "organizations/{organization}".format( + organization=organization, + ) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path( + project: str, + ) -> str: + """Returns a fully-qualified project string.""" + return "projects/{project}".format( + project=project, + ) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path( + project: str, + location: str, + ) -> str: + """Returns a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, + location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)$", path + ) + return m.groupdict() if m else {} + + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Deprecated. Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variable is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + + warnings.warn( + "get_mtls_endpoint_and_cert_source is deprecated. Use the api_endpoint property instead.", + DeprecationWarning, + ) + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = should_use_client_cert() + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert: + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT # type: ignore + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + + @staticmethod + def _get_client_cert_source(provided_cert_source, use_cert_flag): + """Return the client cert source to be used by the client. + + Args: + provided_cert_source (bytes): The client certificate source provided. + use_cert_flag (bool): A flag indicating whether to use the client certificate. + + Returns: + bytes or None: The client cert source to be used by the client. + """ + client_cert_source = None + if use_cert_flag: + if provided_cert_source: + client_cert_source = provided_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + return client_cert_source + + def _validate_universe_domain(self): + """Validates client's and credentials' universe domains are consistent. + + Returns: + bool: True iff the configured universe domain is valid. + + Raises: + ValueError: If the configured universe domain is not valid. + """ + + # NOTE (b/349488459): universe validation is disabled until further notice. + return True + + def _add_cred_info_for_auth_errors( + self, error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [ + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + HTTPStatus.NOT_FOUND, + ]: + return + + cred = self._transport._credentials + + # get_cred_info is only available in google-auth>=2.35.0 + if not hasattr(cred, "get_cred_info"): + return + + # ignore the type check since pypy test fails when get_cred_info + # is not available + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + + @property + def api_endpoint(self) -> str: + """Return the API endpoint used by the client instance. + + Returns: + str: The API endpoint used by the client instance. + """ + return self._api_endpoint + + @property + def universe_domain(self) -> str: + """Return the universe domain used by the client instance. + + Returns: + str: The universe domain used by the client instance. + """ + return self._universe_domain + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Optional[ + Union[ + str, + LocalServicesEligibilityServiceTransport, + Callable[..., LocalServicesEligibilityServiceTransport], + ] + ] = None, + client_options: Optional[ + Union[client_options_lib.ClientOptions, dict] + ] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the local services eligibility service client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Optional[Union[str,LocalServicesEligibilityServiceTransport,Callable[..., LocalServicesEligibilityServiceTransport]]]): + The transport to use, or a Callable that constructs and returns a new transport. + If a Callable is given, it will be called with the same set of initialization + arguments as used in the LocalServicesEligibilityServiceTransport constructor. + If set to None, a transport is chosen automatically. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client. + + 1. The ``api_endpoint`` property can be used to override the + default endpoint provided by the client when ``transport`` is + not explicitly provided. Only if this property is not set and + ``transport`` was not explicitly provided, the endpoint is + determined by the GOOGLE_API_USE_MTLS_ENDPOINT environment + variable, which have one of the following values: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto-switch to the + default mTLS endpoint if client certificate is present; this is + the default value). + + 2. If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide a client certificate for mTLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + 3. The ``universe_domain`` property can be used to override the + default "googleapis.com" universe. Note that the ``api_endpoint`` + property still takes precedence; and ``universe_domain`` is + currently not supported for mTLS. + + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client_options = client_options + if isinstance(self._client_options, dict): + self._client_options = client_options_lib.from_dict( + self._client_options + ) + if self._client_options is None: + self._client_options = client_options_lib.ClientOptions() + self._client_options = cast( + client_options_lib.ClientOptions, self._client_options + ) + + universe_domain_opt = getattr( + self._client_options, "universe_domain", None + ) + + ( + self._use_client_cert, + self._use_mtls_endpoint, + self._universe_domain_env, + ) = read_environment_variables() + self._client_cert_source = ( + LocalServicesEligibilityServiceClient._get_client_cert_source( + self._client_options.client_cert_source, self._use_client_cert + ) + ) + self._universe_domain = get_universe_domain( + universe_domain_opt, + self._universe_domain_env, + default_universe=LocalServicesEligibilityServiceClient._DEFAULT_UNIVERSE, + ) + self._api_endpoint: str = "" # updated below, depending on `transport` + + # Initialize the universe domain validation. + self._is_universe_domain_valid = False + + if CLIENT_LOGGING_SUPPORTED: # pragma: NO COVER + # Setup logging. + client_logging.initialize_logging() + + api_key_value = getattr(self._client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + transport_provided = isinstance( + transport, LocalServicesEligibilityServiceTransport + ) + if transport_provided: + # transport is a LocalServicesEligibilityServiceTransport instance. + if ( + credentials + or self._client_options.credentials_file + or api_key_value + ): + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if self._client_options.scopes: + raise ValueError( + "When providing a transport instance, provide its scopes " + "directly." + ) + self._transport = cast( + LocalServicesEligibilityServiceTransport, transport + ) + self._api_endpoint = self._transport.host + + self._api_endpoint = self._api_endpoint or get_api_endpoint( + api_override=self._client_options.api_endpoint, + universe_domain=self._universe_domain, + default_universe=LocalServicesEligibilityServiceClient._DEFAULT_UNIVERSE, + default_mtls_endpoint=LocalServicesEligibilityServiceClient.DEFAULT_MTLS_ENDPOINT, + default_endpoint_template=LocalServicesEligibilityServiceClient._DEFAULT_ENDPOINT_TEMPLATE, + use_mtls=self._use_mtls_endpoint == "always" + or (self._use_mtls_endpoint == "auto" and self._client_cert_source), + ) + + if not transport_provided: + import google.auth._default # type: ignore + + if api_key_value and hasattr( + google.auth._default, "get_api_key_credentials" + ): + credentials = google.auth._default.get_api_key_credentials( + api_key_value + ) + + transport_init: Union[ + Type[LocalServicesEligibilityServiceTransport], + Callable[..., LocalServicesEligibilityServiceTransport], + ] = ( + LocalServicesEligibilityServiceClient.get_transport_class( + transport + ) + if isinstance(transport, str) or transport is None + else cast( + Callable[..., LocalServicesEligibilityServiceTransport], + transport, + ) + ) + # initialize with the provided callable or the passed in class + self._transport = transport_init( + credentials=credentials, + credentials_file=self._client_options.credentials_file, + host=self._api_endpoint, + scopes=self._client_options.scopes, + client_cert_source_for_mtls=self._client_cert_source, + quota_project_id=self._client_options.quota_project_id, + client_info=client_info, + always_use_jwt_access=True, + api_audience=self._client_options.api_audience, + ) + + if "async" not in str(self._transport): + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ): # pragma: NO COVER + _LOGGER.debug( + "Created client `google.ads.googleads.v26.services.LocalServicesEligibilityServiceClient`.", + extra=( + { + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "universeDomain": getattr( + self._transport._credentials, + "universe_domain", + "", + ), + "credentialsType": f"{type(self._transport._credentials).__module__}.{type(self._transport._credentials).__qualname__}", + "credentialsInfo": getattr( + self.transport._credentials, + "get_cred_info", + lambda: None, + )(), + } + if hasattr(self._transport, "_credentials") + else { + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "credentialsType": None, + } + ), + ) + + def check_local_services_eligibility( + self, + request: Optional[ + Union[ + local_services_eligibility_service.CheckLocalServicesEligibilityRequest, + dict, + ] + ] = None, + *, + customer_id: Optional[str] = None, + listing_id: Optional[str] = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Union[float, object] = gapic_v1.method.DEFAULT, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> ( + local_services_eligibility_service.CheckLocalServicesEligibilityResponse + ): + r"""Returns the Local Services eligibility status and + valid categories for a given Customer and GBP Listing + ID. + + Args: + request (Union[google.ads.googleads.v26.services.types.CheckLocalServicesEligibilityRequest, dict]): + The request object. Request message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + customer_id (str): + Required. The ID of the customer + whose eligibility is being checked. + + This corresponds to the ``customer_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + listing_id (str): + Required. The Google Business Profile + listing ID to check eligibility for. + + This corresponds to the ``listing_id`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + google.ads.googleads.v26.services.types.CheckLocalServicesEligibilityResponse: + Response message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + + """ + # Create or coerce a protobuf request object. + # - Quick check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + flattened_params = [customer_id, listing_id] + has_flattened_params = ( + len([param for param in flattened_params if param is not None]) > 0 + ) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # - Use the request object if provided (there's no risk of modifying the input as + # there are no flattened fields), or create one. + if not isinstance( + request, + local_services_eligibility_service.CheckLocalServicesEligibilityRequest, + ): + request = local_services_eligibility_service.CheckLocalServicesEligibilityRequest( + request + ) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if customer_id is not None: + request.customer_id = customer_id + if listing_id is not None: + request.listing_id = listing_id + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[ + self._transport.check_local_services_eligibility + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("customer_id", request.customer_id),) + ), + ) + + # Validate the universe domain. + self._validate_universe_domain() + + # Send the request. + response = rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # Done; return the response. + return response + + def __enter__(self) -> "LocalServicesEligibilityServiceClient": + return self + + def __exit__(self, type, value, traceback): + """Releases underlying transport's resources. + + .. warning:: + ONLY use as a context manager if the transport is NOT shared + with other clients! Exiting the with block will CLOSE the transport + and may cause errors in other clients! + """ + self.transport.close() + + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + +__all__ = ("LocalServicesEligibilityServiceClient",) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/README.rst b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/README.rst new file mode 100644 index 000000000..1d75c3aea --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/README.rst @@ -0,0 +1,10 @@ + +transport inheritance structure +_______________________________ + +``LocalServicesEligibilityServiceTransport`` is the ABC for all transports. + +- public child ``LocalServicesEligibilityServiceGrpcTransport`` for sync gRPC transport (defined in ``grpc.py``). +- public child ``LocalServicesEligibilityServiceGrpcAsyncIOTransport`` for async gRPC transport (defined in ``grpc_asyncio.py``). +- private child ``_BaseLocalServicesEligibilityServiceRestTransport`` for base REST transport with inner classes ``_BaseMETHOD`` (defined in ``rest_base.py``). +- public child ``LocalServicesEligibilityServiceRestTransport`` for sync REST transport with inner classes ``METHOD`` derived from the parent's corresponding ``_BaseMETHOD`` classes (defined in ``rest.py``). diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/__init__.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/__init__.py new file mode 100644 index 000000000..bd44d724c --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from collections import OrderedDict + +from .base import LocalServicesEligibilityServiceTransport +from .grpc import LocalServicesEligibilityServiceGrpcTransport +from .grpc_asyncio import LocalServicesEligibilityServiceGrpcAsyncIOTransport + + +# Compile a registry of transports. +_transport_registry = ( + OrderedDict() +) # type: Dict[str, Type[LocalServicesEligibilityServiceTransport]] +_transport_registry["grpc"] = LocalServicesEligibilityServiceGrpcTransport +_transport_registry["grpc_asyncio"] = ( + LocalServicesEligibilityServiceGrpcAsyncIOTransport +) + +__all__ = ( + "LocalServicesEligibilityServiceTransport", + "LocalServicesEligibilityServiceGrpcTransport", + "LocalServicesEligibilityServiceGrpcAsyncIOTransport", +) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/base.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/base.py new file mode 100644 index 000000000..f9ec67984 --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/base.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import abc +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union + +from google.ads.googleads.v26 import gapic_version as package_version + +import google.auth # type: ignore +import google.api_core +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore +import google.protobuf + +from google.ads.googleads.v26.services.types import ( + local_services_eligibility_service, +) + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=package_version.__version__ +) +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +class LocalServicesEligibilityServiceTransport(abc.ABC): + """Abstract transport class for LocalServicesEligibilityService.""" + + AUTH_SCOPES = ("https://www.googleapis.com/auth/adwords",) + + DEFAULT_HOST: str = "googleads.googleapis.com" + + def __init__( + self, + *, + host: str = DEFAULT_HOST, + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + **kwargs, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A list of scopes. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + """ + + # Save the scopes. + self._scopes = scopes + if not hasattr(self, "_ignore_credentials"): + self._ignore_credentials: bool = False + + # If no credentials are provided, then determine the appropriate + # defaults. + if credentials and credentials_file: + raise core_exceptions.DuplicateCredentialArgs( + "'credentials_file' and 'credentials' are mutually exclusive" + ) + + if credentials_file is not None: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + default_scopes=self.AUTH_SCOPES, + ) + elif credentials is None and not self._ignore_credentials: + credentials, _ = google.auth.default( + scopes=scopes, + quota_project_id=quota_project_id, + default_scopes=self.AUTH_SCOPES, + ) + # Don't apply audience if the credentials file passed from user. + if hasattr(credentials, "with_gdch_audience"): + credentials = credentials.with_gdch_audience( + api_audience if api_audience else host + ) + + # If the credentials are service account credentials, then always try to use self signed JWT. + if ( + always_use_jwt_access + and isinstance(credentials, service_account.Credentials) + and hasattr( + service_account.Credentials, "with_always_use_jwt_access" + ) + ): + credentials = credentials.with_always_use_jwt_access(True) + + # Save the credentials. + self._credentials = credentials + + # Save the hostname. Default to port 443 (HTTPS) if none is specified. + if ":" not in host: + host += ":443" + self._host = host + + self._wrapped_methods: Dict[Callable, Callable] = {} + + @property + def host(self): + return self._host + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.check_local_services_eligibility: gapic_v1.method.wrap_method( + self.check_local_services_eligibility, + default_timeout=None, + client_info=client_info, + ), + } + + def close(self): + """Closes resources associated with the transport. + + .. warning:: + Only call this method if the transport is NOT shared + with other clients - this may cause errors in other clients! + """ + raise NotImplementedError() + + @property + def check_local_services_eligibility( + self, + ) -> Callable[ + [ + local_services_eligibility_service.CheckLocalServicesEligibilityRequest + ], + Union[ + local_services_eligibility_service.CheckLocalServicesEligibilityResponse, + Awaitable[ + local_services_eligibility_service.CheckLocalServicesEligibilityResponse + ], + ], + ]: + raise NotImplementedError() + + @property + def kind(self) -> str: + raise NotImplementedError() + + +__all__ = ("LocalServicesEligibilityServiceTransport",) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc.py new file mode 100644 index 000000000..9a744eae7 --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import logging as std_logging +import pickle +import warnings +from typing import Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import grpc_helpers +from google.api_core import gapic_v1 +import google.auth # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.protobuf.json_format import MessageToJson +import google.protobuf.message + +import grpc # type: ignore +import proto # type: ignore + +from google.ads.googleads.v26.services.types import ( + local_services_eligibility_service, +) +from .base import LocalServicesEligibilityServiceTransport, DEFAULT_CLIENT_INFO + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class _LoggingClientInterceptor( + grpc.UnaryUnaryClientInterceptor +): # pragma: NO COVER + def intercept_unary_unary(self, continuation, client_call_details, request): + logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ) + if logging_enabled: # pragma: NO COVER + request_metadata = client_call_details.metadata + if isinstance(request, proto.Message): + request_payload = type(request).to_json(request) + elif isinstance(request, google.protobuf.message.Message): + request_payload = MessageToJson(request) + else: + request_payload = ( + f"{type(request).__name__}: {pickle.dumps(request)!r}" + ) + + request_metadata = { + key: ( + value.decode("utf-8") if isinstance(value, bytes) else value + ) + for key, value in request_metadata + } + grpc_request = { + "payload": request_payload, + "requestMethod": "grpc", + "metadata": dict(request_metadata), + } + _LOGGER.debug( + f"Sending request for {client_call_details.method}", + extra={ + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "rpcName": str(client_call_details.method), + "request": grpc_request, + "metadata": grpc_request["metadata"], + }, + ) + response = continuation(client_call_details, request) + if logging_enabled: # pragma: NO COVER + response_metadata = response.trailing_metadata() + # Convert gRPC metadata `` to list of tuples + metadata = ( + dict([(k, str(v)) for k, v in response_metadata]) + if response_metadata + else None + ) + result = response.result() + if isinstance(result, proto.Message): + response_payload = type(result).to_json(result) + elif isinstance(result, google.protobuf.message.Message): + response_payload = MessageToJson(result) + else: + response_payload = ( + f"{type(result).__name__}: {pickle.dumps(result)!r}" + ) + grpc_response = { + "payload": response_payload, + "metadata": metadata, + "status": "OK", + } + _LOGGER.debug( + f"Received response for {client_call_details.method}.", + extra={ + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "rpcName": client_call_details.method, + "response": grpc_response, + "metadata": grpc_response["metadata"], + }, + ) + return response + + +class LocalServicesEligibilityServiceGrpcTransport( + LocalServicesEligibilityServiceTransport +): + """gRPC backend transport for LocalServicesEligibilityService. + + This service allows eligibility checking and discovery of + category/job type details for Local Services. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _stubs: Dict[str, Callable] + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: Optional[ + Union[grpc.Channel, Callable[..., grpc.Channel]] + ] = None, + api_mtls_endpoint: Optional[str] = None, + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + ssl_channel_credentials: Optional[grpc.ChannelCredentials] = None, + client_cert_source_for_mtls: Optional[ + Callable[[], Tuple[bytes, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if a ``channel`` instance is provided. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. + scopes (Optional(Sequence[str])): A list of scopes. This argument is + ignored if a ``channel`` instance is provided. + channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): + A ``Channel`` instance through which to make calls, or a Callable + that constructs and returns one. If set to None, ``self.create_channel`` + is used to create the channel. If a Callable is given, it will be called + with the same arguments as used in ``self.create_channel``. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or application default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for the grpc channel. It is ignored if a ``channel`` instance is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure a mutual TLS channel. It is + ignored if a ``channel`` instance or ``ssl_channel_credentials`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn( + "client_cert_source is deprecated", DeprecationWarning + ) + + if isinstance(channel, grpc.Channel): + # Ignore credentials if a channel was passed. + credentials = None + self._ignore_credentials = True + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + else: + self._ssl_channel_credentials = ( + SslCredentials().ssl_credentials + ) + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + + if not self._grpc_channel: + # initialize with the provided callable or the default channel + channel_init = channel or type(self).create_channel + self._grpc_channel = channel_init( + self._host, + # use the credentials which are saved + credentials=self._credentials, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + self._interceptor = _LoggingClientInterceptor() + self._logged_channel = grpc.intercept_channel( + self._grpc_channel, self._interceptor + ) + + # Wrap messages. This must be done after self._logged_channel exists + self._prep_wrapped_messages(client_info) + + @classmethod + def create_channel( + cls, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> grpc.Channel: + """Create and return a gRPC channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + grpc.Channel: A gRPC channel object. + + Raises: + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + + return grpc_helpers.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + @property + def grpc_channel(self) -> grpc.Channel: + """Return the channel designed to connect to this service.""" + return self._grpc_channel + + @property + def check_local_services_eligibility( + self, + ) -> Callable[ + [ + local_services_eligibility_service.CheckLocalServicesEligibilityRequest + ], + local_services_eligibility_service.CheckLocalServicesEligibilityResponse, + ]: + r"""Return a callable for the check local services + eligibility method over gRPC. + + Returns the Local Services eligibility status and + valid categories for a given Customer and GBP Listing + ID. + + Returns: + Callable[[~.CheckLocalServicesEligibilityRequest], + ~.CheckLocalServicesEligibilityResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "check_local_services_eligibility" not in self._stubs: + self._stubs["check_local_services_eligibility"] = ( + self._logged_channel.unary_unary( + "/google.ads.googleads.v26.services.LocalServicesEligibilityService/CheckLocalServicesEligibility", + request_serializer=local_services_eligibility_service.CheckLocalServicesEligibilityRequest.serialize, + response_deserializer=local_services_eligibility_service.CheckLocalServicesEligibilityResponse.deserialize, + ) + ) + return self._stubs["check_local_services_eligibility"] + + def close(self): + self._logged_channel.close() + + @property + def kind(self) -> str: + return "grpc" + + +__all__ = ("LocalServicesEligibilityServiceGrpcTransport",) diff --git a/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc_asyncio.py new file mode 100644 index 000000000..b64c7fd81 --- /dev/null +++ b/google/ads/googleads/v26/services/services/local_services_eligibility_service/transports/grpc_asyncio.py @@ -0,0 +1,420 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import inspect +import pickle +import logging as std_logging +import warnings +from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import gapic_v1 +from google.api_core import grpc_helpers_async +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.protobuf.json_format import MessageToJson +import google.protobuf.message + +import grpc # type: ignore +import proto # type: ignore +from grpc.experimental import aio # type: ignore + +from google.ads.googleads.v26.services.types import ( + local_services_eligibility_service, +) +from .base import LocalServicesEligibilityServiceTransport, DEFAULT_CLIENT_INFO + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = std_logging.getLogger(__name__) + + +class _LoggingClientAIOInterceptor( + grpc.aio.UnaryUnaryClientInterceptor +): # pragma: NO COVER + async def intercept_unary_unary( + self, continuation, client_call_details, request + ): + logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + std_logging.DEBUG + ) + if logging_enabled: # pragma: NO COVER + request_metadata = client_call_details.metadata + if isinstance(request, proto.Message): + request_payload = type(request).to_json(request) + elif isinstance(request, google.protobuf.message.Message): + request_payload = MessageToJson(request) + else: + request_payload = ( + f"{type(request).__name__}: {pickle.dumps(request)!r}" + ) + + request_metadata = { + key: ( + value.decode("utf-8") if isinstance(value, bytes) else value + ) + for key, value in request_metadata + } + grpc_request = { + "payload": request_payload, + "requestMethod": "grpc", + "metadata": dict(request_metadata), + } + _LOGGER.debug( + f"Sending request for {client_call_details.method}", + extra={ + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "rpcName": str(client_call_details.method), + "request": grpc_request, + "metadata": grpc_request["metadata"], + }, + ) + response = await continuation(client_call_details, request) + if logging_enabled: # pragma: NO COVER + response_metadata = await response.trailing_metadata() + # Convert gRPC metadata `` to list of tuples + metadata = ( + dict([(k, str(v)) for k, v in response_metadata]) + if response_metadata + else None + ) + result = await response + if isinstance(result, proto.Message): + response_payload = type(result).to_json(result) + elif isinstance(result, google.protobuf.message.Message): + response_payload = MessageToJson(result) + else: + response_payload = ( + f"{type(result).__name__}: {pickle.dumps(result)!r}" + ) + grpc_response = { + "payload": response_payload, + "metadata": metadata, + "status": "OK", + } + _LOGGER.debug( + f"Received response to rpc {client_call_details.method}.", + extra={ + "serviceName": "google.ads.googleads.v26.services.LocalServicesEligibilityService", + "rpcName": str(client_call_details.method), + "response": grpc_response, + "metadata": grpc_response["metadata"], + }, + ) + return response + + +class LocalServicesEligibilityServiceGrpcAsyncIOTransport( + LocalServicesEligibilityServiceTransport +): + """gRPC AsyncIO backend transport for LocalServicesEligibilityService. + + This service allows eligibility checking and discovery of + category/job type details for Local Services. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _grpc_channel: aio.Channel + _stubs: Dict[str, Callable] = {} + + @classmethod + def create_channel( + cls, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> aio.Channel: + """Create and return a gRPC AsyncIO channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. This argument will be + removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + aio.Channel: A gRPC AsyncIO channel object. + """ + + return grpc_helpers_async.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: Optional[ + Union[aio.Channel, Callable[..., aio.Channel]] + ] = None, + api_mtls_endpoint: Optional[str] = None, + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + ssl_channel_credentials: Optional[grpc.ChannelCredentials] = None, + client_cert_source_for_mtls: Optional[ + Callable[[], Tuple[bytes, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if a ``channel`` instance is provided. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + channel (Optional[Union[aio.Channel, Callable[..., aio.Channel]]]): + A ``Channel`` instance through which to make calls, or a Callable + that constructs and returns one. If set to None, ``self.create_channel`` + is used to create the channel. If a Callable is given, it will be called + with the same arguments as used in ``self.create_channel``. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or application default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for the grpc channel. It is ignored if a ``channel`` instance is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure a mutual TLS channel. It is + ignored if a ``channel`` instance or ``ssl_channel_credentials`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn( + "client_cert_source is deprecated", DeprecationWarning + ) + + if isinstance(channel, aio.Channel): + # Ignore credentials if a channel was passed. + credentials = None + self._ignore_credentials = True + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + else: + self._ssl_channel_credentials = ( + SslCredentials().ssl_credentials + ) + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = ( + grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + + if not self._grpc_channel: + # initialize with the provided callable or the default channel + channel_init = channel or type(self).create_channel + self._grpc_channel = channel_init( + self._host, + # use the credentials which are saved + credentials=self._credentials, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + self._interceptor = _LoggingClientAIOInterceptor() + self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + self._logged_channel = self._grpc_channel + self._wrap_with_kind = ( + "kind" + in inspect.signature(gapic_v1.method_async.wrap_method).parameters + ) + # Wrap messages. This must be done after self._logged_channel exists + self._prep_wrapped_messages(client_info) + + @property + def grpc_channel(self) -> aio.Channel: + """Create the channel designed to connect to this service. + + This property caches on the instance; repeated calls return + the same channel. + """ + # Return the channel from cache. + return self._grpc_channel + + @property + def check_local_services_eligibility( + self, + ) -> Callable[ + [ + local_services_eligibility_service.CheckLocalServicesEligibilityRequest + ], + Awaitable[ + local_services_eligibility_service.CheckLocalServicesEligibilityResponse + ], + ]: + r"""Return a callable for the check local services + eligibility method over gRPC. + + Returns the Local Services eligibility status and + valid categories for a given Customer and GBP Listing + ID. + + Returns: + Callable[[~.CheckLocalServicesEligibilityRequest], + Awaitable[~.CheckLocalServicesEligibilityResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "check_local_services_eligibility" not in self._stubs: + self._stubs["check_local_services_eligibility"] = ( + self._logged_channel.unary_unary( + "/google.ads.googleads.v26.services.LocalServicesEligibilityService/CheckLocalServicesEligibility", + request_serializer=local_services_eligibility_service.CheckLocalServicesEligibilityRequest.serialize, + response_deserializer=local_services_eligibility_service.CheckLocalServicesEligibilityResponse.deserialize, + ) + ) + return self._stubs["check_local_services_eligibility"] + + def _prep_wrapped_messages(self, client_info): + """Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + self._wrapped_methods = { + self.check_local_services_eligibility: self._wrap_method( + self.check_local_services_eligibility, + default_timeout=None, + client_info=client_info, + ), + } + + def _wrap_method(self, func, *args, **kwargs): + if self._wrap_with_kind: # pragma: NO COVER + kwargs["kind"] = self.kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + + def close(self): + return self._logged_channel.close() + + @property + def kind(self) -> str: + return "grpc_asyncio" + + +__all__ = ("LocalServicesEligibilityServiceGrpcAsyncIOTransport",) diff --git a/google/ads/googleads/v26/services/services/offline_user_data_job_service/async_client.py b/google/ads/googleads/v26/services/services/offline_user_data_job_service/async_client.py index 3949cfa8f..da2f76d1e 100644 --- a/google/ads/googleads/v26/services/services/offline_user_data_job_service/async_client.py +++ b/google/ads/googleads/v26/services/services/offline_user_data_job_service/async_client.py @@ -58,7 +58,14 @@ class OfflineUserDataJobServiceAsyncClient: - """Service to manage offline user data jobs.""" + """Service to manage offline user data jobs. For help diagnosing and + resolving conversion or user data upload issues, see the conversion + upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). + """ _client: OfflineUserDataJobServiceClient diff --git a/google/ads/googleads/v26/services/services/offline_user_data_job_service/client.py b/google/ads/googleads/v26/services/services/offline_user_data_job_service/client.py index 7c85f0f3a..17d922138 100644 --- a/google/ads/googleads/v26/services/services/offline_user_data_job_service/client.py +++ b/google/ads/googleads/v26/services/services/offline_user_data_job_service/client.py @@ -125,7 +125,14 @@ def get_transport_class( class OfflineUserDataJobServiceClient( metaclass=OfflineUserDataJobServiceClientMeta ): - """Service to manage offline user data jobs.""" + """Service to manage offline user data jobs. For help diagnosing and + resolving conversion or user data upload issues, see the conversion + upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). + """ # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "googleads.googleapis.com" diff --git a/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc.py b/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc.py index 66ceb4d5d..3ca0b5582 100644 --- a/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc.py +++ b/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc.py @@ -124,7 +124,13 @@ class OfflineUserDataJobServiceGrpcTransport( ): """gRPC backend transport for OfflineUserDataJobService. - Service to manage offline user data jobs. + Service to manage offline user data jobs. For help diagnosing and + resolving conversion or user data upload issues, see the conversion + upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation diff --git a/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc_asyncio.py index 4f63c1741..ed1a8fdf9 100644 --- a/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc_asyncio.py +++ b/google/ads/googleads/v26/services/services/offline_user_data_job_service/transports/grpc_asyncio.py @@ -127,7 +127,13 @@ class OfflineUserDataJobServiceGrpcAsyncIOTransport( ): """gRPC AsyncIO backend transport for OfflineUserDataJobService. - Service to manage offline user data jobs. + Service to manage offline user data jobs. For help diagnosing and + resolving conversion or user data upload issues, see the conversion + upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation diff --git a/google/ads/googleads/v26/services/services/user_data_service/async_client.py b/google/ads/googleads/v26/services/services/user_data_service/async_client.py index dc085c34e..74b220a10 100644 --- a/google/ads/googleads/v26/services/services/user_data_service/async_client.py +++ b/google/ads/googleads/v26/services/services/user_data_service/async_client.py @@ -48,14 +48,18 @@ class UserDataServiceAsyncClient: - """Service to manage user data uploads. - Any uploads made to a Customer Match list through this service - will be eligible for matching as per the customer matching - process. See - https://support.google.com/google-ads/answer/7474263. However, - the uploads made through this service will not be visible under - the 'Segment members' section for the Customer Match List in the - Google Ads UI. + """Service to manage user data uploads. Any uploads made to a Customer + Match list through this service will be eligible for matching as per + the customer matching process. See + https://support.google.com/google-ads/answer/7474263. However, the + uploads made through this service will not be visible under the + 'Segment members' section for the Customer Match List in the Google + Ads UI. For help diagnosing and resolving conversion or user data + upload issues, see the conversion upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). """ _client: UserDataServiceClient diff --git a/google/ads/googleads/v26/services/services/user_data_service/client.py b/google/ads/googleads/v26/services/services/user_data_service/client.py index 699474bcb..49d85f8e0 100644 --- a/google/ads/googleads/v26/services/services/user_data_service/client.py +++ b/google/ads/googleads/v26/services/services/user_data_service/client.py @@ -99,14 +99,18 @@ def get_transport_class( class UserDataServiceClient(metaclass=UserDataServiceClientMeta): - """Service to manage user data uploads. - Any uploads made to a Customer Match list through this service - will be eligible for matching as per the customer matching - process. See - https://support.google.com/google-ads/answer/7474263. However, - the uploads made through this service will not be visible under - the 'Segment members' section for the Customer Match List in the - Google Ads UI. + """Service to manage user data uploads. Any uploads made to a Customer + Match list through this service will be eligible for matching as per + the customer matching process. See + https://support.google.com/google-ads/answer/7474263. However, the + uploads made through this service will not be visible under the + 'Segment members' section for the Customer Match List in the Google + Ads UI. For help diagnosing and resolving conversion or user data + upload issues, see the conversion upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). """ # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. diff --git a/google/ads/googleads/v26/services/services/user_data_service/transports/grpc.py b/google/ads/googleads/v26/services/services/user_data_service/transports/grpc.py index cec6ec0ed..054672695 100644 --- a/google/ads/googleads/v26/services/services/user_data_service/transports/grpc.py +++ b/google/ads/googleads/v26/services/services/user_data_service/transports/grpc.py @@ -118,14 +118,18 @@ def intercept_unary_unary(self, continuation, client_call_details, request): class UserDataServiceGrpcTransport(UserDataServiceTransport): """gRPC backend transport for UserDataService. - Service to manage user data uploads. - Any uploads made to a Customer Match list through this service - will be eligible for matching as per the customer matching - process. See - https://support.google.com/google-ads/answer/7474263. However, - the uploads made through this service will not be visible under - the 'Segment members' section for the Customer Match List in the - Google Ads UI. + Service to manage user data uploads. Any uploads made to a Customer + Match list through this service will be eligible for matching as per + the customer matching process. See + https://support.google.com/google-ads/answer/7474263. However, the + uploads made through this service will not be visible under the + 'Segment members' section for the Customer Match List in the Google + Ads UI. For help diagnosing and resolving conversion or user data + upload issues, see the conversion upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation diff --git a/google/ads/googleads/v26/services/services/user_data_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/user_data_service/transports/grpc_asyncio.py index 74d5e7a81..d2bb1df9d 100644 --- a/google/ads/googleads/v26/services/services/user_data_service/transports/grpc_asyncio.py +++ b/google/ads/googleads/v26/services/services/user_data_service/transports/grpc_asyncio.py @@ -121,14 +121,18 @@ async def intercept_unary_unary( class UserDataServiceGrpcAsyncIOTransport(UserDataServiceTransport): """gRPC AsyncIO backend transport for UserDataService. - Service to manage user data uploads. - Any uploads made to a Customer Match list through this service - will be eligible for matching as per the customer matching - process. See - https://support.google.com/google-ads/answer/7474263. However, - the uploads made through this service will not be visible under - the 'Segment members' section for the Customer Match List in the - Google Ads UI. + Service to manage user data uploads. Any uploads made to a Customer + Match list through this service will be eligible for matching as per + the customer matching process. See + https://support.google.com/google-ads/answer/7474263. However, the + uploads made through this service will not be visible under the + 'Segment members' section for the Customer Match List in the Google + Ads UI. For help diagnosing and resolving conversion or user data + upload issues, see the conversion upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/async_client.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/async_client.py index 683eb05c6..9da179c26 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/async_client.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/async_client.py @@ -14,12 +14,13 @@ # limitations under the License. # import logging as std_logging -from typing import Callable, Optional, BinaryIO, Sequence, Tuple, Union +from typing import Callable, Optional, Sequence, Tuple, Union from google.ads.googleads.v26 import gapic_version as package_version from google.api_core.client_options import ClientOptions from google.api_core import gapic_v1 +from google.api_core.resumable_transfer import ResumableUploadConfig from google.api_core import retry_async as retries from google.auth import credentials as ga_credentials # type: ignore from google.oauth2 import service_account # type: ignore @@ -35,6 +36,7 @@ from google.ads.googleads.v26.resources.types import youtube_video_upload from google.ads.googleads.v26.services.types import youtube_video_upload_service +from google.api_core import resumable_transfer import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore from .transports.base import ( YouTubeVideoUploadServiceTransport, @@ -218,10 +220,6 @@ def __init__( self, *, credentials: Optional[ga_credentials.Credentials] = None, - developer_token: Optional[str] = None, - login_customer_id: Optional[str] = None, - linked_customer_id: Optional[str] = None, - use_cloud_org_for_api_access: bool = False, transport: Optional[ Union[ str, @@ -240,18 +238,6 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - developer_token (Optional[str]): The developer token to use for the - resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - login_customer_id (Optional[str]): The login customer ID to use for - the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - linked_customer_id (Optional[str]): The linked customer ID to use for - the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - use_cloud_org_for_api_access (bool): Whether to use the cloud org for - API access for the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! transport (Optional[Union[str,YouTubeVideoUploadServiceTransport,Callable[..., YouTubeVideoUploadServiceTransport]]]): The transport to use, or a Callable that constructs and returns a new transport to use. If a Callable is given, it will be called with the same set of initialization @@ -295,10 +281,6 @@ def __init__( """ self._client = YouTubeVideoUploadServiceClient( credentials=credentials, - developer_token=developer_token, - login_customer_id=login_customer_id, - linked_customer_id=linked_customer_id, - use_cloud_org_for_api_access=use_cloud_org_for_api_access, transport=transport, client_options=client_options, client_info=client_info, @@ -345,11 +327,11 @@ async def create_you_tube_video_upload( you_tube_video_upload: Optional[ youtube_video_upload.YouTubeVideoUpload ] = None, - stream: BinaryIO, + config: Optional[ResumableUploadConfig] = None, retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), - ) -> youtube_video_upload_service.CreateYouTubeVideoUploadResponse: + ) -> resumable_transfer.AsyncResumableUploadSession: r"""Uploads a video to Google-managed or advertiser owned (brand) YouTube channel. @@ -371,8 +353,8 @@ async def create_you_tube_video_upload( This corresponds to the ``you_tube_video_upload`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - stream (BinaryIO): - The stream to upload. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -382,14 +364,72 @@ async def create_you_tube_video_upload( be of type `bytes`. Returns: - google.ads.googleads.v26.services.types.CreateYouTubeVideoUploadResponse: - Response message for - [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. + google.api_core.resumable_transfer.AsyncResumableUploadSession: + An object representing a resumable + upload session. """ - raise NotImplementedError( - "Resumable upload is not yet supported in the async client." + # Create or coerce a protobuf request object. + # - Quick check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + flattened_params = [customer_id, you_tube_video_upload] + has_flattened_params = ( + len([param for param in flattened_params if param is not None]) > 0 ) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # - Use the request object if provided (there's no risk of modifying the input as + # there are no flattened fields), or create one. + if not isinstance( + request, + youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + ): + request = ( + youtube_video_upload_service.CreateYouTubeVideoUploadRequest( + request + ) + ) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if customer_id is not None: + request.customer_id = customer_id + if you_tube_video_upload is not None: + request.you_tube_video_upload = you_tube_video_upload + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._client._transport._wrapped_methods[ + self._client._transport.create_you_tube_video_upload + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("customer_id", request.customer_id),) + ), + ) + + # Validate the universe domain. + self._client._validate_universe_domain() + + # Send the request. + response = await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + config=config, + start_retry=retry, + ) + + # Done; return the response. + return response async def update_you_tube_video_upload( self, diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/client.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/client.py index e2bfdb8e2..56c51a256 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/client.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/client.py @@ -19,17 +19,7 @@ import logging as std_logging import os import re -from typing import ( - Dict, - Callable, - Optional, - BinaryIO, - Sequence, - Tuple, - Type, - Union, - cast, -) +from typing import Dict, Callable, Optional, Sequence, Tuple, Type, Union, cast import warnings from google.ads.googleads.v26 import gapic_version as package_version @@ -37,6 +27,14 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.resumable_transfer import ResumableUploadConfig +from google.ads.googleads.v26._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -61,14 +59,12 @@ from google.ads.googleads.v26.resources.types import youtube_video_upload from google.ads.googleads.v26.services.types import youtube_video_upload_service +from google.api_core import resumable_transfer import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore from .transports.base import ( YouTubeVideoUploadServiceTransport, DEFAULT_CLIENT_INFO, ) -from .transports.rest_resumable import ( - YouTubeVideoUploadServiceRestResumableTransport, -) from .transports.grpc import YouTubeVideoUploadServiceGrpcTransport from .transports.grpc_asyncio import ( YouTubeVideoUploadServiceGrpcAsyncIOTransport, @@ -118,77 +114,13 @@ class YouTubeVideoUploadServiceClient( ): """Service to manage YouTube video uploads.""" - @staticmethod - def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]: - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - Optional[str]: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - if m is None: - # Could not parse api_endpoint; return as-is. - return api_endpoint - - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - # Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead. DEFAULT_ENDPOINT = "googleads.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore - DEFAULT_ENDPOINT - ) + DEFAULT_MTLS_ENDPOINT = get_default_mtls_endpoint(DEFAULT_ENDPOINT) _DEFAULT_ENDPOINT_TEMPLATE = "googleads.{UNIVERSE_DOMAIN}" _DEFAULT_UNIVERSE = "googleapis.com" - @staticmethod - def _use_client_cert_effective(): - """Returns whether client certificate should be used for mTLS if the - google-auth version supports should_use_client_cert automatic mTLS enablement. - - Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. - - Returns: - bool: whether client certificate should be used for mTLS - Raises: - ValueError: (If using a version of google-auth without should_use_client_cert and - GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.) - """ - # check if google-auth version supports should_use_client_cert for automatic mTLS enablement - if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER - return mtls.should_use_client_cert() - else: # pragma: NO COVER - # if unsupported, fallback to reading from env var - use_client_cert_str = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() - if use_client_cert_str not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" - " either `true` or `false`" - ) - return use_client_cert_str == "true" - @classmethod def from_service_account_info(cls, info: dict, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -380,9 +312,7 @@ def get_mtls_endpoint_and_cert_source( ) if client_options is None: client_options = client_options_lib.ClientOptions() - use_client_cert = ( - YouTubeVideoUploadServiceClient._use_client_cert_effective() - ) + use_client_cert = should_use_client_cert() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( @@ -403,39 +333,12 @@ def get_mtls_endpoint_and_cert_source( elif use_mtls_endpoint == "always" or ( use_mtls_endpoint == "auto" and client_cert_source ): - api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT # type: ignore else: api_endpoint = cls.DEFAULT_ENDPOINT return api_endpoint, client_cert_source - @staticmethod - def _read_environment_variables(): - """Returns the environment variables used by the client. - - Returns: - Tuple[bool, str, str]: returns the GOOGLE_API_USE_CLIENT_CERTIFICATE, - GOOGLE_API_USE_MTLS_ENDPOINT, and GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variables. - - Raises: - ValueError: If GOOGLE_API_USE_CLIENT_CERTIFICATE is not - any of ["true", "false"]. - google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT - is not any of ["auto", "never", "always"]. - """ - use_client_cert = ( - YouTubeVideoUploadServiceClient._use_client_cert_effective() - ) - use_mtls_endpoint = os.getenv( - "GOOGLE_API_USE_MTLS_ENDPOINT", "auto" - ).lower() - universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") - if use_mtls_endpoint not in ("auto", "never", "always"): - raise MutualTLSChannelError( - "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" - ) - return use_client_cert, use_mtls_endpoint, universe_domain_env - @staticmethod def _get_client_cert_source(provided_cert_source, use_cert_flag): """Return the client cert source to be used by the client. @@ -455,68 +358,6 @@ def _get_client_cert_source(provided_cert_source, use_cert_flag): client_cert_source = mtls.default_client_cert_source() return client_cert_source - @staticmethod - def _get_api_endpoint( - api_override, client_cert_source, universe_domain, use_mtls_endpoint - ) -> str: - """Return the API endpoint used by the client. - - Args: - api_override (str): The API endpoint override. If specified, this is always - the return value of this function and the other arguments are not used. - client_cert_source (bytes): The client certificate source used by the client. - universe_domain (str): The universe domain used by the client. - use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters. - Possible values are "always", "auto", or "never". - - Returns: - str: The API endpoint to be used by the client. - """ - if api_override is not None: - api_endpoint = api_override - elif use_mtls_endpoint == "always" or ( - use_mtls_endpoint == "auto" and client_cert_source - ): - _default_universe = ( - YouTubeVideoUploadServiceClient._DEFAULT_UNIVERSE - ) - if universe_domain != _default_universe: - raise MutualTLSChannelError( - f"mTLS is not supported in any universe other than {_default_universe}." - ) - api_endpoint = YouTubeVideoUploadServiceClient.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = YouTubeVideoUploadServiceClient._DEFAULT_ENDPOINT_TEMPLATE.format( - UNIVERSE_DOMAIN=universe_domain - ) - return api_endpoint - - @staticmethod - def _get_universe_domain( - client_universe_domain: Optional[str], - universe_domain_env: Optional[str], - ) -> str: - """Return the universe domain used by the client. - - Args: - client_universe_domain (Optional[str]): The universe domain configured via the client options. - universe_domain_env (Optional[str]): The universe domain configured via the "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable. - - Returns: - str: The universe domain to be used by the client. - - Raises: - ValueError: If the universe domain is an empty string. - """ - universe_domain = YouTubeVideoUploadServiceClient._DEFAULT_UNIVERSE - if client_universe_domain is not None: - universe_domain = client_universe_domain - elif universe_domain_env is not None: - universe_domain = universe_domain_env - if len(universe_domain.strip()) == 0: - raise ValueError("Universe Domain cannot be an empty string.") - return universe_domain - def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. @@ -579,10 +420,6 @@ def __init__( self, *, credentials: Optional[ga_credentials.Credentials] = None, - developer_token: Optional[str] = None, - login_customer_id: Optional[str] = None, - linked_customer_id: Optional[str] = None, - use_cloud_org_for_api_access: bool = False, transport: Optional[ Union[ str, @@ -603,18 +440,6 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - developer_token (Optional[str]): The developer token to use for the - resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - login_customer_id (Optional[str]): The login customer ID to use for - the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - linked_customer_id (Optional[str]): The linked customer ID to use for - the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! - use_cloud_org_for_api_access (bool): Whether to use the cloud org for - API access for the resumable upload transport. This will be used by the resumable upload methods - (create_you_tube_video_upload) ONLY! transport (Optional[Union[str,YouTubeVideoUploadServiceTransport,Callable[..., YouTubeVideoUploadServiceTransport]]]): The transport to use, or a Callable that constructs and returns a new transport. If a Callable is given, it will be called with the same set of initialization @@ -675,16 +500,16 @@ def __init__( self._use_client_cert, self._use_mtls_endpoint, self._universe_domain_env, - ) = YouTubeVideoUploadServiceClient._read_environment_variables() + ) = read_environment_variables() self._client_cert_source = ( YouTubeVideoUploadServiceClient._get_client_cert_source( self._client_options.client_cert_source, self._use_client_cert ) ) - self._universe_domain = ( - YouTubeVideoUploadServiceClient._get_universe_domain( - universe_domain_opt, self._universe_domain_env - ) + self._universe_domain = get_universe_domain( + universe_domain_opt, + self._universe_domain_env, + default_universe=YouTubeVideoUploadServiceClient._DEFAULT_UNIVERSE, ) self._api_endpoint: str = "" # updated below, depending on `transport` @@ -709,7 +534,11 @@ def __init__( ) if transport_provided: # transport is a YouTubeVideoUploadServiceTransport instance. - if self._client_options.credentials_file or api_key_value: + if ( + credentials + or self._client_options.credentials_file + or api_key_value + ): raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -723,36 +552,15 @@ def __init__( YouTubeVideoUploadServiceTransport, transport ) self._api_endpoint = self._transport.host - resumable_creds = credentials or getattr( - self._transport, "_credentials", None - ) - if resumable_creds is None: - raise ValueError( - "For the correct REST resumable transport initialization, either the credentials should be explicitly provided as a `credentials` parameter, or the transport passed should contain readable `_credentials` property. E.g. passing a GRPC channel as a transport does not satisfy this since the credentials cannot be extracted from it. This requirement is unique to the YouTubeVideoUploadService." - ) - - self._rest_resumable_transport = ( - YouTubeVideoUploadServiceRestResumableTransport( - credentials=resumable_creds, - credentials_file=None, - host=self._transport.host, - scopes=self._transport._scopes, - developer_token=developer_token, - login_customer_id=login_customer_id, - linked_customer_id=linked_customer_id, - use_cloud_org_for_api_access=use_cloud_org_for_api_access, - ) - ) - - self._api_endpoint = ( - self._api_endpoint - or YouTubeVideoUploadServiceClient._get_api_endpoint( - self._client_options.api_endpoint, - self._client_cert_source, - self._universe_domain, - self._use_mtls_endpoint, - ) + self._api_endpoint = self._api_endpoint or get_api_endpoint( + api_override=self._client_options.api_endpoint, + universe_domain=self._universe_domain, + default_universe=YouTubeVideoUploadServiceClient._DEFAULT_UNIVERSE, + default_mtls_endpoint=YouTubeVideoUploadServiceClient.DEFAULT_MTLS_ENDPOINT, + default_endpoint_template=YouTubeVideoUploadServiceClient._DEFAULT_ENDPOINT_TEMPLATE, + use_mtls=self._use_mtls_endpoint == "always" + or (self._use_mtls_endpoint == "auto" and self._client_cert_source), ) if not transport_provided: @@ -788,25 +596,6 @@ def __init__( api_audience=self._client_options.api_audience, ) - # Initialize a separate transport for the rest resumable uploads - self._rest_resumable_transport = ( - YouTubeVideoUploadServiceRestResumableTransport( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - developer_token=developer_token, - login_customer_id=login_customer_id, - linked_customer_id=linked_customer_id, - use_cloud_org_for_api_access=use_cloud_org_for_api_access, - ) - ) - if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( std_logging.DEBUG @@ -849,11 +638,11 @@ def create_you_tube_video_upload( you_tube_video_upload: Optional[ youtube_video_upload.YouTubeVideoUpload ] = None, - stream: BinaryIO, + config: Optional[ResumableUploadConfig] = None, retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), - ) -> youtube_video_upload_service.CreateYouTubeVideoUploadResponse: + ) -> resumable_transfer.ResumableUploadSession: r"""Uploads a video to Google-managed or advertiser owned (brand) YouTube channel. @@ -875,8 +664,8 @@ def create_you_tube_video_upload( This corresponds to the ``you_tube_video_upload`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - stream (BinaryIO): - The stream to upload. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -886,9 +675,9 @@ def create_you_tube_video_upload( be of type `bytes`. Returns: - google.ads.googleads.v26.services.types.CreateYouTubeVideoUploadResponse: - Response message for - [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. + google.api_core.resumable_transfer.ResumableUploadSession: + An object representing a resumable + upload session. """ # Create or coerce a protobuf request object. @@ -922,8 +711,11 @@ def create_you_tube_video_upload( if you_tube_video_upload is not None: request.you_tube_video_upload = you_tube_video_upload - if stream is None: - raise ValueError("stream is required for resumable upload methods") + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[ + self._transport.create_you_tube_video_upload + ] # Certain fields should be provided within the metadata header; # add these here. @@ -937,12 +729,13 @@ def create_you_tube_video_upload( self._validate_universe_domain() # Send the request. - response = self._rest_resumable_transport.create_you_tube_video_upload_resumable( - request=request, - stream=stream, + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata, + config=config, + start_retry=retry, ) # Done; return the response. diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/base.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/base.py index 7813236b4..816f839e6 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/base.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/base.py @@ -22,6 +22,7 @@ import google.api_core from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core import resumable_transfer from google.auth import credentials as ga_credentials # type: ignore from google.oauth2 import service_account # type: ignore import google.protobuf @@ -127,6 +128,7 @@ def __init__( # Save the credentials. self._credentials = credentials + self._client_info = client_info # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: @@ -174,10 +176,9 @@ def create_you_tube_video_upload( ) -> Callable[ [youtube_video_upload_service.CreateYouTubeVideoUploadRequest], Union[ - youtube_video_upload_service.CreateYouTubeVideoUploadResponse, - Awaitable[ - youtube_video_upload_service.CreateYouTubeVideoUploadResponse - ], + resumable_transfer.ResumableUploadSession, + resumable_transfer.AsyncResumableUploadSession, + Awaitable[resumable_transfer.AsyncResumableUploadSession], ], ]: raise NotImplementedError() diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc.py index 5c4280a41..218c0393f 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc.py @@ -18,6 +18,8 @@ import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from google.api_core import exceptions as core_exceptions +from google.api_core import resumable_transfer from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -31,6 +33,7 @@ from google.ads.googleads.v26.services.types import youtube_video_upload_service from .base import YouTubeVideoUploadServiceTransport, DEFAULT_CLIENT_INFO +from .rest import YouTubeVideoUploadServiceRestTransport try: from google.api_core import client_logging # type: ignore @@ -212,6 +215,15 @@ def __init__( self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. + self._rest_transport: Optional[ + YouTubeVideoUploadServiceRestTransport + ] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -352,7 +364,7 @@ def create_you_tube_video_upload( self, ) -> Callable[ [youtube_video_upload_service.CreateYouTubeVideoUploadRequest], - youtube_video_upload_service.CreateYouTubeVideoUploadResponse, + resumable_transfer.ResumableUploadSession, ]: r"""Return a callable for the create you tube video upload method over gRPC. @@ -361,7 +373,7 @@ def create_you_tube_video_upload( Returns: Callable[[~.CreateYouTubeVideoUploadRequest], - ~.CreateYouTubeVideoUploadResponse]: + ~.ResumableUploadSession]: A function that, when called, will call the underlying RPC on the server. """ @@ -370,13 +382,37 @@ def create_you_tube_video_upload( # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if "create_you_tube_video_upload" not in self._stubs: - self._stubs["create_you_tube_video_upload"] = ( - self._logged_channel.unary_unary( - "/google.ads.googleads.v26.services.YouTubeVideoUploadService/CreateYouTubeVideoUpload", - request_serializer=youtube_video_upload_service.CreateYouTubeVideoUploadRequest.serialize, - response_deserializer=youtube_video_upload_service.CreateYouTubeVideoUploadResponse.deserialize, + if not self._credentials: + + class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + + def __call__(self, *args, **kwargs): + raise self._error + + self._stubs["create_you_tube_video_upload"] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) ) - ) + else: + transport = self + + class _RestStub: + def __call__(self, *args, **kwargs): + if transport._rest_transport is None: + transport._rest_transport = YouTubeVideoUploadServiceRestTransport( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + client_cert_source_for_mtls=transport._client_cert_source_for_mtls, + ) + return transport._rest_transport.create_you_tube_video_upload( + *args, **kwargs + ) + + self._stubs["create_you_tube_video_upload"] = _RestStub() return self._stubs["create_you_tube_video_upload"] @property @@ -444,6 +480,8 @@ def remove_you_tube_video_upload( def close(self): self._logged_channel.close() + if self._rest_transport is not None: + self._rest_transport.close() @property def kind(self) -> str: diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc_asyncio.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc_asyncio.py index 17c3320a5..3fafc59b9 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc_asyncio.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/grpc_asyncio.py @@ -17,10 +17,21 @@ import pickle import logging as std_logging import warnings -from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Optional, + Sequence, + Tuple, + Union, +) from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async +from google.api_core import exceptions as core_exceptions +from google.api_core import resumable_transfer from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -33,6 +44,13 @@ from google.ads.googleads.v26.services.types import youtube_video_upload_service from .base import YouTubeVideoUploadServiceTransport, DEFAULT_CLIENT_INFO +try: + from .rest_asyncio import AsyncYouTubeVideoUploadServiceRestTransport + + HAS_ASYNC_REST = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + HAS_ASYNC_REST = False + try: from google.api_core import client_logging # type: ignore @@ -260,6 +278,13 @@ def __init__( self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. + self._rest_transport: Optional[Any] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -358,9 +383,7 @@ def create_you_tube_video_upload( self, ) -> Callable[ [youtube_video_upload_service.CreateYouTubeVideoUploadRequest], - Awaitable[ - youtube_video_upload_service.CreateYouTubeVideoUploadResponse - ], + Awaitable[resumable_transfer.AsyncResumableUploadSession], ]: r"""Return a callable for the create you tube video upload method over gRPC. @@ -369,7 +392,7 @@ def create_you_tube_video_upload( Returns: Callable[[~.CreateYouTubeVideoUploadRequest], - Awaitable[~.CreateYouTubeVideoUploadResponse]]: + Awaitable[~.AsyncResumableUploadSession]]: A function that, when called, will call the underlying RPC on the server. """ @@ -378,13 +401,55 @@ def create_you_tube_video_upload( # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if "create_you_tube_video_upload" not in self._stubs: - self._stubs["create_you_tube_video_upload"] = ( - self._logged_channel.unary_unary( - "/google.ads.googleads.v26.services.YouTubeVideoUploadService/CreateYouTubeVideoUpload", - request_serializer=youtube_video_upload_service.CreateYouTubeVideoUploadRequest.serialize, - response_deserializer=youtube_video_upload_service.CreateYouTubeVideoUploadResponse.deserialize, + + class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + + def __call__(self, *args, **kwargs): + async def _raise(): + raise self._error + + return _raise() + + if not self._credentials: + self._stubs["create_you_tube_video_upload"] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + ) + elif self._client_cert_source_for_mtls: + self._stubs["create_you_tube_video_upload"] = _ErrorStub( + core_exceptions.AsyncRestUnsupportedParameterError( + "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." + ) + ) + elif HAS_ASYNC_REST: + transport = self + + class _AsyncRestStub: + def __call__(self, *args, **kwargs): + if transport._rest_transport is None: + transport._rest_transport = ( + AsyncYouTubeVideoUploadServiceRestTransport( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + ) + ) + return transport._rest_transport.create_you_tube_video_upload( + *args, **kwargs + ) + + self._stubs["create_you_tube_video_upload"] = _AsyncRestStub() + else: + self._stubs["create_you_tube_video_upload"] = _ErrorStub( + NotImplementedError( + "Async REST transport is required for async resumable upload methods. " + "Install the library with the `async_rest` extra using " + "`pip install google-ads[async_rest]`." + ) ) - ) return self._stubs["create_you_tube_video_upload"] @property @@ -479,8 +544,10 @@ def _wrap_method(self, func, *args, **kwargs): kwargs["kind"] = self.kind return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): - return self._logged_channel.close() + async def close(self): + await self._logged_channel.close() + if self._rest_transport is not None: + await self._rest_transport.close() @property def kind(self) -> str: diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest.py new file mode 100644 index 000000000..1635e450b --- /dev/null +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +import logging +import json # type: ignore + +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.api_core import rest_helpers +from google.api_core import gapic_v1 +import urllib.parse +from google.api_core import resumable_transfer +from google.ads.googleads.v26._compat import transcode_request +import google.protobuf + +from google.protobuf import json_format + +from requests import __version__ as requests_version +import dataclasses +from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union + + +from google.ads.googleads.v26.services.types import youtube_video_upload_service + + +from .rest_base import _BaseYouTubeVideoUploadServiceRestTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object, None] # type: ignore + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=f"requests@{requests_version}", +) + +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +class YouTubeVideoUploadServiceRestInterceptor: + """Interceptor for YouTubeVideoUploadService. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the YouTubeVideoUploadServiceRestTransport. + + .. code-block:: python + class MyCustomYouTubeVideoUploadServiceInterceptor(YouTubeVideoUploadServiceRestInterceptor): + def pre_create_you_tube_video_upload(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def pre_remove_you_tube_video_upload(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_remove_you_tube_video_upload(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_you_tube_video_upload(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_you_tube_video_upload(self, response): + logging.log(f"Received response: {response}") + return response + + transport = YouTubeVideoUploadServiceRestTransport(interceptor=MyCustomYouTubeVideoUploadServiceInterceptor()) + client = YouTubeVideoUploadServiceClient(transport=transport) + + + """ + + def pre_create_you_tube_video_upload( + self, + request: youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Pre-rpc interceptor for create_you_tube_video_upload + + Override in a subclass to manipulate the request or metadata + before they are sent to the YouTubeVideoUploadService server. + """ + return request, metadata + + def pre_remove_you_tube_video_upload( + self, + request: youtube_video_upload_service.RemoveYouTubeVideoUploadRequest, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.RemoveYouTubeVideoUploadRequest, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Pre-rpc interceptor for remove_you_tube_video_upload + + Override in a subclass to manipulate the request or metadata + before they are sent to the YouTubeVideoUploadService server. + """ + return request, metadata + + def post_remove_you_tube_video_upload( + self, + response: youtube_video_upload_service.RemoveYouTubeVideoUploadResponse, + ) -> youtube_video_upload_service.RemoveYouTubeVideoUploadResponse: + """Post-rpc interceptor for remove_you_tube_video_upload + + DEPRECATED. Please use the `post_remove_you_tube_video_upload_with_metadata` + interceptor instead. + + Override in a subclass to read or manipulate the response + after it is returned by the YouTubeVideoUploadService server but before + it is returned to user code. This `post_remove_you_tube_video_upload` interceptor runs + before the `post_remove_you_tube_video_upload_with_metadata` interceptor. + """ + return response + + def post_remove_you_tube_video_upload_with_metadata( + self, + response: youtube_video_upload_service.RemoveYouTubeVideoUploadResponse, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.RemoveYouTubeVideoUploadResponse, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Post-rpc interceptor for remove_you_tube_video_upload + + Override in a subclass to read or manipulate the response or metadata after it + is returned by the YouTubeVideoUploadService server but before it is returned to user code. + + We recommend only using this `post_remove_you_tube_video_upload_with_metadata` + interceptor in new development instead of the `post_remove_you_tube_video_upload` interceptor. + When both interceptors are used, this `post_remove_you_tube_video_upload_with_metadata` interceptor runs after the + `post_remove_you_tube_video_upload` interceptor. The (possibly modified) response returned by + `post_remove_you_tube_video_upload` will be passed to + `post_remove_you_tube_video_upload_with_metadata`. + """ + return response, metadata + + def pre_update_you_tube_video_upload( + self, + request: youtube_video_upload_service.UpdateYouTubeVideoUploadRequest, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.UpdateYouTubeVideoUploadRequest, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Pre-rpc interceptor for update_you_tube_video_upload + + Override in a subclass to manipulate the request or metadata + before they are sent to the YouTubeVideoUploadService server. + """ + return request, metadata + + def post_update_you_tube_video_upload( + self, + response: youtube_video_upload_service.UpdateYouTubeVideoUploadResponse, + ) -> youtube_video_upload_service.UpdateYouTubeVideoUploadResponse: + """Post-rpc interceptor for update_you_tube_video_upload + + DEPRECATED. Please use the `post_update_you_tube_video_upload_with_metadata` + interceptor instead. + + Override in a subclass to read or manipulate the response + after it is returned by the YouTubeVideoUploadService server but before + it is returned to user code. This `post_update_you_tube_video_upload` interceptor runs + before the `post_update_you_tube_video_upload_with_metadata` interceptor. + """ + return response + + def post_update_you_tube_video_upload_with_metadata( + self, + response: youtube_video_upload_service.UpdateYouTubeVideoUploadResponse, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.UpdateYouTubeVideoUploadResponse, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Post-rpc interceptor for update_you_tube_video_upload + + Override in a subclass to read or manipulate the response or metadata after it + is returned by the YouTubeVideoUploadService server but before it is returned to user code. + + We recommend only using this `post_update_you_tube_video_upload_with_metadata` + interceptor in new development instead of the `post_update_you_tube_video_upload` interceptor. + When both interceptors are used, this `post_update_you_tube_video_upload_with_metadata` interceptor runs after the + `post_update_you_tube_video_upload` interceptor. The (possibly modified) response returned by + `post_update_you_tube_video_upload` will be passed to + `post_update_you_tube_video_upload_with_metadata`. + """ + return response, metadata + + +@dataclasses.dataclass +class YouTubeVideoUploadServiceRestStub: + _session: AuthorizedSession + _host: str + _interceptor: YouTubeVideoUploadServiceRestInterceptor + + +class YouTubeVideoUploadServiceRestTransport( + _BaseYouTubeVideoUploadServiceRestTransport +): + """REST backend synchronous transport for YouTubeVideoUploadService. + + Service to manage YouTube video uploads. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + """ + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[ + Callable[[], Tuple[bytes, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[YouTubeVideoUploadServiceRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + NOTE: This REST transport functionality is currently in a beta + state (preview). We welcome your feedback via a GitHub issue in + this library's repository. Thank you! + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of this library. + scopes (Optional(Sequence[str])): A list of scopes. This argument is + ignored if ``channel`` is provided. + client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + interceptor (Optional[YouTubeVideoUploadServiceRestInterceptor]): Interceptor used + to manipulate requests, request metadata, and responses. + api_audience (Optional[str]): The intended audience for the API calls + to the service that will be set when using certain 3rd party + authentication flows. Audience is typically a resource identifier. + If not set, the host value will be used as a default. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + url_scheme=url_scheme, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = ( + interceptor or YouTubeVideoUploadServiceRestInterceptor() + ) + self._prep_wrapped_messages(client_info) + + class _CreateYouTubeVideoUpload( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload, + YouTubeVideoUploadServiceRestStub, + ): + def __hash__(self): + return hash( + "YouTubeVideoUploadServiceRestTransport.CreateYouTubeVideoUpload" + ) + + def __call__( + self, + request: youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + *, + config: Optional[resumable_transfer.ResumableUploadConfig] = None, + start_retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> resumable_transfer.ResumableUploadSession: + r"""Call the create you tube video + upload method over HTTP. + + Args: + request (~.youtube_video_upload_service.CreateYouTubeVideoUploadRequest): + The request object. Request message for + [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + start_retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + ~.resumable_transfer.ResumableUploadSession: + An object representing a resumable + upload session. + + """ + http_options = ( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload._get_http_options() + ) + request, metadata = ( + self._interceptor.pre_create_you_tube_video_upload( + request, metadata + ) + ) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload, + "_BaseCreateYouTubeVideoUpload__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums=False, + ) + + uri = transcoded_request["uri"] + params = rest_helpers.flatten_query_params( + query_params, strict=True + ) + query_string = ( + f"?{urllib.parse.urlencode(params)}" if params else "" + ) + upload_url = f"{self._host}{uri}{query_string}" + headers: Dict[str, Any] = { + **dict(metadata), + **dict((config.headers or {}) if config else {}), + } + headers["Content-Type"] = "application/json" + if config is None: + config = resumable_transfer.ResumableUploadConfig( + headers=headers + ) + else: + config = dataclasses.replace(config, headers=headers) + + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) + # ``start_retry`` is used instead of ``retry`` because ``_GapicCallable`` + # consumes the ``retry`` argument before invoking the transport callable + # and only forwards extra keyword arguments such as ``start_retry``. + return resumable_transfer.ResumableUploadSession( + upload_url=upload_url, + config=config, + transport=self._session, + response_type=youtube_video_upload_service.CreateYouTubeVideoUploadResponse, + start_retry=( + start_retry + if isinstance(start_retry, retries.Retry) + else None + ), + request_body=body, + **session_kwargs, + ) + + class _RemoveYouTubeVideoUpload( + _BaseYouTubeVideoUploadServiceRestTransport._BaseRemoveYouTubeVideoUpload, + YouTubeVideoUploadServiceRestStub, + ): + def __hash__(self): + return hash( + "YouTubeVideoUploadServiceRestTransport.RemoveYouTubeVideoUpload" + ) + + @staticmethod + def _get_response( + host, + metadata, + query_params, + session, + timeout, + transcoded_request, + body=None, + ): + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(session, method)( + "{host}{uri}".format(host=host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params( + query_params, strict=True + ), + data=body, + ) + return response + + def __call__( + self, + request: youtube_video_upload_service.RemoveYouTubeVideoUploadRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> youtube_video_upload_service.RemoveYouTubeVideoUploadResponse: + r"""Call the remove you tube video + upload method over HTTP. + + Args: + request (~.youtube_video_upload_service.RemoveYouTubeVideoUploadRequest): + The request object. Request message for + [YouTubeVideoUploadService.RemoveYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.RemoveYouTubeVideoUpload]. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + ~.youtube_video_upload_service.RemoveYouTubeVideoUploadResponse: + Response message for + [YouTubeVideoUploadService.RemoveYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.RemoveYouTubeVideoUpload]. + + """ + + http_options = ( + _BaseYouTubeVideoUploadServiceRestTransport._BaseRemoveYouTubeVideoUpload._get_http_options() + ) + request, metadata = ( + self._interceptor.pre_remove_you_tube_video_upload( + request, metadata + ) + ) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _BaseYouTubeVideoUploadServiceRestTransport._BaseRemoveYouTubeVideoUpload, + "_BaseRemoveYouTubeVideoUpload__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums=False, + ) + + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + request_url = "{host}{uri}".format( + host=self._host, uri=transcoded_request["uri"] + ) + method = transcoded_request["method"] + try: + request_payload = type(request).to_json(request) + except: + request_payload = None + http_request = { + "payload": request_payload, + "requestMethod": method, + "requestUrl": request_url, + "headers": dict(metadata), + } + _LOGGER.debug( + f"Sending request for google.ads.googleads.v26.services.YouTubeVideoUploadServiceClient.RemoveYouTubeVideoUpload", + extra={ + "serviceName": "google.ads.googleads.v26.services.YouTubeVideoUploadService", + "rpcName": "RemoveYouTubeVideoUpload", + "httpRequest": http_request, + "metadata": http_request["headers"], + }, + ) + + # Send the request + response = YouTubeVideoUploadServiceRestTransport._RemoveYouTubeVideoUpload._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = ( + youtube_video_upload_service.RemoveYouTubeVideoUploadResponse() + ) + pb_resp = youtube_video_upload_service.RemoveYouTubeVideoUploadResponse.pb( + resp + ) + + json_format.Parse( + response.content, pb_resp, ignore_unknown_fields=True + ) + + resp = self._interceptor.post_remove_you_tube_video_upload(resp) + response_metadata = [ + (k, str(v)) for k, v in response.headers.items() + ] + resp, _ = ( + self._interceptor.post_remove_you_tube_video_upload_with_metadata( + resp, response_metadata + ) + ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + try: + response_payload = youtube_video_upload_service.RemoveYouTubeVideoUploadResponse.to_json( + response + ) + except: + response_payload = None + http_response = { + "payload": response_payload, + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.ads.googleads.v26.services.YouTubeVideoUploadServiceClient.remove_you_tube_video_upload", + extra={ + "serviceName": "google.ads.googleads.v26.services.YouTubeVideoUploadService", + "rpcName": "RemoveYouTubeVideoUpload", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) + return resp + + class _UpdateYouTubeVideoUpload( + _BaseYouTubeVideoUploadServiceRestTransport._BaseUpdateYouTubeVideoUpload, + YouTubeVideoUploadServiceRestStub, + ): + def __hash__(self): + return hash( + "YouTubeVideoUploadServiceRestTransport.UpdateYouTubeVideoUpload" + ) + + @staticmethod + def _get_response( + host, + metadata, + query_params, + session, + timeout, + transcoded_request, + body=None, + ): + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(session, method)( + "{host}{uri}".format(host=host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params( + query_params, strict=True + ), + data=body, + ) + return response + + def __call__( + self, + request: youtube_video_upload_service.UpdateYouTubeVideoUploadRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> youtube_video_upload_service.UpdateYouTubeVideoUploadResponse: + r"""Call the update you tube video + upload method over HTTP. + + Args: + request (~.youtube_video_upload_service.UpdateYouTubeVideoUploadRequest): + The request object. Request message for + [YouTubeVideoUploadService.UpdateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.UpdateYouTubeVideoUpload]. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + ~.youtube_video_upload_service.UpdateYouTubeVideoUploadResponse: + Response message for + [YouTubeVideoUploadService.UpdateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.UpdateYouTubeVideoUpload]. + + """ + + http_options = ( + _BaseYouTubeVideoUploadServiceRestTransport._BaseUpdateYouTubeVideoUpload._get_http_options() + ) + request, metadata = ( + self._interceptor.pre_update_you_tube_video_upload( + request, metadata + ) + ) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _BaseYouTubeVideoUploadServiceRestTransport._BaseUpdateYouTubeVideoUpload, + "_BaseUpdateYouTubeVideoUpload__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums=False, + ) + + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + request_url = "{host}{uri}".format( + host=self._host, uri=transcoded_request["uri"] + ) + method = transcoded_request["method"] + try: + request_payload = type(request).to_json(request) + except: + request_payload = None + http_request = { + "payload": request_payload, + "requestMethod": method, + "requestUrl": request_url, + "headers": dict(metadata), + } + _LOGGER.debug( + f"Sending request for google.ads.googleads.v26.services.YouTubeVideoUploadServiceClient.UpdateYouTubeVideoUpload", + extra={ + "serviceName": "google.ads.googleads.v26.services.YouTubeVideoUploadService", + "rpcName": "UpdateYouTubeVideoUpload", + "httpRequest": http_request, + "metadata": http_request["headers"], + }, + ) + + # Send the request + response = YouTubeVideoUploadServiceRestTransport._UpdateYouTubeVideoUpload._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = ( + youtube_video_upload_service.UpdateYouTubeVideoUploadResponse() + ) + pb_resp = youtube_video_upload_service.UpdateYouTubeVideoUploadResponse.pb( + resp + ) + + json_format.Parse( + response.content, pb_resp, ignore_unknown_fields=True + ) + + resp = self._interceptor.post_update_you_tube_video_upload(resp) + response_metadata = [ + (k, str(v)) for k, v in response.headers.items() + ] + resp, _ = ( + self._interceptor.post_update_you_tube_video_upload_with_metadata( + resp, response_metadata + ) + ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + try: + response_payload = youtube_video_upload_service.UpdateYouTubeVideoUploadResponse.to_json( + response + ) + except: + response_payload = None + http_response = { + "payload": response_payload, + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.ads.googleads.v26.services.YouTubeVideoUploadServiceClient.update_you_tube_video_upload", + extra={ + "serviceName": "google.ads.googleads.v26.services.YouTubeVideoUploadService", + "rpcName": "UpdateYouTubeVideoUpload", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) + return resp + + @property + def create_you_tube_video_upload( + self, + ) -> Callable[ + [youtube_video_upload_service.CreateYouTubeVideoUploadRequest], + resumable_transfer.ResumableUploadSession, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateYouTubeVideoUpload(self._session, self._host, self._interceptor) # type: ignore + + @property + def remove_you_tube_video_upload( + self, + ) -> Callable[ + [youtube_video_upload_service.RemoveYouTubeVideoUploadRequest], + youtube_video_upload_service.RemoveYouTubeVideoUploadResponse, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._RemoveYouTubeVideoUpload(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_you_tube_video_upload( + self, + ) -> Callable[ + [youtube_video_upload_service.UpdateYouTubeVideoUploadRequest], + youtube_video_upload_service.UpdateYouTubeVideoUploadResponse, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateYouTubeVideoUpload(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("YouTubeVideoUploadServiceRestTransport",) diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_asyncio.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_asyncio.py new file mode 100644 index 000000000..7802b21f9 --- /dev/null +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_asyncio.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# + +import google.auth + +try: + import aiohttp # type: ignore + from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore + from google.api_core import rest_streaming_async # type: ignore + from google.api_core.operations_v1 import AsyncOperationsRestClient # type: ignore +except ImportError as e: # pragma: NO COVER + raise ImportError( + "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-ads[async_rest]`" + ) from e + +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.aio import credentials as ga_credentials_async # type: ignore + +from google.api_core import gapic_v1 +import urllib.parse +from google.api_core import resumable_transfer +from google.api_core import retry_async as retries +from google.api_core import rest_helpers +from google.api_core import rest_streaming_async # type: ignore +from google.ads.googleads.v26._compat import transcode_request + +import google.protobuf + + +import json # type: ignore +import dataclasses +from typing import Any, Dict, Callable, Tuple, Optional, Sequence, Union + + +from google.ads.googleads.v26.services.types import youtube_video_upload_service + + +from .rest_base import _BaseYouTubeVideoUploadServiceRestTransport + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + + +import logging + +try: + from google.api_core import client_logging # type: ignore + + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = logging.getLogger(__name__) + +try: + OptionalRetry = Union[ + retries.AsyncRetry, gapic_v1.method._MethodDefault, None + ] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.AsyncRetry, object, None] # type: ignore + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=f"google-auth@{google.auth.__version__}", +) + +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +class AsyncYouTubeVideoUploadServiceRestInterceptor: + """Asynchronous Interceptor for YouTubeVideoUploadService. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the AsyncYouTubeVideoUploadServiceRestTransport. + + .. code-block:: python + class MyCustomYouTubeVideoUploadServiceInterceptor(YouTubeVideoUploadServiceRestInterceptor): + async def pre_create_you_tube_video_upload(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + transport = AsyncYouTubeVideoUploadServiceRestTransport(interceptor=MyCustomYouTubeVideoUploadServiceInterceptor()) + client = async YouTubeVideoUploadServiceClient(transport=transport) + + + """ + + async def pre_create_you_tube_video_upload( + self, + request: youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + metadata: Sequence[Tuple[str, Union[str, bytes]]], + ) -> Tuple[ + youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + Sequence[Tuple[str, Union[str, bytes]]], + ]: + """Pre-rpc interceptor for create_you_tube_video_upload + + Override in a subclass to manipulate the request or metadata + before they are sent to the YouTubeVideoUploadService server. + """ + return request, metadata + + +@dataclasses.dataclass +class AsyncYouTubeVideoUploadServiceRestStub: + _session: AsyncAuthorizedSession + _host: str + _interceptor: AsyncYouTubeVideoUploadServiceRestInterceptor + + +class AsyncYouTubeVideoUploadServiceRestTransport( + _BaseYouTubeVideoUploadServiceRestTransport +): + """Asynchronous REST backend transport for YouTubeVideoUploadService. + + Service to manage YouTube video uploads. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + """ + + def __init__( + self, + *, + host: str = "googleads.googleapis.com", + credentials: Optional[ + Union[ga_credentials.Credentials, ga_credentials_async.Credentials] + ] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + url_scheme: str = "https", + interceptor: Optional[ + AsyncYouTubeVideoUploadServiceRestInterceptor + ] = None, + ) -> None: + """Instantiate the transport. + + NOTE: This async REST transport functionality is currently in a beta + state (preview). We welcome your feedback via a GitHub issue in + this library's repository. Thank you! + + Args: + host (Optional[str]): + The hostname to connect to (default: 'googleads.googleapis.com'). + credentials (Optional[Union[google.auth.credentials.Credentials, google.auth.aio.credentials.Credentials]]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + url_scheme (str): the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + interceptor (Optional[AsyncYouTubeVideoUploadServiceRestInterceptor]): Interceptor used + to manipulate requests, request metadata, and responses. + """ + # Run the base constructor + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=False, + url_scheme=url_scheme, + api_audience=None, + ) + self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + self._interceptor = ( + interceptor or AsyncYouTubeVideoUploadServiceRestInterceptor() + ) + self._wrap_with_kind = True + self._prep_wrapped_messages(client_info) + + def _prep_wrapped_messages(self, client_info): + """Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + self._wrapped_methods = { + self.create_you_tube_video_upload: self._wrap_method( + self.create_you_tube_video_upload, + default_timeout=None, + client_info=client_info, + ), + } + + def _wrap_method(self, func, *args, **kwargs): + if self._wrap_with_kind: # pragma: NO COVER + kwargs["kind"] = self.kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + + class _CreateYouTubeVideoUpload( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload, + AsyncYouTubeVideoUploadServiceRestStub, + ): + def __hash__(self): + return hash( + "AsyncYouTubeVideoUploadServiceRestTransport.CreateYouTubeVideoUpload" + ) + + async def __call__( + self, + request: youtube_video_upload_service.CreateYouTubeVideoUploadRequest, + *, + config: Optional[resumable_transfer.ResumableUploadConfig] = None, + start_retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), + ) -> resumable_transfer.AsyncResumableUploadSession: + r"""Call the create you tube video + upload method over HTTP. + + Args: + request (~.youtube_video_upload_service.CreateYouTubeVideoUploadRequest): + The request object. Request message for + [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + start_retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + ~.resumable_transfer.AsyncResumableUploadSession: + An object representing a resumable + upload session. + + """ + http_options = ( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload._get_http_options() + ) + request, metadata = ( + await self._interceptor.pre_create_you_tube_video_upload( + request, metadata + ) + ) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _BaseYouTubeVideoUploadServiceRestTransport._BaseCreateYouTubeVideoUpload, + "_BaseCreateYouTubeVideoUpload__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums=False, + ) + + uri = transcoded_request["uri"] + params = rest_helpers.flatten_query_params( + query_params, strict=True + ) + query_string = ( + f"?{urllib.parse.urlencode(params)}" if params else "" + ) + upload_url = f"{self._host}{uri}{query_string}" + headers: Dict[str, Any] = { + **dict(metadata), + **dict((config.headers or {}) if config else {}), + } + headers["Content-Type"] = "application/json" + if config is None: + config = resumable_transfer.ResumableUploadConfig( + headers=headers + ) + else: + config = dataclasses.replace(config, headers=headers) + + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) + # ``start_retry`` is used instead of ``retry`` because ``_GapicCallable`` + # consumes the ``retry`` argument before invoking the transport callable + # and only forwards extra keyword arguments such as ``start_retry``. + return resumable_transfer.AsyncResumableUploadSession( + upload_url=upload_url, + config=config, + transport=self._session, + response_type=youtube_video_upload_service.CreateYouTubeVideoUploadResponse, + start_retry=( + start_retry + if isinstance(start_retry, retries.AsyncRetry) + else None + ), + request_body=body, + **session_kwargs, + ) + + @property + def create_you_tube_video_upload( + self, + ) -> Callable[ + [youtube_video_upload_service.CreateYouTubeVideoUploadRequest], + resumable_transfer.AsyncResumableUploadSession, + ]: + return self._CreateYouTubeVideoUpload(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest_asyncio" + + async def close(self): + await self._session.close() diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable_base.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_base.py similarity index 64% rename from google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable_base.py rename to google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_base.py index 58b16e29c..4365e281c 100644 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable_base.py +++ b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_base.py @@ -14,24 +14,27 @@ # limitations under the License. # import json # type: ignore -import re -from typing import Any, Dict, List, Optional - -from google.api_core import gapic_v1, path_template -from google.protobuf import empty_pb2 # type: ignore +from google.api_core import gapic_v1 -from google.ads.googleads.v26.services.types import youtube_video_upload_service +from .base import YouTubeVideoUploadServiceTransport, DEFAULT_CLIENT_INFO -from .base import DEFAULT_CLIENT_INFO, YouTubeVideoUploadServiceTransport +import re +from typing import Any, Dict, List, Optional -class _BaseYouTubeVideoUploadServiceRestResumableTransport( +class _BaseYouTubeVideoUploadServiceRestTransport( YouTubeVideoUploadServiceTransport ): - """Base REST Resumable backend transport for YouTubeVideoUploadService. + """Base REST backend transport for YouTubeVideoUploadService. - Note: This class is not meant to be used directly. Only services that has RPCs - that should be invoked as resumable upload should instantiate it and use it. + Note: This class is not meant to be used directly. Use its sync and + async sub-classes instead. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 """ def __init__( @@ -87,32 +90,56 @@ def __init__( api_audience=api_audience, ) - class _BaseCreateYouTubeVideoUploadRequest: + class _BaseCreateYouTubeVideoUpload: + def __hash__(self): # pragma: NO COVER + return NotImplementedError("__hash__ must be implemented.") + + __REQUIRED_FIELDS_DEFAULT_VALUES: Dict[str, Any] = {} + + @staticmethod + def _get_http_options(): + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/resumable/upload/v26/customers/{customer_id=*}/youTubeVideoUploads:create", + "body": "*", + }, + ] + return http_options + + class _BaseRemoveYouTubeVideoUpload: def __hash__(self): # pragma: NO COVER return NotImplementedError("__hash__ must be implemented.") + __REQUIRED_FIELDS_DEFAULT_VALUES: Dict[str, Any] = {} + @staticmethod def _get_http_options(): http_options: List[Dict[str, str]] = [ { "method": "post", - "uri": "/v26/customers/{customer_id=*}/youTubeVideoUploads:create", + "uri": "/v26/customers/{customer_id=*}/youTubeVideoUploads:remove", "body": "*", }, ] return http_options + class _BaseUpdateYouTubeVideoUpload: + def __hash__(self): # pragma: NO COVER + return NotImplementedError("__hash__ must be implemented.") + + __REQUIRED_FIELDS_DEFAULT_VALUES: Dict[str, Any] = {} + @staticmethod - def _get_transcoded_request(http_options, request): - pb_request = ( - youtube_video_upload_service.CreateYouTubeVideoUploadRequest.pb( - request - ) - ) - transcoded_request = path_template.transcode( - http_options, pb_request - ) - return transcoded_request - - -__all__ = ("_BaseYouTubeVideoUploadServiceRestResumableTransport",) + def _get_http_options(): + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v26/customers/{customer_id=*}/youTubeVideoUploads:update", + "body": "*", + }, + ] + return http_options + + +__all__ = ("_BaseYouTubeVideoUploadServiceRestTransport",) diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable.py deleted file mode 100644 index 54c55c5ee..000000000 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/rest_resumable.py +++ /dev/null @@ -1,343 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 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. -# -import base64 -import json # type: ignore -import os -from typing import Callable, Dict, Optional, Sequence, Tuple, Union, BinaryIO -from datetime import datetime, timedelta, timezone - -from google.api_core import exceptions as core_exceptions -from google.api_core import gapic_v1 -from google.api_core import retry as retries -from google.auth import credentials as ga_credentials # type: ignore -from google.auth.transport.requests import AuthorizedSession # type: ignore - -import google.protobuf -from google.protobuf import empty_pb2 # type: ignore -from google.protobuf import json_format - -from google.ads.googleads.v26.services.types import youtube_video_upload_service - -from .rest_resumable_base import ( - _BaseYouTubeVideoUploadServiceRestResumableTransport, -) -from .resumable_upload import make_resumable_upload - - -try: - OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None] -except AttributeError: # pragma: NO COVER - OptionalRetry = Union[retries.Retry, object, None] # type: ignore - -_DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB -_DEFAULT_TIMEOUT = 14400 -_DEFAULT_CONTENT_TYPE = "application/octet-stream" - -_DEFAULT_RETRY = retries.Retry( - predicate=retries.if_exception_type( - core_exceptions.ServiceUnavailable, - core_exceptions.DeadlineExceeded, - ), - initial=1.0, - maximum=60.0, - multiplier=1.3, -) - -DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() - -if hasattr(DEFAULT_CLIENT_INFO, "protobuf_runtime_version"): # pragma: NO COVER - DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ - - -class YouTubeVideoUploadServiceRestResumableInterceptor: - """Interceptor for YouTubeVideoUploadService. Not implemented yet""" - - -class YouTubeVideoUploadServiceRestResumableTransport( - _BaseYouTubeVideoUploadServiceRestResumableTransport -): - """REST backend synchronous transport for YouTubeVideoUploadService. - It is only used by Resumable Media Upload functionality - """ - - def __init__( - self, - *, - host: str = "googleads.googleapis.com", - credentials: Optional[ga_credentials.Credentials] = None, - credentials_file: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - client_cert_source_for_mtls: Optional[ - Callable[[], Tuple[bytes, bytes]] - ] = None, - quota_project_id: Optional[str] = None, - client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, - always_use_jwt_access: Optional[bool] = False, - url_scheme: str = "https", - interceptor: Optional[ - YouTubeVideoUploadServiceRestResumableInterceptor - ] = None, - api_audience: Optional[str] = None, - developer_token: Optional[str] = None, - login_customer_id: Optional[str] = None, - linked_customer_id: Optional[str] = None, - use_cloud_org_for_api_access: bool = False, - **kwargs, # Catch-all for forward compatibility - ) -> None: - """Instantiate the transport. - - Args: - host (Optional[str]): - The hostname to connect to (default: 'googleads.googleapis.com'). - credentials (Optional[google.auth.credentials.Credentials]): The - authorization credentials to attach to requests. These - credentials identify the application to the service; if none - are specified, the client will attempt to ascertain the - credentials from the environment. - - credentials_file (Optional[str]): Deprecated. A file with credentials that can - be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. This argument will be - removed in the next major version of this library. - scopes (Optional(Sequence[str])): A list of scopes. This argument is - ignored if ``channel`` is provided. - client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client - certificate to configure mutual TLS HTTP channel. It is ignored - if ``channel`` is provided. - quota_project_id (Optional[str]): An optional project to use for billing - and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you are developing - your own client library. - always_use_jwt_access (Optional[bool]): Whether self signed JWT should - be used for service account credentials. - url_scheme: the protocol scheme for the API endpoint. Normally - "https", but for testing or local servers, - "http" can be specified. - developer_token (Optional[str]): The developer token to use for the - resumable upload transport. - login_customer_id (Optional[str]): The login customer ID to use for - the resumable upload transport. - linked_customer_id (Optional[str]): The linked customer ID to use for - the resumable upload transport. - use_cloud_org_for_api_access (bool): Whether to use the cloud org for - API access for the resumable upload transport. - """ - # Run the base constructor - super().__init__( - host=host, - credentials=credentials, - client_info=client_info, - always_use_jwt_access=always_use_jwt_access, - url_scheme=url_scheme, - api_audience=api_audience, - ) - self._session = AuthorizedSession( - self._credentials, default_host=self.DEFAULT_HOST - ) - if client_cert_source_for_mtls: - self._session.configure_mtls_channel(client_cert_source_for_mtls) - - self._developer_token = developer_token - self._login_customer_id = login_customer_id - self._linked_customer_id = linked_customer_id - self._use_cloud_org_for_api_access = use_cloud_org_for_api_access - - def _sanitize_headers_for_rest( - self, metadata: Union[Dict, Sequence[Tuple[str, Union[str, bytes]]]] - ) -> Dict[str, str]: - """ - Converts gRPC metadata into REST-friendly headers. - - - Decodes byte keys to strings. - - Base64 encodes byte values if the key ends with '-bin'. - - Raises ValueError if binary data is found in a non-'-bin' key. - """ - # Normalize input to a dict if it's a sequence - raw_headers = ( - dict(metadata) if not isinstance(metadata, dict) else metadata - ) - clean_headers = {} - - for key, value in raw_headers.items(): - # Ensure Key is a String - k_str = key.decode("utf-8") if isinstance(key, bytes) else key - - if isinstance(value, bytes): - if not k_str.endswith("-bin"): - raise ValueError( - f"Invalid binary header '{k_str}'. Binary data is only " - "allowed in keys ending with '-bin'." - ) - # Valid binary header: Base64 encode - clean_headers[k_str] = base64.b64encode(value).decode("ascii") - else: - # String value: Keep as is - clean_headers[k_str] = str(value) - - return clean_headers - - def _update_headers_with_googleads( - self, headers: dict[str, str] - ) -> dict[str, str]: - """ - Returns a new dictionary with identity headers injected, mirroring the - MetadataInterceptor logic. Does not modify the input dictionary. - """ - # Create a shallow copy to avoid in-place modification - new_headers = headers.copy() if headers else {} - - # 1. Developer Token Logic - # Only add if we are NOT using Cloud Org access AND the token exists. - # This prevents the crash if developer_token is None. - if not self._use_cloud_org_for_api_access and self._developer_token: - new_headers["developer-token"] = self._developer_token - - # 2. Login Customer ID Logic - if self._login_customer_id: - new_headers["login-customer-id"] = self._login_customer_id - - # 3. Linked Customer ID Logic - if self._linked_customer_id: - new_headers["linked-customer-id"] = self._linked_customer_id - - return new_headers - - @staticmethod - def _maybe_rewind(stream, rewind=False): - """Rewind the stream if desired. - - :type stream: IO[bytes] - :param stream: A bytes IO object open for reading. - - :type rewind: bool - :param rewind: Indicates if we should seek to the beginning of the stream. - """ - if rewind: - stream.seek(0, os.SEEK_SET) - - def create_you_tube_video_upload_resumable( - self, - request: youtube_video_upload_service.CreateYouTubeVideoUploadRequest, - *, - stream: BinaryIO, - rewind: bool = False, - size: Optional[int] = None, - content_type: Optional[str] = None, - chunk_size: Optional[int] = None, - retry: OptionalRetry = gapic_v1.method.DEFAULT, - timeout: Union[float, object] = gapic_v1.method.DEFAULT, - metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), - ) -> youtube_video_upload_service.CreateYouTubeVideoUploadResponse: - r"""Uploads a video to Google-managed or advertiser owned - (brand) YouTube channel. - - Args: - request (Union[google.ads.googleads.v26.services.types.CreateYouTubeVideoUploadRequest, dict]): - The request object. Request message for - [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. - stream (BinaryIO): - A stream of bytes, such as a file opened in binary mode for reading. - rewind (bool): - If True, seek to the beginning of the file handle before uploading from the stream. - size (int): - The number of bytes to be uploaded (which will be read from - ``stream``). If not provided, the upload will be concluded once - ``stream`` is exhausted. - content_type (str): - The MIME type of the content being uploaded. - chunk_size (int): - The size of each chunk to be uploaded. - retry (google.api_core.retry.Retry): Designation of what errors, if any, - should be retried. - timeout (float): The timeout for this request. - metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be - sent along with the request as metadata. Normally, each value must be of type `str`, - but for metadata keys ending with the suffix `-bin`, the corresponding values must - be of type `bytes`. - - Returns: - google.ads.googleads.v26.services.types.CreateYouTubeVideoUploadResponse: - Response message for - [YouTubeVideoUploadService.CreateYouTubeVideoUpload][google.ads.googleads.v26.services.YouTubeVideoUploadService.CreateYouTubeVideoUpload]. - - - """ - - self._maybe_rewind(stream, rewind=rewind) - - http_options = ( - _BaseYouTubeVideoUploadServiceRestResumableTransport._BaseCreateYouTubeVideoUploadRequest._get_http_options() - ) - transcoded_request = _BaseYouTubeVideoUploadServiceRestResumableTransport._BaseCreateYouTubeVideoUploadRequest._get_transcoded_request( - http_options, request - ) - request_url = "{host}/resumable/upload{uri}".format( - host=self._host, uri=transcoded_request["uri"] - ) - - # preserving_proto_field_name=True ensures we get snake case "customer_id" - json_body = json_format.MessageToJson( - request._pb, - preserving_proto_field_name=True, - ) - - if chunk_size is None: - chunk_size = _DEFAULT_CHUNK_SIZE - if retry is gapic_v1.method.DEFAULT: - retry = _DEFAULT_RETRY - if timeout is None or timeout is gapic_v1.method.DEFAULT: - timeout = _DEFAULT_TIMEOUT - if content_type is None: - content_type = _DEFAULT_CONTENT_TYPE - - deadline = datetime.now(timezone.utc) + timedelta(seconds=timeout) - headers = dict(metadata) - - # 1. Sanitize (will raise ValueError if headers are invalid) - headers = self._sanitize_headers_for_rest(headers or {}) - - # 2. Inject google-ads specific metadata - headers = self._update_headers_with_googleads(headers) - - response = make_resumable_upload( - transport=self._session, - request_body=json_body, - stream=stream, - upload_url=request_url, - size=size, - content_type=content_type, - chunk_size=chunk_size, - request_retry=retry, - deadline=deadline, - headers=headers, - on_progress=None, - ) - - data = response.json() - response_message = ( - youtube_video_upload_service.CreateYouTubeVideoUploadResponse() - ) - json_format.ParseDict( - data, response_message._pb, ignore_unknown_fields=True - ) - - return response_message - - -__all__ = ("YouTubeVideoUploadServiceRestResumableTransport",) diff --git a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/resumable_upload.py b/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/resumable_upload.py deleted file mode 100644 index a5a2db53d..000000000 --- a/google/ads/googleads/v26/services/services/you_tube_video_upload_service/transports/resumable_upload.py +++ /dev/null @@ -1,483 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 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. -# -import datetime -import logging - -from dataclasses import dataclass -from typing import ( - Any, - BinaryIO, - Callable, - Dict, - Optional, - Sequence, - Tuple, - Union, -) - -import google.api_core.retry -import requests -from google.ads.googleads.errors import raise_formatted_for_status - -_LOGGER = logging.getLogger(__name__) - -_DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB - - -@dataclass(frozen=True) -class ResumableUploadStatus: - """Status of a resumable upload. - - Attributes: - upload_url (str): - The session URL for the resumable upload. - total_bytes (Optional[int]): - The total number of bytes to be uploaded, if known. - bytes_uploaded (int): - The number of bytes successfully uploaded so far. - finished (bool): - Whether the upload has completed. - """ - - upload_url: str - total_bytes: Optional[int] - bytes_uploaded: int - finished: bool - - -class ResumableUpload: - """Manages a resumable upload session. - - This class handles the initiation, chunk transmission, and recovery - of a resumable upload according to the Google Resumable Upload protocol. - """ - - def __init__( - self, - upload_url: str, - stream: BinaryIO, - size: Optional[int] = None, - content_type: Optional[str] = None, - chunk_size: Optional[int] = None, - deadline: Optional[datetime.datetime] = None, - headers: Optional[Sequence[Tuple[str, Union[str, bytes]]]] = None, - on_progress: Optional[Callable[[ResumableUploadStatus], None]] = None, - ): - """Initializes the ResumableUpload object. - - Args: - upload_url (str): - The initial URL to start the resumable upload. - stream (BinaryIO): - A stream of bytes to be uploaded. - size (Optional[int]): - The total number of bytes to be uploaded. If not provided, - the upload continues until the stream is exhausted. - content_type (Optional[str]): - The MIME type of the content being uploaded. - chunk_size (Optional[int]): - The size of each chunk to be uploaded. Defaults to 10MB. - deadline (Optional[datetime.datetime]): - The deadline for the entire upload process. - headers (Optional[Sequence[Tuple[str, Union[str, bytes]]]]): - Extra headers to be sent with the initiation and chunk requests. - on_progress (Optional[Callable[[ResumableUploadStatus], None]]): - A callback function called after each successful chunk upload. - """ - self.initial_url = upload_url - self.stream = stream - self.size = size - self.content_type = content_type - self.chunk_size = chunk_size or _DEFAULT_CHUNK_SIZE - self.deadline = deadline - self.extra_headers = dict(headers or []) - self.on_progress = on_progress - - self.session_url = None - self.bytes_uploaded = 0 - self.finished = False - self.chunk_granularity = None - - def _check_deadline(self) -> Optional[float]: - if self.deadline: - now = datetime.datetime.now(datetime.timezone.utc) - timeout = (self.deadline - now).total_seconds() - if timeout <= 0: - raise RuntimeError( - f"Upload deadline {self.deadline} (as calculated from the timeout argument) was exceeded" - ) - return timeout - return None - - def _get_retry_predicate(self): - def should_retry(exc): - if isinstance(exc, requests.exceptions.RequestException): - if isinstance( - exc, - ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ), - ): - return True - if exc.response is not None: - status_code = exc.response.status_code - return status_code in (408, 429, 500, 502, 503, 504) - return False - - return should_retry - - def _log_request( - self, method: str, url: str, headers: Dict[str, str], data: Any = None - ): - if not _LOGGER.isEnabledFor(logging.DEBUG): - return - - display_data = data - if isinstance(data, bytes) and len(data) > 64: - display_data = f"{data[:64]!r}... ({len(data)} bytes)" - - _LOGGER.debug( - "HTTP Request: %s %s\nHeaders: %s\nBody: %s", - method, - url, - headers, - display_data, - ) - - def _log_response(self, response: requests.Response): - if not _LOGGER.isEnabledFor(logging.DEBUG): - return - - _LOGGER.debug( - "HTTP Response: %s %s\nHeaders: %s\nBody: %s", - response.status_code, - response.reason, - response.headers, - response.text, - ) - - def _prepare_initiate_request( - self, body: str - ) -> Tuple[str, str, str, Dict[str, str]]: - headers = { - "X-Goog-Upload-Protocol": "resumable", - "X-Goog-Upload-Command": "start", - } - - # Prefix logical headers - if self.content_type: - headers["X-Goog-Upload-Header-Content-Type"] = self.content_type - if self.size is not None: - headers["X-Goog-Upload-Header-Content-Length"] = str(self.size) - - # Add extra headers, prefixing if necessary - for k, v in self.extra_headers.items(): - if k.lower() in ( - "content-length", - "content-type", - "content-encoding", - "transfer-encoding", - ): - headers[f"X-Goog-Upload-Header-{k}"] = str(v) - else: - headers[k] = str(v) - - return "POST", self.initial_url, body, headers - - def _process_initiate_response(self, response: requests.Response): - raise_formatted_for_status(response) - - self.session_url = response.headers.get("X-Goog-Upload-URL") - if not self.session_url: - raise ValueError("Server did not return X-Goog-Upload-URL") - - granularity = response.headers.get("X-Goog-Upload-Chunk-Granularity") - if granularity: - self.chunk_granularity = int(granularity) - _LOGGER.debug( - "Server requested chunk granularity: %d", self.chunk_granularity - ) - - if self.on_progress: - self.on_progress( - ResumableUploadStatus( - upload_url=self.session_url, - total_bytes=self.size, - bytes_uploaded=0, - finished=False, - ) - ) - - def initiate( - self, - transport: requests.Session, - request_body: str = "", - request_retry: Optional[google.api_core.retry.Retry] = None, - ): - """Initiates the resumable upload session. - - This method sends the initial request to the server to start a new - resumable upload session and retrieves the session URL. - - Args: - transport (requests.Session): - The authorized session to use for the initiation request. - request_body (str): - The JSON-encoded request body for the initiation request. - request_retry (Optional[google.api_core.retry.Retry]): - Designation of what errors, if any, should be retried. - """ - _LOGGER.info("Initiating resumable upload to %s", self.initial_url) - method, url, body, headers = self._prepare_initiate_request( - request_body - ) - - def do_initiate(): - timeout = self._check_deadline() - self._log_request(method, url, headers, body) - response = transport.request( - method, url, data=body, headers=headers, timeout=timeout - ) - self._log_response(response) - raise_formatted_for_status(response) - return response - - if request_retry: - response = request_retry(do_initiate)() - else: - response = do_initiate() - - self._process_initiate_response(response) - - def _prepare_chunk_request( - self, - ) -> Tuple[str, str, bytes, Dict[str, str], int]: - # Adjust chunk size based on granularity if needed - actual_chunk_size = self.chunk_size - if self.chunk_granularity: - if actual_chunk_size % self.chunk_granularity != 0: - actual_chunk_size = ( - (actual_chunk_size // self.chunk_granularity) + 1 - ) * self.chunk_granularity - _LOGGER.debug( - "Adjusted chunk size to %d based on granularity %d", - actual_chunk_size, - self.chunk_granularity, - ) - - data = self.stream.read(actual_chunk_size) - data_len = len(data) - is_last_chunk = data_len < actual_chunk_size or ( - self.size is not None - and self.bytes_uploaded + data_len >= self.size - ) - - command = "upload" - if is_last_chunk: - command = "upload, finalize" - - headers = { - "X-Goog-Upload-Command": command, - "X-Goog-Upload-Offset": str(self.bytes_uploaded), - } - - return "POST", self.session_url, data, headers, data_len - - def _process_chunk_response( - self, response: requests.Response, chunk_len: int - ): - raise_formatted_for_status(response) - - status = response.headers.get("X-Goog-Upload-Status") - if status == "active": - self.bytes_uploaded += chunk_len - elif status == "final": - self.finished = True - self.bytes_uploaded += chunk_len - - def transmit_next_chunk( - self, transport: requests.Session - ) -> requests.Response: - """Uploads the next chunk of data from the stream. - - This method reads a chunk from the stream, prepares the upload request, - transmits it to the server, and processes the response. - - Args: - transport (requests.Session): - The authorized session to use for the upload. - - Returns: - requests.Response: - The response from the server for this chunk upload. - """ - self._check_deadline() - method, url, data, headers, chunk_len = self._prepare_chunk_request() - - retry = google.api_core.retry.Retry( - predicate=self._get_retry_predicate(), - deadline=self._check_deadline(), - ) - - def do_upload(): - _LOGGER.info( - "Uploading chunk at offset %s, size %d, command %s", - headers["X-Goog-Upload-Offset"], - chunk_len, - headers["X-Goog-Upload-Command"], - ) - timeout = self._check_deadline() - self._log_request(method, url, headers, data) - response = transport.request( - method, url, data=data, headers=headers, timeout=timeout - ) - self._log_response(response) - raise_formatted_for_status(response) - return response - - try: - response = retry(do_upload)() - self._process_chunk_response(response, chunk_len) - except Exception as e: - _LOGGER.error("Error during chunk upload: %s", e) - self._recover(transport, retry) - if ( - isinstance(e, requests.exceptions.HTTPError) - and e.response is not None - ): - return e.response - raise - - if self.on_progress: - self.on_progress( - ResumableUploadStatus( - upload_url=self.session_url, - total_bytes=self.size, - bytes_uploaded=self.bytes_uploaded, - finished=self.finished, - ) - ) - - return response - - def _prepare_recovery_request(self) -> Tuple[str, str, Dict[str, str]]: - headers = {"X-Goog-Upload-Command": "query"} - return "POST", self.session_url, headers - - def _process_recovery_response(self, response: requests.Response): - raise_formatted_for_status(response) - status = response.headers.get("X-Goog-Upload-Status") - - if status == "active": - received = int( - response.headers.get("X-Goog-Upload-Size-Received", 0) - ) - _LOGGER.info( - "Server reported %d bytes received. Seeking stream.", received - ) - self.bytes_uploaded = received - self.stream.seek(received) - elif status == "final": - _LOGGER.info("Upload already finalized according to server.") - self.finished = True - elif status == "cancelled": - raise RuntimeError("Upload was cancelled by server") - - def _recover( - self, transport: requests.Session, retry: google.api_core.retry.Retry - ): - _LOGGER.info("Attempting recovery via query command") - method, url, headers = self._prepare_recovery_request() - - def do_query(): - timeout = self._check_deadline() - self._log_request(method, url, headers) - response = transport.request( - method, url, headers=headers, timeout=timeout - ) - self._log_response(response) - raise_formatted_for_status(response) - return response - - response = retry(do_query)() - self._process_recovery_response(response) - - -def make_resumable_upload( - transport: requests.Session, - request_body: str, - stream: BinaryIO, - upload_url: str, - size: Optional[int] = None, - content_type: Optional[str] = None, - chunk_size: Optional[int] = None, - request_retry: Optional[google.api_core.retry.Retry] = None, - deadline: Optional[datetime.datetime] = None, - headers: Optional[Sequence[Tuple[str, Union[str, bytes]]]] = None, - on_progress: Optional[Callable[[ResumableUploadStatus], None]] = None, -) -> requests.Response: - """Makes a resumable upload. - - Args: - transport (requests.Session): - The authorized session to use for the upload. - request_body (str): - The JSON-encoded request body to send during initiation. - stream (BinaryIO): - A stream of bytes to be uploaded. - upload_url (str): - The initial URL to start the resumable upload. - size (int): - The number of bytes to be uploaded. If not provided, - the upload will continue until the stream is exhausted. - content_type (str): - The MIME type of the content being uploaded. - chunk_size (int): - The size of each chunk to be uploaded. - request_retry (google.api_core.retry.Retry): - Designation of what errors, if any, should be retried during initiation. - deadline (datetime.datetime): - The deadline for the entire upload process. - headers (Sequence[Tuple[str, Union[str, bytes]]]): - Key/value pairs which should be sent along with the request as metadata. - on_progress (Callable[[ResumableUploadStatus], None]): - A callback function that will be called after each chunk is uploaded. - - Returns: - requests.Response: - The final response from the server after the upload is complete. - """ - upload = ResumableUpload( - upload_url, - stream=stream, - size=size, - content_type=content_type, - chunk_size=chunk_size, - deadline=deadline, - headers=headers, - on_progress=on_progress, - ) - upload.initiate( - transport=transport, - request_body=request_body, - request_retry=request_retry, - ) - - final_response = None - while not upload.finished: - final_response = upload.transmit_next_chunk(transport=transport) - return final_response diff --git a/google/ads/googleads/v26/services/types/__init__.py b/google/ads/googleads/v26/services/types/__init__.py index 5719181c7..9e46b01fb 100644 --- a/google/ads/googleads/v26/services/types/__init__.py +++ b/google/ads/googleads/v26/services/types/__init__.py @@ -460,6 +460,12 @@ MutateCustomerAssetSetsRequest, MutateCustomerAssetSetsResponse, ) +from .customer_budget_network_setting_service import ( + CustomerBudgetNetworkSettingOperation, + MutateCustomerBudgetNetworkSettingRequest, + MutateCustomerBudgetNetworkSettingResponse, + MutateCustomerBudgetNetworkSettingResult, +) from .customer_client_link_service import ( CustomerClientLinkOperation, MutateCustomerClientLinkRequest, @@ -675,6 +681,13 @@ MutateLabelsRequest, MutateLabelsResponse, ) +from .local_services_eligibility_service import ( + CheckLocalServicesEligibilityRequest, + CheckLocalServicesEligibilityResponse, + EligibleCategory, + EligibleGoal, + SuggestionReason, +) from .local_services_lead_service import ( AppendLeadConversationRequest, AppendLeadConversationResponse, @@ -1196,6 +1209,10 @@ "MutateCustomerAssetSetResult", "MutateCustomerAssetSetsRequest", "MutateCustomerAssetSetsResponse", + "CustomerBudgetNetworkSettingOperation", + "MutateCustomerBudgetNetworkSettingRequest", + "MutateCustomerBudgetNetworkSettingResponse", + "MutateCustomerBudgetNetworkSettingResult", "CustomerClientLinkOperation", "MutateCustomerClientLinkRequest", "MutateCustomerClientLinkResponse", @@ -1353,6 +1370,11 @@ "MutateLabelResult", "MutateLabelsRequest", "MutateLabelsResponse", + "CheckLocalServicesEligibilityRequest", + "CheckLocalServicesEligibilityResponse", + "EligibleCategory", + "EligibleGoal", + "SuggestionReason", "AppendLeadConversationRequest", "AppendLeadConversationResponse", "Conversation", diff --git a/google/ads/googleads/v26/services/types/campaign_budget_service.py b/google/ads/googleads/v26/services/types/campaign_budget_service.py index 962fa90f2..03a047873 100644 --- a/google/ads/googleads/v26/services/types/campaign_budget_service.py +++ b/google/ads/googleads/v26/services/types/campaign_budget_service.py @@ -45,6 +45,12 @@ class MutateCampaignBudgetsRequest(proto.Message): r"""Request message for [CampaignBudgetService.MutateCampaignBudgets][google.ads.googleads.v26.services.CampaignBudgetService.MutateCampaignBudgets]. + Update operations in ``operations`` require an ``update_mask`` + specifying the fields to modify. Note that ``explicitly_shared`` + defaults to ``true`` if unspecified. A non-shared budget can be + updated to explicitly shared, but an explicitly shared budget cannot + become non-shared. + Attributes: customer_id (str): Required. The ID of the customer whose @@ -96,8 +102,12 @@ class MutateCampaignBudgetsRequest(proto.Message): class CampaignBudgetOperation(proto.Message): - r"""A single operation (create, update, remove) on a campaign - budget. + r"""A single operation (create, update, remove) on a campaign budget. + + Update operations require an ``update_mask`` specifying which fields + to modify. Note that ``explicitly_shared`` defaults to ``true`` if + unspecified. A non-shared budget can be updated to explicitly + shared, but an explicitly shared budget cannot become non-shared. This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. @@ -108,16 +118,23 @@ class CampaignBudgetOperation(proto.Message): Attributes: update_mask (google.protobuf.field_mask_pb2.FieldMask): - FieldMask that determines which resource - fields are modified in an update. + FieldMask that determines which resource fields are modified + in an update. Required for ``update`` operations. create (google.ads.googleads.v26.resources.types.CampaignBudget): - Create operation: No resource name is - expected for the new budget. + Create operation: No resource name is expected for the new + budget. Note that ``explicitly_shared`` defaults to ``true`` + if unspecified. A non-shared budget can be updated to + explicitly shared, but an explicitly shared budget cannot + become non-shared. This field is a member of `oneof`_ ``operation``. update (google.ads.googleads.v26.resources.types.CampaignBudget): - Update operation: The campaign budget is - expected to have a valid resource name. + Update operation: The campaign budget is expected to have a + valid resource name, and ``update_mask`` must be populated + with the fields to modify. Note that ``explicitly_shared`` + defaults to ``true`` if unspecified. A non-shared budget can + be updated to explicitly shared, but an explicitly shared + budget cannot become non-shared. This field is a member of `oneof`_ ``operation``. remove (str): diff --git a/google/ads/googleads/v26/services/types/conversion_upload_service.py b/google/ads/googleads/v26/services/types/conversion_upload_service.py index 18f716bf6..42459fc7c 100644 --- a/google/ads/googleads/v26/services/types/conversion_upload_service.py +++ b/google/ads/googleads/v26/services/types/conversion_upload_service.py @@ -51,6 +51,26 @@ class UploadClickConversionsRequest(proto.Message): r"""Request message for [ConversionUploadService.UploadClickConversions][google.ads.googleads.v26.services.ConversionUploadService.UploadClickConversions]. + Important: Always set ``partial_failure = true``. Partial failure + mode is required for conversion uploads. When enabled, valid + conversions are processed and any invalid conversions return + individual errors in + ``UploadClickConversionsResponse.partial_failure_error``. Setting + ``partial_failure = false`` or omitting the field is not supported + and causes the entire request to fail. + + When uploading enhanced conversions for leads with + ``user_identifiers``, normalize raw identifier strings before + hashing with SHA-256 (trim leading and trailing whitespace, convert + email addresses to lowercase, and format phone numbers in E.164 + format), and populate only one identifier field per + ``UserIdentifier`` object (``oneof identifier`` is mutually + exclusive). + + For help diagnosing conversion upload issues, see the conversion + upload `troubleshooting + guide `__. + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields @@ -62,11 +82,11 @@ class UploadClickConversionsRequest(proto.Message): Required. The conversions that are being uploaded. partial_failure (bool): - Required. If ``true``, successful operations will be carried - out and invalid operations will return errors. If ``false``, - all operations will be carried out in one transaction if and - only if they are all valid. This should always be set to - ``true``. + Required. Must be set to ``true``. Enables partial failure + mode so that valid conversions are processed and any invalid + conversions return individual errors in + ``partial_failure_error``. Setting this field to ``false`` + is not supported and causes the entire request to fail. See `Best practices for partial failures `__. @@ -157,12 +177,11 @@ class UploadCallConversionsRequest(proto.Message): Required. The conversions that are being uploaded. partial_failure (bool): - Required. If ``true``, successful operations will be carried - out and invalid operations will return errors. If ``false``, - all operations will be carried out in one transaction if and - only if they are all valid. - - This should always be set to ``true``. + Required. Must be set to ``true``. Enables partial failure + mode so that valid conversions are processed and any invalid + conversions return individual errors in + ``partial_failure_error``. Setting this field to ``false`` + is not supported and causes the entire request to fail. See `Best practices for partial failures `__. @@ -224,6 +243,13 @@ class UploadCallConversionsResponse(proto.Message): class ClickConversion(proto.Message): r"""A click conversion. + For help diagnosing and resolving conversion upload issues, see the + conversion upload `troubleshooting + guide `__ (or use + the ``troubleshoot-conversions`` workflow in the `Google Ads API + Developer + Assistant `__). + This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. Setting any member of the oneof automatically clears all other @@ -293,7 +319,12 @@ class ClickConversion(proto.Message): The user identifiers associated with this conversion. Only hashed_email and hashed_phone_number are supported for conversion uploads. The maximum number of user identifiers - for each conversion is 5. + for each conversion is 5. Each ``UserIdentifier`` in the + list must have only one identifier field set + (``oneof identifier`` is mutually exclusive), and raw values + must be normalized before SHA-256 hashing (for email + addresses, trim leading and trailing whitespace and convert + to lowercase; for phone numbers, format in E.164 format). conversion_environment (google.ads.googleads.v26.enums.types.ConversionEnvironmentEnum.ConversionEnvironment): The environment this conversion was recorded on, for example, App or Web. diff --git a/google/ads/googleads/v26/services/types/customer_budget_network_setting_service.py b/google/ads/googleads/v26/services/types/customer_budget_network_setting_service.py new file mode 100644 index 000000000..e4a346d00 --- /dev/null +++ b/google/ads/googleads/v26/services/types/customer_budget_network_setting_service.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + + +import proto # type: ignore + +from google.ads.googleads.v26.enums.types import ( + response_content_type as gage_response_content_type, +) +from google.ads.googleads.v26.resources.types import ( + customer_budget_network_setting as gagr_customer_budget_network_setting, +) +import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.services", + marshal="google.ads.googleads.v26", + manifest={ + "MutateCustomerBudgetNetworkSettingRequest", + "CustomerBudgetNetworkSettingOperation", + "MutateCustomerBudgetNetworkSettingResponse", + "MutateCustomerBudgetNetworkSettingResult", + }, +) + + +class MutateCustomerBudgetNetworkSettingRequest(proto.Message): + r"""Request message for + [CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting][google.ads.googleads.v26.services.CustomerBudgetNetworkSettingService.MutateCustomerBudgetNetworkSetting]. + + Attributes: + customer_id (str): + Required. The ID of the customer whose + customer budget network setting is being + modified. + operation (google.ads.googleads.v26.services.types.CustomerBudgetNetworkSettingOperation): + Required. The operation to perform on the + customer budget network setting. + validate_only (bool): + If true, the request is validated but not + executed. Only errors are returned, not results. + response_content_type (google.ads.googleads.v26.enums.types.ResponseContentTypeEnum.ResponseContentType): + The response content type setting. Determines + whether the mutable resource or just the + resource name should be returned post mutation. + """ + + customer_id: str = proto.Field( + proto.STRING, + number=1, + ) + operation: "CustomerBudgetNetworkSettingOperation" = proto.Field( + proto.MESSAGE, + number=2, + message="CustomerBudgetNetworkSettingOperation", + ) + validate_only: bool = proto.Field( + proto.BOOL, + number=3, + ) + response_content_type: ( + gage_response_content_type.ResponseContentTypeEnum.ResponseContentType + ) = proto.Field( + proto.ENUM, + number=4, + enum=gage_response_content_type.ResponseContentTypeEnum.ResponseContentType, + ) + + +class CustomerBudgetNetworkSettingOperation(proto.Message): + r"""A single operation (create, update) on a customer budget + network setting. + + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + update_mask (google.protobuf.field_mask_pb2.FieldMask): + FieldMask that determines which resource + fields are modified in an update. + create (google.ads.googleads.v26.resources.types.CustomerBudgetNetworkSetting): + Create operation: No resource name is + expected for the new setting. + + This field is a member of `oneof`_ ``operation``. + update (google.ads.googleads.v26.resources.types.CustomerBudgetNetworkSetting): + Update operation: The customer budget network + setting is expected to have a valid resource + name. + + This field is a member of `oneof`_ ``operation``. + """ + + update_mask: field_mask_pb2.FieldMask = proto.Field( + proto.MESSAGE, + number=4, + message=field_mask_pb2.FieldMask, + ) + create: ( + gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting + ) = proto.Field( + proto.MESSAGE, + number=1, + oneof="operation", + message=gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting, + ) + update: ( + gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting + ) = proto.Field( + proto.MESSAGE, + number=2, + oneof="operation", + message=gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting, + ) + + +class MutateCustomerBudgetNetworkSettingResponse(proto.Message): + r"""Response message for customer budget network setting mutate. + + Attributes: + result (google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingResult): + All results for the mutate. + """ + + result: "MutateCustomerBudgetNetworkSettingResult" = proto.Field( + proto.MESSAGE, + number=1, + message="MutateCustomerBudgetNetworkSettingResult", + ) + + +class MutateCustomerBudgetNetworkSettingResult(proto.Message): + r"""The result for the customer budget network setting mutate. + + Attributes: + resource_name (str): + Returned for successful operations. + customer_budget_network_setting (google.ads.googleads.v26.resources.types.CustomerBudgetNetworkSetting): + The mutated customer budget network setting with only + mutable fields after mutate. The field will only be returned + when response_content_type is set to "MUTABLE_RESOURCE". + """ + + resource_name: str = proto.Field( + proto.STRING, + number=1, + ) + customer_budget_network_setting: ( + gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting + ) = proto.Field( + proto.MESSAGE, + number=2, + message=gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/services/types/google_ads_service.py b/google/ads/googleads/v26/services/types/google_ads_service.py index 501875761..a27a0adac 100644 --- a/google/ads/googleads/v26/services/types/google_ads_service.py +++ b/google/ads/googleads/v26/services/types/google_ads_service.py @@ -272,6 +272,9 @@ from google.ads.googleads.v26.resources.types import ( customer_asset_set as gagr_customer_asset_set, ) +from google.ads.googleads.v26.resources.types import ( + customer_budget_network_setting as gagr_customer_budget_network_setting, +) from google.ads.googleads.v26.resources.types import ( customer_client as gagr_customer_client, ) @@ -634,6 +637,9 @@ custom_conversion_goal_service, ) from google.ads.googleads.v26.services.types import customer_asset_service +from google.ads.googleads.v26.services.types import ( + customer_budget_network_setting_service, +) from google.ads.googleads.v26.services.types import ( customer_conversion_goal_service, ) @@ -1125,6 +1131,9 @@ class GoogleAdsRow(proto.Message): customer_asset_set (google.ads.googleads.v26.resources.types.CustomerAssetSet): The customer asset set referenced in the query. + customer_budget_network_setting (google.ads.googleads.v26.resources.types.CustomerBudgetNetworkSetting): + The customer budget network setting + referenced in the query. accessible_bidding_strategy (google.ads.googleads.v26.resources.types.AccessibleBiddingStrategy): The accessible bidding strategy referenced in the query. @@ -1903,6 +1912,13 @@ class GoogleAdsRow(proto.Message): number=195, message=gagr_customer_asset_set.CustomerAssetSet, ) + customer_budget_network_setting: ( + gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting + ) = proto.Field( + proto.MESSAGE, + number=268, + message=gagr_customer_budget_network_setting.CustomerBudgetNetworkSetting, + ) accessible_bidding_strategy: ( gagr_accessible_bidding_strategy.AccessibleBiddingStrategy ) = proto.Field( @@ -2863,6 +2879,11 @@ class MutateOperation(proto.Message): customer_asset_operation (google.ads.googleads.v26.services.types.CustomerAssetOperation): A customer asset mutate operation. + This field is a member of `oneof`_ ``operation``. + customer_budget_network_setting_operation (google.ads.googleads.v26.services.types.CustomerBudgetNetworkSettingOperation): + A customer budget network setting mutate + operation. + This field is a member of `oneof`_ ``operation``. customer_conversion_goal_operation (google.ads.googleads.v26.services.types.CustomerConversionGoalOperation): A customer conversion goal mutate operation. @@ -3283,6 +3304,14 @@ class MutateOperation(proto.Message): message=customer_asset_service.CustomerAssetOperation, ) ) + customer_budget_network_setting_operation: ( + customer_budget_network_setting_service.CustomerBudgetNetworkSettingOperation + ) = proto.Field( + proto.MESSAGE, + number=92, + oneof="operation", + message=customer_budget_network_setting_service.CustomerBudgetNetworkSettingOperation, + ) customer_conversion_goal_operation: ( customer_conversion_goal_service.CustomerConversionGoalOperation ) = proto.Field( @@ -3641,6 +3670,11 @@ class MutateOperationResponse(proto.Message): customer_asset_result (google.ads.googleads.v26.services.types.MutateCustomerAssetResult): The result for the customer asset mutate. + This field is a member of `oneof`_ ``response``. + customer_budget_network_setting_result (google.ads.googleads.v26.services.types.MutateCustomerBudgetNetworkSettingResult): + The result for the customer budget network + setting mutate. + This field is a member of `oneof`_ ``response``. customer_conversion_goal_result (google.ads.googleads.v26.services.types.MutateCustomerConversionGoalResult): The result for the customer conversion goal @@ -4066,6 +4100,14 @@ class MutateOperationResponse(proto.Message): message=customer_asset_service.MutateCustomerAssetResult, ) ) + customer_budget_network_setting_result: ( + customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResult + ) = proto.Field( + proto.MESSAGE, + number=92, + oneof="response", + message=customer_budget_network_setting_service.MutateCustomerBudgetNetworkSettingResult, + ) customer_conversion_goal_result: ( customer_conversion_goal_service.MutateCustomerConversionGoalResult ) = proto.Field( diff --git a/google/ads/googleads/v26/services/types/local_services_eligibility_service.py b/google/ads/googleads/v26/services/types/local_services_eligibility_service.py new file mode 100644 index 000000000..74e194e7b --- /dev/null +++ b/google/ads/googleads/v26/services/types/local_services_eligibility_service.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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. +# +from __future__ import annotations + +from typing import MutableSequence + +import proto # type: ignore + +from google.ads.googleads.v26.enums.types import local_services_lead_type + + +__protobuf__ = proto.module( + package="google.ads.googleads.v26.services", + marshal="google.ads.googleads.v26", + manifest={ + "CheckLocalServicesEligibilityRequest", + "CheckLocalServicesEligibilityResponse", + "SuggestionReason", + "EligibleGoal", + "EligibleCategory", + }, +) + + +class CheckLocalServicesEligibilityRequest(proto.Message): + r"""Request message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + + Attributes: + customer_id (str): + Required. The ID of the customer whose + eligibility is being checked. + listing_id (str): + Required. The Google Business Profile listing + ID to check eligibility for. + """ + + customer_id: str = proto.Field( + proto.STRING, + number=1, + ) + listing_id: str = proto.Field( + proto.STRING, + number=2, + ) + + +class CheckLocalServicesEligibilityResponse(proto.Message): + r"""Response message for + [LocalServicesEligibilityService.CheckLocalServicesEligibility][google.ads.googleads.v26.services.LocalServicesEligibilityService.CheckLocalServicesEligibility]. + + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + eligible (bool): + Overall eligibility status of the customer. + + This field is a member of `oneof`_ ``_eligible``. + suggestion_reasons (MutableSequence[google.ads.googleads.v26.services.types.SuggestionReason]): + Detailed suggestion reasons for checking + eligibility (e.g. licensing issues). + eligible_goals (MutableSequence[google.ads.googleads.v26.services.types.EligibleGoal]): + List of eligible goals mapped to their + categories. + """ + + eligible: bool = proto.Field( + proto.BOOL, + number=1, + optional=True, + ) + suggestion_reasons: MutableSequence["SuggestionReason"] = ( + proto.RepeatedField( + proto.MESSAGE, + number=2, + message="SuggestionReason", + ) + ) + eligible_goals: MutableSequence["EligibleGoal"] = proto.RepeatedField( + proto.MESSAGE, + number=3, + message="EligibleGoal", + ) + + +class SuggestionReason(proto.Message): + r"""Suggestion reason details. + + Attributes: + short_text (str): + Short summary text explaining the reason. + long_text (str): + Detailed long text description. + """ + + short_text: str = proto.Field( + proto.STRING, + number=1, + ) + long_text: str = proto.Field( + proto.STRING, + number=2, + ) + + +class EligibleGoal(proto.Message): + r"""Eligible goal mapping details. + + Attributes: + lead_type (google.ads.googleads.v26.enums.types.LocalServicesLeadTypeEnum.LeadType): + Eligible lead type. + categories (MutableSequence[google.ads.googleads.v26.services.types.EligibleCategory]): + Eligible categories for this lead type. + """ + + lead_type: local_services_lead_type.LocalServicesLeadTypeEnum.LeadType = ( + proto.Field( + proto.ENUM, + number=1, + enum=local_services_lead_type.LocalServicesLeadTypeEnum.LeadType, + ) + ) + categories: MutableSequence["EligibleCategory"] = proto.RepeatedField( + proto.MESSAGE, + number=2, + message="EligibleCategory", + ) + + +class EligibleCategory(proto.Message): + r"""Eligible category information. + + Attributes: + category_id (str): + The unique identifier for the service category. For example, + 'xcat:service_area_business_plumber'. + display_name (str): + The localized display name of the category. + For example, 'Plumber'. + job_types (MutableSequence[str]): + The list of available job type IDs for this category. For + example, 'install_faucet', 'drain_cleaning'. + callouts (MutableSequence[str]): + The list of available business callout IDs for this + category. For example, 'family_owned', '24_7_service'. + """ + + category_id: str = proto.Field( + proto.STRING, + number=1, + ) + display_name: str = proto.Field( + proto.STRING, + number=2, + ) + job_types: MutableSequence[str] = proto.RepeatedField( + proto.STRING, + number=3, + ) + callouts: MutableSequence[str] = proto.RepeatedField( + proto.STRING, + number=4, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/ads/googleads/v26/services/types/recommendation_service.py b/google/ads/googleads/v26/services/types/recommendation_service.py index bdfab3c3f..9aafad289 100644 --- a/google/ads/googleads/v26/services/types/recommendation_service.py +++ b/google/ads/googleads/v26/services/types/recommendation_service.py @@ -248,6 +248,11 @@ class ApplyRecommendationOperation(proto.Message): Parameters to use when applying Raise Budget Performance Too Low recommendation. + This field is a member of `oneof`_ ``apply_parameters``. + optimize_for_new_customers (google.ads.googleads.v26.services.types.ApplyRecommendationOperation.OptimizeForNewCustomersParameters): + Parameters to use when applying Optimize For + New Customers recommendation. + This field is a member of `oneof`_ ``apply_parameters``. """ @@ -606,6 +611,44 @@ class RaiseBudgetPerformanceTooLowParameters(proto.Message): number=1, ) + class OptimizeForNewCustomersParameters(proto.Message): + r"""Parameters to use when applying Optimize For New Customers + recommendations. + + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + account_default_incremental_conversion_value (float): + Optional. Incremental conversion value set at + the account level for each new customer + acquired. This is a standard floating-point + value expressed in units of the account's local + currency, not in micros. + + This field is a member of `oneof`_ ``_account_default_incremental_conversion_value``. + campaign_override_incremental_conversion_value (float): + Optional. Incremental conversion value set at + the campaign level for each new customer + acquired, overriding the account default value. + This is a standard floating-point value + expressed in units of the account's local + currency, not in micros. + + This field is a member of `oneof`_ ``_campaign_override_incremental_conversion_value``. + """ + + account_default_incremental_conversion_value: float = proto.Field( + proto.DOUBLE, + number=1, + optional=True, + ) + campaign_override_incremental_conversion_value: float = proto.Field( + proto.DOUBLE, + number=2, + optional=True, + ) + class AdAssetApplyParameters(proto.Message): r"""Common parameters used when applying ad asset recommendations. @@ -1015,6 +1058,12 @@ class LeadFormAssetParameters(proto.Message): message=RaiseBudgetPerformanceTooLowParameters, ) ) + optimize_for_new_customers: OptimizeForNewCustomersParameters = proto.Field( + proto.MESSAGE, + number=29, + oneof="apply_parameters", + message=OptimizeForNewCustomersParameters, + ) class ApplyRecommendationResponse(proto.Message): diff --git a/noxfile.py b/noxfile.py index 0e32007a7..495fede0a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,7 @@ import pathlib -PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] +PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] PROTOBUF_IMPLEMENTATIONS = ["python", "upb"] TEST_COMMAND = [ diff --git a/pyproject.toml b/pyproject.toml index 3a7c12cd5..2bc6bd9aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,10 +18,10 @@ build-backend = "setuptools.build_meta" [project] name = "google-ads" -version = "34.0.0" +version = "35.0.0" description = "Client library for the Google Ads API" readme = "./README.rst" -requires-python = ">=3.9, <3.15" +requires-python = ">=3.10, <3.15" license = { file = "LICENSE" } authors = [ {name = "Google LLC", email = "googleapis-packages@google.com"} @@ -30,7 +30,6 @@ classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -39,10 +38,8 @@ classifiers = [ ] dependencies = [ "google-auth-oauthlib >= 1.0.0, < 2.0.0", - "google-api-core >= 2.13.0, <= 3.0.0", - "google-api-core >= 2.22.0, <= 3.0.0; python_version >= '3.13'", - "googleapis-common-protos >= 1.56.3, < 2.0.0", - "googleapis-common-protos >= 1.63.2, < 2.0.0; python_version >= '3.13'", + "google-api-core >= 2.41.0, <= 3.0.0", + "googleapis-common-protos >= 1.69.2, < 2.0.0", # NOTE: Source code for grpcio and grpcio-status exist in the same # grpc/grpc monorepo and thus these two dependencies should always # have the same version range. @@ -52,16 +49,17 @@ dependencies = [ "grpcio-status >= 1.59.0, < 2.0.0", "grpcio-status >= 1.66.2, < 2.0.0; python_version >= '3.13'", "grpcio-status >= 1.75.1, < 2.0.0; python_version >= '3.14'", - "proto-plus >= 1.22.3, < 2.0.0", - "proto-plus >= 1.25.0, < 2.0.0; python_version >= '3.13'", + "proto-plus >= 1.26.1, < 2.0.0", "PyYAML >= 5.1, < 7.0", - "protobuf >= 4.25.0, < 7.0.0; python_version < '3.10'", - "protobuf >= 4.25.0, < 8.0.0; python_version >= '3.10'", - "protobuf >= 5.26.1; python_version >= '3.13'", + "protobuf >= 6.33.5, < 8.0.0", ] [project.optional-dependencies] tests = ["nox >= 2025.02.09"] +async_rest = [ + "google-api-core[async_rest] >= 2.41.0, <= 3.0.0", + "google-auth[aiohttp] >= 2.60.0, < 3.0.0", +] [project.urls] Repository = "https://github.com/googleads/google-ads-python" diff --git a/tests/client_test.py b/tests/client_test.py index 8ef6e4324..8092cb326 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -13,13 +13,14 @@ # limitations under the License. """Tests for the Google Ads API client library.""" +import asyncio from importlib import import_module from inspect import getmembers -from unittest import mock +import json +from unittest import IsolatedAsyncioTestCase, TestCase, mock import os import pickle from pyfakefs.fake_filesystem_unittest import TestCase as FileTestCase -from unittest import TestCase import yaml from google.protobuf.message import Message as ProtobufMessageType @@ -1040,6 +1041,167 @@ def test_client_info_package_not_found(self): reload(Client) + def test_youtube_video_upload_service_sync_resumable_session(self): + rest_mod = import_module( + f"google.ads.googleads.{latest_version}.services.services." + "you_tube_video_upload_service.transports.rest" + ) + test_proxy = "http://localhost:8000" + client = Client.GoogleAdsClient( + credentials=mock.Mock(), + developer_token=self.developer_token, + login_customer_id=self.login_customer_id, + linked_customer_id=self.linked_customer_id, + endpoint="custom.ads.googleapis.com", + http_proxy=test_proxy, + use_proto_plus=True, + ads_assistant="1.2.3", + version=latest_version, + ) + self.addCleanup( + Client._GRPC_CHANNEL_OPTIONS.remove, + ("grpc.http_proxy", test_proxy), + ) + + svc = client.get_service("YouTubeVideoUploadService") + self.assertIsNone(svc.transport._rest_transport._transport) + + request = client.get_type("CreateYouTubeVideoUploadRequest") + request.customer_id = "9998887777" + request.you_tube_video_upload.video_title = "My Video" + request.you_tube_video_upload.video_privacy = ( + client.enums.YouTubeVideoPrivacyEnum.UNLISTED + ) + + with mock.patch.object( + rest_mod.resumable_transfer, + "ResumableUploadSession", + wraps=rest_mod.resumable_transfer.ResumableUploadSession, + ) as mock_session_cls: + session = svc.create_you_tube_video_upload(request=request) + + self.assertIsInstance( + session, rest_mod.resumable_transfer.ResumableUploadSession + ) + call_kwargs = mock_session_cls.call_args.kwargs + self.assertEqual( + call_kwargs["upload_url"], + f"https://custom.ads.googleapis.com/resumable/upload/" + f"{latest_version}/customers/9998887777/youTubeVideoUploads:create", + ) + headers = call_kwargs["config"].headers + self.assertEqual(headers["developer-token"], self.developer_token) + self.assertEqual(headers["login-customer-id"], self.login_customer_id) + self.assertEqual(headers["linked-customer-id"], self.linked_customer_id) + self.assertEqual( + headers["x-goog-request-params"], "customer_id=9998887777" + ) + self.assertIn("gaada/1.2.3", headers["x-goog-api-client"]) + self.assertEqual(headers["Content-Type"], "application/json") + self.assertEqual( + call_kwargs["transport"].proxies, + {"http": test_proxy, "https": test_proxy}, + ) + body_dict = json.loads(call_kwargs["request_body"]) + self.assertEqual( + body_dict["youTubeVideoUpload"]["videoTitle"], "My Video" + ) + self.assertEqual( + body_dict["youTubeVideoUpload"]["videoPrivacy"], "UNLISTED" + ) + svc.transport.close() + + def test_youtube_video_upload_service_sync_protobuf_request(self): + rest_mod = import_module( + f"google.ads.googleads.{latest_version}.services.services." + "you_tube_video_upload_service.transports.rest" + ) + client = Client.GoogleAdsClient( + credentials=mock.Mock(), + developer_token=self.developer_token, + use_proto_plus=False, + version=latest_version, + ) + svc = client.get_service("YouTubeVideoUploadService") + request = client.get_type("CreateYouTubeVideoUploadRequest") + request.customer_id = "9998887777" + request.you_tube_video_upload.video_title = "Protobuf Video" + + with mock.patch.object( + rest_mod.resumable_transfer, + "ResumableUploadSession", + wraps=rest_mod.resumable_transfer.ResumableUploadSession, + ) as mock_session_cls: + svc.create_you_tube_video_upload(request=request) + + body_dict = json.loads( + mock_session_cls.call_args.kwargs["request_body"] + ) + self.assertEqual( + body_dict["youTubeVideoUpload"]["videoTitle"], "Protobuf Video" + ) + svc.transport.close() + + def test_youtube_video_upload_service_legacy_signature(self): + class FakeLegacyClient: + DEFAULT_ENDPOINT = "googleads.googleapis.com" + + def __init__( + self, + *, + transport=None, + credentials=None, + developer_token=None, + login_customer_id=None, + linked_customer_id=None, + ): + self.transport = transport + self.credentials = credentials + self.developer_token = developer_token + self.login_customer_id = login_customer_id + self.linked_customer_id = linked_customer_id + + @classmethod + def get_transport_class(cls, label=None): + transport_cls = mock.MagicMock() + transport_cls.create_channel.return_value = mock.MagicMock() + return transport_cls + + fake_module = mock.MagicMock() + fake_module.YouTubeVideoUploadServiceClient = FakeLegacyClient + + creds = mock.Mock() + client = Client.GoogleAdsClient( + credentials=creds, + developer_token=self.developer_token, + login_customer_id=self.login_customer_id, + linked_customer_id=self.linked_customer_id, + version=latest_version, + ) + + orig_import = Client.import_module + + def _fake_import(mod_name, *args, **kwargs): + if mod_name.endswith(".services.you_tube_video_upload_service"): + return fake_module + return orig_import(mod_name, *args, **kwargs) + + with ( + mock.patch.object( + Client, "import_module", side_effect=_fake_import + ), + mock.patch.object( + Client.grpc, "intercept_channel", return_value=mock.MagicMock() + ), + ): + svc = client.get_service("YouTubeVideoUploadService") + + self.assertIsInstance(svc, FakeLegacyClient) + self.assertIs(svc.credentials, creds) + self.assertEqual(svc.developer_token, self.developer_token) + self.assertEqual(svc.login_customer_id, self.login_customer_id) + self.assertEqual(svc.linked_customer_id, self.linked_customer_id) + class GoogleAdsClientTestFs(FileTestCase): """Tests for the GoogleAdsClient class that require a fake filesystem.""" @@ -1337,3 +1499,136 @@ def test_load_from_storage_service_account_config(self): http_proxy=None, ads_assistant=None, ) + + +class GoogleAdsClientTestAsync(IsolatedAsyncioTestCase): + """Async tests for google.ads.googleads.client.GoogleAdsClient.""" + + def setUp(self): + self.developer_token = DEFAULT_CONFIG["developer_token"] + self.login_customer_id = "1234567890" + self.linked_customer_id = "0987654321" + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + # Restore a default event loop so subsequent synchronous TestCases that + # initialize grpc.aio channels continue to find an event loop. + asyncio.set_event_loop(asyncio.new_event_loop()) + + async def test_youtube_video_upload_service_async_without_extra(self): + grpc_asyncio_mod = import_module( + f"google.ads.googleads.{latest_version}.services.services." + "you_tube_video_upload_service.transports.grpc_asyncio" + ) + client = Client.GoogleAdsClient( + credentials=mock.Mock(), + developer_token=self.developer_token, + version=latest_version, + ) + + with mock.patch.object(grpc_asyncio_mod, "HAS_ASYNC_REST", False): + svc_async = client.get_service( + "YouTubeVideoUploadService", is_async=True + ) + request = client.get_type("CreateYouTubeVideoUploadRequest") + request.customer_id = "9998887777" + + with self.assertRaisesRegex( + NotImplementedError, r"google-ads\[async_rest\]" + ): + await svc_async.create_you_tube_video_upload(request=request) + await svc_async.transport.close() + + async def test_youtube_video_upload_service_async_with_extra(self): + grpc_asyncio_mod = import_module( + f"google.ads.googleads.{latest_version}.services.services." + "you_tube_video_upload_service.transports.grpc_asyncio" + ) + fake_rest_asyncio = mock.MagicMock() + + class FakeBaseInterceptor: + async def pre_create_you_tube_video_upload(self, request, metadata): + return request, metadata + + captured = {} + + class FakeAsyncRestTransport: + def __init__(self, *, host, credentials, client_info, interceptor): + captured["host"] = host + captured["credentials"] = credentials + captured["interceptor"] = interceptor + captured["closed"] = False + + async def create_you_tube_video_upload(self, request, **kwargs): + _, metadata = ( + await captured[ + "interceptor" + ].pre_create_you_tube_video_upload( + request, [("x-goog-api-client", "gapic/1.0")] + ) + ) + captured["metadata"] = dict(metadata) + return "fake-async-session" + + async def close(self): + captured["closed"] = True + + fake_rest_asyncio.AsyncYouTubeVideoUploadServiceRestInterceptor = ( + FakeBaseInterceptor + ) + fake_rest_asyncio.AsyncYouTubeVideoUploadServiceRestTransport = ( + FakeAsyncRestTransport + ) + + creds = mock.Mock() + client = Client.GoogleAdsClient( + credentials=creds, + developer_token=self.developer_token, + login_customer_id=self.login_customer_id, + linked_customer_id=self.linked_customer_id, + endpoint="async.ads.googleapis.com", + ads_assistant="2.0.0", + version=latest_version, + ) + + orig_import = Client.import_module + + def _fake_import(mod_name, *args, **kwargs): + if mod_name.endswith(".transports.rest_asyncio"): + return fake_rest_asyncio + return orig_import(mod_name, *args, **kwargs) + + with ( + mock.patch.object(grpc_asyncio_mod, "HAS_ASYNC_REST", True), + mock.patch.object( + Client, "import_module", side_effect=_fake_import + ), + ): + svc_async = client.get_service( + "YouTubeVideoUploadService", is_async=True + ) + self.assertEqual(captured, {}) + + request = client.get_type("CreateYouTubeVideoUploadRequest") + request.customer_id = "8887776666" + + result = await svc_async.create_you_tube_video_upload( + request=request + ) + self.assertEqual(result, "fake-async-session") + await svc_async.transport.close() + + self.assertEqual(captured["host"], "async.ads.googleapis.com") + self.assertIs(captured["credentials"], creds) + self.assertEqual( + captured["metadata"]["developer-token"], self.developer_token + ) + self.assertEqual( + captured["metadata"]["login-customer-id"], self.login_customer_id + ) + self.assertEqual( + captured["metadata"]["linked-customer-id"], self.linked_customer_id + ) + self.assertIn("gaada/2.0.0", captured["metadata"]["x-goog-api-client"]) + self.assertTrue(captured["closed"]) diff --git a/tests/constraints/minimums/constraints-3.13.txt b/tests/constraints/minimums/constraints-3.13.txt index eeaccdf2d..81acd34ce 100644 --- a/tests/constraints/minimums/constraints-3.13.txt +++ b/tests/constraints/minimums/constraints-3.13.txt @@ -2,9 +2,9 @@ # This constraints file is required for unit tests. # List all library dependencies and extras in this file. google-auth-oauthlib==1.0.0 -google-api-core==2.22.0 -proto-plus==1.25.0 -protobuf==5.26.1 -googleapis-common-protos==1.63.2 +google-api-core==2.41.0 +proto-plus==1.26.1 +protobuf==6.33.5 +googleapis-common-protos==1.69.2 grpcio==1.66.2 grpcio-status==1.66.2 \ No newline at end of file diff --git a/tests/constraints/minimums/constraints-3.14.txt b/tests/constraints/minimums/constraints-3.14.txt index 731398344..caba003bc 100644 --- a/tests/constraints/minimums/constraints-3.14.txt +++ b/tests/constraints/minimums/constraints-3.14.txt @@ -2,9 +2,9 @@ # This constraints file is required for unit tests. # List all library dependencies and extras in this file. google-auth-oauthlib==1.0.0 -google-api-core==2.22.0 -proto-plus==1.25.0 -protobuf==5.26.1 -googleapis-common-protos==1.63.2 +google-api-core==2.41.0 +proto-plus==1.26.1 +protobuf==6.33.5 +googleapis-common-protos==1.69.2 grpcio==1.75.1 grpcio-status==1.75.1 \ No newline at end of file diff --git a/tests/constraints/minimums/constraints-less-than-3.13.txt b/tests/constraints/minimums/constraints-less-than-3.13.txt index f4ccd45c1..7321cd8fc 100644 --- a/tests/constraints/minimums/constraints-less-than-3.13.txt +++ b/tests/constraints/minimums/constraints-less-than-3.13.txt @@ -2,9 +2,9 @@ # This constraints file is required for unit tests. # List all library dependencies and extras in this file. google-auth-oauthlib==1.0.0 -google-api-core==2.13.0 -proto-plus==1.22.3 -protobuf==4.25.0 -googleapis-common-protos==1.56.3 +google-api-core==2.41.0 +proto-plus==1.26.1 +protobuf==6.33.5 +googleapis-common-protos==1.69.2 grpcio==1.59.0 grpcio-status==1.59.0