-
-
Save omarryhan/0d2cb90c386fbf689ea204002c16ca18 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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