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