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.
@@ -6,7 +6,9 @@ module Adapi
6
6
  #
7
7
  class CampaignCriterion < Api
8
8
 
9
- attr_accessor :campaign_id, :criteria
9
+ ATTRIBUTES = [ :campaign_id, :criteria ]
10
+
11
+ attr_accessor *ATTRIBUTES
10
12
 
11
13
  validates_presence_of :campaign_id
12
14
 
@@ -16,7 +18,7 @@ module Adapi
16
18
  :criterion_user_list, :vertical ]
17
19
 
18
20
  def attributes
19
- super.merge( 'campaign_id' => campaign_id, 'criteria' => criteria )
21
+ super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
20
22
  end
21
23
 
22
24
  def initialize(params = {})
@@ -29,8 +31,8 @@ module Adapi
29
31
  'CampaignCriterion'
30
32
  end
31
33
 
32
- %w{ campaign_id criteria }.each do |param_name|
33
- self.send "#{param_name}=", params[param_name.to_sym]
34
+ ATTRIBUTES.each do |param_name|
35
+ self.send "#{param_name}=", params[param_name]
34
36
  end
35
37
 
36
38
  # HOTFIX backward compatibility with old field for criteria
@@ -9,12 +9,14 @@ module Adapi
9
9
  #
10
10
  class CampaignTarget < Api
11
11
 
12
- attr_accessor :campaign_id, :targets
12
+ ATTRIBUTES = [ :campaign_id, :targets ]
13
+
14
+ attr_accessor *ATTRIBUTES
13
15
 
14
16
  validates_presence_of :campaign_id
15
17
 
16
18
  def attributes
17
- super.merge( 'campaign_id' => campaign_id, 'targets' => targets )
19
+ super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
18
20
  end
19
21
 
20
22
  def initialize(params = {})
@@ -22,8 +24,8 @@ module Adapi
22
24
 
23
25
  @xsi_type = 'CampaignTarget'
24
26
 
25
- %w{ campaign_id targets }.each do |param_name|
26
- self.send "#{param_name}=", params[param_name.to_sym]
27
+ ATTRIBUTES.each do |param_name|
28
+ self.send "#{param_name}=", params[param_name]
27
29
  end
28
30
 
29
31
  super(params)
@@ -8,10 +8,10 @@ module Adapi
8
8
  attr_accessor :dir, :filename
9
9
  end
10
10
 
11
- self.dir = ENV['HOME']
11
+ self.dir = ENV['HOME'] || '.'
12
12
  self.filename = 'adapi.yml'
13
13
 
14
- DEFAULT_LOG_PATH = File.join(ENV['HOME'], 'adapi.log')
14
+ DEFAULT_LOG_PATH = File.join(self.dir, 'adapi.log')
15
15
 
16
16
  # display hash of all account settings
17
17
  #
@@ -21,7 +21,7 @@ module Adapi
21
21
  attr_accessor :keywords
22
22
 
23
23
  def attributes
24
- super.merge('keywords' => keywords)
24
+ super.merge( keywords: keywords )
25
25
  end
26
26
 
27
27
  def initialize(params = {})
@@ -85,13 +85,12 @@ module Adapi
85
85
  response = self.mutate(operations)
86
86
 
87
87
  # check for PolicyViolationErrors, set exemptions and try again
88
- # TODO for now, this is only done once. how about setting a number of retries?
89
- unless self.errors[:PolicyViolationError].empty?
90
- # FIXME this works, but add exemptions_requests only for related keyword if possible
91
- operations.each_with_index do |operation, i|
92
- operations[i][:exemption_requests] = self.errors[:PolicyViolationError].map do |error_key|
93
- { :key => error_key }
94
- end
88
+ # do it only once. from my experience with AdWords API, multiple retries are bad practice
89
+ if self.errors[:PolicyViolationError].any?
90
+
91
+ self.errors[:PolicyViolationError].each do |e|
92
+ i = e.delete(:operation_index)
93
+ operations[i][:exemption_requests] = [ { :key => e } ]
95
94
  end
96
95
 
97
96
  self.errors.clear
@@ -99,57 +98,13 @@ module Adapi
99
98
  response = self.mutate(operations)
100
99
  end
101
100
 
102
- return false unless self.errors.empty?
101
+ return false if self.errors.any?
103
102
 
104
103
  self.keywords = response[:value].map { |keyword| keyword[:criterion] }
