adapi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +3 -0
  2. data/GUIDELINES.markdown +61 -0
  3. data/README.markdown +79 -45
  4. data/Rakefile +31 -3
  5. data/adapi.gemspec +10 -3
  6. data/examples/add_ad_group.rb +1 -1
  7. data/examples/add_bare_ad_group.rb +1 -4
  8. data/examples/add_campaign.rb +1 -0
  9. data/examples/add_campaign_criteria.rb +27 -0
  10. data/examples/add_invalid_ad_group.rb +3 -1
  11. data/examples/add_invalid_text_ad.rb +1 -1
  12. data/examples/add_keywords.rb +1 -1
  13. data/examples/add_negative_campaign_criteria.rb +23 -0
  14. data/examples/add_text_ad.rb +1 -1
  15. data/examples/customize_configuration.rb +1 -2
  16. data/examples/delete_keyword.rb +1 -1
  17. data/examples/find_campaign.rb +5 -5
  18. data/examples/find_campaign_ad_groups.rb +1 -1
  19. data/examples/find_campaign_criteria.rb +103 -0
  20. data/examples/find_locations.rb +21 -0
  21. data/examples/rollback_campaign.rb +7 -6
  22. data/examples/update_campaign.rb +1 -1
  23. data/examples/update_campaign_status.rb +1 -1
  24. data/lib/adapi.rb +11 -9
  25. data/lib/adapi/ad/text_ad.rb +2 -1
  26. data/lib/adapi/ad_group.rb +13 -9
  27. data/lib/adapi/ad_param.rb +89 -0
  28. data/lib/adapi/api.rb +8 -0
  29. data/lib/adapi/campaign.rb +27 -18
  30. data/lib/adapi/campaign_criterion.rb +278 -0
  31. data/lib/adapi/campaign_target.rb +5 -123
  32. data/lib/adapi/config.rb +36 -31
  33. data/lib/adapi/constant_data.rb +13 -0
  34. data/lib/adapi/constant_data/language.rb +45 -0
  35. data/lib/adapi/keyword.rb +15 -5
  36. data/lib/adapi/location.rb +91 -0
  37. data/lib/adapi/version.rb +8 -1
  38. data/lib/httpi_request_monkeypatch.rb +4 -0
  39. data/test/config/adapi.yml.template +21 -0
  40. data/test/config/adwords_api.yml.template +10 -0
  41. data/test/integration/create_campaign_test.rb +54 -0
  42. data/test/test_helper.rb +2 -3
  43. data/test/unit/ad_group_test.rb +3 -4
  44. data/test/unit/ad_test.rb +1 -1
  45. data/test/unit/campaign_criterion_test.rb +23 -0
  46. data/test/unit/config_test.rb +52 -0
  47. metadata +48 -35
  48. data/examples/add_campaign_targets.rb +0 -26
  49. 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
- # PS: what about this config setting?
4
- # Campaign.create(:data => campaign_data, :account => :my_account_alias)
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
- @settings ||= self.load_settings
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
- # * path - default: user's home directory
43
- # * filename - default: adapi.yml
44
- # TODO: set to HOME/adwords_api as default
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
- adapi_path = File.join(params[:path], params[:filename])
57
- adwords_api_path = File.join(ENV['HOME'], 'adwords_api.yml')
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?(adapi_path)
60
- @settings = YAML::load(File.read(adapi_path)) rescue {}
61
- elsif File.exists?(adwords_api_path)
62
- @settings = { :default => YAML::load(File.read(adwords_api_path)) } rescue {}
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,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Adapi
4
+ class ConstantData < Api
5
+
6
+ def initialize(params = {})
7
+ params[:service_name] = :ConstantDataService
8
+
9
+ super(params)
10
+ end
11
+
12
+ end
13
+ 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
- # supported condition parameters: ad_group_id and id
104
- predicates = [ :ad_group_id ].map do |param_name|
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', 'Text'],
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"
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,10 @@
1
+ :authentication:
2
+ :method: ClientLogin
3
+ :email: adwords_api_yml@example.com
4
+ :password: default_password
5
+ :developer_token: default_token
6
+ :client_customer_id: 555-666-7777
7
+ :user_agent: My Adwords API Client
8
+ :service:
9
+ :environment: PRODUCTION
10
+
@@ -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
@@ -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
- # FIXME factory doesn't work in this case for some reason
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
- :keyword_max_cpc => { :amount => { :micro_amount => 10000000 } }
28
+ :proxy_keyword_max_cpc => { :amount => { :micro_amount => 10000000 } }
30
29
  }
31
30
  end
32
31