adapi 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,20 +1,33 @@
1
1
 
2
2
  require 'adapi'
3
3
 
4
- # use specific config data
5
- Adapi::Config.set({
6
- :authentication => {
7
- :method => 'ClientLogin',
8
- :email => 'sandbox_email@gmail.com',
9
- :password => 'sandbox_password',
10
- :developer_token => 'sandbox_developer_token',
11
- :client_email => 'sandbox_client_email@gmail.com',
12
- :user_agent => 'Adwords API Test'
4
+ Adapi::Config.load_settings(:in_hash => {
5
+ :coca_cola => {
6
+ :authentication => {
7
+ :method => 'ClientLogin',
8
+ :email => 'coca_cola_email@gmail.com',
9
+ :password => 'coca_cola_password',
10
+ :developer_token => 'coca_cola_developer_token',
11
+ :user_agent => 'Coca-Cola Adwords API Test'
12
+ },
13
+ :service => {
14
+ :environment => 'SANDBOX'
15
+ }
13
16
  },
14
- :service => {
15
- :environment => 'SANDBOX'
17
+ :pepsi => {
18
+ :authentication => {
19
+ :method => 'ClientLogin',
20
+ :email => 'pepsi_email@gmail.com',
21
+ :password => 'pepsi_password',
22
+ :developer_token => 'pepsi_developer_token',
23
+ :user_agent => 'Pepsi Adwords API Test'
24
+ },
25
+ :service => {
26
+ :environment => 'SANDBOX'
27
+ }
16
28
  }
17
29
  })
18
30
 
19
- # create campaign
20
- require 'add_bare_campaign'
31
+ Adapi::Config.set(:pepsi, :client_customer_id => '555-666-7777')
32
+
33
+ p Adapi::Config.read
@@ -0,0 +1,56 @@
1
+
2
+ require 'adapi'
3
+
4
+ # create campaign by single command, with campaing targets, with ad_groups
5
+ # including keywords and ads
6
+
7
+ campaign_data = {
8
+ :name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
9
+ :status => 'PAUSED',
10
+ # Automatic CPC: BudgetOptimizer or ManualCPC
11
+ :bidding_strategy => { :xsi_type => 'BudgetOptimizer', :bid_ceiling => 100 },
12
+ :budget => { :amount => 50, :delivery_method => 'STANDARD' },
13
+
14
+ :network_setting => {
15
+ :target_google_search => true,
16
+ :target_search_network => true,
17
+ :target_content_network => false,
18
+ :target_content_contextual => false
19
+ },
20
+
21
+ :targets => {
22
+ :language => [ 'en', 'cs' ],
23
+ # TODO test together with city target
24
+ :geo => { :proximity => { :geo_point => '38.89859,-77.035971', :radius => '10 km' } }
25
+ },
26
+
27
+ :ad_groups => [
28
+ {
29
+ :name => "AdGroup #%d" % (Time.new.to_f * 1000).to_i,
30
+ :status => 'ENABLED',
31
+
32
+ :keywords => [ 'dem codez', '"top coder"', "[-code]" ],
33
+
34
+ :ads => [
35
+ {
36
+ :headline => "Code like Neo",
37
+ :description1 => 'Need mad coding skills?',
38
+ :description2 => 'Check out my new blog!',
39
+ :url => '', # THIS FAILS
40
+ :display_url => 'http://www.demcodez.com'
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+
46
+ }
47
+
48
+ $campaign = Adapi::Campaign.create(campaign_data)
49
+
50
+ p "Campaign ID #{$campaign.id} created"
51
+ p "with status DELETED and changed name"
52
+ pp $campaign.attributes
53
+
54
+ p "with errors:"
55
+ pp $campaign.errors.to_a
56
+
@@ -1,23 +1,22 @@
1
-
2
1
  require 'adapi'
3
2
 
4
3
  # create campaign
5
- require 'add_bare_campaign'
6
-
7
- p 'original status: %s' % $campaign[:status]
8
-
9
- campaign_updates = {
10
- :id => $campaign[:id],
11
- :status => 'ACTIVE'
12
- }
13
-
14
- $campaign = Adapi::Campaign.update(:data => campaign_updates)
4
+ require File.join(File.dirname(__FILE__), 'add_bare_campaign')
15
5
 
16
- p 'updated status: %s' % $campaign[:status]
6
+ p "ORIGINAL CAMPAIGN:"
7
+ pp $campaign.attributes
17
8
 
18
- $campaign = Adapi::Campaign.update(
9
+ $updated_campaign = Adapi::Campaign.update(
19
10
  :id => $campaign[:id],
20
- :data => {:status => 'DELETED'}
11
+ :status => 'ACTIVE',
12
+ :name => "UPDATED_#{$campaign[:name]}",
13
+ :network_setting => {
14
+ :target_google_search => false,
15
+ :target_search_network => false,
16
+ :target_content_network => true,
17
+ :target_content_contextual => true
18
+ }
21
19
  )
22
20
 
23
- p 'updated status (again): %s' % $campaign[:status]
21
+ p "UPDATED CAMPAIGN:"
22
+ pp $updated_campaign.attributes
@@ -2,14 +2,14 @@
2
2
  require 'adapi'
3
3
 
4
4
  # create campaign
5
- require 'add_bare_campaign'
5
+ require File.join(File.dirname(__FILE__), 'add_bare_campaign')
6
6
 
7
- p 'original status: %s' % $campaign[:status]
7
+ p "ORIGINAL STATUS: %s" % $campaign.status
8
8
 
9
- $campaign = Adapi::Campaign.activate(:id => $campaign[:id])
9
+ $campaign.activate
10
10
 
11
- p 'updated status: %s' % $campaign[:status]
11
+ p "STATUS UPDATE 1: %s" % $campaign.status
12
12
 
13
- $campaign = Adapi::Campaign.delete(:id => $campaign[:id])
13
+ $campaign.delete
14
14
 
15
- p 'updated status (again): %s' % $campaign[:status]
15
+ p "STATUS UPDATE 2: %s" % $campaign.status
@@ -3,15 +3,33 @@ require 'rubygems'
3
3
  require 'adwords_api'
4
4
  require 'collection'
5
5
  require 'yaml'
6
+ require 'pp'
6
7
 
8
+ require 'active_model'
9
+ # require only ActiveSupport parts that we actually use
10
+ require 'active_support/all'
11
+
12
+ require 'adapi/version'
7
13
  require 'adapi/config'
8
14
  require 'adapi/api'
9
15
  require 'adapi/campaign'
10
16
  require 'adapi/campaign_target'
11
17
  require 'adapi/ad_group'
12
18
  require 'adapi/ad_group_criterion'
19
+ require 'adapi/keyword'
13
20
  require 'adapi/ad'
14
- require 'adapi/version'
21
+ require 'adapi/ad/text_ad'
22
+
23
+ # monkeypatch HTTPI
24
+ require 'httpi_request_monkeypatch'
25
+
26
+ HTTPI.adapter = :curb
27
+ # supress HTTPI output
28
+ # HTTPI.log = false
29
+
30
+ # load factories for development environment
31
+ # require 'factory_girl'
32
+ # Dir[ File.join(File.dirname(__FILE__), '../test/factories/*.rb') ].each { |f| require f }
15
33
 
16
34
  module Adapi
17
35
  API_VERSION = :v201101
@@ -1,63 +1,76 @@
1
1
  module Adapi
2
2
 
3
- # TODO add synonym for actual service name: AdGroupAd
4
- #
3
+ # Ad == AdGroupAd
4
+ # wraps all types of ads: text ads, image ads...
5
5
  class Ad < Api
6
6
 
7
- def initialize(params = {})
8
- params[:service_name] = :AdGroupAdService
9
- super(params)
10
- end
7
+ attr_accessor :ad_group_id, :url, :display_url, :approval_status,
8
+ :disapproval_reasons, :trademark_disapproved
11
9
 
12
- def self.create(params = {})
13
- ad_service = Ad.new
10
+ validates_presence_of :ad_group_id
14
11
 
15
- ad_group_id = params[:data].delete(:ad_group_id)
16
-
17
- operation = { :operator => 'ADD',
18
- :operand => { :ad_group_id => ad_group_id, :ad => params[:data] }
19
- }
20
-
21
- response = ad_service.service.mutate([operation])
12
+ # PS: create won't work with id and ad_group_id
13
+ # 'id' => id, 'ad_group_id' => ad_group_id,
14
+ def attributes
15
+ super.merge('url' => url, 'display_url' => display_url)
16
+ end
22
17
 
23
- ad_group = response[:value].first
18
+ def initialize(params = {})
19
+ params[:service_name] = :AdGroupAdService
24
20
 
25
- ad = nil
26
- if response and response[:value]
27
- ad = response[:value].first
28
- puts " Ad ID is #{ad[:ad][:id]}, type is '#{ad[:ad][:xsi_type]}' and status is '#{ad[:status]}'."
21
+ %w{ id ad_group_id url display_url status }.each do |param_name|
22
+ self.send "#{param_name}=", params[param_name.to_sym]
29
23
  end
30
24
 
31
- ad
25
+ super(params)
32
26
  end
33
27
 
34
- def self.find(params = {})
35
- ad_service = Ad.new
28
+ # deletes ad
29
+ #
30
+ def destroy
31
+ response = self.mutate(
32
+ :operator => 'REMOVE',
33
+ :operand => {
34
+ :ad_group_id => self.ad_group_id,
35
+ :ad => { :id => self.id, :xsi_type => 'Ad' }
36
+ }
37
+ )
36
38
 
37
- raise "No AdGroup ID" unless params[:ad_group_id]
38
- ad_group_id = params[:ad_group_id].to_i
39
+ (response and response[:value]) ? true : false
40
+ end
39
41
 
40
- selector = {
41
- :fields => ['Id', 'Headline'],
42
- :ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
43
- :predicates => [
44
- {:field => 'AdGroupId', :operator => 'EQUALS', :values => ad_group_id }
45
- # { :field => 'Status', :operator => 'IN', :values => ['ENABLED', 'PAUSED', 'DISABLED'] }
46
- ]
47
- }
48
42
 
49
- response = ad_service.service.get(selector)
43
+ # ad-specific mutate wrapper, deals with PolicyViolations for ads
44
+ #
45
+ def mutate(operation)
46
+ operation = [operation] unless operation.is_a?(Array)
47
+
48
+ # fix to save space during specifyng operations
49
+ operation = operation.map do |op|
50
+ op[:operand].delete(:status) if op[:operand][:status].nil?
51
+ op
52
+ end
53
+
54
+ begin
55
+ response = @service.mutate(operation)
56
+
57
+ rescue AdsCommon::Errors::HttpError => e
58
+ self.errors.add(:base, e.message)
50
59
 
51
- if response and response[:entries]
52
- ads = response[:entries]
53
- puts "Ad group ##{ad_group_id} has #{ads.length} ad(s)."
54
- ads.each do |ad|
55
- puts " Ad id is #{ad[:ad][:id]}, type is #{ad[:ad][:xsi_type]} and " +
56
- "status is \"#{ad[:status]}\"."
60
+ # traps any exceptions raised by AdWords API
61
+ rescue AdwordsApi::Errors::ApiException => e
62
+ # return PolicyViolations so they can be sent again
63
+ e.errors.each do |error|
64
+ if (error[:api_error_type] == 'PolicyViolationError') && error[:is_exemptable]
65
+ self.errors.add(error[:api_error_type], error[:key])
66
+ else
67
+ # otherwise, just report the errors
68
+ self.errors.add( "[#{self.xsi_type.underscore}]", "#{error[:error_string]} @ #{error[:field_path]}")
69
+ end
57
70
  end
58
- else
59
- puts "No ads found for ad group ##{ad_group_id}."
60
71
  end
72
+
73
+ response
61
74
  end
62
75
 
63
76
  end
@@ -0,0 +1,126 @@
1
+ module Adapi
2
+ # Ad::TextAd == AdGroupAd::TextAd
3
+ #
4
+ # http://code.google.com/apis/adwords/docs/reference/latest/AdGroupAdService.TextAd.html
5
+ #
6
+ class Ad::TextAd < Ad
7
+
8
+ attr_accessor :headline, :description1, :description2
9
+
10
+ def attributes
11
+ super.merge('headline' => headline, 'description1' => description1, 'description2' => description2)
12
+ end
13
+
14
+ def initialize(params = {})
15
+ params[:service_name] = :AdGroupAdService
16
+
17
+ @xsi_type = 'TextAd'
18
+
19
+ %w{ headline description1 description2 }.each do |param_name|
20
+ self.send "#{param_name}=", params[param_name.to_sym]
21
+ end
22
+
23
+ super(params)
24
+ end
25
+
26
+ def save
27
+ self.new? ? self.create : self.update
28
+ end
29
+
30
+ def create
31
+ operation = {
32
+ :operator => 'ADD',
33
+ :operand => {
34
+ :ad_group_id => @ad_group_id,
35
+ :status => @status,
36
+ :ad => self.data
37
+ }
38
+ }
39
+
40
+ response = self.mutate(operation)
41
+
42
+ # check for PolicyViolationError(s)
43
+ # PS: check google-adwords-api/examples/handle_policy_violation_error.rb
44
+ if (self.errors['PolicyViolationError'].size > 0)
45
+ # set exemptions and try again
46
+ operation[:exemption_requests] = errors['PolicyViolationError'].map do |error|
47
+ { :key => error }
48
+ end
49
+
50
+ self.errors.clear
51
+
52
+ response = self.mutate(operation)
53
+ end
54
+
55
+ return false unless (response and response[:value])
56
+
57
+ self.id = response[:value].first[:ad][:id] rescue nil
58
+
59
+ true
60
+ end
61
+
62
+ # params - specify hash of params and values to update
63
+ # PS: I think it's possible to edit only status, but not headline,
64
+ # descriptions... instead you should delete existing ad and create a new one
65
+ #
66
+ def update(params = {})
67
+ # set params (:status param makes it a little complicated)
68
+ #
69
+ updated_params = (params || self.attributes).symbolize_keys
70
+ updated_status = updated_params.delete(:status)
71
+
72
+ response = self.mutate(
73
+ :operator => 'SET',
74
+ :operand => {
75
+ :ad_group_id => self.ad_group_id,
76
+ :ad => updated_params.merge(:id => self.id),
77
+ :status => updated_status
78
+ }
79
+ )
80
+
81
+ (response and response[:value]) ? true : false
82
+ end
83
+
84
+ def find # == refresh
85
+ TextAd.find(:first, :ad_group_id => self.ad_group_id, :id => self.id)
86
+ end
87
+
88
+ def self.find(amount = :all, params = {})
89
+ params.symbolize_keys!
90
+ first_only = (amount.to_sym == :first)
91
+
92
+ # for ActiveRecord compatibility, we don't use anything besides conditions
93
+ # params for now
94
+ params = params[:conditions] if params[:conditions]
95
+
96
+ # we need ad_group_id
97
+ raise ArgumentError, "AdGroup ID is required" unless params[:ad_group_id]
98
+
99
+ # supported condition parameters: ad_group_id and id
100
+ predicates = [ :ad_group_id, :id ].map do |param_name|
101
+ if params[param_name]
102
+ {:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
103
+ end
104
+ end.compact
105
+
106
+ selector = {
107
+ :fields => ['Id', 'Headline'],
108
+ :ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
109
+ :predicates => predicates
110
+ }
111
+
112
+ response = TextAd.new.service.get(selector)
113
+
114
+ response = (response and response[:entries]) ? response[:entries] : []
115
+
116
+ response.map! do |data|
117
+ TextAd.new(data[:ad].merge(:ad_group_id => data[:ad_group_id], :status => data[:status]))
118
+ end
119
+
120
+ # TODO convert to TextAd instances
121
+ # PS: we already have ad_group_id parameter
122
+ first_only ? response.first : response
123
+ end
124
+
125
+ end
126
+ end
@@ -1,69 +1,107 @@
1
1
  module Adapi
2
2
  class AdGroup < Api
3
+
4
+ attr_accessor :campaign_id, :name, :bids, :keywords, :ads
5
+
6
+ validates_presence_of :campaign_id, :name, :status
7
+ validates_inclusion_of :status, :in => %w{ ENABLED PAUSED DELETED }
8
+
9
+ def attributes
10
+ super.merge('campaign_id' => campaign_id, 'name' => name, 'bids' => bids)
11
+ end
3
12
 
4
13
  def initialize(params = {})
5
14
  params[:service_name] = :AdGroupService
6
- super(params)
7
- end
8
15
 
9
- def self.create(params = {})
10
- ad_group_service = AdGroup.new
16
+ @xsi_type = 'AdGroup'
11
17
 
12
- criteria = params[:data].delete(:criteria)
13
- ads = params[:data].delete(:ads) || []
18
+ %w{ campaign_id name status bids keywords ads }.each do |param_name|
19
+ self.send "#{param_name}=", params[param_name.to_sym]
20
+ end
14
21
 
15
- # prepare for adding campaign
16
- operation = { :operator => 'ADD', :operand => params[:data] }
17
-
18
- response = ad_group_service.service.mutate([operation])
22
+ @keywords ||= []
23
+ @ads ||= []
19
24
 
20
- ad_group = response[:value].first
25
+ super(params)
26
+ end
27
+
28
+ def create
29
+ return false unless self.valid?
30
+
31
+ operand = Hash[
32
+ [:campaign_id, :name, :status, :bids].map do |k|
33
+ [ k.to_sym, self.send(k) ] if self.send(k)
34
+ end.compact
35
+ ]
21
36
 
22
- if criteria
23
- Adapi::AdGroupCriterion.create(
24
- :ad_group_id => ad_group[:id],
25
- :criteria => criteria
37
+ response = self.mutate(
38
+ :operator => 'ADD',
39
+ :operand => operand
40
+ )
41
+
42
+ return false unless (response and response[:value])
43
+
44
+ self.id = response[:value].first[:id] rescue nil
45
+
46
+ if @keywords.size > 0
47
+ keyword = Adapi::Keyword.create(
48
+ :ad_group_id => @id,
49
+ :keywords => @keywords
26
50
  )
51
+
52
+ if (keyword.errors.size > 0)
53
+ self.errors.add("[keyword]", keyword.errors.to_a)
54
+ return false
55
+ end
27
56
  end
28
57
 
29
- ads.each do |ad_data|
30
- Adapi::Ad.create(
31
- :data => ad_data.merge(:ad_group_id => ad_group[:id])
32
- )
58
+ @ads.each do |ad_data|
59
+ ad = Adapi::Ad::TextAd.create( ad_data.merge(:ad_group_id => @id) )
60
+
61
+ if (ad.errors.size > 0)
62
+ self.errors.add("[ad] \"#{ad.headline}\"", ad.errors.to_a)
63
+ return false
64
+ end
33
65
  end
34
66
 
35
- # puts "Ad group ID %d was successfully added." % ad_group[:id]
36
- ad_group
67
+ true
37
68
  end
69
+
70
+ def self.find(amount = :all, params = {})
71
+ params.symbolize_keys!
72
+ first_only = (amount.to_sym == :first)
38
73
 
39
- # should be sorted out later, but leave it be for now
40
- #
41
- def self.find(params = {})
42
- ad_group_service = AdGroup.new
74
+ raise "No Campaign ID is required" unless params[:campaign_id]
43
75
 
44
- raise "No Campaign ID" unless params[:campaign_id]
45
- campaign_id = params[:campaign_id]
76
+ predicates = [ :campaign_id, :id ].map do |param_name|
77
+ if params[param_name]
78
+ {:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
79
+ end
80
+ end.compact
46
81
 
47
82
  selector = {
48
- :fields => ['Id', 'Name'],
49
- # :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
50
- :predicates => [{
51
- :field => 'CampaignId', :operator => 'EQUALS', :values => campaign_id
52
- }]
83
+ :fields => ['Id', 'Name', 'Status'],
84
+ :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
85
+ :predicates => predicates
53
86
  }
54
87
 
55
- response = ad_group_service.service.get(selector)
88
+ response = AdGroup.new.service.get(selector)
56
89
 
57
- if response and response[:entries]
58
- ad_groups = response[:entries]
59
- puts "Campaign ##{campaign_id} has #{ad_groups.length} ad group(s)."
60
- ad_groups.each do |ad_group|
61
- puts " Ad group name is \"#{ad_group[:name]}\" and id is #{ad_group[:id]}."
62
- end
63
- else
64
- puts "No ad groups found for campaign ##{campaign_id}."
65
- end
90
+ response = (response and response[:entries]) ? response[:entries] : []
91
+
92
+ #response.map! do |data|
93
+ # TextAd.new(data[:ad].merge(:ad_group_id => data[:ad_group_id], :status => data[:status]))
94
+ #end
95
+
96
+ first_only ? response.first : response
97
+ end
98
+
99
+ def find_keywords(first_only = false)
100
+ Keyword.find( (first_only ? :first : :all), :ad_group_id => self.id )
101
+ end
66
102
 
103
+ def find_ads(first_only = false)
104
+ Ad::TextAd.find( (first_only ? :first : :all), :ad_group_id => self.id )
67
105
  end
68
106
 
69
107
  end