adapi 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/GUIDELINES.markdown +61 -0
- data/README.markdown +79 -45
- data/Rakefile +31 -3
- data/adapi.gemspec +10 -3
- data/examples/add_ad_group.rb +1 -1
- data/examples/add_bare_ad_group.rb +1 -4
- data/examples/add_campaign.rb +1 -0
- data/examples/add_campaign_criteria.rb +27 -0
- data/examples/add_invalid_ad_group.rb +3 -1
- data/examples/add_invalid_text_ad.rb +1 -1
- data/examples/add_keywords.rb +1 -1
- data/examples/add_negative_campaign_criteria.rb +23 -0
- data/examples/add_text_ad.rb +1 -1
- data/examples/customize_configuration.rb +1 -2
- data/examples/delete_keyword.rb +1 -1
- data/examples/find_campaign.rb +5 -5
- data/examples/find_campaign_ad_groups.rb +1 -1
- data/examples/find_campaign_criteria.rb +103 -0
- data/examples/find_locations.rb +21 -0
- data/examples/rollback_campaign.rb +7 -6
- data/examples/update_campaign.rb +1 -1
- data/examples/update_campaign_status.rb +1 -1
- data/lib/adapi.rb +11 -9
- data/lib/adapi/ad/text_ad.rb +2 -1
- data/lib/adapi/ad_group.rb +13 -9
- data/lib/adapi/ad_param.rb +89 -0
- data/lib/adapi/api.rb +8 -0
- data/lib/adapi/campaign.rb +27 -18
- data/lib/adapi/campaign_criterion.rb +278 -0
- data/lib/adapi/campaign_target.rb +5 -123
- data/lib/adapi/config.rb +36 -31
- data/lib/adapi/constant_data.rb +13 -0
- data/lib/adapi/constant_data/language.rb +45 -0
- data/lib/adapi/keyword.rb +15 -5
- data/lib/adapi/location.rb +91 -0
- data/lib/adapi/version.rb +8 -1
- data/lib/httpi_request_monkeypatch.rb +4 -0
- data/test/config/adapi.yml.template +21 -0
- data/test/config/adwords_api.yml.template +10 -0
- data/test/integration/create_campaign_test.rb +54 -0
- data/test/test_helper.rb +2 -3
- data/test/unit/ad_group_test.rb +3 -4
- data/test/unit/ad_test.rb +1 -1
- data/test/unit/campaign_criterion_test.rb +23 -0
- data/test/unit/config_test.rb +52 -0
- metadata +48 -35
- data/examples/add_campaign_targets.rb +0 -26
- data/test/unit/campaign_target_test.rb +0 -51
data/lib/adapi/config.rb
CHANGED
@@ -1,15 +1,27 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
#
|
4
|
-
|
3
|
+
# This class hold configuration data for AdWords API
|
4
|
+
|
5
|
+
# TODO enable this way of using configuration
|
6
|
+
# Adapi::Campaign.create(:data => campaign_data, :account => :my_account_alias)
|
5
7
|
|
6
8
|
module Adapi
|
7
9
|
class Config
|
10
|
+
class << self
|
11
|
+
attr_accessor :dir, :filename
|
12
|
+
end
|
13
|
+
|
14
|
+
self.dir = ENV['HOME']
|
15
|
+
self.filename = 'adapi.yml'
|
8
16
|
|
9
17
|
# display hash of all account settings
|
10
18
|
#
|
11
|
-
def self.settings
|
12
|
-
|
19
|
+
def self.settings(reload = false)
|
20
|
+
if reload
|
21
|
+
@settings = self.load_settings
|
22
|
+
else
|
23
|
+
@settings ||= self.load_settings
|
24
|
+
end
|
13
25
|
end
|
14
26
|
|
15
27
|
# display actual account settings
|
@@ -26,44 +38,37 @@ module Adapi
|
|
26
38
|
custom_settings = @settings[account_alias.to_sym]
|
27
39
|
custom_settings[:authentication] = custom_settings[:authentication].merge(authentication_params)
|
28
40
|
@data = custom_settings
|
29
|
-
|
30
|
-
=begin original method, to be merged into the new one
|
31
|
-
# hash of params - default
|
32
|
-
if params.is_a?(Hash)
|
33
|
-
@data = params
|
34
|
-
# set alias from adapi.yml
|
35
|
-
elsif params.is_a?(Symbol)
|
36
|
-
@data = @settings[params]
|
37
|
-
end
|
38
|
-
=end
|
39
41
|
end
|
40
42
|
|
43
|
+
# Loads adapi configuration from given hash or from external configuration
|
44
|
+
# file
|
45
|
+
#
|
41
46
|
# params:
|
42
|
-
# *
|
43
|
-
# * filename
|
44
|
-
#
|
47
|
+
# * dir (default: HOME)
|
48
|
+
# * filename (default: adapi.yml)
|
49
|
+
# * in_hash - hash to use instead of external configuration
|
50
|
+
#
|
45
51
|
def self.load_settings(params = {})
|
46
|
-
params[:path] ||= ENV['HOME']
|
47
|
-
params[:filename] ||= 'adapi.yml'
|
48
|
-
params[:in_hash] ||= nil
|
49
|
-
|
50
|
-
# HOTFIX enable load by hash
|
51
52
|
if params[:in_hash]
|
52
|
-
@settings = params[:in_hash]
|
53
|
-
return @settings
|
53
|
+
return @settings = params[:in_hash]
|
54
54
|
end
|
55
55
|
|
56
|
-
|
57
|
-
|
56
|
+
# load external config file (defaults to ~/adapi.yml)
|
57
|
+
self.dir = params[:dir] if params[:dir]
|
58
|
+
self.filename = params[:filename] if params[:filename]
|
59
|
+
path = (self.dir.present? ? File.join(self.dir, self.filename) : self.filename)
|
58
60
|
|
59
|
-
if File.exists?(
|
60
|
-
@settings = YAML::load(File.read(
|
61
|
-
|
62
|
-
|
61
|
+
if File.exists?(path)
|
62
|
+
@settings = YAML::load(File.read(path)) rescue {}
|
63
|
+
@settings.symbolize_keys!
|
64
|
+
|
65
|
+
# is it an adwords_api config-file?
|
66
|
+
if @settings.present? && @settings[:authentication].present?
|
67
|
+
@settings = {:default => @settings}
|
68
|
+
end
|
63
69
|
end
|
64
70
|
|
65
71
|
@settings
|
66
72
|
end
|
67
|
-
|
68
73
|
end
|
69
74
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Adapi
|
4
|
+
class ConstantData::Language < ConstantData
|
5
|
+
|
6
|
+
LANGUAGE_IDS = { :en => 1000, :de => 1001, :fr => 1002, :es => 1003,
|
7
|
+
:it => 1004, :ja => 1005, :da => 1009, :nl => 1010, :fi => 1011, :ko => 1012,
|
8
|
+
:no => 1013, :pt => 1014, :sv => 1015, :zh_CN => 1017, :zh_TW => 1018,
|
9
|
+
:ar => 1019, :bg => 1020, :cs => 1021, :el => 1022, :hi => 1023, :hu => 1024,
|
10
|
+
:id => 1025, :is => 1026, :iw => 1027, :lv => 1028, :lt => 1029, :pl => 1030,
|
11
|
+
:ru => 1031, :ro => 1032, :sk => 1033, :sl => 1034, :sr => 1035, :uk => 1036,
|
12
|
+
:tr => 1037, :ca => 1038, :hr => 1039, :vi => 1040, :ur => 1041, :tl => 1042,
|
13
|
+
:et => 1043, :th => 1044
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_accessor :id, :code
|
17
|
+
|
18
|
+
def initialize(params = {})
|
19
|
+
@id = params[:id]
|
20
|
+
@code = params[:code]
|
21
|
+
|
22
|
+
super(params)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns AdWords API language id based for language code
|
26
|
+
#
|
27
|
+
def self.find(code)
|
28
|
+
|
29
|
+
# let's also allow searching by id
|
30
|
+
if code.is_a?(Integer)
|
31
|
+
Language.new(
|
32
|
+
:id => code,
|
33
|
+
:code => LANGUAGE_IDS.find { |k,v| v == code }.first
|
34
|
+
)
|
35
|
+
|
36
|
+
else
|
37
|
+
Language.new(
|
38
|
+
:id => LANGUAGE_IDS[code.to_sym.downcase],
|
39
|
+
:code => code.to_sym.downcase
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
data/lib/adapi/keyword.rb
CHANGED
@@ -65,6 +65,7 @@ module Adapi
|
|
65
65
|
{ :text => keyword, :match_type => match_type, :negative => negative }
|
66
66
|
end
|
67
67
|
|
68
|
+
|
68
69
|
def create
|
69
70
|
operations = @keywords.map do |keyword|
|
70
71
|
{
|
@@ -100,8 +101,13 @@ module Adapi
|
|
100
101
|
# we need ad_group_id
|
101
102
|
raise ArgumentError, "AdGroup ID is required" unless params[:ad_group_id]
|
102
103
|
|
103
|
-
#
|
104
|
-
predicates = [
|
104
|
+
# basic predicates for searching keywords - both are required
|
105
|
+
predicates = [
|
106
|
+
{ :field => 'CriteriaType', :operator => 'EQUALS', :values => [ 'KEYWORD' ] },
|
107
|
+
{ :field => 'AdGroupId', :operator => 'EQUALS', :values => [ params[:ad_group_id] ] }
|
108
|
+
]
|
109
|
+
# supported parameters: id
|
110
|
+
predicates += [ :id ].map do |param_name|
|
105
111
|
if params[param_name]
|
106
112
|
{:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
|
107
113
|
end
|
@@ -109,9 +115,13 @@ module Adapi
|
|
109
115
|
|
110
116
|
# Get all the criteria for this ad group.
|
111
117
|
selector = {
|
112
|
-
:fields => ['Id', '
|
118
|
+
:fields => ['Id', 'CriteriaType', 'KeywordText'],
|
113
119
|
:ordering => [{ :field => 'AdGroupId', :sort_order => 'ASCENDING' }],
|
114
|
-
:predicates => predicates
|
120
|
+
:predicates => predicates,
|
121
|
+
:paging => {
|
122
|
+
:start_index => 0,
|
123
|
+
:number_results => 10
|
124
|
+
}
|
115
125
|
}
|
116
126
|
|
117
127
|
response = Keyword.new.service.get(selector)
|
@@ -173,4 +183,4 @@ module Adapi
|
|
173
183
|
end
|
174
184
|
|
175
185
|
end
|
176
|
-
end
|
186
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# TODO map results of find to location object
|
4
|
+
|
5
|
+
module Adapi
|
6
|
+
class Location < Api
|
7
|
+
|
8
|
+
# display types. region was formerly called province, and province is still
|
9
|
+
# supported as parameter, a synonym for region
|
10
|
+
LOCATIONS_HIERARCHY = [ :city, :region, :country ]
|
11
|
+
|
12
|
+
def initialize(params = {})
|
13
|
+
params[:service_name] = :LocationCriterionService
|
14
|
+
|
15
|
+
@xsi_type = 'LocationCriterion'
|
16
|
+
|
17
|
+
super(params)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Example:
|
21
|
+
# Location.find(:city => 'Prague')
|
22
|
+
# Location.find(:country => 'CZ', :city => 'Prague')
|
23
|
+
# Location.find(:country => 'CZ', :region => 'Prague' :city => 'Prague')
|
24
|
+
#
|
25
|
+
# TODO add legacy aliases: :city_name, :province_code, :country_code
|
26
|
+
#
|
27
|
+
def self.find(amount = :all, params = {})
|
28
|
+
# set amount = :first by default
|
29
|
+
if amount.is_a?(Hash) and params.empty?
|
30
|
+
params = amount.clone
|
31
|
+
amount = :first
|
32
|
+
end
|
33
|
+
|
34
|
+
params.symbolize_keys!
|
35
|
+
first_only = (amount.to_sym == :first)
|
36
|
+
# in which language to retrieve locations
|
37
|
+
params[:locale] ||= 'en'
|
38
|
+
# support for legacy parameter
|
39
|
+
if params[:province] and not params[:region]
|
40
|
+
params[:region] = params[:province]
|
41
|
+
end
|
42
|
+
|
43
|
+
# determine by what criteria to search
|
44
|
+
location_type, location_name = nil, nil
|
45
|
+
LOCATIONS_HIERARCHY.each do |param_name|
|
46
|
+
if params[param_name]
|
47
|
+
# FIXME use correct helper instead of humanize HOTFIX
|
48
|
+
location_type, location_name = param_name.to_s.humanize, params[param_name]
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
raise "Invalid params" unless location_name
|
54
|
+
|
55
|
+
selector = {
|
56
|
+
:fields => ['Id', 'LocationName', 'CanonicalName', 'DisplayType', 'ParentLocations', 'Reach'],
|
57
|
+
:predicates => [
|
58
|
+
# PS: for searching more locations at once, switch to IN operator
|
59
|
+
# values array for EQUALS can contain only one value (sic!)
|
60
|
+
{ :field => 'LocationName', :operator => 'EQUALS', :values => [ location_name ] },
|
61
|
+
{ :field => 'Locale', :operator => 'EQUALS', :values => [ params[:locale] ] }
|
62
|
+
]
|
63
|
+
}
|
64
|
+
|
65
|
+
# returns array of locations. and now the fun begins
|
66
|
+
locations = Location.new.service.get(selector)
|
67
|
+
|
68
|
+
# now we have to find location with correct display_type and TODO hierarchy
|
69
|
+
# problematic example: Prague is both city and province (region)
|
70
|
+
|
71
|
+
location = nil
|
72
|
+
locations.each do |entry|
|
73
|
+
if entry[:location][:display_type] == location_type
|
74
|
+
location = entry[:location]
|
75
|
+
break
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# FIXME for now, omit hierarchy as check just display type. results will be in 99,5% the same
|
80
|
+
|
81
|
+
location
|
82
|
+
end
|
83
|
+
|
84
|
+
# Displays location tree - location with its parents
|
85
|
+
#
|
86
|
+
def self.location_tree(location = {})
|
87
|
+
"TODO"
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
data/lib/adapi/version.rb
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Adapi
|
4
|
-
VERSION = "0.0.
|
4
|
+
VERSION = "0.0.5"
|
5
5
|
|
6
6
|
# CHANGELOG:
|
7
7
|
#
|
8
|
+
# 0.0.5
|
9
|
+
# converted to AdWords API version v201109
|
10
|
+
# moved from CampaignTarget to CampaignCriterion
|
11
|
+
# implemented Location and Language finders
|
12
|
+
# started writing integration tests
|
13
|
+
# added logging of SOAP requests
|
14
|
+
#
|
8
15
|
# 0.0.4
|
9
16
|
# changed README to markdown format
|
10
17
|
# updated DSL for creating campaign and campaign target
|
@@ -29,6 +29,10 @@ module HTTPI
|
|
29
29
|
setup_http_auth request if request.auth.http?
|
30
30
|
# setup_ssl_auth request.auth.ssl if request.auth.ssl?
|
31
31
|
# setup_ntlm_auth request if request.auth.ntlm?
|
32
|
+
|
33
|
+
# HOTFIX for bug in curb 0.7.16, see issue:
|
34
|
+
# https://github.com/taf2/curb/issues/96
|
35
|
+
client.resolve_mode = :ipv4
|
32
36
|
end
|
33
37
|
|
34
38
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
:default:
|
2
|
+
:authentication:
|
3
|
+
:method: ClientLogin
|
4
|
+
:email: adapi_yml@example.com
|
5
|
+
:password: default_password
|
6
|
+
:developer_token: default_token
|
7
|
+
:client_customer_id: 555-666-7777
|
8
|
+
:user_agent: My Adwords API Client
|
9
|
+
:service:
|
10
|
+
:environment: PRODUCTION
|
11
|
+
|
12
|
+
:sandbox:
|
13
|
+
:authentication:
|
14
|
+
:method: ClientLogin
|
15
|
+
:email: sandbox_email@example.com
|
16
|
+
:password: sandbox_password
|
17
|
+
:developer_token: sandbox_token
|
18
|
+
:client_customer_id: 555-666-7777
|
19
|
+
:user_agent: Adwords API Test
|
20
|
+
:service:
|
21
|
+
:environment: SANDBOX
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Adapi
|
6
|
+
class CreateCampaignTest < Test::Unit::TestCase
|
7
|
+
context "non-existent Campaign" do
|
8
|
+
should "not be found" do
|
9
|
+
# FIXME randomly generated id, but it might actually exist
|
10
|
+
assert_nil Adapi::Campaign.find(Time.new.to_i)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "existing Campaign" do
|
15
|
+
setup do
|
16
|
+
@campaign_data = {
|
17
|
+
:name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
|
18
|
+
:status => 'PAUSED',
|
19
|
+
:bidding_strategy => { :xsi_type => 'BudgetOptimizer', :bid_ceiling => 55 },
|
20
|
+
:budget => 50,
|
21
|
+
:network_setting => {
|
22
|
+
:target_google_search => true,
|
23
|
+
:target_search_network => true,
|
24
|
+
:target_content_network => false,
|
25
|
+
:target_content_contextual => false
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
c = Adapi::Campaign.create(@campaign_data)
|
30
|
+
|
31
|
+
@campaign = Adapi::Campaign.find(c.id)
|
32
|
+
end
|
33
|
+
|
34
|
+
# this basically tests creating bare campaign
|
35
|
+
should "be found" do
|
36
|
+
assert_not_nil @campaign
|
37
|
+
|
38
|
+
assert_equal @campaign_data[:status], @campaign.status
|
39
|
+
assert_equal @campaign_data[:name], @campaign.name
|
40
|
+
end
|
41
|
+
|
42
|
+
should "still be found after deletion" do
|
43
|
+
@campaign.delete
|
44
|
+
|
45
|
+
deleted_campaign = Adapi::Campaign.find(@campaign.id)
|
46
|
+
|
47
|
+
assert_equal "DELETED", deleted_campaign.status
|
48
|
+
assert_equal @campaign_data[:name], deleted_campaign.name
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# FIXME for some reason, tests don't return errors, only quietly fail
|
4
|
+
|
3
5
|
require 'rubygems'
|
4
6
|
gem 'minitest'
|
5
7
|
require 'test/unit'
|
@@ -16,8 +18,5 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'adapi')
|
|
16
18
|
Dir[ File.join(File.dirname(__FILE__), 'factories', '*.rb') ].each { |f| require f }
|
17
19
|
|
18
20
|
class Test::Unit::TestCase
|
19
|
-
|
20
21
|
FakeWeb.allow_net_connect = false
|
21
|
-
|
22
|
-
|
23
22
|
end
|
data/test/unit/ad_group_test.rb
CHANGED
@@ -4,7 +4,7 @@ require 'test_helper'
|
|
4
4
|
|
5
5
|
module Adapi
|
6
6
|
class AdGroupTest < Test::Unit::TestCase
|
7
|
-
include ActiveModel::Lint::Tests
|
7
|
+
# include ActiveModel::Lint::Tests
|
8
8
|
|
9
9
|
def setup
|
10
10
|
@model = AdGroup.new
|
@@ -20,13 +20,12 @@ module Adapi
|
|
20
20
|
end
|
21
21
|
|
22
22
|
should "parse :bids correctly" do
|
23
|
-
|
24
|
-
ag = AdGroup.new( :bids => { :xsi_type => 'ManualCPCAdGroupBids', :keyword_max_cpc => 10 } )
|
23
|
+
ag = AdGroup.new( :bids => { :xsi_type => 'ManualCPCAdGroupBids', :proxy_keyword_max_cpc => 10 } )
|
25
24
|
|
26
25
|
assert_equal ag.bids,
|
27
26
|
{
|
28
27
|
:xsi_type => 'ManualCPCAdGroupBids',
|
29
|
-
:
|
28
|
+
:proxy_keyword_max_cpc => { :amount => { :micro_amount => 10000000 } }
|
30
29
|
}
|
31
30
|
end
|
32
31
|
|