adapi 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/README.markdown +176 -0
- data/Rakefile +32 -0
- data/adapi.gemspec +7 -4
- data/examples/add_ad_group.rb +1 -0
- data/examples/add_bare_ad_group.rb +2 -6
- data/examples/add_bare_campaign.rb +1 -0
- data/examples/add_campaign.rb +1 -0
- data/examples/add_campaign_targets.rb +1 -0
- data/examples/add_invalid_ad_group.rb +1 -0
- data/examples/add_keywords.rb +13 -1
- data/examples/custom_settings.yml +2 -2
- data/examples/customize_configuration.rb +2 -0
- data/examples/delete_keyword.rb +27 -0
- data/examples/find_all_campaigns.rb +13 -0
- data/examples/find_campaign.rb +39 -0
- data/examples/find_campaign_ad_groups.rb +25 -0
- data/examples/log_to_specific_account.rb +1 -0
- data/examples/rollback_campaign.rb +1 -0
- data/lib/adapi.rb +8 -5
- data/lib/adapi/ad.rb +2 -0
- data/lib/adapi/ad/text_ad.rb +15 -0
- data/lib/adapi/ad_group.rb +45 -6
- data/lib/adapi/ad_group_criterion.rb +14 -0
- data/lib/adapi/api.rb +6 -0
- data/lib/adapi/campaign.rb +48 -5
- data/lib/adapi/campaign_target.rb +67 -8
- data/lib/adapi/config.rb +1 -0
- data/lib/adapi/keyword.rb +78 -11
- data/lib/adapi/version.rb +9 -1
- data/lib/httpi_request_monkeypatch.rb +1 -0
- data/test/factories/ad_group_factory.rb +4 -13
- data/test/factories/ad_text_factory.rb +1 -0
- data/test/test_helper.rb +3 -0
- data/test/unit/ad/ad_text_test.rb +2 -0
- data/test/unit/ad_group_test.rb +11 -2
- data/test/unit/ad_test.rb +2 -0
- data/test/unit/campaign_target_test.rb +15 -0
- metadata +67 -32
- data/README.rdoc +0 -162
- data/lib/collection.rb +0 -429
data/lib/adapi.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
|
2
3
|
require 'rubygems'
|
3
4
|
require 'adwords_api'
|
4
|
-
require 'collection'
|
5
5
|
require 'yaml'
|
6
6
|
require 'pp'
|
7
7
|
|
@@ -27,10 +27,13 @@ HTTPI.adapter = :curb
|
|
27
27
|
# supress HTTPI output
|
28
28
|
# HTTPI.log = false
|
29
29
|
|
30
|
-
# load factories for development environment
|
31
|
-
# require 'factory_girl'
|
32
|
-
# Dir[ File.join(File.dirname(__FILE__), '../test/factories/*.rb') ].each { |f| require f }
|
33
|
-
|
34
30
|
module Adapi
|
35
31
|
API_VERSION = :v201101
|
36
32
|
end
|
33
|
+
|
34
|
+
# check ruby version, should be 1.9
|
35
|
+
# FIXME there's gotta be more elegant way to do this
|
36
|
+
`ruby -v`.to_s =~ /^ruby (\d\.\d)\./
|
37
|
+
if $1.to_f < 1.9
|
38
|
+
puts "WARNING: please use ruby 1.9, adapi gem won't work properly in 1.8 and earlier versions"
|
39
|
+
end
|
data/lib/adapi/ad.rb
CHANGED
data/lib/adapi/ad/text_ad.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adapi
|
2
4
|
# Ad::TextAd == AdGroupAd::TextAd
|
3
5
|
#
|
@@ -122,5 +124,18 @@ module Adapi
|
|
122
124
|
first_only ? response.first : response
|
123
125
|
end
|
124
126
|
|
127
|
+
# Converts text ad data to hash - of the same structure which is used when
|
128
|
+
# creating a complete campaign.
|
129
|
+
#
|
130
|
+
def to_hash
|
131
|
+
{
|
132
|
+
:headline => self[:headline],
|
133
|
+
:description1 => self[:description1],
|
134
|
+
:description2 => self[:description2],
|
135
|
+
:url => self[:url],
|
136
|
+
:display_url => self[:display_url]
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
125
140
|
end
|
126
141
|
end
|
data/lib/adapi/ad_group.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adapi
|
2
4
|
class AdGroup < Api
|
3
5
|
|
@@ -19,6 +21,25 @@ module Adapi
|
|
19
21
|
self.send "#{param_name}=", params[param_name.to_sym]
|
20
22
|
end
|
21
23
|
|
24
|
+
# convert bids to GoogleApi format
|
25
|
+
#
|
26
|
+
# can be either string (just xsi_type) or hash (xsi_type with params)
|
27
|
+
# althogh I'm not sure if just string makes sense in this case
|
28
|
+
#
|
29
|
+
if @bids
|
30
|
+
unless @bids.is_a?(Hash)
|
31
|
+
@bids = { :xsi_type => @bids }
|
32
|
+
end
|
33
|
+
|
34
|
+
if @bids[:keyword_max_cpc] and not @bids[:keyword_max_cpc].is_a?(Hash)
|
35
|
+
@bids[:keyword_max_cpc] = {
|
36
|
+
:amount => {
|
37
|
+
:micro_amount => Api.to_micro_units(@bids[:keyword_max_cpc])
|
38
|
+
}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
22
43
|
@keywords ||= []
|
23
44
|
@ads ||= []
|
24
45
|
|
@@ -71,7 +92,7 @@ module Adapi
|
|
71
92
|
params.symbolize_keys!
|
72
93
|
first_only = (amount.to_sym == :first)
|
73
94
|
|
74
|
-
raise "
|
95
|
+
raise "Campaign ID is required" unless params[:campaign_id]
|
75
96
|
|
76
97
|
predicates = [ :campaign_id, :id ].map do |param_name|
|
77
98
|
if params[param_name]
|
@@ -87,22 +108,40 @@ module Adapi
|
|
87
108
|
|
88
109
|
response = AdGroup.new.service.get(selector)
|
89
110
|
|
90
|
-
|
111
|
+
ad_groups = (response and response[:entries]) ? response[:entries] : []
|
91
112
|
|
92
|
-
|
93
|
-
# TextAd.new(data[:ad].merge(:ad_group_id => data[:ad_group_id], :status => data[:status]))
|
94
|
-
#end
|
113
|
+
ad_groups = ad_groups.slice(0,1) if first_only
|
95
114
|
|
96
|
-
|
115
|
+
# find keywords and ads
|
116
|
+
ad_groups.map! do |ad_group|
|
117
|
+
ad_group.merge(
|
118
|
+
:keywords => Keyword.shortened(Keyword.find(:all, :ad_group_id => ad_group[:id]).keywords),
|
119
|
+
:ads => Ad::TextAd.find(:all, :ad_group_id => ad_group[:id]).map(&:to_hash)
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
first_only ? ad_groups.first : ad_groups
|
97
124
|
end
|
98
125
|
|
99
126
|
def find_keywords(first_only = false)
|
100
127
|
Keyword.find( (first_only ? :first : :all), :ad_group_id => self.id )
|
101
128
|
end
|
102
129
|
|
130
|
+
# TODO find all types of ads
|
103
131
|
def find_ads(first_only = false)
|
104
132
|
Ad::TextAd.find( (first_only ? :first : :all), :ad_group_id => self.id )
|
105
133
|
end
|
106
134
|
|
135
|
+
# Converts ad group data to hash - of the same structure which is used when
|
136
|
+
# creating an ad group.
|
137
|
+
#
|
138
|
+
def to_hash
|
139
|
+
ad_group_hash = {
|
140
|
+
:id => self[:id],
|
141
|
+
:name => self[:name],
|
142
|
+
:status => self[:status]
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
107
146
|
end
|
108
147
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adapi
|
2
4
|
|
3
5
|
# http://code.google.com/apis/adwords/docs/reference/latest/AdGroupCriterionService.html
|
@@ -22,5 +24,17 @@ module Adapi
|
|
22
24
|
super(params)
|
23
25
|
end
|
24
26
|
|
27
|
+
def delete(criterion_id)
|
28
|
+
response = self.mutate(
|
29
|
+
:operator => 'REMOVE',
|
30
|
+
:operand => {
|
31
|
+
:ad_group_id => self.ad_group_id,
|
32
|
+
:criterion => { :id => criterion_id.to_i }
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
(response and response[:value])
|
37
|
+
end
|
38
|
+
|
25
39
|
end
|
26
40
|
end
|
data/lib/adapi/api.rb
CHANGED
data/lib/adapi/campaign.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adapi
|
2
4
|
class Campaign < Api
|
3
5
|
|
@@ -34,7 +36,7 @@ module Adapi
|
|
34
36
|
@bidding_strategy = { :xsi_type => @bidding_strategy }
|
35
37
|
end
|
36
38
|
|
37
|
-
if @bidding_strategy[:bid_ceiling]
|
39
|
+
if @bidding_strategy[:bid_ceiling] and not @bidding_strategy[:bid_ceiling].is_a?(Hash)
|
38
40
|
@bidding_strategy[:bid_ceiling] = {
|
39
41
|
:micro_amount => Api.to_micro_units(@bidding_strategy[:bid_ceiling])
|
40
42
|
}
|
@@ -46,7 +48,9 @@ module Adapi
|
|
46
48
|
# budget can be integer (amount) or hash
|
47
49
|
@budget = { :amount => @budget } unless @budget.is_a?(Hash)
|
48
50
|
@budget[:period] ||= 'DAILY'
|
49
|
-
@budget[:amount]
|
51
|
+
if @budget[:amount] and not @budget[:amount].is_a?(Hash)
|
52
|
+
@budget[:amount] = { :micro_amount => Api.to_micro_units(@budget[:amount]) }
|
53
|
+
end
|
50
54
|
# PS: not sure if this should be a default. maybe we don't even need it
|
51
55
|
@budget[:delivery_method] ||= 'STANDARD'
|
52
56
|
|
@@ -150,12 +154,20 @@ module Adapi
|
|
150
154
|
Campaign.find(:first, :id => @id)
|
151
155
|
end
|
152
156
|
|
157
|
+
|
158
|
+
# if nothing else than single number or string at the input, assume it's an
|
159
|
+
# id and we want to find campaign by id
|
160
|
+
#
|
153
161
|
def self.find(amount = :all, params = {})
|
162
|
+
# find campaign by id - related syntactic sugar
|
163
|
+
if params.empty? and not amount.is_a?(Symbol)
|
164
|
+
params[:id] = amount.to_i
|
165
|
+
amount = :first
|
166
|
+
end
|
167
|
+
|
154
168
|
params.symbolize_keys!
|
155
169
|
first_only = (amount.to_sym == :first)
|
156
170
|
|
157
|
-
raise "Campaign ID (:id param) is required" unless params[:id]
|
158
|
-
|
159
171
|
predicates = [ :id ].map do |param_name|
|
160
172
|
if params[param_name]
|
161
173
|
{:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
|
@@ -165,7 +177,7 @@ module Adapi
|
|
165
177
|
# TODO display the rest of the data
|
166
178
|
# TODO get NetworkSetting - setting as in fields doesn't work
|
167
179
|
selector = {
|
168
|
-
:fields => ['Id', 'Name', 'Status', 'BiddingStrategy'
|
180
|
+
:fields => ['Id', 'Name', 'Status', 'BiddingStrategy'],
|
169
181
|
:ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
|
170
182
|
:predicates => predicates
|
171
183
|
}
|
@@ -188,5 +200,36 @@ module Adapi
|
|
188
200
|
AdGroup.find( (first_only ? :first : :all), :campaign_id => self.id )
|
189
201
|
end
|
190
202
|
|
203
|
+
# Returns complete campaign data: targets, ad groups, keywords and ads.
|
204
|
+
# Basically everything what you can set when creating a campaign.
|
205
|
+
#
|
206
|
+
def self.find_complete(campaign_id)
|
207
|
+
campaign = self.find(campaign_id)
|
208
|
+
|
209
|
+
campaign[:targets] = CampaignTarget.find(:campaign_id => campaign.to_param)
|
210
|
+
|
211
|
+
campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param)
|
212
|
+
|
213
|
+
campaign
|
214
|
+
end
|
215
|
+
|
216
|
+
# Converts campaign data to hash - of the same structure which is used when
|
217
|
+
# creating a campaign.
|
218
|
+
#
|
219
|
+
# PS: could be implemented more succintly, but let's leave it like this for
|
220
|
+
# now, code can change and this is more readable
|
221
|
+
#
|
222
|
+
def to_hash
|
223
|
+
{
|
224
|
+
:id => self[:id],
|
225
|
+
:name => self[:name],
|
226
|
+
:status => self[:status],
|
227
|
+
:budget => self[:budget],
|
228
|
+
:bidding_strategy => self[:bidding_strategy],
|
229
|
+
:targets => self[:targets],
|
230
|
+
:ad_groups => self[:ad_groups]
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
191
234
|
end
|
192
235
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adapi
|
2
4
|
|
3
5
|
# http://code.google.com/apis/adwords/docs/reference/latest/CampaignTargetService.html
|
@@ -48,12 +50,15 @@ module Adapi
|
|
48
50
|
|
49
51
|
alias :create :set
|
50
52
|
|
51
|
-
|
52
|
-
#
|
53
|
-
def self.find(amount = :all, params = {})
|
53
|
+
def self.find(params = {})
|
54
54
|
params.symbolize_keys!
|
55
|
-
|
56
|
-
|
55
|
+
|
56
|
+
# by default, return skip target types that have no target data
|
57
|
+
params[:skip_empty_target_types] ||= true
|
58
|
+
|
59
|
+
if params[:conditions]
|
60
|
+
params[:campaign_id] = params[:campaign_id] || params[:conditions][:campaign_id]
|
61
|
+
end
|
57
62
|
|
58
63
|
raise ArgumentError, "Campaing ID is required" unless params[:campaign_id]
|
59
64
|
|
@@ -63,15 +68,59 @@ module Adapi
|
|
63
68
|
|
64
69
|
response = (response and response[:entries]) ? response[:entries] : []
|
65
70
|
|
66
|
-
|
71
|
+
# return everything or only
|
72
|
+
if params[:skip_empty_target_types]
|
73
|
+
response.select! { |target_type| target_type.has_key?(:targets) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# TODO optionally return just certain target type(s)
|
77
|
+
# easy, just add condition (single type or array), filter and set
|
78
|
+
# :skip_empty_target_types option to false
|
79
|
+
|
80
|
+
# optionally convert to original input shortcuts specified by Adapi DSL
|
81
|
+
# TODO on second thought, no need to do that now. it's simpler to make
|
82
|
+
# CampaignTarget.new accept original AdWords data
|
83
|
+
# if params[:format] == :adapi_dsl
|
84
|
+
# response = Hash[ response.map { |t| CampaignTarget.parse_dsl(t) } ]
|
85
|
+
# end
|
86
|
+
|
87
|
+
response
|
67
88
|
end
|
68
89
|
|
90
|
+
=begin TODO
|
91
|
+
def self.parse_dsl(target)
|
92
|
+
case target[:target_list_type]
|
93
|
+
when "LanguageTargetList"
|
94
|
+
[ :language, target[:targets].map { |t| t[:language_code] } ]
|
95
|
+
when "GeoTargetList"
|
96
|
+
targets = Hash[ target[:targets].map do |t|
|
97
|
+
target_type = t[:target_type].gsub(/Target$/, '')
|
98
|
+
|
99
|
+
case target_type
|
100
|
+
when :proximity
|
101
|
+
[ target_type, { :geo_point => '', :radius => '' } ]
|
102
|
+
when :city
|
103
|
+
else
|
104
|
+
[ target_type, t ]
|
105
|
+
end
|
106
|
+
end ]
|
107
|
+
|
108
|
+
[ :geo, targets ]
|
109
|
+
else
|
110
|
+
warn "Unsupported target type! %s" % target.inspect
|
111
|
+
[ target[:target_list_type], target[:targets] ]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
=end
|
115
|
+
|
69
116
|
# transform our own high-level target parameters to google low-level
|
117
|
+
#
|
118
|
+
# TODO allow to enter AdWords parameters in original format
|
70
119
|
#
|
71
120
|
def self.create_targets(target_type, target_data)
|
72
121
|
case target_type
|
73
122
|
when :language
|
74
|
-
target_data.map { |language| { :language_code => language } }
|
123
|
+
target_data.map { |language| { :language_code => language.to_s.downcase } }
|
75
124
|
# example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
|
76
125
|
when :geo
|
77
126
|
target_data.map do |geo_type, geo_values|
|
@@ -101,7 +150,7 @@ module Adapi
|
|
101
150
|
{
|
102
151
|
:xsi_type => "#{geo_type.to_s.capitalize}Target",
|
103
152
|
:excluded => false,
|
104
|
-
"#{geo_type}_code".to_sym => geo_values
|
153
|
+
"#{geo_type}_code".to_sym => to_uppercase(geo_values)
|
105
154
|
}
|
106
155
|
end
|
107
156
|
end
|
@@ -134,5 +183,15 @@ module Adapi
|
|
134
183
|
Api.to_micro_units(x)
|
135
184
|
end
|
136
185
|
|
186
|
+
# convert either single value or array of value to uppercase
|
187
|
+
#
|
188
|
+
def self.to_uppercase(values)
|
189
|
+
if values.is_a?(Array)
|
190
|
+
values.map { |value| value.to_s.upcase }
|
191
|
+
else
|
192
|
+
values.to_s.upcase
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
137
196
|
end
|
138
197
|
end
|
data/lib/adapi/config.rb
CHANGED
data/lib/adapi/keyword.rb
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# TODO user should be able to delete keywords
|
4
|
+
# TODO user should not see deleted keywords by default
|
5
|
+
#
|
6
|
+
# TODO program should be able to detect keywords in shortened or Google form
|
7
|
+
# automatically on input (outsource into separate method?)
|
8
|
+
#
|
9
|
+
# TODO user should be able to enter keywords in both shortened, parameterized and Google form
|
10
|
+
#
|
11
|
+
# FIXME broken Keyword.negative param
|
12
|
+
|
13
|
+
# Currently the Keyword DSL is a mess. There are basically three forms:
|
14
|
+
# * ultra short form on input: keyword example
|
15
|
+
# * shortened form: {:text=>"keyword example", :match_type=>"BROAD", :negative=>false}
|
16
|
+
# * google form
|
17
|
+
|
1
18
|
module Adapi
|
2
19
|
class Keyword < AdGroupCriterion
|
3
20
|
|
@@ -22,22 +39,22 @@ module Adapi
|
|
22
39
|
super(params)
|
23
40
|
end
|
24
41
|
|
25
|
-
#
|
42
|
+
# Converts keyword specification from shortened form to Google format
|
26
43
|
#
|
27
44
|
def self.keyword_attributes(keyword)
|
28
45
|
# detect match type
|
29
46
|
match_type = case keyword[0]
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
47
|
+
when '"'
|
48
|
+
keyword = keyword.slice(1, (keyword.size - 2))
|
49
|
+
'PHRASE'
|
50
|
+
when '['
|
51
|
+
keyword = keyword.slice(1, (keyword.size - 2))
|
52
|
+
'EXACT'
|
53
|
+
else
|
54
|
+
'BROAD'
|
38
55
|
end
|
39
56
|
|
40
|
-
#
|
57
|
+
# sets whether keyword is negative or not
|
41
58
|
negative = if (keyword =~ /^\-/)
|
42
59
|
keyword.slice!(0, 1)
|
43
60
|
true
|
@@ -74,6 +91,8 @@ module Adapi
|
|
74
91
|
end
|
75
92
|
|
76
93
|
def self.find(amount = :all, params = {})
|
94
|
+
params[:format] ||= :google # default, don't do anything with the data from google
|
95
|
+
|
77
96
|
params.symbolize_keys!
|
78
97
|
# this has no effect, it's here just to have the same interface everywhere
|
79
98
|
first_only = (amount.to_sym == :first)
|
@@ -99,11 +118,59 @@ module Adapi
|
|
99
118
|
|
100
119
|
response = (response and response[:entries]) ? response[:entries] : []
|
101
120
|
|
121
|
+
# for now, always return keywords in :google format
|
122
|
+
=begin
|
123
|
+
response = case params[:format].to_sym
|
124
|
+
when :short
|
125
|
+
Keyword.shortened(response)
|
126
|
+
when :params
|
127
|
+
Keyword.parameterized(response)
|
128
|
+
else
|
129
|
+
response
|
130
|
+
end
|
131
|
+
=end
|
132
|
+
|
102
133
|
Keyword.new(
|
103
134
|
:ad_group_id => params[:ad_group_id],
|
104
|
-
:keywords => response
|
135
|
+
:keywords => response
|
105
136
|
)
|
106
137
|
end
|
107
138
|
|
139
|
+
# PS: create a better UI for this?
|
140
|
+
# Keyword.convert(:to => :params, :source => $google_keywords)
|
141
|
+
# and Keyword.parametrized($google_keywords) just calling that?
|
142
|
+
|
143
|
+
# Converts list of keywords from Google format to short format
|
144
|
+
#
|
145
|
+
def self.shortened(google_keywords = [])
|
146
|
+
self.parameterized(google_keywords).map do |keyword|
|
147
|
+
keyword[:text] = "-%s" % keyword[:text] if keyword[:negative]
|
148
|
+
|
149
|
+
case keyword[:match_type]
|
150
|
+
when 'PHRASE'
|
151
|
+
"\"%s\"" % keyword[:text]
|
152
|
+
when 'EXACT'
|
153
|
+
"[%s]" % keyword[:text]
|
154
|
+
else # 'BROAD'
|
155
|
+
keyword[:text]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Converts list of keywords from Google format to params format
|
161
|
+
# (the way it can be entered into Keywords model)
|
162
|
+
#
|
163
|
+
def self.parameterized(google_keywords = [])
|
164
|
+
google_keywords.map do |keyword|
|
165
|
+
kw = keyword[:text][:criterion]
|
166
|
+
|
167
|
+
{
|
168
|
+
:text => kw[:text],
|
169
|
+
:match_type => kw[:match_type],
|
170
|
+
:negative => (keyword[:text][:xsi_type] == "NegativeAdGroupCriterion")
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
108
175
|
end
|
109
176
|
end
|