adapi 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +74 -39
- data/adapi.gemspec +4 -0
- data/examples/add_ad_group.rb +6 -22
- data/examples/add_bare_ad_group.rb +11 -6
- data/examples/add_bare_campaign.rb +5 -7
- data/examples/add_campaign.rb +12 -29
- data/examples/add_campaign_targets.rb +16 -7
- data/examples/add_invalid_ad_group.rb +35 -0
- data/examples/add_invalid_text_ad.rb +30 -0
- data/examples/add_keywords.rb +14 -0
- data/examples/add_text_ad.rb +23 -0
- data/examples/log_to_specific_account.rb +26 -13
- data/examples/rollback_campaign.rb +56 -0
- data/examples/update_campaign.rb +14 -15
- data/examples/update_campaign_status.rb +6 -6
- data/lib/adapi.rb +19 -1
- data/lib/adapi/ad.rb +55 -42
- data/lib/adapi/ad/text_ad.rb +126 -0
- data/lib/adapi/ad_group.rb +80 -42
- data/lib/adapi/ad_group_criterion.rb +13 -55
- data/lib/adapi/api.rb +107 -1
- data/lib/adapi/campaign.rb +144 -94
- data/lib/adapi/campaign_target.rb +93 -45
- data/lib/adapi/config.rb +16 -2
- data/lib/adapi/keyword.rb +109 -0
- data/lib/adapi/version.rb +12 -1
- data/lib/httpi_request_monkeypatch.rb +35 -0
- data/test/factories/ad_group_factory.rb +20 -0
- data/test/factories/ad_text_factory.rb +9 -0
- data/test/test_helper.rb +3 -3
- data/test/unit/ad/ad_text_test.rb +30 -0
- data/test/unit/ad_group_test.rb +34 -0
- data/test/unit/ad_test.rb +12 -0
- data/test/unit/campaign_target_test.rb +18 -3
- metadata +122 -109
- data/examples/add_ad.rb +0 -17
- data/examples/add_ad_group_criteria.rb +0 -20
- data/examples/update_campaign_name.rb +0 -14
@@ -4,63 +4,66 @@ module Adapi
|
|
4
4
|
#
|
5
5
|
class CampaignTarget < Api
|
6
6
|
|
7
|
+
attr_accessor :campaign_id, :targets
|
8
|
+
|
9
|
+
validates_presence_of :campaign_id
|
10
|
+
|
11
|
+
# TODO validate if targets are in correct format
|
12
|
+
|
13
|
+
def attributes
|
14
|
+
super.merge( 'campaign_id' => campaign_id, 'targets' => targets )
|
15
|
+
end
|
16
|
+
|
7
17
|
def initialize(params = {})
|
8
18
|
params[:service_name] = :CampaignTargetService
|
9
|
-
super(params)
|
10
|
-
end
|
11
19
|
|
12
|
-
|
13
|
-
#
|
14
|
-
def self.create(params = {})
|
15
|
-
campaign_target_service = CampaignTarget.new
|
20
|
+
@xsi_type = 'CampaignTarget'
|
16
21
|
|
17
|
-
|
18
|
-
|
22
|
+
%w{ campaign_id targets }.each do |param_name|
|
23
|
+
self.send "#{param_name}=", params[param_name.to_sym]
|
24
|
+
end
|
19
25
|
|
26
|
+
super(params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def set
|
20
30
|
# transform our own high-level target parameters to google low-level
|
21
31
|
# target parameters
|
22
32
|
operations = []
|
23
33
|
|
24
|
-
|
34
|
+
@targets.each_pair do |targetting_type, targetting_settings|
|
25
35
|
operations << { :operator => 'SET',
|
26
36
|
:operand => {
|
27
37
|
:xsi_type => "#{targetting_type.to_s.capitalize}TargetList",
|
28
|
-
:campaign_id => campaign_id,
|
29
|
-
:targets =>
|
38
|
+
:campaign_id => @campaign_id,
|
39
|
+
:targets => CampaignTarget::create_targets(targetting_type, targetting_settings)
|
30
40
|
}
|
31
41
|
}
|
32
42
|
end
|
33
43
|
|
34
|
-
response =
|
44
|
+
response = self.mutate(operations)
|
35
45
|
|
36
|
-
|
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
|
46
|
+
(response and response[:value]) ? true : false
|
43
47
|
end
|
44
48
|
|
45
|
-
|
46
|
-
|
49
|
+
alias :create :set
|
50
|
+
|
51
|
+
# FIXME doesn't display everything, check the issues in google-adwords-api
|
52
|
+
#
|
53
|
+
def self.find(amount = :all, params = {})
|
54
|
+
params.symbolize_keys!
|
55
|
+
params = params[:conditions] if params[:conditions]
|
56
|
+
first_only = (amount.to_sym == :first)
|
47
57
|
|
48
|
-
|
49
|
-
selector[:campaign_ids] = params[:campaign_ids] if params[:campaign_ids]
|
58
|
+
raise ArgumentError, "Campaing ID is required" unless params[:campaign_id]
|
50
59
|
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
60
|
+
selector = { :campaign_ids => [ params[:campaign_id].to_i ] }
|
61
|
+
|
62
|
+
response = CampaignTarget.new.service.get(selector)
|
62
63
|
|
63
|
-
|
64
|
+
response = (response and response[:entries]) ? response[:entries] : []
|
65
|
+
|
66
|
+
first_only ? response.first : response
|
64
67
|
end
|
65
68
|
|
66
69
|
# transform our own high-level target parameters to google low-level
|
@@ -71,20 +74,65 @@ module Adapi
|
|
71
74
|
target_data.map { |language| { :language_code => language } }
|
72
75
|
# example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
|
73
76
|
when :geo
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
target_data.map do |geo_type, geo_values|
|
78
|
+
case geo_type
|
79
|
+
when :proximity
|
80
|
+
radius_in_units, radius_units = parse_radius(geo_values[:radius])
|
81
|
+
long, lat = parse_geodata(geo_values[:geo_point])
|
82
|
+
|
83
|
+
{
|
84
|
+
:xsi_type => "#{geo_type.to_s.capitalize}Target",
|
85
|
+
:excluded => false,
|
86
|
+
:radius_in_units => radius_in_units,
|
87
|
+
:radius_distance_units => radius_units,
|
88
|
+
:geo_point => {
|
89
|
+
:longitude_in_micro_degrees => long,
|
90
|
+
:latitude_in_micro_degrees => lat
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
when :city
|
95
|
+
geo_values.merge(
|
96
|
+
:xsi_type => "#{geo_type.to_s.capitalize}Target",
|
97
|
+
:excluded => false
|
98
|
+
)
|
99
|
+
|
100
|
+
else # :country, :province
|
101
|
+
{
|
102
|
+
:xsi_type => "#{geo_type.to_s.capitalize}Target",
|
103
|
+
:excluded => false,
|
104
|
+
"#{geo_type}_code".to_sym => geo_values
|
105
|
+
}
|
82
106
|
end
|
83
107
|
end
|
84
|
-
|
85
|
-
else nil
|
108
|
+
else nil
|
86
109
|
end
|
87
110
|
end
|
88
111
|
|
112
|
+
def self.parse_radius(radius)
|
113
|
+
radius_in_units, radius_units = radius.split(' ', 2)
|
114
|
+
[
|
115
|
+
radius_in_units.to_i,
|
116
|
+
(radius_units == 'm') ? 'MILES' : 'KILOMETERS'
|
117
|
+
]
|
118
|
+
end
|
119
|
+
|
120
|
+
# parse longitude and lattitude from string in this format:
|
121
|
+
# "longitude,lattitude" to [int,int] in Google microdegrees
|
122
|
+
# for example: "38.89859,-77.035971" -> [38898590, -77035971]
|
123
|
+
#
|
124
|
+
def self.parse_geodata(long_lat)
|
125
|
+
long_lat.split(',', 2).map { |x| to_microdegrees(x) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# convert latitude or longitude data to microdegrees,
|
129
|
+
# a format with AdWords API accepts
|
130
|
+
#
|
131
|
+
# TODO alias :to_microdegrees :to_micro_units
|
132
|
+
#
|
133
|
+
def self.to_microdegrees(x)
|
134
|
+
Api.to_micro_units(x)
|
135
|
+
end
|
136
|
+
|
89
137
|
end
|
90
138
|
end
|
data/lib/adapi/config.rb
CHANGED
@@ -18,9 +18,15 @@ module Adapi
|
|
18
18
|
@data ||= self.settings[:default]
|
19
19
|
end
|
20
20
|
|
21
|
-
#
|
21
|
+
# account_alias - alias of an account set in settings
|
22
|
+
# authentication_params - ...which we want to override
|
22
23
|
#
|
23
|
-
def self.set(
|
24
|
+
def self.set(account_alias = :default, authentication_params = {})
|
25
|
+
custom_settings = @settings[account_alias.to_sym]
|
26
|
+
custom_settings[:authentication] = custom_settings[:authentication].merge(authentication_params)
|
27
|
+
@data = custom_settings
|
28
|
+
|
29
|
+
=begin original method, to be merged into the new one
|
24
30
|
# hash of params - default
|
25
31
|
if params.is_a?(Hash)
|
26
32
|
@data = params
|
@@ -28,6 +34,7 @@ module Adapi
|
|
28
34
|
elsif params.is_a?(Symbol)
|
29
35
|
@data = @settings[params]
|
30
36
|
end
|
37
|
+
=end
|
31
38
|
end
|
32
39
|
|
33
40
|
# params:
|
@@ -37,6 +44,13 @@ module Adapi
|
|
37
44
|
def self.load_settings(params = {})
|
38
45
|
params[:path] ||= ENV['HOME']
|
39
46
|
params[:filename] ||= 'adapi.yml'
|
47
|
+
params[:in_hash] ||= nil
|
48
|
+
|
49
|
+
# HOTFIX enable load by hash
|
50
|
+
if params[:in_hash]
|
51
|
+
@settings = params[:in_hash]
|
52
|
+
return @settings
|
53
|
+
end
|
40
54
|
|
41
55
|
adapi_path = File.join(params[:path], params[:filename])
|
42
56
|
adwords_api_path = File.join(ENV['HOME'], 'adwords_api.yml')
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Adapi
|
2
|
+
class Keyword < AdGroupCriterion
|
3
|
+
|
4
|
+
attr_accessor :keywords
|
5
|
+
|
6
|
+
def attributes
|
7
|
+
super.merge('keywords' => keywords)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(params = {})
|
11
|
+
params[:service_name] = :AdGroupCriterionService
|
12
|
+
|
13
|
+
@xsi_type = 'AdGroupCriterion'
|
14
|
+
|
15
|
+
%w{ keywords }.each do |param_name|
|
16
|
+
self.send "#{param_name}=", params[param_name.to_sym]
|
17
|
+
end
|
18
|
+
|
19
|
+
self.keywords ||= []
|
20
|
+
self.keywords.map! { |k| Keyword.keyword_attributes(k) }
|
21
|
+
|
22
|
+
super(params)
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO include formatting in create method
|
26
|
+
#
|
27
|
+
def self.keyword_attributes(keyword)
|
28
|
+
# detect match type
|
29
|
+
match_type = case keyword[0]
|
30
|
+
when '"'
|
31
|
+
keyword = keyword.slice(1, (keyword.size - 2))
|
32
|
+
'PHRASE'
|
33
|
+
when '['
|
34
|
+
keyword = keyword.slice(1, (keyword.size - 2))
|
35
|
+
'EXACT'
|
36
|
+
else
|
37
|
+
'BROAD'
|
38
|
+
end
|
39
|
+
|
40
|
+
# detect if keyword is negative
|
41
|
+
negative = if (keyword =~ /^\-/)
|
42
|
+
keyword.slice!(0, 1)
|
43
|
+
true
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
{ :text => keyword, :match_type => match_type, :negative => negative }
|
49
|
+
end
|
50
|
+
|
51
|
+
def create
|
52
|
+
operations = @keywords.map do |keyword|
|
53
|
+
{
|
54
|
+
:operator => 'ADD',
|
55
|
+
:operand => {
|
56
|
+
:xsi_type => (keyword[:negative] ? 'NegativeAdGroupCriterion' : 'BiddableAdGroupCriterion'),
|
57
|
+
:ad_group_id => @ad_group_id,
|
58
|
+
:criterion => {
|
59
|
+
:xsi_type => 'Keyword',
|
60
|
+
:text => keyword[:text],
|
61
|
+
:match_type => keyword[:match_type]
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
response = self.mutate(operations)
|
68
|
+
|
69
|
+
return false unless (response and response[:value])
|
70
|
+
|
71
|
+
self.keywords = response[:value].map { |keyword| keyword[:criterion] }
|
72
|
+
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.find(amount = :all, params = {})
|
77
|
+
params.symbolize_keys!
|
78
|
+
# this has no effect, it's here just to have the same interface everywhere
|
79
|
+
first_only = (amount.to_sym == :first)
|
80
|
+
|
81
|
+
# we need ad_group_id
|
82
|
+
raise ArgumentError, "AdGroup ID is required" unless params[:ad_group_id]
|
83
|
+
|
84
|
+
# supported condition parameters: ad_group_id and id
|
85
|
+
predicates = [ :ad_group_id ].map do |param_name|
|
86
|
+
if params[param_name]
|
87
|
+
{:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
|
88
|
+
end
|
89
|
+
end.compact
|
90
|
+
|
91
|
+
# Get all the criteria for this ad group.
|
92
|
+
selector = {
|
93
|
+
:fields => ['Id', 'Text'],
|
94
|
+
:ordering => [{ :field => 'AdGroupId', :sort_order => 'ASCENDING' }],
|
95
|
+
:predicates => predicates
|
96
|
+
}
|
97
|
+
|
98
|
+
response = Keyword.new.service.get(selector)
|
99
|
+
|
100
|
+
response = (response and response[:entries]) ? response[:entries] : []
|
101
|
+
|
102
|
+
Keyword.new(
|
103
|
+
:ad_group_id => params[:ad_group_id],
|
104
|
+
:keywords => response.map { |keyword| keyword[:criterion] }
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
data/lib/adapi/version.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
module Adapi
|
2
|
-
VERSION = "0.0.
|
2
|
+
VERSION = "0.0.3"
|
3
3
|
|
4
4
|
# CHANGELOG:
|
5
|
+
#
|
6
|
+
# 0.0.3
|
7
|
+
# converted to ActiveModel
|
8
|
+
# moved common functionality to Api class
|
9
|
+
# changed http client to curb and hotfix ssl authentication bug in HTTPI
|
10
|
+
# added basic error handling
|
11
|
+
# changed DSL for Campaign attributes
|
12
|
+
# changed Ad model to general Ad model and moved TextAd to separate model
|
13
|
+
# added support for more target types and changed DSL for CampaignTarget
|
14
|
+
# converted to Ruby 1.9.2 (should work in Ruby 1.8.7 as well)
|
15
|
+
#
|
5
16
|
# 0.0.2
|
6
17
|
# [FIX] switched google gem dependencies from edge to stable release
|
7
18
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
# manually hardcode timeouts for HTTPI to 5 minutes (300 seconds)
|
3
|
+
# HOTFIX there's no way how to do it properly through HTTPI
|
4
|
+
|
5
|
+
module HTTPI
|
6
|
+
class Request
|
7
|
+
def open_timeout
|
8
|
+
300
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_timeout
|
12
|
+
300
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# disable ssl authentication in curb
|
18
|
+
# HOTFIX for bug in HTTPI
|
19
|
+
|
20
|
+
module HTTPI
|
21
|
+
module Adapter
|
22
|
+
class Curb
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def setup_client(request)
|
27
|
+
basic_setup request
|
28
|
+
setup_http_auth request if request.auth.http?
|
29
|
+
# setup_ssl_auth request.auth.ssl if request.auth.ssl?
|
30
|
+
# setup_ntlm_auth request if request.auth.ntlm?
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
Factory.define :ad_group, :class => Adapi::AdGroup do |f|
|
3
|
+
f.sequence(:campaign_id) { |n| n }
|
4
|
+
f.name "AdGroup %d" % (Time.new.to_f * 1000).to_i
|
5
|
+
f.status 'ENABLED'
|
6
|
+
# f.bids {}
|
7
|
+
f.keywords [ 'dem codez', '"top coder"', '[-code]' ]
|
8
|
+
end
|
9
|
+
|
10
|
+
=begin
|
11
|
+
ad_group_data = {
|
12
|
+
:bids => {
|
13
|
+
:xsi_type => 'ManualCPCAdGroupBids',
|
14
|
+
:keyword_max_cpc => {
|
15
|
+
:amount => {
|
16
|
+
:micro_amount => 10000000
|
17
|
+
}
|
18
|
+
}
|
19
|
+
},
|
20
|
+
=end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
|
2
|
+
Factory.define :text_ad, :class => Adapi::Ad::TextAd do |f|
|
3
|
+
f.sequence(:ad_group_id) { |n| n }
|
4
|
+
f.headline 'Code like Neo'
|
5
|
+
f.description1 'Need mad coding skills?'
|
6
|
+
f.description2 'Check out my new blog!'
|
7
|
+
f.url 'http://www.demcodez.com'
|
8
|
+
f.display_url 'http://www.demcodez.com'
|
9
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -8,13 +8,13 @@ require 'factory_girl'
|
|
8
8
|
|
9
9
|
# always test the latest version of the gem
|
10
10
|
# TODO make it an option only through ENV variable switch
|
11
|
-
require 'lib
|
11
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'adapi')
|
12
12
|
|
13
13
|
# load factories
|
14
|
-
Dir[ File.join(File.dirname(__FILE__), 'factories
|
14
|
+
Dir[ File.join(File.dirname(__FILE__), 'factories', '*.rb') ].each { |f| require f }
|
15
15
|
|
16
16
|
class Test::Unit::TestCase
|
17
|
-
|
17
|
+
|
18
18
|
FakeWeb.allow_net_connect = false
|
19
19
|
|
20
20
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Adapi
|
4
|
+
class TextAdTest < Test::Unit::TestCase
|
5
|
+
include ActiveModel::Lint::Tests
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@model = Ad::TextAd.new
|
9
|
+
end
|
10
|
+
|
11
|
+
context "valid new TextAd instance" do
|
12
|
+
setup do
|
13
|
+
@text_ad = Factory.build(:text_ad)
|
14
|
+
end
|
15
|
+
|
16
|
+
should "be valid" do
|
17
|
+
assert @text_ad.valid?
|
18
|
+
end
|
19
|
+
|
20
|
+
context " / data method" do
|
21
|
+
should "return TextAd params in hash" do
|
22
|
+
assert @text_ad.data.is_a?(Hash)
|
23
|
+
assert_equal @text_ad.headline, @text_ad.data[:headline]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|