adapi 0.0.3 → 0.0.4
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/.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
|