#!/usr/bin/ruby
#
# Copyright 2009, Google Inc. All Rights Reserved.
#
# 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.
#
# Contains extensions to the API, that is, service helper methods provided in
# client-side by the client library.
require 'rexml/document'
require 'csv'
module AdWords
module Extensions
# Maintains a list of all extension methods, indexed by version and service.
# Using camelCase to match API method names.
@@extensions = {
[13, 'Report'] => ['downloadXmlReport', 'downloadCsvReport'],
[13, 'Info'] => ['getMethodUsage', 'getClientUnitsUsage']
}
# Defines the parameter list for every extension method
@@methods = {
'downloadXmlReport' => ['job_id'],
'downloadCsvReport' => ['job_id'],
'getMethodUsage' => ['start_date', 'end_date'],
'getClientUnitsUsage' => ['start_date', 'end_date']
}
# Return list of all extension methods, indexed by version and service.
def self.extensions
return @@extensions
end
# Return the parameter list for every extension method.
def self.methods
return @@methods
end
#########################################################################
# NOTE: The following extension methods shouldn't be used directly; they
# should instead be used from the services wrappers they get mapped to.
# For example, you should use ReportServiceWrapper::downloadXmlReport
# instead of Extensions::downloadXmlReport.
#########################################################################
# Extension method -- Download and return report data in XML format.
#
# *Warning*: this method is blocking for the calling thread.
#
# Args:
# - wrapper: the service wrapper object for any API methods that need to be
# called
# - job_id: the job id for the report to be downloaded
#
# Returns:
# The xml for the report (as a string)
#
def self.downloadXmlReport(wrapper, job_id)
sleep_interval = 30
# Repeatedly check the report status until it is finished.
# 'Pending' and 'InProgress' statuses indicate the job is still being run.
status = wrapper.getReportJobStatus(job_id).getReportJobStatusReturn
while status != 'Completed' && status != 'Failed'
sleep(sleep_interval)
status = wrapper.getReportJobStatus(job_id).getReportJobStatusReturn
end
if status == 'Completed'
report_url =
wrapper.getReportDownloadUrl(job_id).getReportDownloadUrlReturn
# Download the report via the HTTPClient library and return its
# contents. The report is an XML document; the actual element names vary
# depending on the type of report run and columns requested.
begin
client = HTTPClient.new
return client.get_content(report_url)
rescue Errno::ECONNRESET, SOAP::HTTPStreamError, SocketError => e
# This exception indicates a connection-level error.
# In general, it is likely to be transitory.
raise AdWords::Error::Error, "Connection Error: %s\nSource: %s" %
[e, e.backtrace.first]
end
else
# Reports that pass validation will normally not fail, but if there is
# an error in the report generation service it can sometimes happen.
raise AdWords::Error::Error, 'Report generation failed.'
end
end
# Extension method -- Download and return report data in CSV format.
#
# *Warning*: this method is blocking for the calling thread.
#
# Args:
# - wrapper: the service wrapper object for any API methods that need to be
# called
# - job_id: the job id for the report to be downloaded
# - xml: optional parameter used for testing and debugging
#
# Returns:
# The CSV data for the report (as a string)
#
def self.downloadCsvReport(wrapper, job_id, report_xml=nil)
# Get XML report data.
report_xml = downloadXmlReport(wrapper, job_id) if report_xml.nil?
begin
# Construct DOM object.
doc = REXML::Document.new(report_xml)
# Get data columns.
columns = []
doc.elements.each('report/table/columns/column') do |column_elem|
name = column_elem.attributes['name']
columns << name unless name.nil?
end
# Get data rows.
rows = []
doc.elements.each('report/table/rows/row') do |row_elem|
rows << row_elem.attributes unless row_elem.attributes.nil?
end
# Build CSV
csv = ''
CSV::Writer.generate(csv) do |writer|
writer << columns
rows.each do |row|
row_values = []
columns.each { |column| row_values << row[column] }
writer << row_values
end
end
return csv
rescue REXML::ParseException => e
# Error parsing XML
raise AdWords::Error::Error,
"Error parsing report XML: %s\nSource: %s" % [e, e.backtrace.first]
end
end
# Extension method -- Get a mapping between API methods and the
# number of units used through them for a given amount of time.
#
# Running this helper method will consume 71 units.
#
# *Note*: unit data is not available in real time.
#
# Args:
# - wrapper: the service wrapper object for any API methods that need to be
# called
# - start_date: starting date for unit spend count (as a Date)
# - end_date: starting date for unit spend count (as a Date)
#
# Returns:
# Hash of service.method to the number of units used, e.g.,
# { 'AccountService.getAccountInfo' => 10,
# 'AccountService.getClientAccountInfos' => 0, ...}
#
def self.getMethodUsage(wrapper, start_date, end_date)
op_rates = AdWords::Utils.get_operation_rates
usage = {}
op_rates.each do |op|
service, method = op
usage[service + '.' + method] = wrapper.getUnitCountForMethod(service,
method, start_date, end_date).getUnitCountForMethodReturn
end
return usage
end
# Extension method -- Gets the quota usage per child of the entire
# account tree below the root user. That is, for each child that is a client
# manager, all units below that client manager are summed upwards. The
# result is very useful for invoicing sub-MCCs that may have many clients
# that units may be spent on.
#
# *Note*: unit data is not available in real time.
#
# Args:
# - wrapper: the service wrapper object for any API methods that need to be
# called
# - start_date: starting date for unit spend count (as a Date)
# - end_date: starting date for unit spend count (as a Date)
#
# Returns:
# - Hash of account to unit usage,
# { 'account1@domain.tld' => 10,
# 'account2@domain.tld' => 0, ...}
# - List of double counted children (account emails)
#
def self.getClientUnitsUsage(wrapper, start_date, end_date)
# Create a new AdWords::API object to ensure thread-safety (we'll need to
# change the clientEmail)
adwords = AdWords::API.new(wrapper.api.credentials.dup)
adwords.credentials.set_header('clientEmail', '')
# Call unit_adder on the main user
unit_map = client_unit_adder(adwords, start_date, end_date)
# Pass back the spent unit information to the main AdWords::API object
wrapper.api.mutex.synchronize do
wrapper.api.last_units = adwords.total_units
wrapper.api.total_units += adwords.total_units
end
return unit_map
end
private
# Auxiliary recursive method to get the sum of units for an account and all
# those under it, if any.
#
# Args:
# - adwords: the AdWords::API object to be used for retrieving the client
# data
# - start_date: starting date for unit spend count (as a Date)
# - end_date: starting date for unit spend count (as a Date)
#
# Returns:
# - Hash of account to unit usage,
# { 'account1@domain.tld' => 10,
# 'account2@domain.tld' => 0, ...}
# - List of double counted children (account emails)
#
def self.client_unit_adder(adwords, start_date, end_date)
account_srv = adwords.get_service(13, 'Account')
if adwords.credentials.credentials['clientEmail'] == ''
account_email = adwords.credentials.credentials['email']
else
account_email = adwords.credentials.credentials['clientEmail']
end
# Get list of accounts under the current one
accounts = account_srv.getClientAccountInfos
unit_map = {}
doubles = []
clients = accounts.select { |account| !account.isCustomerManager }
managers = accounts.select { |account| account.isCustomerManager }
client_emails = clients.map { |account| account.emailAddress }
info_srv = adwords.get_service(13, 'Info')
# Get usage for clients
client_usage =
info_srv.getUnitCountForClients(client_emails, start_date, end_date)
client_usage.each do |record|
unit_map[record.clientEmail] = record.quotaUnits
end
managers.each do |account|
# Create a new AdWords::API object to ensure thread-safety (we'll need
# to change the clientEmail)
sub_mcc = AdWords::API.new(adwords.credentials.dup)
sub_mcc.credentials.set_header('clientEmail', account.emailAddress)
# Recurse for sub-MCCs
sub_unit_map, sub_doubles =
client_unit_adder(sub_mcc, start_date, end_date)
sub_unit_map.each_key do |entry|
# Add any accounts already accounted for to the doubles list
unless unit_map[entry].nil?
doubles << entry unless doubles.include?(entry)
end
end
# Merge unit maps, doubles and unit spend from the sub with the main
unit_map.merge! sub_unit_map
sub_doubles.each do |entry|
doubles << entry unless doubles.include?(entry)
end
doubles += sub_doubles
adwords.mutex.synchronize do
adwords.total_units += sub_mcc.total_units
end
end
# Calculate the sum for this account and add it to the hash as well
sum = accounts.inject(0) do |result, account|
result + unit_map[account.emailAddress]
end
unit_map[account_email] = sum
return unit_map, doubles
end
end
end