legato 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +5 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/README.md +171 -0
  5. data/Rakefile +31 -0
  6. data/legato.gemspec +29 -0
  7. data/lib/legato/core_ext/array.rb +13 -0
  8. data/lib/legato/core_ext/string.rb +49 -0
  9. data/lib/legato/filter.rb +57 -0
  10. data/lib/legato/filter_set.rb +27 -0
  11. data/lib/legato/list_parameter.rb +29 -0
  12. data/lib/legato/management/account.rb +31 -0
  13. data/lib/legato/management/finder.rb +14 -0
  14. data/lib/legato/management/profile.rb +33 -0
  15. data/lib/legato/management/web_property.rb +28 -0
  16. data/lib/legato/model.rb +42 -0
  17. data/lib/legato/profile_methods.rb +16 -0
  18. data/lib/legato/query.rb +183 -0
  19. data/lib/legato/reports.rb +16 -0
  20. data/lib/legato/request.rb +21 -0
  21. data/lib/legato/response.rb +91 -0
  22. data/lib/legato/result_set.rb +21 -0
  23. data/lib/legato/user.rb +34 -0
  24. data/lib/legato/version.rb +3 -0
  25. data/lib/legato.rb +51 -0
  26. data/spec/cassettes/management/accounts.json +1 -0
  27. data/spec/cassettes/management/profiles.json +1 -0
  28. data/spec/cassettes/management/web_properties.json +1 -0
  29. data/spec/cassettes/model/basic.json +1 -0
  30. data/spec/fixtures/simple_response.json +1 -0
  31. data/spec/integration/management_spec.rb +34 -0
  32. data/spec/integration/model_spec.rb +20 -0
  33. data/spec/lib/legato/filter_spec.rb +49 -0
  34. data/spec/lib/legato/list_parameter_spec.rb +35 -0
  35. data/spec/lib/legato/management/account_spec.rb +39 -0
  36. data/spec/lib/legato/management/profile_spec.rb +45 -0
  37. data/spec/lib/legato/management/web_property_spec.rb +34 -0
  38. data/spec/lib/legato/model_spec.rb +77 -0
  39. data/spec/lib/legato/query_spec.rb +324 -0
  40. data/spec/lib/legato/response_spec.rb +15 -0
  41. data/spec/lib/legato/user_spec.rb +38 -0
  42. data/spec/spec_helper.rb +23 -0
  43. data/spec/support/examples/management_finder.rb +18 -0
  44. data/spec/support/macros/oauth.rb +26 -0
  45. metadata +182 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ lib/demo.rb
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format nested
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in legato.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # Legato: Google Analytics Model/Mapper #
2
+
3
+ ## [Check out the Wiki!](https://github.com/tpitale/legato/wiki) ##
4
+
5
+ ## Google Analytics Management ##
6
+
7
+ 1. Get an OAuth2 Access Token from Google, Read about [OAuth2](https://github.com/tpitale/legato/wiki/OAuth2-and-Google)
8
+
9
+ access_token = OAuth2 Access Token # from Google
10
+
11
+ 2. Create a New User with the Access Token
12
+
13
+ user = Legato::User.new(access_token)
14
+
15
+ 3. List the Accounts and Profiles of the first Account
16
+
17
+ user.accounts
18
+ user.accounts.first.profiles
19
+
20
+ 4. List all the Profiles the User has Access to
21
+
22
+ user.profiles
23
+
24
+ 5. Get a Profile
25
+
26
+ profile = user.profiles.first
27
+
28
+ 6. The Profile Carries the User
29
+
30
+ profile.user == user #=> true
31
+
32
+
33
+ ## Google Analytics Model ##
34
+
35
+ class Exit
36
+ extend Legato::Model
37
+
38
+ metrics :exits, :pageviews
39
+ dimensions :page_path, :operating_system, :browser
40
+ end
41
+
42
+ profile.exits #=> returns a Legato::Query
43
+ profile.exits.each {} #=> any enumerable kicks off the request to GA
44
+
45
+ ## Metrics & Dimensions ##
46
+
47
+ http://code.google.com/apis/analytics/docs/gdata/dimsmets/dimsmets.html
48
+
49
+ metrics :exits, :pageviews
50
+ dimensions :page_path, :operating_system, :browser
51
+
52
+ ## Filtering ##
53
+
54
+ Create named filters to wrap query filters.
55
+
56
+ Here's what google has to say: http://code.google.com/apis/analytics/docs/gdata/v3/reference.html#filters
57
+
58
+ ### Examples ###
59
+
60
+ Return entries with exits counts greater than or equal to 2000
61
+
62
+ filter :high_exits, lambda {gte(:exits, 2000)}
63
+
64
+ Return entries with pageview metric less than or equal to 200
65
+
66
+ filter :low_pageviews, lambda {lte(:pageviews, 200)}
67
+
68
+ Filters with dimensions
69
+
70
+ filter :for_browser, lambda {|browser| matches(:broswer, browser)}
71
+
72
+ Filters with OR
73
+
74
+ filter :browsers, lambda {|*browsers| browsers.map {|browser| matches(:broswer, browser)}}
75
+
76
+
77
+ ## Using and Chaining Filters ##
78
+
79
+ Pass the profile as the first or last parameter into any filter.
80
+
81
+ Exit.for_browser("Safari", profile)
82
+
83
+ Chain two filters.
84
+
85
+ Exit.high_exits.low_pageviews(profile)
86
+
87
+ Profile gets a method for each class extended by Legato::Model
88
+
89
+ Exit.results(profile) == profile.exit
90
+
91
+ We can chain off of that method, too.
92
+
93
+ profile.exit.high_exits.low_pageviews.by_pageviews
94
+
95
+ Chaining order doesn't matter. Profile can be given to any filter.
96
+
97
+ Exit.high_exits(profile).low_pageviews == Exit.low_pageviews(profile).high_exits
98
+
99
+ Be sure to pass the appropriate number of arguments matching the lambda for your filter.
100
+
101
+ For a filter defined like this:
102
+
103
+ filter :browsers, lambda {|*browsers| browsers.map {|browser| matches(:broswer, browser)}}
104
+
105
+ We can use it like this, passing any number of arguments:
106
+
107
+ Exit.browsers("Firefox", "Safari", profile)
108
+
109
+ ## Google Analytics Supported Filtering Methods ##
110
+
111
+ Google Analytics supports a significant number of filtering options.
112
+
113
+ Here is what we can do currently:
114
+ (the operator is a method available in filters for the appropriate metric or dimension)
115
+
116
+ Operators on metrics (method => GA equivalent):
117
+
118
+ eql => '==',
119
+ not_eql => '!=',
120
+ gt => '>',
121
+ gte => '>=',
122
+ lt => '<',
123
+ lte => '<='
124
+
125
+ Operators on dimensions:
126
+
127
+ matches => '==',
128
+ does_not_match => '!=',
129
+ contains => '=~',
130
+ does_not_contain => '!~',
131
+ substring => '=@',
132
+ not_substring => '!@'
133
+
134
+ ## Accounts, WebProperties, Profiles, and Goals ##
135
+
136
+ > Legato::Management::Account.all(user)
137
+ > Legato::Management::WebProperty.all(user)
138
+ > Legato::Management::Profile.all(user)
139
+
140
+ ## Other Parameters Can be Passed to a call to #results ##
141
+
142
+ * :start_date - The date of the period you would like this report to start
143
+ * :end_date - The date to end, inclusive
144
+ * :limit - The maximum number of results to be returned
145
+ * :offset - The starting index
146
+ * :order - metric/dimension to order by
147
+
148
+ ## License ##
149
+
150
+ (The MIT License)
151
+
152
+ Copyright (c) 2012 Tony Pitale
153
+
154
+ Permission is hereby granted, free of charge, to any person obtaining
155
+ a copy of this software and associated documentation files (the
156
+ 'Software'), to deal in the Software without restriction, including
157
+ without limitation the rights to use, copy, modify, merge, publish,
158
+ distribute, sublicense, and/or sell copies of the Software, and to
159
+ permit persons to whom the Software is furnished to do so, subject to
160
+ the following conditions:
161
+
162
+ The above copyright notice and this permission notice shall be
163
+ included in all copies or substantial portions of the Software.
164
+
165
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
166
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
167
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
168
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
169
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
170
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
171
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'oauth2'
4
+
5
+ namespace :oauth do
6
+ def client
7
+ # This is my test client account for Legato.
8
+ OAuth2::Client.new('779170787975.apps.googleusercontent.com', 'mbCISoZiSwyVQIDEbLj4EeEc', {
9
+ :authorize_url => 'https://accounts.google.com/o/oauth2/auth',
10
+ :token_url => 'https://accounts.google.com/o/oauth2/token'
11
+ })
12
+ end
13
+
14
+ def auth_url
15
+ client.auth_code.authorize_url({
16
+ :scope => 'https://www.googleapis.com/auth/analytics.readonly',
17
+ :redirect_uri => 'http://localhost'
18
+ })
19
+ end
20
+
21
+ desc "Get a new OAuth2 Token"
22
+ task :token do
23
+ `open "#{auth_url}"`
24
+
25
+ print 'OAuth2 Code: '
26
+ code = $stdin.gets
27
+
28
+ access_token = client.auth_code.get_token(code.strip, :redirect_uri => 'http://localhost')
29
+ puts access_token.token
30
+ end
31
+ end
data/legato.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "legato/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "legato"
7
+ s.version = Legato::VERSION
8
+ s.authors = ["Tony Pitale"]
9
+ s.email = ["tpitale@gmail.com"]
10
+ s.homepage = "http://github.com/tpitale/legato"
11
+ s.summary = %q{Access the Google Analytics API with Ruby}
12
+ s.description = %q{Access the Google Analytics Core Reporting and Management APIs with Ruby. Create models for metrics and dimensions. Filter your data to tell you what you need.}
13
+
14
+ s.rubyforge_project = "legato" # ?
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "mocha"
24
+ s.add_development_dependency "bourne"
25
+ s.add_development_dependency "vcr", "2.0.0.beta2"
26
+ s.add_development_dependency "fakeweb"
27
+
28
+ s.add_runtime_dependency "oauth2"
29
+ end
@@ -0,0 +1,13 @@
1
+ unless Object.const_defined?("ActiveSupport")
2
+ class Array
3
+ def self.wrap(object)
4
+ if object.nil?
5
+ []
6
+ elsif object.respond_to?(:to_ary)
7
+ object.to_ary
8
+ else
9
+ [object]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ # Pull in some AS String utilities (not loaded if AS is available)
2
+ unless Object.const_defined?("ActiveSupport")
3
+ class String
4
+ def camelize(first_letter = :upper)
5
+ case first_letter
6
+ when :upper then Legato::Inflector.camelize(self, true)
7
+ when :lower then Legato::Inflector.camelize(self, false)
8
+ end
9
+ end
10
+ alias_method :camelcase, :camelize
11
+
12
+ def underscore
13
+ Legato::Inflector.underscore(self)
14
+ end
15
+
16
+ def demodulize
17
+ Legato::Inflector.demodulize(self)
18
+ end
19
+ end
20
+
21
+
22
+ module Legato
23
+ module Inflector
24
+ extend self
25
+
26
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
27
+ if first_letter_in_uppercase
28
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
29
+ else
30
+ lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
31
+ end
32
+ end
33
+
34
+ def underscore(camel_cased_word)
35
+ word = camel_cased_word.to_s.dup
36
+ word.gsub!(/::/, '/')
37
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
38
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
39
+ word.tr!("-", "_")
40
+ word.downcase!
41
+ word
42
+ end
43
+
44
+ def demodulize(class_name_in_module)
45
+ class_name_in_module.to_s.gsub(/^.*::/, '')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ module Legato
2
+ class Filter
3
+ attr_accessor :field, :operator, :value, :join_character
4
+
5
+ OPERATORS = {
6
+ :eql => '==',
7
+ :not_eql => '!=',
8
+ :gt => '>',
9
+ :gte => '>=',
10
+ :lt => '<',
11
+ :lte => '<=',
12
+ :matches => '==',
13
+ :does_not_match => '!=',
14
+ :contains => '=~',
15
+ :does_not_contain => '!~',
16
+ :substring => '=@',
17
+ :not_substring => '!@',
18
+ :desc => '-',
19
+ :descending => '-'
20
+ }
21
+
22
+ def initialize(field, operator, value, join_character=';')
23
+ self.field = field
24
+ self.operator = operator
25
+ self.value = value
26
+ self.join_character = join_character
27
+ end
28
+
29
+ def google_field
30
+ Legato.to_ga_string(field)
31
+ end
32
+
33
+ def google_operator
34
+ OPERATORS[operator]
35
+ end
36
+
37
+ def escaped_value
38
+ CGI.escape(value.to_s.gsub(/([,;\\])/) {|c| '\\'+c})
39
+ end
40
+
41
+ def to_param
42
+ "#{google_field}#{google_operator}#{escaped_value}"
43
+ end
44
+
45
+ def join_with(param)
46
+ param << join_character unless param.nil?
47
+ param.nil? ? to_param : (param << to_param)
48
+ end
49
+
50
+ def ==(other)
51
+ field == other.field &&
52
+ operator == other.operator &&
53
+ value == other.value &&
54
+ join_character == other.join_character
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module Legato
2
+ class FilterSet
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @filters = []
7
+ end
8
+
9
+ def each(&block)
10
+ @filters.each(&block)
11
+ end
12
+
13
+ def to_a
14
+ @filters
15
+ end
16
+
17
+ def <<(filter)
18
+ @filters << filter
19
+ end
20
+
21
+ def to_params
22
+ @filters.inject(nil) do |params, filter|
23
+ filter.join_with(params)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module Legato
2
+ class ListParameter
3
+
4
+ attr_reader :name, :elements
5
+
6
+ def initialize(name, elements=[])
7
+ @name = name
8
+ @elements = Array.wrap(elements)
9
+ end
10
+
11
+ def name
12
+ @name.to_s
13
+ end
14
+
15
+ def <<(element)
16
+ (@elements += Array.wrap(element)).compact!
17
+ self
18
+ end
19
+
20
+ def to_params
21
+ value = elements.map{|element| Legato.to_ga_string(element)}.join(',')
22
+ value.empty? ? {} : {name => value}
23
+ end
24
+
25
+ def ==(other)
26
+ name == other.name && elements == other.elements
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Legato
2
+ module Management
3
+ class Account
4
+ extend Finder
5
+
6
+ def self.default_path
7
+ "/accounts"
8
+ end
9
+
10
+ def path
11
+ "/accounts/#{id}"
12
+ end
13
+
14
+ attr_accessor :id, :name, :user
15
+
16
+ def initialize(attributes, user)
17
+ self.user = user
18
+ self.id = attributes['id']
19
+ self.name = attributes['name']
20
+ end
21
+
22
+ def web_properties
23
+ WebProperty.for_account(self)
24
+ end
25
+
26
+ def profiles
27
+ Profile.for_account(self)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module Legato
2
+ module Management
3
+ module Finder
4
+ def base_uri
5
+ "https://www.googleapis.com/analytics/v3/management"
6
+ end
7
+
8
+ def all(user, path=default_path)
9
+ json = user.access_token.get(base_uri + path).body
10
+ JSON.parse(json)['items'].map {|item| new(item, user)}
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ module Legato
2
+ module Management
3
+ class Profile
4
+
5
+ extend Finder
6
+ include ProfileMethods
7
+
8
+ def self.default_path
9
+ "/accounts/~all/webproperties/~all/profiles"
10
+ end
11
+
12
+ def path
13
+ self.class.default_path + "/" + id.to_s
14
+ end
15
+
16
+ attr_accessor :id, :name, :user
17
+
18
+ def initialize(attributes, user)
19
+ self.user = user
20
+ self.id = attributes['id']
21
+ self.name = attributes['name']
22
+ end
23
+
24
+ def self.for_account(account)
25
+ all(account.user, account.path+'/webproperties/~all/profiles')
26
+ end
27
+
28
+ def self.for_web_property(web_property)
29
+ all(web_property.user, web_property.path+'/profiles')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ module Legato
2
+ module Management
3
+ class WebProperty
4
+ extend Finder
5
+
6
+ def self.default_path
7
+ "/accounts/~all/webproperties"
8
+ end
9
+
10
+ def path
11
+ self.class.default_path + "/" + id.to_s
12
+ end
13
+
14
+ attr_accessor :id, :name, :website_url, :user
15
+
16
+ def initialize(attributes, user)
17
+ self.user = user
18
+ self.id = attributes['id']
19
+ self.name = attributes['name']
20
+ self.website_url = attributes['websiteUrl']
21
+ end
22
+
23
+ def self.for_account(account)
24
+ all(account.user, account.path+'/webproperties')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module Legato
2
+ module Model
3
+ def self.extended(base)
4
+ ProfileMethods.add_profile_method(base)
5
+ end
6
+
7
+ def metrics(*fields)
8
+ @metrics ||= ListParameter.new(:metrics)
9
+ @metrics << fields
10
+ end
11
+
12
+ def dimensions(*fields)
13
+ @dimensions ||= ListParameter.new(:dimensions)
14
+ @dimensions << fields
15
+ end
16
+
17
+ def filters
18
+ @filters ||= {}
19
+ end
20
+
21
+ def filter(name, block)
22
+ filters[name] = block
23
+
24
+ (class << self; self; end).instance_eval do
25
+ define_method(name) {|*args| Query.new(self).apply_filter(*args, block)}
26
+ end
27
+ end
28
+
29
+ # def set_instance_klass(klass)
30
+ # @instance_klass = klass
31
+ # end
32
+
33
+ # def instance_klass
34
+ # @instance_klass || OpenStruct
35
+ # end
36
+
37
+ def results(profile, options = {})
38
+ Query.new(self).results(profile, options)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ module Legato
2
+ module ProfileMethods
3
+ def self.add_profile_method(klass)
4
+ # demodulize leaves potential to redefine
5
+ # these methods given different namespaces
6
+ method_name = klass.name.to_s.demodulize.underscore
7
+ return unless method_name.length > 0
8
+
9
+ class_eval <<-CODE
10
+ def #{method_name}(opts={})
11
+ #{klass}.results(self, opts)
12
+ end
13
+ CODE
14
+ end
15
+ end
16
+ end