gapi 0.0.1

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.
@@ -0,0 +1,2 @@
1
+ Dir[ File.join( File.dirname( __FILE__ ),'gapi', '*.rb' ) ].each {|f| puts( f); require( f )}
2
+
@@ -0,0 +1,47 @@
1
+ # Uses simple_oauth gem and em to make requests
2
+ # requires ruby 1.9 as it uses a Fiber to achieve async behaviour without a callback
3
+
4
+ module Gapi
5
+
6
+ class EmOauthMiddle
7
+
8
+ # connection timeout in seconds
9
+ CONNECT_TIMEOUT_S = 10
10
+
11
+ # inactivity timeout in seconds
12
+ INACTIVITY_TIMEOUT_S = 0
13
+
14
+ # these are all strings not the access token object from ruby-oauth
15
+ def initialize( consumer_key, consumer_secret, access_token, access_secret )
16
+ @oauth_opts = {
17
+ :consumer_key => consumer_key,
18
+ :consumer_secret => consumer_secret,
19
+ :access_token => access_token,
20
+ :access_token_secret => access_secret
21
+ }
22
+ end
23
+
24
+ def get( domain, path, opts )
25
+ data = opts.collect {|kv| kv.join('=') }.join('&')
26
+ path = "#{path}?#{data}" if data && data.length > 0
27
+ http = http_get( "https://#{domain}#{path}" )
28
+ return http.response_header.status, http.response
29
+ end
30
+
31
+ private
32
+
33
+ def http_get( url )
34
+ f = Fiber.current
35
+ f.nil? and raise "http_get called with no current fiber"
36
+ conn = EM::HttpRequest.new( url, :connect_timeout => CONNECT_TIMEOUT_S, :inactivity_timeout => INACTIVITY_TIMEOUT_S )
37
+ conn.use( EventMachine::Middleware::OAuth, @oauth_opts )
38
+ http = conn.get
39
+ http.callback { f.resume( http ) }
40
+ http.errback {|r| f.resume( http ) }
41
+ return Fiber.yield
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
@@ -0,0 +1,50 @@
1
+ require 'pathname'
2
+
3
+ module Gapi
4
+
5
+ # wraps another middle, caches using MD5 hexdigest of path as key
6
+ class FileCacheMiddle
7
+
8
+ def initialize( actual_middle, cache_dir, uid )
9
+ @actual_middle = actual_middle
10
+ @cache_dir = Pathname.new( cache_dir )
11
+ @uid = uid
12
+ end
13
+
14
+ def get( domain, path, opts )
15
+ data = opts.collect {|kv| kv.join('=') }.join('&')
16
+ uri = path
17
+ uri = "#{path}?#{data}" if data && data.length > 0
18
+ cache_fname = cache_filename_for_uri( uri )
19
+ code = 500
20
+ body = ""
21
+ if File.exists?( cache_fname )
22
+ File.open( cache_fname, 'rb' ) do |f|
23
+ body = f.read
24
+ code = 200
25
+ end
26
+ else
27
+ code, body = @actual_middle.get( domain, path, opts )
28
+ if 200 == code
29
+ File.open( cache_fname, 'wb' ) do |f|
30
+ f.write( body )
31
+ end
32
+ end
33
+ end
34
+ return code, body
35
+ end
36
+
37
+ private
38
+
39
+ # TODO consider how we'll clear down cache? Could perhaps just
40
+ # delete files created before today
41
+ def cache_filename_for_uri( path )
42
+ uid = Digest::MD5.hexdigest( "#{@uid}:#{path}" )
43
+ # NOTE: this path needs to exist with r/w permissions for webserver
44
+ @cache_dir.join( uid )
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
@@ -0,0 +1,18 @@
1
+ module Gapi
2
+
3
+ class FixtureMiddle
4
+
5
+ def initialize( filename )
6
+ File.open( filename, 'rb' ) do |f|
7
+ @data = f.read
8
+ end
9
+ end
10
+
11
+ def get( domain, path, opts )
12
+ return 200, @data
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
@@ -0,0 +1,52 @@
1
+ module Gapi
2
+
3
+ class LoginMiddle
4
+
5
+ def initialize( email, password, source="gapi-v1" )
6
+ @auth_token = login( email, password, "source=#{source}" )
7
+ end
8
+
9
+ def get( domain, path, opts )
10
+ http = Net::HTTP.new( domain, 443 )
11
+ http.use_ssl = true
12
+ data = opts.collect {|kv| kv.join('=') }.join('&')
13
+ path = "#{path}?#{data}" if data && data.length > 0
14
+ resp, data = http.get( path, auth_headers )
15
+ code = resp.code.to_i
16
+ body = resp.body
17
+ return code, body
18
+ end
19
+
20
+ private
21
+
22
+ def login( email, password, source )
23
+ params = {"Email" => email, "Passwd" => password, "accountType" => "GOOGLE", "source" => source, "service" => "analytics"}
24
+ code, body = post( "www.google.com", "/accounts/ClientLogin", params );
25
+ if code == 200
26
+ body.split( "\n" ).each do |line|
27
+ if line.match( /^Auth/ )
28
+ return line
29
+ end
30
+ end
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def auth_header
37
+ {"Authorization" => "AuthSub token=#{@auth_token}"}
38
+ end
39
+
40
+ def post( domain, path, opts )
41
+ http = Net::HTTP.new( domain, 443 )
42
+ http.use_ssl = true
43
+ data = opts.collect {|kv| kv.join('=') }.join('&')
44
+ headers = {'Content-Type' => 'application/x-www-form-urlencoded'}.merge( auth_headers )
45
+ resp, data = http.post( path, data, headers )
46
+ code = resp.code.to_i
47
+ return code, resp.body
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,21 @@
1
+ module Gapi
2
+
3
+ class OauthMiddle
4
+
5
+ # the ruby-oauth gem access_token
6
+ def initialize( access_token )
7
+ @access_token = access_token
8
+ end
9
+
10
+ def get( domain, path, opts )
11
+ data = opts.collect {|kv| kv.join('=') }.join('&')
12
+ path = "#{path}?#{data}" if data && data.length > 0
13
+ resp, data = @access_token.get( path, {} )
14
+ code = resp.code.to_i
15
+ body = resp.body
16
+ return code, body
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,108 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'openssl'
4
+ require 'nokogiri'
5
+ require 'ostruct'
6
+ require 'cgi'
7
+
8
+ module Gapi
9
+
10
+ class Service
11
+
12
+ # middle is used to fetch using net:http, oauth, em etc
13
+ def initialize( middle )
14
+ @profile_id = nil
15
+ @middle = middle
16
+ end
17
+
18
+ # dimensions and metrics are arrays e.g. ['ga:date', 'ga:sources'] - see Google Analytics Data Export API
19
+ # filters and sort follow Google Analytics Data Export API documentation e.g. to sort most recent date first: -ga:date
20
+ # Must be called from a fiber if EmOauthMiddle used
21
+ def fetch( start_date, end_date, dimensions, metrics, filters=nil, max_results=nil, sort=nil )
22
+ opts = query_opts( start_date, end_date, dimensions, metrics, filters, max_results, sort )
23
+ code, body = @middle.get( "www.google.com", "/analytics/feeds/data", opts )
24
+ if code == 200
25
+ return code, parse( body )
26
+ else
27
+ return code, nil
28
+ end
29
+ end
30
+
31
+ # Must be called from a fiber if EmOauthMiddle used
32
+ def accounts
33
+ code, body = @middle.get( "www.google.com", "/analytics/feeds/accounts/default", {:date => Time.now.strftime("%Y-%m-%d")} )
34
+ if code == 200
35
+ return code, parse_accounts( body )
36
+ else
37
+ return code, nil
38
+ end
39
+ end
40
+
41
+ def use_account_with_table_id( table_id )
42
+ @profile_id = table_id.split(':').last
43
+ end
44
+
45
+ def use_account_with_title( title )
46
+ code, accs = accounts
47
+ account = accs.find {|acc| acc.title == title}
48
+ if account
49
+ @profile_id = use_account_with_table_id( account.table_id )
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def self.to_underlined( str )
56
+ str.gsub( /[a-z]([A-Z])/ ) {|m| m.insert( 1, "_" )}.downcase
57
+ end
58
+
59
+ private
60
+
61
+ def parse( body )
62
+ doc = Nokogiri::XML(body)
63
+ results = (doc/:entry).collect do |entry|
64
+ hash = {}
65
+ # as (entry/'dxp:metric') with namespace doesn't work, instead iterate over all sub-elements
66
+ (entry/'*').each do |element|
67
+ if element.name == "dimension" or element.name == "metric"
68
+ name = Gapi::Service.to_underlined( element[:name].sub(/^ga\:/,'') )
69
+ hash[name] = element[:value]
70
+ end
71
+ end
72
+ OpenStruct.new(hash)
73
+ end
74
+ results
75
+ end
76
+
77
+ def parse_accounts( body )
78
+ doc = Nokogiri::XML(body)
79
+ results = (doc/:entry).collect do |entry|
80
+ hash = {}
81
+ # as (entry/'dxp:metric') with namespace doesn't work, instead iterate over all sub-elements
82
+ (entry/'*').each do |element|
83
+ if element.name == "title"
84
+ hash['title'] = element.content
85
+ elsif element.name == "tableId"
86
+ hash['table_id'] = element.content
87
+ end
88
+ end
89
+ OpenStruct.new(hash)
90
+ end
91
+ end
92
+
93
+ def query_opts( start_date, end_date, dimensions, metrics, filters=nil, max_results=nil, sort=nil )
94
+ opts = {'ids' => "ga:#{@profile_id}",
95
+ 'start-date' => start_date,
96
+ 'end-date' => end_date,
97
+ 'dimensions' => dimensions.join(','),
98
+ 'metrics' => metrics.join(',')}
99
+ opts['max-results'] = max_results if max_results
100
+ opts['filters'] = filters unless filters.nil?
101
+ opts['sort'] = sort unless sort.nil?
102
+ opts
103
+ end
104
+
105
+ end
106
+
107
+ end
108
+
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gapi
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Paul Grayson
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-27 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Google Analytics Data Export API wrapper which supports rack style middleware for e.g. testing, caching, oauth requests, event-machine based http etc
23
+ email: paul.grayson@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - ./lib/gapi/em_oauth_middle.rb
32
+ - ./lib/gapi/file_cache_middle.rb
33
+ - ./lib/gapi/fixture_middle.rb
34
+ - ./lib/gapi/login_middle.rb
35
+ - ./lib/gapi/oauth_middle.rb
36
+ - ./lib/gapi/service.rb
37
+ - ./lib/gapi.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/paulgrayson/GAPI
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ hash: 3
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.6.2
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: GAPI
72
+ test_files: []
73
+