Skip to content

Instantly share code, notes, and snippets.

@omarryhan
Created December 16, 2021 06:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omarryhan/0d2cb90c386fbf689ea204002c16ca18 to your computer and use it in GitHub Desktop.
Save omarryhan/0d2cb90c386fbf689ea204002c16ca18 to your computer and use it in GitHub Desktop.
import argparse
import sys
from datetime import datetime, timezone
from smtplib import SMTP_SSL as SMTP
from email.mime.text import MIMEText
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
from google.api_core import protobuf_helpers
# These are the config settings you're allowed to touch.
DEFAULT_COST_CAP = 100 # $100 AUD
DEFAULT_CUSTOMER_ID = '1424133656'
START_HOUR = 9 # In 24 hr format
START_MINUTE = 0
END_HOUR = 20 # In 24 hr format
END_MINUTE = 20
TIMEZONE = 10 # AEST is UTC +10 NOTE: Doesn't work with negative timezones.
# Email configs
SMTP_SERVER = 'smtp.gmail.com'
SMTP_USERNAME = "example@gmail.com"
SMTP_PASSWORD = "onetimepassword from accounts.google.com -> security -> App passwords"
SENDER = 'example@gmail.com'
RECEIVERS = [
'example@example.com'
]
EMAIL_TEXT_SUBTYPE = 'html' # or html, xml, plain
# Private constants. Please don't change unless you know what you're doing
_COST_MULTIPLIER = 1000000 # DO NOT TOUCH THIS. You don't need to understand it. It just works..
_TEXT_FORMATTER = "{:<60} {:<30} {:<30} {:<20}" # Don't worry about this. This is used for terminal print formatting.
def main(client, customer_id, pause_at, dry_run):
# Constant
PAUSE_AT_MICROS = pause_at * _COST_MULTIPLIER
# Changing variables
total_costs_micros = 0
active_campaigns_ids = []
active_campaigns_names = []
# Available durations
# https://developers.google.com/adwords/api/docs/guides/reporting#date_ranges
# Available statuses
# https://developers.google.com/google-ads/api/reference/rpc/v8/CampaignStatusEnum.CampaignStatus
query = """
SELECT
customer.descriptive_name,
campaign.id,
campaign.status,
campaign.name,
ad_group_ad.ad.name,
metrics.cost_micros
FROM ad_group_ad
WHERE
segments.date DURING TODAY
AND
campaign.status = 'ENABLED'
ORDER BY metrics.cost_micros DESC
LIMIT 100
"""
# Execute query
ga_service = client.get_service("GoogleAdsService")
search_request = client.get_type("SearchGoogleAdsStreamRequest")
search_request.customer_id = customer_id
search_request.query = query
response = ga_service.search_stream(search_request)
print(f'Selected cost cap: {pause_at}\n')
print(_TEXT_FORMATTER.format(
'Ad name',
'Campaign name',
'Campaign ID',
'Cost'
))
print(_TEXT_FORMATTER.format(
'-----------------------',
'---------------',
'---------------',
'---------------'
))
try:
for batch in response:
for row in batch.results:
total_costs_micros = total_costs_micros + row.metrics.cost_micros
active_campaigns_ids.append(row.campaign.id)
active_campaigns_names.append(row.campaign.name)
print(_TEXT_FORMATTER.format(
row.ad_group_ad.ad.name,
row.campaign.name,
row.campaign.id,
row.metrics.cost_micros
))
print(_TEXT_FORMATTER.format(
'',
'',
'',
'---------------'
))
print(_TEXT_FORMATTER.format(
'',
'',
'',
f'{total_costs_micros / _COST_MULTIPLIER if total_costs_micros != 0 else total_costs_micros}'
))
# If haven't reached max, exit.
if (total_costs_micros < PAUSE_AT_MICROS):
print(f'\n\nCurrent spend is still below the daily max of: {pause_at}')
print('Exiting...')
sys.exit(0)
# Else, pause all active campaigns
else:
print(f'\n\nCurrent spend has exceeded the daily max of: {pause_at}')
print('Pausing all active campaigns listed above...')
active_campaigns_ids = list(set(active_campaigns_ids))
for active_campaign_id in active_campaigns_ids:
print(f'\nPausing campaign with ID of: {active_campaign_id}')
campaign_service = client.get_service("CampaignService")
campaign_operation = client.get_type("CampaignOperation")
campaign = campaign_operation.update
campaign.status = client.enums.CampaignStatusEnum.PAUSED
campaign.resource_name = campaign_service.campaign_path(
customer_id, active_campaign_id
)
campaign.network_settings.target_search_network = False
client.copy_from(
campaign_operation.update_mask,
protobuf_helpers.field_mask(None, campaign._pb),
)
# Execute update
if not dry_run:
campaign_response = campaign_service.mutate_campaigns(
customer_id=customer_id, operations=[campaign_operation]
)
print(f"Paused campaign {campaign_response.results[0].resource_name}.")
else:
print(f'(fake) Paused campaign {active_campaign_id}')
# Send email
all_campaign_names = '<ul>'
active_campaigns_names = list(set(active_campaigns_names))
for name in active_campaigns_names:
all_campaign_names += f'<li>{name}</li>'
all_campaign_names += '</ul>'
msg_text = f'<h1>Campaigns paused:</h1>{all_campaign_names}'
msg_text += f'<h2>Paused at: {total_costs_micros / _COST_MULTIPLIER if total_costs_micros != 0 else "0"} AUD'
msg = MIMEText(
msg_text,
EMAIL_TEXT_SUBTYPE
)
msg['Subject'] = f'Paused all campaigns under customer with ID of: {customer_id}'
msg['From'] = SENDER
msg['To'] = ','.join(RECEIVERS)
conn = SMTP(SMTP_SERVER)
conn.set_debuglevel(False)
conn.login(SMTP_USERNAME, SMTP_PASSWORD)
try:
conn.sendmail(SENDER, RECEIVERS, msg.as_string())
finally:
conn.quit()
except GoogleAdsException as ex:
print(
f'Request with ID "{ex.request_id}" failed with status '
f'"{ex.error.code().name}" and includes the following errors:'
)
for error in ex.failure.errors:
print(f'\tError with message "{error.message}".')
if error.location:
for field_path_element in error.location.field_path_elements:
print(f"\t\tOn field: {field_path_element.field_name}")
sys.exit(1)
if __name__ == "__main__":
# For more configs:
# https://developers.google.com/google-ads/api/docs/client-libs/python/configuration#configuration_using_a_dict
google_ads_client = GoogleAdsClient.load_from_dict({
"developer_token": "",
"refresh_token": "",
"client_id": "",
"client_secret": ""
})
parser = argparse.ArgumentParser(
description="Retrieves total costs of all campaigns within a \
Google Ads account during 'today'."
)
parser.add_argument(
"-c",
"--customer_id",
type=str,
required=False,
default=DEFAULT_CUSTOMER_ID,
help="The Google Ads customer ID of the account you would like to change"
)
parser.add_argument(
"-p",
"--pause_at",
type=int,
required=False,
default=DEFAULT_COST_CAP,
help="Cost cap in the account's currency e.g. AUD",
)
parser.add_argument(
"-h1",
"--start_hour",
type=int,
required=False,
default=START_HOUR,
help="Start hour in 24 hour time",
)
parser.add_argument(
"-h2",
"--end_hour",
type=int,
required=False,
default=END_HOUR,
help="End hour in 24 hour format",
)
parser.add_argument(
"-m1",
"--start_minute",
type=int,
required=False,
default=START_MINUTE,
help="Start minute",
)
parser.add_argument(
"-m2",
"--end_minute",
type=int,
required=False,
default=END_MINUTE,
help="End minute",
)
parser.add_argument(
"-tz",
"--timezone",
type=int,
required=False,
default=TIMEZONE,
help="Timezone. Negative timezones will not work.",
)
parser.add_argument(
"-f",
"--force_time",
action='store_true',
help="Ignore time boundaries and run anyway. Useful for running localhost test runs.",
)
parser.add_argument(
"-d",
"--dry_run",
action='store_true',
help="Don't execute any changes, only print logs",
)
args = parser.parse_args()
# Check if proper timing
now = datetime.now(timezone.utc)
utc_hour = now.hour
utc_min = now.minute
timezoned_hour = utc_hour + args.timezone if utc_hour + args.timezone < 24 else (utc_hour + args.timezone) - 24
if (
timezoned_hour < args.start_hour
) or (
timezoned_hour == args.start_hour and utc_min < args.start_minute
) or (
timezoned_hour > args.end_hour
) or (
timezoned_hour == args.end_hour and utc_min >= args.end_minute
):
if not args.force_time:
print('Exiting because attempted to run outside of specified time.')
print('Exiting...')
sys.exit(0)
main(
google_ads_client,
args.customer_id,
args.pause_at,
args.dry_run
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment