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.
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