adapi 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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