105
104
 
106
105
  true
107
106
  end
108
107
 
109
- # keywords mutate wrapper, deals with PolicyViolations for ads
110
- #
111
- # REFACTOR use just one mutate method in Api class
112
- #
113
- def mutate(operation)
114
- operation = [operation] unless operation.is_a?(Array)
115
-
116
- # fix to save space during specifyng operations
117
- operation = operation.map do |op|
118
- op[:operand].delete(:status) if op[:operand][:status].nil?
119
- op
120
- end
121
-
122
- begin
123
-
124
- response = @service.mutate(operation)
125
-
126
- rescue *API_EXCEPTIONS => e
127
-
128
- # return PolicyViolations in specific format so they can be sent again
129
- # see adwords-api gem example for details: handle_policy_violation_error.rb
130
- e.errors.each do |error|
131
- # error[:xsi_type] seems to be broken, so using also alternative key
132
- # also could try: :"@xsi:type" (but api_error_type seems to be more robust)
133
- if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
134
- if error[:is_exemptable]
135
- self.errors.add(:PolicyViolationError, error[:key])
136
- end
137
-
138
- # return also exemptable errors, operation may fail even with them
139
- self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
140
- error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
141
- error[:key][:policy_name],
142
- error[:key][:violating_text]
143
- ])
144
- else
145
- self.errors.add(:base, e.message)
146
- end
147
- end # of errors.each
148
- end
149
-
150
- response
151
- end
152
-
153
108
  def self.find(amount = :all, params = {})
154
109
  params[:format] ||= :google # default, don't do anything with the data from google
155
110
 
@@ -176,11 +131,9 @@ module Adapi
176
131
  selector = {
177
132
  :fields => ['Id', 'CriteriaType', 'KeywordText'],
178
133
  :ordering => [{ :field => 'AdGroupId', :sort_order => 'ASCENDING' }],
179
- :predicates => predicates,
180
- :paging => {
181
- :start_index => 0,
182
- :number_results => 10
183
- }
134
+ :predicates => predicates
135
+ # obsolete, not needed in newer versions of AdWords API
136
+ # :paging => { :start_index => 0, :number_results => 10 }
184
137
  }
185
138
 
186
139
  response = Keyword.new.service.get(selector)
@@ -1,10 +1,16 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Adapi
4
- VERSION = "0.0.9"
4
+ VERSION = "0.1.0"
5
5
 
6
6
  # CHANGELOG:
7
7
  #
8
+ # 0.1.0
9
+ # updated to AdWords API version v201206
10
+ # updated gem dependencies (including google-adwords-api to 0.7.0)
11
+ # improved and debugged error handling for complex create/update methods
12
+ # optimized batch creating of ads
13
+ #
8
14
  # 0.0.9
9
15
  # added Campaign#update method - updates campaign, criteria and ad groups by a single method call
10
16
  # fixed PolicyViolation exemptions for keywords and ads
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ require 'test_helper'
4
+
5
+ module Adapi
6
+ class AdGroupCreateTest < Test::Unit::TestCase
7
+ context "non-existent ad group" do
8
+ should "not be found" do
9
+ assert assert_nil Adapi::AdGroup.find(:first, :campaign_id => Time.new.to_i)
10
+ end
11
+ end
12
+
13
+ context "existing ad group" do
14
+ setup do
15
+ @campaign_id = create_bare_campaign!
16
+
17
+ @ad_group_data = {
18
+ :campaign_id => @campaign_id,
19
+ :name => "AdGroup #%d" % (Time.new.to_f * 1000).to_i,
20
+ :status => 'ENABLED',
21
+ :bids => {
22
+ :xsi_type => 'BudgetOptimizerAdGroupBids',
23
+ :proxy_keyword_max_cpc => 15
24
+ }
25
+ }
26
+
27
+ ag = Adapi::AdGroup.create(@ad_group_data)
28
+
29
+ @ad_group = Adapi::AdGroup.find(:first, :id => ag.id, :campaign_id => @campaign_id)
30
+ end
31
+
32
+ should "be found" do
33
+ assert_not_nil @ad_group
34
+
35
+ assert_equal @ad_group_data[:status], @ad_group.status
36
+ assert_equal @ad_group_data[:name], @ad_group.name
37
+ end
38
+
39
+ should "not be found after deletion" do
40
+ @ad_group.delete
41
+
42
+ assert_nil Adapi::AdGroup.find(:first, :id => @ad_group.id, :campaign_id => @campaign_id)
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
@@ -6,7 +6,6 @@ module Adapi
6
6
  class CampaignCreateTest < Test::Unit::TestCase
