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