adapi 0.0.2 → 0.0.3

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