7
7
  context "non-existent campaign" do
8
8
  should "not be found" do
9
- # FIXME randomly generated id, but it might actually exist
10
9
  assert_nil Adapi::Campaign.find(Time.new.to_i)
11
10
  end
12
11
  end
@@ -21,8 +20,7 @@ module Adapi
21
20
  :network_setting => {
22
21
  :target_google_search => true,
23
22
  :target_search_network => true,
24
- :target_content_network => false,
25
- :target_content_contextual => false
23
+ :target_content_network => false
26
24
  },
27
25
 
28
26
  :criteria => {
@@ -38,7 +36,6 @@ module Adapi
38
36
  @campaign = Adapi::Campaign.find(c.id)
39
37
  end
40
38
 
41
- # this basically tests creating bare campaign
42
39
  should "be found" do
43
40
  assert_not_nil @campaign
44
41
 
@@ -19,4 +19,53 @@ Dir[ File.join(File.dirname(__FILE__), 'factories', '*.rb') ].each { |f| require
19
19
 
20
20
  class Test::Unit::TestCase
21
21
  FakeWeb.allow_net_connect = false
22
+
23
+ # many integration tests need to use campaign or ad group
24
+ # instead of creating them in every test, we do it here
25
+ #
26
+ @@bare_campaign_id = nil
27
+ @@bare_ad_group_id = nil
28
+
29
+ def create_bare_campaign!(force = false)
30
+ if force or @@bare_campaign_id.nil?
31
+ campaign = Adapi::Campaign.create(
32
+ name: "Campaign #%d" % (Time.new.to_f * 1000).to_i,
33
+ status: 'PAUSED',
34
+ bidding_strategy: {
35
+ xsi_type: 'BudgetOptimizer',
36
+ bid_ceiling: 20
37
+ },
38
+ budget: 50
39
+ )
40
+
41
+ if campaign.errors.any?
42
+ raise "CANNOT CREATE BARE CAMPAIGN (during test initialization):\n" + campaign.errors.join("\n")
43
+ end
44
+
45
+ @@bare_campaign_id = campaign[:id]
46
+ end
47
+
48
+ @@bare_campaign_id
49
+ end
50
+
51
+ def create_bare_ad_group!(force = false)
52
+ if force or @@bare_ad_group_id.nil?
53
+ @@bare_campaign_id ||= create_bare_campaign!(true)
54
+
55
+ ad_group = Adapi::AdGroup.create(
56
+ campaign_id: @@bare_campaign_id,
57
+ name: "AdGroup #%d" % (Time.new.to_f * 1000).to_i,
58
+ status: 'ENABLED'
59
+ )
60
+
61
+ if ad_group.errors.any?
62
+ raise "CANNOT CREATE BARE AD GROUP (during test initialization):\n" + ad_group.errors.join("\n")
63
+ end
64
+
65
+ @@bare_ad_group_id = ad_group[:id]
66
+ end
67
+
68
+ @@bare_ad_group_id
69
+ end
70
+
22
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,33 +9,33 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-23 00:00:00.000000000 Z
12
+ date: 2012-09-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-ads-common
16
- requirement: &14836360 !ruby/object:Gem::Requirement
16
+ requirement: &8558440 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - =
20
20
  - !ruby/object:Gem::Version
21
- version: 0.7.3
21
+ version: 0.8.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *14836360
24
+ version_requirements: *8558440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: google-adwords-api
27
- requirement: &14835520 !ruby/object:Gem::Requirement
27
+ requirement: &8554360 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - =
31
31
  - !ruby/object:Gem::Version
32
- version: 0.6.2
32
+ version: 0.7.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *14835520
35
+ version_requirements: *8554360
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: activemodel
38
- requirement: &14834820 !ruby/object:Gem::Requirement
38
+ requirement: &8562720 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '3.0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *14834820
46
+ version_requirements: *8562720
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: activesupport
49
- requirement: &14834120 !ruby/object:Gem::Requirement
49
+ requirement: &9409620 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '3.0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *14834120
57
+ version_requirements: *9409620
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &14849580 !ruby/object:Gem::Requirement
60
+ requirement: &9443740 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,32 +65,32 @@ dependencies:
65
65
  version: 0.9.2
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *14849580
68
+ version_requirements: *9443740
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: curb
71
- requirement: &14848220 !ruby/object:Gem::Requirement
71
+ requirement: &9448120 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
75
75
  - !ruby/object:Gem::Version
76
- version: 0.8.0
76
+ version: 0.8.1
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *14848220
79
+ version_requirements: *9448120
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: yard
82
- requirement: &14846280 !ruby/object:Gem::Requirement
82
+ requirement: &9038540 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
86
86
  - !ruby/object:Gem::Version
87
- version: '0.7'
87
+ version: '0.8'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *14846280
90
+ version_requirements: *9038540
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: rcov
93
- requirement: &14845320 !ruby/object:Gem::Requirement
93
+ requirement: &9037880 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ~>
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0.9'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *14845320
101
+ version_requirements: *9037880
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: turn
104
- requirement: &14843740 !ruby/object:Gem::Requirement
104
+ requirement: &9036120 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ~>
@@ -109,51 +109,51 @@ dependencies:
109
109
  version: 0.9.6
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *14843740
112
+ version_requirements: *9036120
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: shoulda
115
- requirement: &14842660 !ruby/object:Gem::Requirement
115
+ requirement: &9034380 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
- - - ! '>='
118
+ - - ~>
119
119
  - !ruby/object:Gem::Version
120
- version: '0'
120
+ version: '3.1'
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *14842660
123
+ version_requirements: *9034380
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: fakeweb
126
- requirement: &14858140 !ruby/object:Gem::Requirement
126
+ requirement: &9032760 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
- - - ! '>='
129
+ - - ~>
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '1.3'
132
132
  type: :development
133
133
  prerelease: false
134
- version_requirements: *14858140
134
+ version_requirements: *9032760
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: factory_girl
137
- requirement: &14857440 !ruby/object:Gem::Requirement
137
+ requirement: &9159820 !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
140
140
  - - ~>
141
141
  - !ruby/object:Gem::Version
142
- version: 3.3.0
142
+ version: '3.3'
143
143
  type: :development
144
144
  prerelease: false
145
- version_requirements: *14857440
145
+ version_requirements: *9159820
146
146
  - !ruby/object:Gem::Dependency
147
147
  name: minitest
148
- requirement: &14856840 !ruby/object:Gem::Requirement
148
+ requirement: &9157340 !ruby/object:Gem::Requirement
149
149
  none: false
150
150
  requirements:
151
- - - ! '>='
151
+ - - ~>
152
152
  - !ruby/object:Gem::Version
153
- version: '0'
153
+ version: '3.3'
154
154
  type: :development
155
155
  prerelease: false
156
- version_requirements: *14856840
156
+ version_requirements: *9157340
157
157
  description: This gem provides user-friendly interface to Google Adwords API.
158
158
  email:
159
159
  - lucastej@gmail.com
@@ -179,6 +179,7 @@ files:
179
179
  - examples/add_keywords.rb
180
180
  - examples/add_negative_campaign_criteria.rb
181
181
  - examples/add_text_ad.rb
182
+ - examples/add_text_ads.rb
182
183
  - examples/custom_settings.yml
183
184
  - examples/customize_configuration.rb
184
185
  - examples/delete_ad_group.rb
@@ -225,6 +226,7 @@ files:
225
226
  - test/factories/campaign_factory.rb
226
227
  - test/fixtures/adapi.yml
227
228
  - test/fixtures/adwords_api.yml
229
+ - test/integration/create_ad_group_test.rb
228
230
  - test/integration/create_campaign_test.rb
229
231
  - test/integration/find_location_test.rb
230
232
  - test/test_helper.rb
@@ -250,7 +252,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
250
252
  version: '0'
251
253
  segments:
252
254
  - 0
253
- hash: 3921754169281817969
255
+ hash: 2590493232299727233
254
256
  required_rubygems_version: !ruby/object:Gem::Requirement
255
257
  none: false
256
258
  requirements:
@@ -259,7 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
259
261
  version: '0'
260
262
  segments:
261
263
  - 0
262
- hash: 3921754169281817969
264
+ hash: 2590493232299727233
263
265
  requirements: []
264
266
  rubyforge_project: adapi
265
267
  rubygems_version: 1.8.15