adknowledge 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in adstation.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ Adknowledge <a id='top'></a>
2
+ ===========
3
+
4
+ A very Ruby client library for [Adknowledge](http://www.adknowledge.com) APIs
5
+
6
+ Right now it supports two API end-points:
7
+ * Integrated - pulls down creatives for recipients using ADK's integrated API
8
+ * Performance - report on product performance
9
+
10
+ Integrated
11
+ ----------
12
+
13
+ Mapping content for recipients is super easy:
14
+
15
+ ```ruby
16
+ # Start with an array of hashes for your recipients...
17
+ recipients = [
18
+ { recipient: '004c58927df600d73d58c817bafc2155',
19
+ list: 9250,
20
+ domain: 'hotmail.com',
21
+ countrycode: 'US',
22
+ state: 'MO'
23
+ },
24
+ { recipient: 'a2a8c7a5ce7c4249663803c7d040401f',
25
+ list: 9250,
26
+ domain: 'mail.com',
27
+ countrycode: 'CA'
28
+ }
29
+ ]
30
+
31
+ # Specify your auth token:
32
+ Adknowledge.token = "your token here"
33
+
34
+ # Create an Integrated object
35
+ mapper = Adknowledge::Integrated.new \
36
+ domain: 'www.mydomain.com',
37
+ subid: 1234,
38
+ recipients: recipients
39
+
40
+ # Make stuff happen
41
+ mapper.map!
42
+
43
+ # Get yer results
44
+ mapper.each do |recipient|
45
+ # your hash now has extra stuff
46
+ # like:
47
+ puts recipient['creative']['subject']
48
+ puts recipient['creative']['body']
49
+ end
50
+
51
+ # Further short-cut with selections for mapped/errored
52
+ mapper.mapped_recipients.each do |recipient|
53
+ # All of these were successful
54
+ end
55
+
56
+ mapper.errored_recipients.each do |recipient|
57
+ # All of these errored :(
58
+ puts recipient['error']['num']
59
+ puts recipient['error']['str']
60
+ end
61
+ ```
62
+
63
+ [Back to top](#top)
64
+ Performance
65
+ -----------
66
+
67
+ We also give you a nice ActiveRecord-inspired query interface
68
+
69
+ ```ruby
70
+ # Specify your auth token:
71
+ Adknowledge.token = "your token here"
72
+
73
+ # Create a query object
74
+ perf = Adknowlege::Performance.new
75
+
76
+ perf.where(start_date: 1, domain_group: 'AOL Group').
77
+ select(:revenue, :paid_clicks).
78
+ group_by(:subid, :report_date)
79
+
80
+ perf.each do |row|
81
+ # do something with the results
82
+ end
83
+ ```
84
+
85
+ Supports the following query options:
86
+ * Select - Measures that you'd like to report
87
+ * Where - Filter criteria
88
+ * Group By - Dimensions to aggregate on
89
+ * Pivot - Advanced Pivot options
90
+ * No Cache - disable ADK default caching (60 seconds)
91
+ * Display All - Display dimensions even if they've been filtered
92
+ * Full - Return all days in the date range even if values are 0
93
+ * Sort - Column index to sort on
94
+ * Limit - Limit query to a number of rows
95
+
96
+ For more details see the specs, and [ADK documentation](https://publisher.adknowledge.com/help/documentation/chapter/data-pull-api)
97
+
98
+ [Back to top](#top)
99
+ TODO
100
+ ----
101
+ There are several things currently unfinished:
102
+ * Error handling when requests are unsuccessful
103
+ * Options/convenience methods for parsing pivot query results
104
+ * Add an end-point for "lookup" API
105
+
106
+ How to Contribute
107
+ -----------------
108
+ * Fork this repository on Github
109
+ * Run the test suite - FYI: Auth tokens have been removed to protect the innocent
110
+ * Add code and tests
111
+ * Submit a pull request
112
+ * I'll merge if it looks good & passes
113
+ * Rinse
114
+ * Repeat
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "adknowledge/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "adknowledge"
7
+ s.version = Adknowledge::VERSION
8
+ s.authors = ["Aaron Spiegel"]
9
+ s.email = ["spiegela@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Adknowledge API Tools}
12
+ s.description = %q{A collection of web-api helpers and parsing utils for Adknowlege's APIs}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ # specify any dependencies here; for example:
20
+ s.add_development_dependency "pry"
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "webmock", "1.10"
23
+ s.add_development_dependency "vcr"
24
+ s.add_runtime_dependency "rake"
25
+ s.add_runtime_dependency "activesupport"
26
+ s.add_runtime_dependency "oj"
27
+ s.add_runtime_dependency "ox"
28
+ s.add_runtime_dependency "multi_xml"
29
+ s.add_runtime_dependency "addressable"
30
+ s.add_runtime_dependency "faraday"
31
+ s.add_runtime_dependency "faraday_middleware"
32
+ s.add_runtime_dependency "faraday_middleware-parse_oj"
33
+ end
@@ -0,0 +1,9 @@
1
+ module Adknowledge
2
+ def self.token= token
3
+ @token = token
4
+ end
5
+
6
+ def self.token
7
+ @token
8
+ end
9
+ end
@@ -0,0 +1,193 @@
1
+ require 'faraday'
2
+ require 'ox'
3
+ require 'addressable/uri'
4
+ require 'faraday_middleware/response/parse_xml'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ module Adknowledge
8
+ class Integrated
9
+ include Enumerable
10
+
11
+ attr_reader :request, :recipients
12
+ attr_accessor :idomain, :cdomain, :subid, :test
13
+
14
+ delegate :[], to: :recipients
15
+
16
+ URL = 'http://integrated.adstation.com'
17
+ API_VER = '1.3'
18
+
19
+ VALID_FIELDS = [
20
+ :recipient, :list, :domain, :subid, :sendingdomain, :sendingip,
21
+ :numberofrecipients, :redirect, :countrycode, :metrocode, :state,
22
+ :postalcode, :gender, :dayofbirth, :monthofbirth, :yearofbirth
23
+ ]
24
+
25
+ MANDATORY_FIELDS = [ :recipient, :list, :domain ]
26
+
27
+ # Create integrated query object
28
+ #
29
+ # @param parameters
30
+ # @option [Symbol] domain set both click-domain and image-domain to the
31
+ # same domain name for the request
32
+ # @option [Symbol] cdomain set the click-domain for the request
33
+ # @option [Symbol] idomain set the image-domain for the request
34
+ # @option [Symbol] subid set the subid for the request
35
+ # @option [Symbol] test set the test flag for the request
36
+ def initialize params={}
37
+ @records = []
38
+ @mapped = false
39
+ params.each do |k,v|
40
+ send("#{k}=", v)
41
+ end
42
+ end
43
+
44
+ # Set the array of recipients to map
45
+ #
46
+ # @param [Array] recipients an array of hashes containing recipient details
47
+ # @return [String] prepared XML request for recipient array
48
+ def recipients= recipient_hashes
49
+ @recipients = recipient_hashes
50
+ doc = Ox::Document.new version: '1.0'
51
+ req = Ox::Element.new 'request'
52
+ doc << Ox::Instruct.new('xml version="1.0" encoding="UTF-8"') << req
53
+ recipient_hashes.each do |recipient_hash|
54
+ email_hash = recipient_hash.select{|x| VALID_FIELDS.include? x}
55
+ req << email_xml(email_hash)
56
+ end
57
+ @request = Ox.dump(doc, indent: 0, with_instruct: true).gsub(/\n/, '')
58
+ end
59
+
60
+
61
+ # Map content for specified recipients
62
+ #
63
+ # @return [Boolean] map attempt submitted
64
+ def map!
65
+ unless Adknowledge.token
66
+ raise ArgumentError, 'Adknowledge token required to perform queries'
67
+ end
68
+
69
+ merge_recipients! query_result
70
+ @mapped = true
71
+ end
72
+
73
+ # Return all successfully mapped recipients
74
+ #
75
+ # @return [Array] mapped recipients
76
+ def mapped_recipients
77
+ return [] unless mapped?
78
+ recipients.select{|r| r['success']}
79
+ end
80
+
81
+ # Return all errored recipients
82
+ #
83
+ # @return [Array] errored recipients
84
+ def errored_recipients
85
+ return [] unless mapped?
86
+ recipients.select{|r| ! r['success']}
87
+ end
88
+
89
+ # Set both click-domain and image-domain
90
+ #
91
+ # @param [Symbol] domain
92
+ # @return [Symbol] domain
93
+ def domain= dom
94
+ self.cdomain = self.idomain = dom
95
+ end
96
+
97
+ # Return the query params that will be sent to Adknowledge integrated API
98
+ #
99
+ # @return [Hash] query params
100
+ def query_params
101
+ { token: Adknowledge.token,
102
+ idomain: idomain,
103
+ cdomain: cdomain,
104
+ request: request,
105
+ subid: subid,
106
+ test: test ? 1 : 0
107
+ }
108
+ end
109
+
110
+ # Return confirmation if the mapping query has been run yet
111
+ #
112
+ # @return [Boolean] mapped?
113
+ def mapped?
114
+ @mapped
115
+ end
116
+
117
+ private
118
+
119
+ def merge_recipients! results
120
+ case results['email']
121
+ when Array
122
+ results['email'].each{ |record| merge_recipient! record, true }
123
+ when Hash
124
+ merge_recipient! results['email'], true
125
+ end
126
+
127
+ case results['error']
128
+ when Array
129
+ results['error'].each { |record| merge_recipient! record, false }
130
+ when Hash
131
+ merge_recipient! results['error'], false
132
+ end
133
+ end
134
+
135
+ def merge_recipient! record, success
136
+ fields = success ? get_email(record) : get_error(record)
137
+ recipients.find do |recipient|
138
+ record["recipient"] == recipient[:recipient]
139
+ end.merge! fields
140
+ end
141
+
142
+ def get_email record
143
+ record.merge 'success' => true
144
+ end
145
+
146
+ def get_error record
147
+ { 'success' => false,
148
+ 'error' => record.select{|k,v| %w[str num].include? k}
149
+ }
150
+ end
151
+
152
+ def query_result
153
+ conn.post do |req|
154
+ req.headers = headers
155
+ req.body = post_body
156
+ req.url API_VER
157
+ end.body
158
+ end
159
+
160
+ def conn
161
+ @conn ||= Faraday.new(:url => URL) do |b|
162
+ b.adapter Faraday.default_adapter
163
+ b.response :adknowledge
164
+ end
165
+ end
166
+
167
+ def headers
168
+ {:Accepts => 'application/xml', :'Accept-Encoding' => 'gzip'}
169
+ end
170
+
171
+ def post_body
172
+ uri = Addressable::URI.new
173
+ uri.query_values = query_params
174
+ uri.query
175
+ end
176
+
177
+ def email_xml email_hash
178
+ unless (MANDATORY_FIELDS - email_hash.keys).empty?
179
+ raise ArgumentError, 'One or more mandatory fields were not submitted'
180
+ end
181
+ e = Ox::Element.new(:email)
182
+ email_hash.each do |field, value|
183
+ e << field_xml(field, value)
184
+ end
185
+ e
186
+ end
187
+
188
+ def field_xml field, value
189
+ Ox::Element.new(field) << value.to_s
190
+ end
191
+
192
+ end
193
+ end
@@ -0,0 +1,255 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'faraday_middleware/parse_oj'
4
+ require 'active_support/core_ext/module/delegation'
5
+
6
+ module Adknowledge
7
+ class Performance
8
+ include Enumerable
9
+
10
+ attr_reader :measures, :dimensions, :filter, :sort_option, :pivot_options,
11
+ :records, :options
12
+
13
+ delegate :[], to: :records
14
+
15
+ URL = 'http://api.publisher.adknowledge.com/performance'
16
+
17
+ VALID_MEASURES = [
18
+ :revenue, :schedules, :clicks, :paid_clicks, :valid_clicks,
19
+ :invalid_clicks, :test_clicks, :domestic_paid_clicks,
20
+ :domestic_unpaid_clicks, :foreign_paid_clicks, :foreign_unpaid_clicks,
21
+ :foreign_clicks, :badip_clicks, :badagent_clicks, :badreferrer_clicks,
22
+ :ecpm, :epc, :source_expense, :source_profit, :affiliate_percent,
23
+ :gross_revenue, :ppc, :adjustments, :promotions, :referrals,
24
+ :expense_accruals, :adjustment_accruals, :promotion_accruals,
25
+ :referral_accruals, :accruals, :total_payment, :sent_amount,
26
+ :domain_group, :source_account_name, :records
27
+ ]
28
+
29
+ VALID_DIMENSIONS = [
30
+ :product_guid, :report_date, :report_hour, :report_30min, :report_15min,
31
+ :is_accrued, :revenue_type, :source_product_guid, :list_id, :product_id,
32
+ :source_account_name, :domain_group_id, :domain_group, :report_time,
33
+ :subid, :country_cd, :accrual_date, :suppress_date, :suppress_md5,
34
+ :suppress_type
35
+ ]
36
+
37
+ VALID_FILTERS = [:start_date, :end_date] + VALID_DIMENSIONS
38
+
39
+ VALID_PIVOT_KEYS = [:pivot, :sum, :count]
40
+
41
+ DEFAULT_FILTER = {product_id: '2', product_guid: '*'}
42
+
43
+ def initialize
44
+ @measures = {}
45
+ @dimensions = {}
46
+ @filter = DEFAULT_FILTER.dup
47
+ @options = {}
48
+ @pivot_options = {}
49
+ end
50
+
51
+ # Iterate the query results. Runs the query if it hasn't been already.
52
+ #
53
+ # @param [Block] block
54
+ def each
55
+ if block_given?
56
+ records.each do |doc|
57
+ yield doc
58
+ end
59
+ else
60
+ to_enum
61
+ end
62
+ end
63
+
64
+ # Specify the measure(s) to select in the query
65
+ #
66
+ # @param [Array] selection(s)
67
+ # @return [Adknowledge::Performance] query object
68
+ def select *selection
69
+ selection = selection.map{|x| x.to_sym} # handle strings & symbols equally
70
+ unless (selection - VALID_MEASURES).empty?
71
+ raise ArgumentError, 'Invalid measurement selection'
72
+ end
73
+ @measures.merge! Hash[selection.zip([1] * selection.count)]
74
+ self
75
+ end
76
+
77
+ # Specify the dimension(s) to group measures by
78
+ #
79
+ # @param [Array] grouping(s)
80
+ # @return [Adknowledge::Performance] query object
81
+ def group_by *groupings
82
+ groupings = groupings.map{|x| x.to_sym} # handle strings & symbols equally
83
+ unless (groupings - VALID_DIMENSIONS).empty?
84
+ raise ArgumentError, 'Invalid dimension grouping'
85
+ end
86
+ @dimensions.merge! Hash[groupings.zip([1] * groupings.count)]
87
+ self
88
+ end
89
+
90
+ # Specify the filter criteria to limit query by
91
+ #
92
+ # @param [Hash] criteria
93
+ # @return [Adknowledge::Performance] query object
94
+ def where criteria
95
+ unless(criteria.keys - VALID_FILTERS).empty?
96
+ raise ArgumentError, 'Invalid filter criteria'
97
+ end
98
+ criteria.each{|k,v| criteria[k] = v.to_s}
99
+ @filter.merge! criteria
100
+ self
101
+ end
102
+
103
+ # Specify a number of results to retrun
104
+ #
105
+ # @param [Integer] limit
106
+ # @return [Adknowledge::Performance] query object
107
+ def limit limit
108
+ unless limit.is_a? Fixnum
109
+ raise ArgumentError, 'Limit must be an integer'
110
+ end
111
+ @options[:limit] = limit.to_s
112
+ self
113
+ end
114
+
115
+ # Specify the column index to sort by
116
+ #
117
+ # @param [Integer] sort_option
118
+ # @return [Adknowledge::Performance] query object
119
+ def sort sort_option
120
+ unless sort_option.is_a? Fixnum
121
+ raise ArgumentError, 'Sort option must be an integer'
122
+ end
123
+ @sort_option = sort_option.to_s
124
+ self
125
+ end
126
+
127
+ # Specify whether to display the full set even if entries are 0
128
+ #
129
+ # @param [Boolean] full
130
+ # @return [Adknowledge::Performance] query object
131
+ def full full
132
+ unless !!full == full #Boolean check
133
+ raise ArgumentError, 'Full option must be a boolean'
134
+ end
135
+ @options[:full] = full ? '1' : '0'
136
+ self
137
+ end
138
+
139
+ # Disable caching of queries. By default queries are cached for 60 seconds
140
+ #
141
+ # @param [Boolean] nocache
142
+ # @return [Adknowledge::Performance] query object
143
+ def nocache nocache
144
+ unless !!nocache == nocache #Boolean check
145
+ raise ArgumentError, 'NoCache option must be a boolean'
146
+ end
147
+ @options[:nocache] = nocache ? '1' : '0'
148
+ self
149
+ end
150
+
151
+ # Force query to show filtered dimensions to be shown
152
+ #
153
+ # @param [Boolean] display_all
154
+ # @return [Adknowledge::Performance] query object
155
+ def display_all display_all
156
+ unless !!display_all == display_all #Boolean check
157
+ raise ArgumentError, "display_all option must be a boolean"
158
+ end
159
+ @options[:all] = display_all ? '1' : '0'
160
+ self
161
+ end
162
+
163
+ # Specify pivot options
164
+ #
165
+ # @param [Object] pivot Existing grouped field (as symbol) or Hash of options
166
+ # @return [Adknowledge::Performance] query object
167
+ def pivot pivot_opt
168
+ case pivot_opt
169
+ when Symbol
170
+ unless valid_pivot_values.include? pivot_opt
171
+ raise ArgumentError, 'Pivotted field must be a grouped dimension'
172
+ end
173
+ @pivot_options[:pivot] = pivot_opt.to_s
174
+ when Hash
175
+ unless (pivot_opt.values - VALID_MEASURES).empty?
176
+ raise ArgumentError, 'Pivotted value must be a measurement'
177
+ end
178
+ unless (pivot_opt.keys - [:sum, :count]).empty?
179
+ raise ArgumentError, 'Pivot must be sum or count'
180
+ end
181
+ @pivot_options = pivot_opt
182
+ else
183
+ raise ArgumentError, 'Pivot options must be a symbol or hash'
184
+ end
185
+ self
186
+ end
187
+
188
+ # Displays the query parameters passed to Adknowledge performance API
189
+ #
190
+ # @return [Hash] query parameters
191
+ def query_params
192
+ p = base_params.merge(filter_params).
193
+ merge(options_params).
194
+ merge(pivot_params)
195
+ p.merge!(measures: measures_param) unless measures.empty?
196
+ p.merge!(dimensions: dimensions_param) unless dimensions.empty?
197
+ p.merge!(sort: sort_option) if sort_option
198
+ p
199
+ end
200
+
201
+ # Return the query result records
202
+ #
203
+ # @return [Array] query result records
204
+ def records
205
+ unless Adknowledge.token
206
+ raise ArgumentError, 'Adknowledge token required to perform queries'
207
+ end
208
+ results.body['data'] if results.body.has_key?('data')
209
+ end
210
+
211
+
212
+ private
213
+
214
+ def base_params
215
+ {token: Adknowledge.token}
216
+ end
217
+
218
+ def results
219
+ @results ||= conn.get do |req|
220
+ req.url '/performance.json', query_params
221
+ end
222
+ end
223
+
224
+ def conn
225
+ @conn ||= Faraday.new(:url => URL) do |b|
226
+ b.response :oj
227
+ b.adapter Faraday.default_adapter
228
+ end
229
+ end
230
+
231
+ def valid_pivot_values
232
+ dimensions.keys + ['*']
233
+ end
234
+
235
+ def measures_param
236
+ measures.keys.join(',')
237
+ end
238
+
239
+ def dimensions_param
240
+ dimensions.keys.join(',')
241
+ end
242
+
243
+ def options_params
244
+ options
245
+ end
246
+
247
+ def filter_params
248
+ filter
249
+ end
250
+
251
+ def pivot_params
252
+ pivot_options
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,3 @@
1
+ module Adknowledge
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "adknowledge/version"
2
+
3
+ module Adknowledge
4
+ end
5
+
6
+ require 'faraday_middleware/adknowledge'
7
+ require 'adknowledge/config'
8
+ require 'adknowledge/performance'
9
+ require 'adknowledge/integrated'
@@ -0,0 +1,33 @@
1
+ require 'faraday'
2
+ require 'zlib'
3
+
4
+ module FaradayMiddleware
5
+ class Adknowledge < Faraday::Response::Middleware
6
+ dependency 'multi_xml'
7
+ dependency 'zlib'
8
+
9
+ def on_complete env
10
+ encoding = env[:response_headers]['content-encoding'].to_s.downcase
11
+
12
+ return unless env[:body].is_a? String
13
+
14
+ case encoding
15
+ when 'gzip'
16
+ env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body]), encoding: 'ASCII-8BIT').read
17
+ env[:response_headers].delete 'content-encoding'
18
+ when 'deflate'
19
+ env[:body] = Zlib::Inflate.inflate env[:body]
20
+ env[:response_headers].delete 'content-encoding'
21
+ end
22
+
23
+ begin
24
+ env[:body] = ::MultiXml.parse(env[:body])['result']
25
+ rescue Faraday::Error::ParsingError
26
+ env[:body]
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ Faraday::Response.register_middleware adknowledge: FaradayMiddleware::Adknowledge