adapi 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +35 -27
- data/adapi.gemspec +8 -8
- data/examples/add_ad_group.rb +13 -7
- data/examples/add_bare_ad_group.rb +12 -6
- data/examples/add_bare_campaign.rb +13 -6
- data/examples/add_campaign.rb +22 -10
- data/examples/add_campaign_criteria.rb +22 -8
- data/examples/add_invalid_ad_group.rb +9 -5
- data/examples/add_invalid_keywords.rb +3 -0
- data/examples/add_invalid_text_ad.rb +12 -6
- data/examples/add_keywords.rb +20 -9
- data/examples/add_text_ads.rb +45 -0
- data/examples/rollback_campaign.rb +37 -14
- data/examples/update_campaign.rb +1 -5
- data/examples/update_complete_campaign.rb +3 -4
- data/lib/adapi.rb +5 -4
- data/lib/adapi/ad.rb +14 -39
- data/lib/adapi/ad/text_ad.rb +38 -23
- data/lib/adapi/ad_group.rb +35 -30
- data/lib/adapi/ad_group_criterion.rb +7 -4
- data/lib/adapi/ad_param.rb +8 -16
- data/lib/adapi/api.rb +88 -8
- data/lib/adapi/campaign.rb +87 -76
- data/lib/adapi/campaign_criterion.rb +6 -4
- data/lib/adapi/campaign_target.rb +6 -4
- data/lib/adapi/config.rb +2 -2
- data/lib/adapi/keyword.rb +11 -58
- data/lib/adapi/version.rb +7 -1
- data/test/integration/create_ad_group_test.rb +48 -0
- data/test/integration/create_campaign_test.rb +1 -4
- data/test/test_helper.rb +49 -0
- metadata +43 -41
data/examples/add_keywords.rb
CHANGED
@@ -10,17 +10,28 @@ $keywords = Adapi::Keyword.new(
|
|
10
10
|
:keywords => [ 'dem codez', '"top coder"', '[-code]' ]
|
11
11
|
)
|
12
12
|
|
13
|
-
$
|
13
|
+
$keywords.create
|
14
14
|
|
15
|
-
|
16
|
-
$google_keywords = Adapi::Keyword.find(:all, :ad_group_id => $ad_group[:id]).keywords
|
15
|
+
if $keywords.errors.any?
|
17
16
|
|
18
|
-
|
17
|
+
puts "\nERRORS:"
|
18
|
+
pp $keywords.errors.messages
|
19
19
|
|
20
|
-
|
20
|
+
else
|
21
21
|
|
22
|
-
|
23
|
-
pp $params_keywords
|
22
|
+
puts "\nKEYWORDS CREATED\n"
|
24
23
|
|
25
|
-
|
26
|
-
|
24
|
+
# get array of keywords from Keyword instance
|
25
|
+
$google_keywords = Adapi::Keyword.find(:all, :ad_group_id => $ad_group[:id]).keywords
|
26
|
+
|
27
|
+
$params_keywords = Adapi::Keyword.parameterized($google_keywords)
|
28
|
+
|
29
|
+
$short_keywords = Adapi::Keyword.shortened($google_keywords)
|
30
|
+
|
31
|
+
puts "DETAILED:"
|
32
|
+
pp $params_keywords
|
33
|
+
|
34
|
+
puts "\nSHORT:"
|
35
|
+
pp $short_keywords
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
require 'adapi'
|
3
|
+
|
4
|
+
require_relative 'add_bare_ad_group'
|
5
|
+
|
6
|
+
$ad = Adapi::Ad::TextAd.create( :ads => [
|
7
|
+
{
|
8
|
+
:ad_group_id => $ad_group[:id],
|
9
|
+
:headline => "Code like Neo",
|
10
|
+
:description1 => 'Need mad coding skills?',
|
11
|
+
:description2 => 'Check out my new blog!',
|
12
|
+
:url => 'http://www.demcodez.com',
|
13
|
+
:display_url => 'http://www.demcodez.com',
|
14
|
+
:status => 'PAUSED'
|
15
|
+
},
|
16
|
+
|
17
|
+
{
|
18
|
+
:ad_group_id => $ad_group[:id],
|
19
|
+
:headline => "Code like Trinity",
|
20
|
+
:description1 => 'The power of awesomeness?',
|
21
|
+
:description2 => 'Check out my new blog!',
|
22
|
+
:url => 'http://www.demcodez.com',
|
23
|
+
:display_url => 'http://www.demcodez.com',
|
24
|
+
:status => 'PAUSED'
|
25
|
+
},
|
26
|
+
|
27
|
+
{
|
28
|
+
:ad_group_id => $ad_group[:id],
|
29
|
+
:headline => "Code like Morpheus",
|
30
|
+
:description1 => 'Unleash the power of Matrix',
|
31
|
+
:description2 => 'Check out my new blog',
|
32
|
+
:url => 'http://www.demcodez.com',
|
33
|
+
:display_url => 'http://www.demcodez.com',
|
34
|
+
:status => 'PAUSED'
|
35
|
+
}
|
36
|
+
] )
|
37
|
+
|
38
|
+
puts "\nCREATED ADS FOR AD GROUP #{$ad_group[:id]}:"
|
39
|
+
|
40
|
+
$ads = Adapi::Ad::TextAd.find( :all, :ad_group_id => $ad_group[:id] )
|
41
|
+
|
42
|
+
$ads.each_with_index do |ad, i|
|
43
|
+
puts "\nAD NR. #{i + 1}:"
|
44
|
+
pp ad.attributes
|
45
|
+
end
|
@@ -2,24 +2,21 @@
|
|
2
2
|
|
3
3
|
require 'adapi'
|
4
4
|
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# eventually)
|
5
|
+
# Tries to create a campaign and fails because ad is intentionally left
|
6
|
+
# without url. We test if campaign then rollbacks correctly:
|
7
|
+
# * status should be set to DELETED
|
8
|
+
# * renamed so the original campaign name isn't blocked
|
10
9
|
|
11
10
|
campaign_data = {
|
12
11
|
:name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
|
13
12
|
:status => 'PAUSED',
|
14
|
-
# Automatic CPC: BudgetOptimizer or ManualCPC
|
15
13
|
:bidding_strategy => { :xsi_type => 'BudgetOptimizer', :bid_ceiling => 100 },
|
16
14
|
:budget => { :amount => 50, :delivery_method => 'STANDARD' },
|
17
15
|
|
18
16
|
:network_setting => {
|
19
17
|
:target_google_search => true,
|
20
18
|
:target_search_network => true,
|
21
|
-
:target_content_network => false
|
22
|
-
:target_content_contextual => false
|
19
|
+
:target_content_network => false
|
23
20
|
},
|
24
21
|
|
25
22
|
:criteria => {
|
@@ -47,12 +44,38 @@ campaign_data = {
|
|
47
44
|
]
|
48
45
|
|
49
46
|
}
|
50
|
-
|
47
|
+
|
51
48
|
$campaign = Adapi::Campaign.create(campaign_data)
|
52
49
|
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
unless $campaign.errors.empty?
|
51
|
+
|
52
|
+
puts "\nERRORS WHEN UPDATING CAMPAIGN #{$campaign.id}:"
|
53
|
+
pp $campaign.errors.full_messages
|
54
|
+
|
55
|
+
puts "\nROLLBACKING CAMPAIGN #{$campaign.id}\n"
|
56
|
+
|
57
|
+
$campaign.rollback!
|
58
|
+
|
59
|
+
unless $campaign.errors.empty?
|
60
|
+
|
61
|
+
puts "\nERRORS WHEN ROLLBACKING CAMPAIGN #{$campaign.id}:"
|
62
|
+
pp $campaign.errors.full_messages
|
63
|
+
|
64
|
+
else
|
65
|
+
|
66
|
+
puts "\nOK, ROLLBACKED CAMPAIGN #{$campaign.id}"
|
67
|
+
|
68
|
+
$campaign = Adapi::Campaign.find($campaign.id)
|
69
|
+
|
70
|
+
puts "\nCAMPAIGN DATA:"
|
71
|
+
pp $campaign.attributes
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
else
|
76
|
+
|
77
|
+
puts "\nCREATED CAMPAIGN #{$campaign.id}"
|
78
|
+
|
79
|
+
puts "\nSOMETHING IS WRONG, THIS SHOULDN'T HAPPEN!"
|
56
80
|
|
57
|
-
|
58
|
-
pp $campaign.errors.to_a
|
81
|
+
end
|
data/examples/update_campaign.rb
CHANGED
@@ -12,11 +12,7 @@ $campaign = Adapi::Campaign.update(
|
|
12
12
|
:network_setting => {
|
13
13
|
:target_google_search => false,
|
14
14
|
:target_search_network => false,
|
15
|
-
:target_content_network => true
|
16
|
-
:target_content_contextual => true
|
17
|
-
# FIXME returns error which is not trapped:
|
18
|
-
# TargetError.CANNOT_TARGET_PARTNER_SEARCH_NETWORK
|
19
|
-
# :target_partner_search_network => true
|
15
|
+
:target_content_network => true
|
20
16
|
},
|
21
17
|
|
22
18
|
# deletes all criteria (except :platform) and create these new ones
|
@@ -19,8 +19,7 @@ campaign_data = {
|
|
19
19
|
:network_setting => {
|
20
20
|
:target_google_search => true,
|
21
21
|
:target_search_network => true,
|
22
|
-
:target_content_network => false
|
23
|
-
:target_content_contextual => false
|
22
|
+
:target_content_network => false
|
24
23
|
},
|
25
24
|
|
26
25
|
:criteria => {
|
@@ -82,7 +81,7 @@ p "Created campaign ID #{$campaign.id}"
|
|
82
81
|
# * change second ad_group
|
83
82
|
# * add new ad_group
|
84
83
|
|
85
|
-
|
84
|
+
$campaign.update(
|
86
85
|
:id => $campaign[:id],
|
87
86
|
:status => 'ACTIVE',
|
88
87
|
:name => "UPDATED #{$campaign[:name]}",
|
@@ -171,7 +170,7 @@ else
|
|
171
170
|
puts "\nAD GROUPS (#{$ad_groups.size}):"
|
172
171
|
$ad_groups.each_with_index do |ad_group, i|
|
173
172
|
puts "\nAD GROUP #{i + 1}:\n"
|
174
|
-
pp ad_group
|
173
|
+
pp ad_group
|
175
174
|
end
|
176
175
|
|
177
176
|
end
|
data/lib/adapi.rb
CHANGED
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'adwords_api'
|
5
|
-
#
|
6
|
-
|
7
|
-
#
|
5
|
+
# provides various utility methods
|
6
|
+
require 'adwords_api/utils'
|
7
|
+
# TODO log of general activity (currently log only SOAP requests)
|
8
8
|
# require 'logger'
|
9
9
|
require 'yaml'
|
10
10
|
require 'pp'
|
11
11
|
|
12
12
|
require 'active_model'
|
13
13
|
# load only ActiveSupport core extensions
|
14
|
+
# TODO require only parts that are really needed
|
14
15
|
require 'active_support/core_ext'
|
15
16
|
|
16
17
|
require 'adapi/version'
|
@@ -44,7 +45,7 @@ HTTPI.adapter = :curb
|
|
44
45
|
HTTPI.log = false # supress HTTPI output
|
45
46
|
|
46
47
|
module Adapi
|
47
|
-
API_VERSION = :
|
48
|
+
API_VERSION = :v201206
|
48
49
|
end
|
49
50
|
|
50
51
|
if RUBY_VERSION < '1.9'
|
data/lib/adapi/ad.rb
CHANGED
@@ -16,8 +16,7 @@ module Adapi
|
|
16
16
|
# PS: create won't work with id and ad_group_id
|
17
17
|
# 'id' => id, 'ad_group_id' => ad_group_id,
|
18
18
|
def attributes
|
19
|
-
super.merge(
|
20
|
-
'url' => url, 'display_url' => display_url )
|
19
|
+
super.merge( id: id, ad_group_id: ad_group_id, url: url, display_url: display_url )
|
21
20
|
end
|
22
21
|
|
23
22
|
def initialize(params = {})
|
@@ -31,6 +30,7 @@ module Adapi
|
|
31
30
|
end
|
32
31
|
|
33
32
|
# deletes ad
|
33
|
+
# TODO call by delete method
|
34
34
|
#
|
35
35
|
def destroy
|
36
36
|
response = self.mutate(
|
@@ -44,46 +44,21 @@ module Adapi
|
|
44
44
|
(response and response[:value]) ? true : false
|
45
45
|
end
|
46
46
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
# FIXME add validations
|
48
|
+
def delete(params = {})
|
49
|
+
operations = params[:ad_ids].map do |ad_id|
|
50
|
+
{
|
51
|
+
:operator => 'REMOVE',
|
52
|
+
:operand => {
|
53
|
+
:ad_group_id => params[:ad_group_id],
|
54
|
+
:ad => { :id => ad_id, :xsi_type => 'Ad' }
|
55
|
+
}
|
56
|
+
}
|
56
57
|
end
|
57
|
-
|
58
|
-
begin
|
59
|
-
|
60
|
-
response = @service.mutate(operation)
|
61
|
-
|
62
|
-
rescue *API_EXCEPTIONS => e
|
63
58
|
|
64
|
-
|
65
|
-
# see adwords-api gem example for details: handle_policy_violation_error.rb
|
66
|
-
e.errors.each do |error|
|
67
|
-
# error[:xsi_type] seems to be broken, so using also alternative key
|
68
|
-
# also could try: :"@xsi:type" (but api_error_type seems to be more robust)
|
69
|
-
if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
|
70
|
-
if error[:is_exemptable]
|
71
|
-
self.errors.add(:PolicyViolationError, error[:key])
|
72
|
-
end
|
59
|
+
response = self.mutate(operations)
|
73
60
|
|
74
|
-
|
75
|
-
self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
|
76
|
-
error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
|
77
|
-
error[:key][:policy_name],
|
78
|
-
error[:key][:violating_text]
|
79
|
-
])
|
80
|
-
else
|
81
|
-
self.errors.add(:base, e.message)
|
82
|
-
end
|
83
|
-
end # of errors.each
|
84
|
-
end
|
85
|
-
|
86
|
-
response
|
61
|
+
(response and response[:value]) ? true : false
|
87
62
|
end
|
88
63
|
|
89
64
|
end
|
data/lib/adapi/ad/text_ad.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Adapi
|
4
|
+
# http://code.google.com/apis/adwords/docs/reference/latest/AdGroupAdService.TextAd.html
|
5
|
+
#
|
4
6
|
# Ad::TextAd == AdGroupAd::TextAd
|
5
7
|
#
|
6
|
-
#
|
8
|
+
# This model supports both individual and batch create/update of ads.
|
9
|
+
# If :ads parameter is not nil, it is considered as array of ads to
|
10
|
+
# be updated in batch.
|
7
11
|
#
|
8
12
|
class Ad::TextAd < Ad
|
9
13
|
|
10
|
-
ATTRIBUTES = [ :headline, :description1, :description2 ]
|
14
|
+
ATTRIBUTES = [ :headline, :description1, :description2, :ads ]
|
11
15
|
|
12
16
|
attr_accessor *ATTRIBUTES
|
13
17
|
|
@@ -34,37 +38,48 @@ module Adapi
|
|
34
38
|
end
|
35
39
|
|
36
40
|
def create
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
:
|
41
|
+
@ads = [ self.attributes ] unless @ads
|
42
|
+
|
43
|
+
operations = []
|
44
|
+
|
45
|
+
@ads.each do |ad_params|
|
46
|
+
|
47
|
+
ad = TextAd.new(ad_params)
|
48
|
+
|
49
|
+
operand = ad.attributes.delete_if do |k|
|
50
|
+
[ :campaign_id, :ad_group_id, :id, :status, :ads ].include?(k.to_sym)
|
51
|
+
end.symbolize_keys
|
52
|
+
|
53
|
+
operations << {
|
54
|
+
:operator => 'ADD',
|
55
|
+
:operand => {
|
56
|
+
:ad_group_id => ad.ad_group_id,
|
57
|
+
:status => ad.status,
|
58
|
+
:ad => operand
|
59
|
+
}
|
47
60
|
}
|
48
|
-
|
61
|
+
end
|
49
62
|
|
50
|
-
|
63
|
+
self.mutate(operations)
|
51
64
|
|
52
65
|
# check for PolicyViolationErrors, set exemptions and try again
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
66
|
+
# do it only once. from my experience with AdWords API, multiple retries are bad practice
|
67
|
+
if self.errors[:PolicyViolationError].any?
|
68
|
+
|
69
|
+
self.errors[:PolicyViolationError].each do |e|
|
70
|
+
i = e.delete(:operation_index)
|
71
|
+
operations[i][:exemption_requests] = [ { :key => e } ]
|
57
72
|
end
|
58
73
|
|
59
74
|
self.errors.clear
|
60
75
|
|
61
|
-
|
76
|
+
self.mutate(operations)
|
62
77
|
end
|
63
78
|
|
64
|
-
return false
|
65
|
-
|
66
|
-
#
|
67
|
-
self.id = response[:value].first[:ad][:id] rescue nil
|
79
|
+
return false if self.errors.any?
|
80
|
+
|
81
|
+
# FIXME return ids of newly created ads
|
82
|
+
# self.id = response[:value].first[:ad][:id] rescue nil
|
68
83
|
|
69
84
|
true
|
70
85
|
end
|
data/lib/adapi/ad_group.rb
CHANGED
@@ -50,7 +50,7 @@ module Adapi
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# convert bid amounts to micro_amounts
|
53
|
-
[ :proxy_keyword_max_cpc
|
53
|
+
[ :proxy_keyword_max_cpc ].each do |k|
|
54
54
|
if @bids[k] and not @bids[k].is_a?(Hash)
|
55
55
|
@bids[k] = {
|
56
56
|
:amount => {
|
@@ -76,7 +76,7 @@ module Adapi
|
|
76
76
|
:operand => operand
|
77
77
|
)
|
78
78
|
|
79
|
-
|
79
|
+
check_for_errors(self)
|
80
80
|
|
81
81
|
self.id = response[:value].first[:id] rescue nil
|
82
82
|
|
@@ -86,22 +86,19 @@ module Adapi
|
|
86
86
|
:keywords => @keywords
|
87
87
|
)
|
88
88
|
|
89
|
-
|
90
|
-
self.errors.add("[keyword]", keyword.errors.to_a)
|
91
|
-
return false
|
92
|
-
end
|
89
|
+
check_for_errors(keyword, :prefix => "Keyword")
|
93
90
|
end
|
94
91
|
|
95
|
-
@ads.
|
96
|
-
ad = Adapi::Ad::TextAd.create( ad_data.merge(:ad_group_id => @id) )
|
92
|
+
if not @ads.empty?
|
93
|
+
ad = Adapi::Ad::TextAd.create( :ads => @ads.map { |ad_data| ad_data.merge(:ad_group_id => @id) } )
|
97
94
|
|
98
|
-
|
99
|
-
self.errors.add("[ad] \"#{ad.headline}\"", ad.errors.to_a)
|
100
|
-
return false
|
101
|
-
end
|
95
|
+
check_for_errors(ad, :prefix => "Ad \"#{ad.headline}\"")
|
102
96
|
end
|
103
97
|
|
104
|
-
|
98
|
+
self.errors.empty?
|
99
|
+
|
100
|
+
rescue AdGroupError => e
|
101
|
+
false
|
105
102
|
end
|
106
103
|
|
107
104
|
def update(params = {})
|
@@ -120,7 +117,7 @@ module Adapi
|
|
120
117
|
:operand => core_params.merge( :id => @id, :campaign_id => @campaign_id )
|
121
118
|
)
|
122
119
|
|
123
|
-
|
120
|
+
check_for_errors(ad_group, :store_errors => false)
|
124
121
|
|
125
122
|
# step 2. update keywords
|
126
123
|
# delete everything and create new keywords
|
@@ -136,36 +133,45 @@ module Adapi
|
|
136
133
|
:ad_group_id => @id,
|
137
134
|
:keywords => params[:keywords]
|
138
135
|
)
|
139
|
-
|
140
|
-
|
141
|
-
self.errors.add("Keyword", result.errors.to_a)
|
142
|
-
return false
|
143
|
-
end
|
136
|
+
|
137
|
+
check_for_errors(result, :prefix => "Keyword")
|
144
138
|
end
|
145
139
|
|
146
140
|
# step 3. update ads
|
147
141
|
# ads can't be updated, gotta remove them all and add new ads
|
148
142
|
if params[:ads] and not params[:ads].empty?
|
149
143
|
# remove all existing ads
|
144
|
+
# TODO change into class method
|
145
|
+
existing_ads = self.find_ads
|
146
|
+
ad = existing_ads.first.delete(
|
147
|
+
:ad_group_id => @id,
|
148
|
+
:ad_ids => existing_ads.map { |ad| ad.id }
|
149
|
+
)
|
150
|
+
|
151
|
+
unless ad
|
152
|
+
# REFACTOR
|
153
|
+
self.errors.add(:base, add.errors.full_messages)
|
154
|
+
return false
|
155
|
+
end
|
156
|
+
|
157
|
+
=begin
|
150
158
|
self.find_ads.each do |ad|
|
151
159
|
unless ad.destroy
|
152
160
|
self.errors.add("Ad \"#{ad.headline}\"", ["cannot be deleted"])
|
153
161
|
return false
|
154
162
|
end
|
155
163
|
end
|
156
|
-
|
164
|
+
=end
|
157
165
|
# create new ads
|
158
|
-
params[:ads].
|
159
|
-
ad = Adapi::Ad::TextAd.create( ad.merge(:ad_group_id => @id) )
|
166
|
+
ad = Adapi::Ad::TextAd.create( :ads => params[:ads].map { |ad_data| ad_data.merge(:ad_group_id => @id) } )
|
160
167
|
|
161
|
-
|
162
|
-
self.errors.add("Ad \"#{ad.headline}\"", ad.errors.to_a)
|
163
|
-
return false
|
164
|
-
end
|
165
|
-
end
|
168
|
+
check_for_errors(ad, :prefix => "Ad \"#{ad.headline}\"")
|
166
169
|
end
|
167
170
|
|
168
|
-
|
171
|
+
self.errors.empty?
|
172
|
+
|
173
|
+
rescue AdGroupError => e
|
174
|
+
false
|
169
175
|
end
|
170
176
|
|
171
177
|
# PS: perhaps also change the ad_group name when deleting
|
@@ -189,8 +195,7 @@ module Adapi
|
|
189
195
|
|
190
196
|
select_fields = %w{ Id CampaignId Name Status }
|
191
197
|
# add Bids atributes
|
192
|
-
select_fields += %w{ EnhancedCpcEnabled
|
193
|
-
ProxyKeywordMaxCpc ProxySiteMaxCpc
|
198
|
+
select_fields += %w{ EnhancedCpcEnabled ProxyKeywordMaxCpc
|
194
199
|
KeywordMaxCpc KeywordContentMaxCpc }
|
195
200
|
|
196
201
|
selector = {
|