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