adapi 0.0.1
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 +4 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.rdoc +127 -0
- data/Rakefile +12 -0
- data/adapi.gemspec +28 -0
- data/examples/add_ad.rb +17 -0
- data/examples/add_ad_group.rb +46 -0
- data/examples/add_ad_group_criteria.rb +20 -0
- data/examples/add_bare_ad_group.rb +26 -0
- data/examples/add_bare_campaign.rb +27 -0
- data/examples/add_campaign.rb +68 -0
- data/examples/add_campaign_targets.rb +16 -0
- data/examples/custom_settings.yml +22 -0
- data/examples/customize_configuration.rb +17 -0
- data/examples/log_to_specific_account.rb +20 -0
- data/examples/update_campaign.rb +23 -0
- data/examples/update_campaign_name.rb +14 -0
- data/examples/update_campaign_status.rb +15 -0
- data/lib/adapi.rb +18 -0
- data/lib/adapi/ad.rb +64 -0
- data/lib/adapi/ad_group.rb +70 -0
- data/lib/adapi/ad_group_criterion.rb +68 -0
- data/lib/adapi/api.rb +20 -0
- data/lib/adapi/campaign.rb +142 -0
- data/lib/adapi/campaign_target.rb +90 -0
- data/lib/adapi/config.rb +54 -0
- data/lib/adapi/version.rb +3 -0
- data/lib/collection.rb +429 -0
- data/test/factories/.gitignore +0 -0
- data/test/test_helper.rb +20 -0
- data/test/unit/.gitignore +0 -0
- data/test/unit/campaign_target_test.rb +21 -0
- metadata +187 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
module Adapi
|
2
|
+
|
3
|
+
# http://code.google.com/apis/adwords/docs/reference/latest/CampaignTargetService.html
|
4
|
+
#
|
5
|
+
class CampaignTarget < Api
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
params[:service_name] = :CampaignTargetService
|
9
|
+
super(params)
|
10
|
+
end
|
11
|
+
|
12
|
+
# FIXME params should be the same as in other services, for example ad_group
|
13
|
+
#
|
14
|
+
def self.create(params = {})
|
15
|
+
campaign_target_service = CampaignTarget.new
|
16
|
+
|
17
|
+
raise "No Campaign ID" unless params[:campaign_id]
|
18
|
+
campaign_id = params[:campaign_id].to_i
|
19
|
+
|
20
|
+
# transform our own high-level target parameters to google low-level
|
21
|
+
# target parameters
|
22
|
+
operations = []
|
23
|
+
|
24
|
+
params[:targets].each_pair do |targetting_type, targetting_settings|
|
25
|
+
operations << { :operator => 'SET',
|
26
|
+
:operand => {
|
27
|
+
:xsi_type => "#{targetting_type.to_s.capitalize}TargetList",
|
28
|
+
:campaign_id => campaign_id,
|
29
|
+
:targets => self.create_targets(targetting_type, targetting_settings)
|
30
|
+
}
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
response = campaign_target_service.service.mutate(operations)
|
35
|
+
|
36
|
+
targets = response[:value] || []
|
37
|
+
targets.each do |target|
|
38
|
+
puts "Campaign target of type #{target[:"@xsi:type"]} for campaign id " +
|
39
|
+
"#{target[:campaign_id]} was set."
|
40
|
+
end
|
41
|
+
|
42
|
+
targets
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.find(params = {})
|
46
|
+
campaign_target_service = CampaignTarget.new
|
47
|
+
|
48
|
+
selector = {} # select all campaign targets by default
|
49
|
+
selector[:campaign_ids] = params[:campaign_ids] if params[:campaign_ids]
|
50
|
+
|
51
|
+
response = campaign_target_service.service.get(selector)
|
52
|
+
|
53
|
+
targets = nil
|
54
|
+
if response and response[:entries]
|
55
|
+
targets = response[:entries]
|
56
|
+
targets.each do |target|
|
57
|
+
p target
|
58
|
+
end
|
59
|
+
else
|
60
|
+
puts "No campaign targets found."
|
61
|
+
end
|
62
|
+
|
63
|
+
targets
|
64
|
+
end
|
65
|
+
|
66
|
+
# transform our own high-level target parameters to google low-level
|
67
|
+
#
|
68
|
+
def self.create_targets(target_type, target_data)
|
69
|
+
case target_type
|
70
|
+
when :language
|
71
|
+
target_data.map { |language| { :language_code => language } }
|
72
|
+
# example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
|
73
|
+
when :geo
|
74
|
+
geo_targets = []
|
75
|
+
target_data.each_pair do |geo_type, geo_values|
|
76
|
+
geo_values.each do |geo_value|
|
77
|
+
geo_targets << {
|
78
|
+
:xsi_type => "#{geo_type.to_s.capitalize}Target",
|
79
|
+
:excluded => false,
|
80
|
+
"#{geo_type}_code".to_sym => geo_value
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
geo_targets
|
85
|
+
else nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
data/lib/adapi/config.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
# PS: what about this config setting?
|
3
|
+
# Campaign.create(:data => campaign_data, :account => :my_account_alias)
|
4
|
+
|
5
|
+
module Adapi
|
6
|
+
class Config
|
7
|
+
|
8
|
+
# display hash of all account settings
|
9
|
+
#
|
10
|
+
def self.settings
|
11
|
+
@settings ||= self.load_settings
|
12
|
+
end
|
13
|
+
|
14
|
+
# display actual account settings
|
15
|
+
# if it's not available, set to :default account settings
|
16
|
+
#
|
17
|
+
def self.read # = @data
|
18
|
+
@data ||= self.settings[:default]
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO described in README, but should be documented here as well
|
22
|
+
#
|
23
|
+
def self.set(params = {})
|
24
|
+
# hash of params - default
|
25
|
+
if params.is_a?(Hash)
|
26
|
+
@data = params
|
27
|
+
# set alias from adapi.yml
|
28
|
+
elsif params.is_a?(Symbol)
|
29
|
+
@data = @settings[params]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# params:
|
34
|
+
# * path - default: user's home directory
|
35
|
+
# * filename - default: adapi.yml
|
36
|
+
# TODO: set to HOME/adwords_api as default
|
37
|
+
def self.load_settings(params = {})
|
38
|
+
params[:path] ||= ENV['HOME']
|
39
|
+
params[:filename] ||= 'adapi.yml'
|
40
|
+
|
41
|
+
adapi_path = File.join(params[:path], params[:filename])
|
42
|
+
adwords_api_path = File.join(ENV['HOME'], 'adwords_api.yml')
|
43
|
+
|
44
|
+
if File.exists?(adapi_path)
|
45
|
+
@settings = YAML::load(File.read(adapi_path)) rescue {}
|
46
|
+
elsif File.exists?(adwords_api_path)
|
47
|
+
@settings = { :default => YAML::load(File.read(adwords_api_path)) } rescue {}
|
48
|
+
end
|
49
|
+
|
50
|
+
@settings
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/lib/collection.rb
ADDED
@@ -0,0 +1,429 @@
|
|
1
|
+
# = Collection
|
2
|
+
#
|
3
|
+
# Module which allows to use a proxy class for wrapping collections of all sorts.
|
4
|
+
#
|
5
|
+
# Let's take a collection of articles, for example (see also the test suite below).
|
6
|
+
#
|
7
|
+
# The collection item class could look like this:
|
8
|
+
#
|
9
|
+
# class Article
|
10
|
+
# attr_reader :title
|
11
|
+
# def initialize(title); @title = title; end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# The collection class could look like this, using the provided DSL:
|
15
|
+
#
|
16
|
+
# class ArticleCollection
|
17
|
+
# include Collection
|
18
|
+
#
|
19
|
+
# item_class Article # 1)
|
20
|
+
# item_key :title # 2)
|
21
|
+
# load_collection do |*args| # 3)
|
22
|
+
# args.pop.map { |title| item_class.new(title) }
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# As you can see, you include the module and specify, which class should be collection items wrapped in [1],
|
27
|
+
# what is the main attribute for a collection item [2], and how you would like to load the collection [3].
|
28
|
+
#
|
29
|
+
# Note, that you can also override the corresponding methods directly (see tests below).
|
30
|
+
#
|
31
|
+
# This allows to do following operations with the collection:
|
32
|
+
#
|
33
|
+
# articles = ArticleCollection.new ['one', 'two']
|
34
|
+
#
|
35
|
+
# puts "\n~~~ The collection..."
|
36
|
+
# p articles
|
37
|
+
#
|
38
|
+
# puts "\n~~~ Last item..."
|
39
|
+
# p articles.last
|
40
|
+
#
|
41
|
+
# puts "\n~~~ Add 'three' and 'four'..."
|
42
|
+
# articles << 'three'
|
43
|
+
# articles.add 'four'
|
44
|
+
#
|
45
|
+
# p articles
|
46
|
+
#
|
47
|
+
# puts "\n~~~ Deleting 'three' and 'four'..."
|
48
|
+
# articles >> 'three'
|
49
|
+
# articles.delete Article.new('four')
|
50
|
+
#
|
51
|
+
# p articles
|
52
|
+
#
|
53
|
+
# puts "\n~~~ Iteration..."
|
54
|
+
# articles.each_with_index do |a, i|
|
55
|
+
# puts "#{i+1}. #{a.title}"
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# puts "\n~~~ Mapping..."
|
59
|
+
# p articles.map { |a| a.title }
|
60
|
+
#
|
61
|
+
# puts "\n~~~ Accessors..."
|
62
|
+
# p articles['one']
|
63
|
+
# p articles.find 'two'
|
64
|
+
#
|
65
|
+
# puts "\n~~~ Size..."
|
66
|
+
# p articles.size
|
67
|
+
#
|
68
|
+
# You may want to customize adding/removing items to the collection, for example.
|
69
|
+
#
|
70
|
+
# That's easy: just re-implement the `<<` or `>>` methods with custom logic, and call `super()`:
|
71
|
+
#
|
72
|
+
# def << item
|
73
|
+
# return false unless Tag.new(:article => @article, :value => item).valid?
|
74
|
+
#
|
75
|
+
# @article.tags = super(:article => @article, :value => item)
|
76
|
+
# self
|
77
|
+
# end
|
78
|
+
# alias :add :<<
|
79
|
+
#
|
80
|
+
# -----------------------------------
|
81
|
+
# (c) 2001 Karel Minarik; MIT License
|
82
|
+
#
|
83
|
+
module Collection
|
84
|
+
include Enumerable
|
85
|
+
|
86
|
+
def self.included(base)
|
87
|
+
base.extend DSL
|
88
|
+
base.class_eval do
|
89
|
+
def self.method_added(name)
|
90
|
+
case name
|
91
|
+
when :<< then alias_method :add, name
|
92
|
+
when :>> then alias_method :delete, name
|
93
|
+
when :[] then alias_method :find, name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
module DSL
|
100
|
+
|
101
|
+
def item_class klass=nil
|
102
|
+
klass ? @item_class = klass : @item_class
|
103
|
+
end
|
104
|
+
|
105
|
+
def item_key key=nil
|
106
|
+
key ? @item_key = key : @item_key
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_collection &block
|
110
|
+
block_given? ? @load_collection = block : @load_collection
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
def initialize(*args)
|
116
|
+
if self.class.instance_variable_defined?(:@load_collection)
|
117
|
+
@collection = load_collection.call(*args)
|
118
|
+
else
|
119
|
+
@collection = load_collection(*args)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def << item
|
124
|
+
item = item_class.new(item) unless item.is_a? item_class
|
125
|
+
@collection << item
|
126
|
+
self
|
127
|
+
end
|
128
|
+
alias :add :<<
|
129
|
+
|
130
|
+
def >> item
|
131
|
+
item = item.send(item_key) if item.respond_to? item_key
|
132
|
+
@collection.reject! { |a| a.send(item_key) == item }
|
133
|
+
self
|
134
|
+
end
|
135
|
+
alias :delete :>>
|
136
|
+
|
137
|
+
def [] key
|
138
|
+
@collection.select { |a| a.send(item_key) == key }.first
|
139
|
+
end
|
140
|
+
alias :find :[]
|
141
|
+
|
142
|
+
def last
|
143
|
+
@collection.reverse.first
|
144
|
+
end
|
145
|
+
|
146
|
+
def <=> other
|
147
|
+
self <=> other
|
148
|
+
end
|
149
|
+
|
150
|
+
def each(&block)
|
151
|
+
@collection.each(&block)
|
152
|
+
end
|
153
|
+
|
154
|
+
def include? value
|
155
|
+
@collection.any? { |i| i.send(item_key) == value }
|
156
|
+
end
|
157
|
+
|
158
|
+
def empty?
|
159
|
+
@collection.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_a
|
163
|
+
@collection.map { |i| i.send(item_key) }
|
164
|
+
end
|
165
|
+
|
166
|
+
def size
|
167
|
+
@collection.size
|
168
|
+
end
|
169
|
+
|
170
|
+
def inspect
|
171
|
+
%Q|<#{self.class.name} #{@collection.inspect}>|
|
172
|
+
end
|
173
|
+
|
174
|
+
def load_collection
|
175
|
+
self.class.load_collection ||
|
176
|
+
raise(NoMethodError, "Please implement 'load_collection' method in your collection class")
|
177
|
+
end
|
178
|
+
|
179
|
+
def item_class
|
180
|
+
self.class.item_class ||
|
181
|
+
raise(NoMethodError, "Please implement 'item_class' method in your collection class")
|
182
|
+
end
|
183
|
+
|
184
|
+
def item_key
|
185
|
+
self.class.item_key ||
|
186
|
+
raise(NoMethodError, "Please implement 'item_key' method in your collection class")
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
if $0 == __FILE__
|
193
|
+
require 'rubygems'
|
194
|
+
require 'test/unit'
|
195
|
+
require 'shoulda'
|
196
|
+
require 'mocha'
|
197
|
+
|
198
|
+
class CollectionTest < Test::Unit::TestCase
|
199
|
+
|
200
|
+
context "Collection module" do
|
201
|
+
|
202
|
+
setup { class MyCollection; include Collection; end }
|
203
|
+
|
204
|
+
should "have abstract methods" do
|
205
|
+
assert_raise(NoMethodError) do
|
206
|
+
MyCollection.new.load_collection
|
207
|
+
MyCollection.new.item_class
|
208
|
+
MyCollection.new.item_key
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
should "pass arguments from initialize to load_collection" do
|
213
|
+
list = ['one', 'two']
|
214
|
+
MyCollection.any_instance.expects(:load_collection).with( list ).returns( list )
|
215
|
+
|
216
|
+
MyCollection.new list
|
217
|
+
end
|
218
|
+
|
219
|
+
should "be iterable" do
|
220
|
+
MyCollection.any_instance.stubs(:load_collection).returns( [] )
|
221
|
+
|
222
|
+
assert_respond_to MyCollection.new, :each
|
223
|
+
assert_respond_to MyCollection.new, :size
|
224
|
+
assert_respond_to MyCollection.new, :empty?
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
context "Collection module included" do
|
230
|
+
|
231
|
+
setup do
|
232
|
+
class Article
|
233
|
+
attr_reader :title
|
234
|
+
def initialize(title); @title = title; end
|
235
|
+
end
|
236
|
+
|
237
|
+
class ArticleCollection
|
238
|
+
include Collection
|
239
|
+
|
240
|
+
item_class Article
|
241
|
+
item_key :title
|
242
|
+
load_collection do |*args|
|
243
|
+
args.pop.map { |title| item_class.new(title) }
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
@articles = ArticleCollection.new ['One', 'Two']
|
248
|
+
end
|
249
|
+
|
250
|
+
should "set item_class" do
|
251
|
+
assert_equal Article, @articles.item_class
|
252
|
+
end
|
253
|
+
|
254
|
+
should "set item_key" do
|
255
|
+
assert_equal :title, @articles.item_key
|
256
|
+
end
|
257
|
+
|
258
|
+
should "load the collection" do
|
259
|
+
assert_equal 2, @articles.size
|
260
|
+
assert_same_elements ['One', 'Two'], @articles.to_a
|
261
|
+
end
|
262
|
+
|
263
|
+
should "walk like an Enumerable" do
|
264
|
+
assert_same_elements ['One', 'Two'], @articles.map { |a| a.title }
|
265
|
+
end
|
266
|
+
|
267
|
+
should "answer to empty?" do
|
268
|
+
assert ! @articles.empty?
|
269
|
+
end
|
270
|
+
|
271
|
+
should "return size" do
|
272
|
+
assert_equal 2, @articles.size
|
273
|
+
end
|
274
|
+
|
275
|
+
should "return first" do
|
276
|
+
assert_equal 'One', @articles.first.title
|
277
|
+
end
|
278
|
+
|
279
|
+
should "return last" do
|
280
|
+
assert_equal 'Two', @articles.last.title
|
281
|
+
end
|
282
|
+
|
283
|
+
should "add item by key" do
|
284
|
+
assert @articles << 'Three'
|
285
|
+
assert_equal 3, @articles.size
|
286
|
+
end
|
287
|
+
|
288
|
+
should "add item instance" do
|
289
|
+
assert @articles << Article.new('Three')
|
290
|
+
assert_equal 3, @articles.size
|
291
|
+
assert_equal 'Three', @articles.last.title
|
292
|
+
end
|
293
|
+
|
294
|
+
should "remove item by key" do
|
295
|
+
assert @articles >> 'Two'
|
296
|
+
assert_equal 1, @articles.size
|
297
|
+
end
|
298
|
+
|
299
|
+
should "remove item instance" do
|
300
|
+
assert @articles >> Article.new('Two')
|
301
|
+
assert_equal 1, @articles.size
|
302
|
+
assert_equal 'One', @articles.last.title
|
303
|
+
end
|
304
|
+
|
305
|
+
should "get item by key" do
|
306
|
+
assert_not_nil @articles['One']
|
307
|
+
assert_equal 'One', @articles['One'].title
|
308
|
+
end
|
309
|
+
|
310
|
+
should "query for item by key" do
|
311
|
+
assert @articles.include?('One'), "#{@articles.inspect} should contain 'One'"
|
312
|
+
assert ! @articles.include?('FourtyTwo'), "#{@articles.inspect} should NOT contain 'FourtyTwo'"
|
313
|
+
end
|
314
|
+
|
315
|
+
should "serialize collection to an Array, by key" do
|
316
|
+
assert_same_elements ['One', 'Two'], @articles.to_a
|
317
|
+
end
|
318
|
+
|
319
|
+
should "have aliases" do
|
320
|
+
assert_respond_to @articles, :add
|
321
|
+
assert_respond_to @articles, :delete
|
322
|
+
assert_respond_to @articles, :find
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
326
|
+
|
327
|
+
context "Collection module used without DSL" do
|
328
|
+
|
329
|
+
setup do
|
330
|
+
class Article
|
331
|
+
attr_reader :title
|
332
|
+
def initialize(title); @title = title; end
|
333
|
+
end
|
334
|
+
|
335
|
+
class NoDSLArticleCollection
|
336
|
+
include Collection
|
337
|
+
|
338
|
+
def load_collection(*args); args.pop.map { |title| item_class.new(title) }; end
|
339
|
+
def item_class; Article; end
|
340
|
+
def item_key; :title; end
|
341
|
+
end
|
342
|
+
|
343
|
+
@articles = NoDSLArticleCollection.new ['One', 'Two']
|
344
|
+
end
|
345
|
+
|
346
|
+
should "set item_class" do
|
347
|
+
assert_equal Article, @articles.item_class
|
348
|
+
end
|
349
|
+
|
350
|
+
should "set item_key" do
|
351
|
+
assert_equal :title, @articles.item_key
|
352
|
+
end
|
353
|
+
|
354
|
+
should "load the collection" do
|
355
|
+
assert_equal 2, @articles.size
|
356
|
+
assert_same_elements ['One', 'Two'], @articles.to_a
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
context "Collection with customized manipulation methods" do
|
362
|
+
|
363
|
+
setup do
|
364
|
+
class Article
|
365
|
+
attr_reader :title
|
366
|
+
def initialize(title); @title = title; end
|
367
|
+
end
|
368
|
+
|
369
|
+
class ArticleCollection
|
370
|
+
include Collection
|
371
|
+
|
372
|
+
item_class Article
|
373
|
+
item_key :title
|
374
|
+
load_collection do |*args|
|
375
|
+
args.pop.map { |title| item_class.new(title) }
|
376
|
+
end
|
377
|
+
|
378
|
+
def << item
|
379
|
+
return false if item == 'foo'
|
380
|
+
super
|
381
|
+
end
|
382
|
+
|
383
|
+
def >> item
|
384
|
+
raise "Foorbidden!" if item == 'foo'
|
385
|
+
super
|
386
|
+
end
|
387
|
+
|
388
|
+
def [] item
|
389
|
+
return nil if item == 'One'
|
390
|
+
super
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
@articles = ArticleCollection.new ['One', 'Two']
|
396
|
+
end
|
397
|
+
|
398
|
+
should "return false when adding adding 'foo'" do
|
399
|
+
assert ! (@articles << 'foo')
|
400
|
+
assert_equal 2, @articles.size
|
401
|
+
end
|
402
|
+
|
403
|
+
should "raise exception when trying to remove 'foo'" do
|
404
|
+
assert_raise(RuntimeError) do
|
405
|
+
@articles >> 'foo'
|
406
|
+
assert_equal 2, @articles.size
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
should "have alias for add" do
|
411
|
+
assert ! @articles.add('foo')
|
412
|
+
assert_equal 2, @articles.size
|
413
|
+
end
|
414
|
+
|
415
|
+
should "have alias for delete" do
|
416
|
+
assert_raise(RuntimeError) do
|
417
|
+
@articles.delete('foo')
|
418
|
+
assert_equal 2, @articles.size
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
should "have alias for find" do
|
423
|
+
assert_nil @articles.find('One')
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|
429
|
+
end
|