Skip to content

Commit

Permalink
Add additional options for campaign creation (#879)
Browse files Browse the repository at this point in the history
* Add Manual CPC bidding strategy for the campaign creation

* Add option for Individual campaign budget

* wip

* Implement campaign language targeting
  • Loading branch information
rjambrecic committed Aug 13, 2024
1 parent 5c3a1b0 commit 19e52e6
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
args: []#[--ignore-words=.codespell-whitelist.txt]
exclude: |
(?x)^(
docs/overrides/home.html
google_ads/language_codes.csv
)$
- repo: local
Expand Down
106 changes: 96 additions & 10 deletions google_ads/application.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import json
import os
import urllib.parse
import uuid
from os import environ
from typing import Any, Dict, List, Optional, Tuple, Union

import httpx
import pandas as pd
from asyncer import asyncify
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from fastapi.responses import RedirectResponse
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.v17.common.types.criteria import LanguageInfo
from google.api_core import protobuf_helpers
from google.auth.exceptions import RefreshError
from google.protobuf import json_format
Expand All @@ -23,6 +26,7 @@
AdGroupCriterion,
Campaign,
CampaignCriterion,
CampaignLanguageCriterion,
Criterion,
GeoTargetCriterion,
RemoveResource,
Expand Down Expand Up @@ -726,7 +730,9 @@ def _create_campaign_setattr(
) -> None:
for attribute_name, attribute_value in model_dict.items():
if attribute_value is not None:
if "network_settings" in attribute_name:
if attribute_name == "manual_cpc":
operation_create.manual_cpc.enhanced_cpc_enabled = attribute_value
elif "network_settings" in attribute_name:
attribute_name = attribute_name.replace("network_settings_", "")
setattr(
operation_create.network_settings, attribute_name, attribute_value
Expand All @@ -738,12 +744,12 @@ def _create_campaign_setattr(
client.enums.AdvertisingChannelTypeEnum.SEARCH
)

# Set the bidding strategy and budget.
# The bidding strategy for Maximize Clicks is TargetSpend.
# The target_spend_micros is deprecated so don't put any value.
# See other bidding strategies you can select in the link below.
# https://developers.google.com/google-ads/api/reference/rpc/v11/Campaign#campaign_bidding_strategy
operation_create.target_spend.target_spend_micros = 0
if not model_dict["manual_cpc"]:
# The bidding strategy for Maximize Clicks is TargetSpend.
# The target_spend_micros is deprecated so don't put any value.
# See other bidding strategies you can select in the link below.
# https://developers.google.com/google-ads/api/reference/rpc/v11/Campaign#campaign_bidding_strategy
operation_create.target_spend.target_spend_micros = 0

# # Optional: Set the start date.
# start_time = datetime.date.today() + datetime.timedelta(days=1)
Expand Down Expand Up @@ -908,7 +914,9 @@ async def update_ad_group_ad(
)


def _create_campaign_budget(client: Any, customer_id: str, amount_micros: int) -> Any:
def _create_campaign_budget(
client: Any, customer_id: str, amount_micros: int, explicitly_shared: Optional[bool]
) -> Any:
"""Creates campaign budget resource.
Args:
Expand All @@ -925,6 +933,7 @@ def _create_campaign_budget(client: Any, customer_id: str, amount_micros: int) -
campaign_budget.name = f"Campaign budget {uuid.uuid4()}"
campaign_budget.delivery_method = client.enums.BudgetDeliveryMethodEnum.STANDARD
campaign_budget.amount_micros = amount_micros
campaign_budget.explicitly_shared = explicitly_shared

# Add budget.
campaign_budget_response = campaign_budget_service.mutate_campaign_budgets(
Expand All @@ -934,6 +943,39 @@ def _create_campaign_budget(client: Any, customer_id: str, amount_micros: int) -
return campaign_budget_response.results[0].resource_name


def _read_avaliable_languages() -> Dict[str, int]:
language_codes = pd.read_csv(
os.path.join(os.path.dirname(__file__), "language_codes.csv")
)
return dict(
zip(
language_codes["Language code"],
language_codes["Criterion ID"],
strict=False,
)
)


avalible_languages = _read_avaliable_languages()


def get_languages(languages_codes: List[str], negative: bool) -> Dict[str, int]:
languages_codes = [code.lower().strip() for code in languages_codes]

# check if the language codes are valid
non_valid_codes = [
code for code in languages_codes if code not in avalible_languages.keys()
]
if non_valid_codes:
raise ValueError(f"Invalid language codes: {non_valid_codes}")

if negative:
# take all languages except the ones in the list
return {k: v for k, v in avalible_languages.items() if k not in languages_codes}
# take only the languages in the list
return {k: v for k, v in avalible_languages.items() if k in languages_codes}


@router.get("/create-campaign")
async def create_campaign(
user_id: int,
Expand All @@ -945,7 +987,7 @@ async def create_campaign(

(
client,
service,
campaign_service,
operation,
model_dict,
customer_id,
Expand All @@ -965,8 +1007,10 @@ async def create_campaign(
client=client,
customer_id=customer_id,
amount_micros=ad_model.budget_amount_micros, # type: ignore
explicitly_shared=ad_model.budget_explicitly_shared,
)
model_dict.pop("budget_amount_micros")
model_dict.pop("budget_explicitly_shared")
operation_create.campaign_budget = campaign_budget

setattr_func = service_operation_and_function_names["setattr_create_func"]
Expand All @@ -975,7 +1019,7 @@ async def create_campaign(
)

response = await _mutate(
service,
campaign_service,
service_operation_and_function_names["mutate"],
customer_id,
operation,
Expand All @@ -987,6 +1031,48 @@ async def create_campaign(
return f"Created {response.results[0].resource_name}."


@router.get("/update-campaign-language-criterion")
async def update_campaign_language_criterion(
user_id: int,
language_criterion_model: CampaignLanguageCriterion = Depends(),
login_customer_id: Optional[str] = Query(None, title="Login customer ID"),
) -> str:
client = await _get_client(user_id=user_id, login_customer_id=login_customer_id)
campaign_service = client.get_service("CampaignService")
campaign_criterion_service = client.get_service("CampaignCriterionService")
campaign_criterion_operation = client.get_type("CampaignCriterionOperation")

campaign_criterion = campaign_criterion_operation.create
campaign_criterion.campaign = campaign_service.campaign_path(
customer_id=language_criterion_model.customer_id,
campaign_id=language_criterion_model.campaign_id,
)
try:
languages = get_languages(
languages_codes=language_criterion_model.language_codes,
negative=language_criterion_model.negative,
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
) from e

# Add one by one the languages to the campaign
for language_id in languages.values():
campaign_criterion.language = LanguageInfo(
language_constant=f"languageConstants/{language_id}"
)

await _mutate(
campaign_criterion_service,
"mutate_campaign_criteria",
language_criterion_model.customer_id,
campaign_criterion_operation,
)

return f"Updated campaign '{language_criterion_model.campaign_id}' with language codes: {list(languages.keys())}"


@router.get("/create-ad-group")
async def create_ad_group(
user_id: int,
Expand Down
52 changes: 52 additions & 0 deletions google_ads/language_codes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Language name,Language code,Criterion ID
Arabic,ar,1019
Bengali,bn,1056
Bulgarian,bg,1020
Catalan,ca,1038
Chinese (simplified),zh_cn,1017
Chinese (traditional),zh_tw,1018
Croatian,hr,1039
Czech,cs,1021
Danish,da,1009
Dutch,nl,1010
English,en,1000
Estonian,et,1043
Filipino,tl,1042
Finnish,fi,1011
French,fr,1002
German,de,1001
Greek,el,1022
Gujarati,gu,1072
Hebrew,iw,1027
Hindi,hi,1023
Hungarian,hu,1024
Icelandic,is,1026
Indonesian,id,1025
Italian,it,1004
Japanese,ja,1005
Kannada,kn,1086
Korean,ko,1012
Latvian,lv,1028
Lithuanian,lt,1029
Malay,ms,1102
Malayalam,ml,1098
Marathi,mr,1101
Norwegian,no,1013
Persian,fa,1064
Polish,pl,1030
Portuguese,pt,1014
Punjabi,pa,1110
Romanian,ro,1032
Russian,ru,1031
Serbian,sr,1035
Slovak,sk,1033
Slovenian,sl,1034
Spanish,es,1003
Swedish,sv,1015
Tamil,ta,1130
Telugu,te,1131
Thai,th,1044
Turkish,tr,1037
Ukrainian,uk,1036
Urdu,ur,1041
Vietnamese,vi,1040
9 changes: 9 additions & 0 deletions google_ads/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ class AdBase(BaseModel):
status: Optional[Literal["ENABLED", "PAUSED"]] = None


class CampaignLanguageCriterion(BaseModel):
customer_id: str
campaign_id: str
language_codes: List[str] = Field(Query(default=[]))
negative: bool = False


class Campaign(AdBase):
campaign_id: Optional[str] = None
budget_amount_micros: Optional[int] = None
budget_explicitly_shared: Optional[bool] = None
network_settings_target_google_search: Optional[bool] = None
network_settings_target_search_network: Optional[bool] = None
# network_settings_target_partner_search_network: Optional[bool] = None
network_settings_target_content_network: Optional[bool] = None
manual_cpc: Optional[bool] = None


class AdGroup(Campaign):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from google_ads.application import (
MAX_HEADLINES_OR_DESCRIPTIONS_ERROR_MSG,
_check_if_customer_id_is_manager_or_exception_is_raised,
_read_avaliable_languages,
_set_fields_ad_copy,
_set_headline_or_description,
create_geo_targeting_for_campaign,
get_languages,
)
from google_ads.model import AdCopy, GeoTargetCriterion

Expand Down Expand Up @@ -333,3 +335,36 @@ async def test_set_fields_ad_copy_max_headlines_and_descriptions() -> None:
str(exc.value)
== f"{MAX_HEADLINES_OR_DESCRIPTIONS_ERROR_MSG} headlines: 15\n{MAX_HEADLINES_OR_DESCRIPTIONS_ERROR_MSG} descriptions: 4"
)


class TestLanguages:
NUMBERS_OF_LANGUAGES = 51

def test_read_avaliable_languages(self) -> None:
languages = _read_avaliable_languages()
assert len(languages) == TestLanguages.NUMBERS_OF_LANGUAGES

@pytest.mark.parametrize(
("language_codes", "negative", "expected"),
[
(["en", "HR "], False, 2),
(["EN", "hr"], True, NUMBERS_OF_LANGUAGES - 2),
(["en", "hr", "es", "de", "fr"], False, 5),
(["en", "hr", "es", "de", "fr"], True, NUMBERS_OF_LANGUAGES - 5),
],
)
def test_get_languages(self, language_codes, negative, expected) -> None:
languages = get_languages(language_codes, negative)
assert len(languages) == expected

language_codes = [language.lower().strip() for language in language_codes]
if negative:
assert all(language not in language_codes for language in languages)
else:
assert all(language in language_codes for language in languages)

def test_get_languages_for_non_valid_codes(self) -> None:
language_codes = ["en", "hr", "xyz"]
with pytest.raises(ValueError) as exc:
get_languages(language_codes, False)
assert str(exc.value) == "Invalid language codes: ['xyz']"

0 comments on commit 19e52e6

Please sign in to comment.