gapi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+