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