cannikin-gattica 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ github-test.rb
3
+ examples/active_example.rb
4
+ examples/.DS_Store
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.4.0
2
+ * er1c added start_index and max_results
3
+ * er1c added paging for all results
4
+ * er1c added get_to_csv to "stream" saving results to a file IO
5
+ * thieso2 fix DataPoint when GA-Path includec colons
6
+ * jeremyf Added ability to parse addition query strings
7
+ * nedski Added support for proxy via env var
8
+
9
+ == 0.3.4
10
+ * rumble updated the regex used to pull apart the filters so it didn't get confused when there a filename, for example, started with a /
11
+
1
12
  == 0.3.2
2
13
  * er1c updated to use standard Ruby CSV library
3
14
 
@@ -28,4 +39,4 @@
28
39
  * When outputting as CSV, surround each piece of data with double quotes (appears pretty common for various properties (like Browser name) to contain commas
29
40
 
30
41
  == 0.1.0 / 2009-03-26
31
- * Basic functionality working good. Can't use filters yet.
42
+ * Basic functionality working good. Can't use filters yet.
data/README.rdoc CHANGED
@@ -27,7 +27,7 @@ There are generally three steps to getting info from the GA API:
27
27
  = Usage
28
28
  This library does all three. A typical transaction will look like this:
29
29
 
30
- gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', profile_id => 123456})
30
+ gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
31
31
  results = gs.get({ :start_date => '2008-01-01',
32
32
  :end_date => '2008-02-01',
33
33
  :dimensions => 'browser',
@@ -84,7 +84,7 @@ the result to be returned.
84
84
  :dimensions => ['browser','browserVersion'],
85
85
  :metrics => ['pageviews','visits'],
86
86
  :sort => ['-pageviews'],
87
- :filter => ['browser == Firefox','pageviews >= 10000']})
87
+ :filters => ['browser == Firefox','pageviews >= 10000']})
88
88
 
89
89
  This says "return only results where the 'browser' dimension contains the word 'Firefox' and the
90
90
  'pageviews' metric is greater than or equal to 10,000.
@@ -92,9 +92,9 @@ This says "return only results where the 'browser' dimension contains the word '
92
92
  Filters can contain spaces around the operators, or not. These two lines are equivalent (I think
93
93
  the spaces make the filter more readable):
94
94
 
95
- :filter => ['browser == Firefox','pageviews >= 10000']
95
+ :filters => ['browser == Firefox','pageviews >= 10000']
96
96
 
97
- :filter => ['browser==Firefox','pageviews>=10000']
97
+ :filters => ['browser==Firefox','pageviews>=10000']
98
98
 
99
99
  Once again, do _not_ include the +ga:+ prefix before the dimension/metric you're filtering against.
100
100
  Gattica will add this automatically.
@@ -187,6 +187,4 @@ A couple of things I have planned:
187
187
 
188
188
  1. Tests!
189
189
  2. The option to use a custom delimiter for output
190
- 3. Automatically handle paging (the API only returns 1000 results at a time). Gattica will request
191
- one result set, see how many pages there are, then do several calls until all pages are retrieved
192
- or it hits the limit of the number of results you want and return all that data as one big block.
190
+
data/Rakefile CHANGED
@@ -11,6 +11,7 @@ begin
11
11
  gemspec.homepage = "http://github.com/cannikin/gattica"
12
12
  gemspec.description = "Gattica is a Ruby library for extracting data from the Google Analytics API."
13
13
  gemspec.authors = ["Rob Cameron"]
14
+ gemspec.add_dependency('hpricot','>=0.6.164')
14
15
  end
15
16
  rescue LoadError
16
17
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 3
4
- :patch: 3
3
+ :minor: 4
4
+ :patch: 0
data/gattica.gemspec ADDED
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{gattica}
8
+ s.version = "0.4.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Rob Cameron"]
12
+ s.date = %q{2009-09-04}
13
+ s.description = %q{Gattica is a Ruby library for extracting data from the Google Analytics API.}
14
+ s.email = %q{cannikinn@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "History.txt",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION.yml",
26
+ "examples/example.rb",
27
+ "gattica.gemspec",
28
+ "lib/gattica.rb",
29
+ "lib/gattica/account.rb",
30
+ "lib/gattica/auth.rb",
31
+ "lib/gattica/convertible.rb",
32
+ "lib/gattica/core_extensions.rb",
33
+ "lib/gattica/data_point.rb",
34
+ "lib/gattica/data_set.rb",
35
+ "lib/gattica/exceptions.rb",
36
+ "lib/gattica/user.rb",
37
+ "test/helper.rb",
38
+ "test/suite.rb",
39
+ "test/test_auth.rb",
40
+ "test/test_engine.rb",
41
+ "test/test_gattica.rb",
42
+ "test/test_user.rb"
43
+ ]
44
+ s.has_rdoc = true
45
+ s.homepage = %q{http://github.com/cannikin/gattica}
46
+ s.rdoc_options = ["--charset=UTF-8"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = %q{1.3.1}
49
+ s.summary = %q{Gattica is a Ruby library for extracting data from the Google Analytics API.}
50
+ s.test_files = [
51
+ "test/helper.rb",
52
+ "test/suite.rb",
53
+ "test/test_auth.rb",
54
+ "test/test_engine.rb",
55
+ "test/test_gattica.rb",
56
+ "test/test_user.rb",
57
+ "examples/example.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 2
63
+
64
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<hpricot>, [">= 0.6.164"])
66
+ else
67
+ s.add_dependency(%q<hpricot>, [">= 0.6.164"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<hpricot>, [">= 0.6.164"])
71
+ end
72
+ end
data/lib/gattica.rb CHANGED
@@ -26,7 +26,7 @@ require 'gattica/data_point'
26
26
 
27
27
  module Gattica
28
28
 
29
- VERSION = '0.3.1'
29
+ VERSION = '0.4.0'
30
30
 
31
31
  # Creates a new instance of Gattica::Engine and gets us going. Please see the README for usage docs.
32
32
  #
@@ -44,7 +44,7 @@ module Gattica
44
44
  SERVER = 'www.google.com'
45
45
  PORT = 443
46
46
  SECURE = true
47
- DEFAULT_ARGS = { :start_date => nil, :end_date => nil, :dimensions => [], :metrics => [], :filters => [], :sort => [] }
47
+ DEFAULT_ARGS = { :start_date => nil, :end_date => nil, :dimensions => [], :metrics => [], :filters => [], :sort => [], :start_index => 1, :max_results => 10000, :page => false }
48
48
  DEFAULT_OPTIONS = { :email => nil, :password => nil, :token => nil, :profile_id => nil, :debug => false, :headers => {}, :logger => Logger.new(STDOUT) }
49
49
  FILTER_METRIC_OPERATORS = %w{ == != > < >= <= }
50
50
  FILTER_DIMENSION_OPERATORS = %w{ == != =~ !~ =@ ~@ }
@@ -71,8 +71,18 @@ module Gattica
71
71
  @user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
72
72
  @headers = {}.merge(@options[:headers]) # headers used for any HTTP requests (Google requires a special 'Authorization' header which is set any time @token is set)
73
73
 
74
- # save an http connection for everyone to use
75
- @http = Net::HTTP.new(SERVER, PORT)
74
+ # save a proxy-aware http connection for everyone to use
75
+ proxy_host = nil
76
+ proxy_port = nil
77
+ proxy_var = SECURE ? 'https_proxy' : 'http_proxy'
78
+ [proxy_var, proxy_var.upcase].each do |pxy|
79
+ if ENV[pxy]
80
+ uri = URI::parse(ENV[pxy])
81
+ proxy_host = uri.host
82
+ proxy_port = uri.port
83
+ end
84
+ end
85
+ @http = Net::HTTP::Proxy(proxy_host,proxy_port).new(SERVER, PORT)
76
86
  @http.use_ssl = SECURE
77
87
  @http.set_debug_output $stdout if @options[:debug]
78
88
 
@@ -116,7 +126,26 @@ module Gattica
116
126
  end
117
127
  return @user_accounts
118
128
  end
119
-
129
+
130
+ # Performs a Gattica::Engine#get but instead of returning the dataset streams it to the file handle in a CSV format
131
+ #
132
+ # == Usage
133
+ #
134
+ # gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
135
+ # fh = File.new("file.csv", "w")
136
+ # gs.get_to_csv({ :start_date => '2008-01-01',
137
+ # :end_date => '2008-02-01',
138
+ # :dimensions => 'browser',
139
+ # :metrics => 'pageviews',
140
+ # :sort => 'pageviews',
141
+ # :filters => ['browser == Firefox']}, fh, :short)
142
+ #
143
+ # See Gattica::Engine#get to see details of arguments
144
+
145
+ def get_to_csv(args={}, fh = nil, format = :long)
146
+ raise GatticaError::InvalidFileType, "Invalid file handle" unless !fh.nil?
147
+ results(args, fh, :csv, format)
148
+ end
120
149
 
121
150
  # This is the method that performs the actual request to get data.
122
151
  #
@@ -146,6 +175,9 @@ module Gattica
146
175
  # * +metrics+ => an array of GA metrics (without the ga: prefix)
147
176
  # * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
148
177
  # * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
178
+ # * +page+ => true|false Does the paging to create a single set of all of the data
179
+ # * +start_index+ => Beginning offset of the query (default 1)
180
+ # * +max_results+ => How many results to grab (maximum 10,000)
149
181
  #
150
182
  # == Exceptions
151
183
  #
@@ -154,13 +186,44 @@ module Gattica
154
186
  # error back from Google Analytics telling you so.
155
187
 
156
188
  def get(args={})
157
- args = validate_and_clean(DEFAULT_ARGS.merge(args))
158
- query_string = build_query_string(args,@profile_id)
159
- @logger.debug(query_string) if @debug
160
- data = do_http_get("/analytics/feeds/data?#{query_string}")
161
- return DataSet.new(Hpricot.XML(data))
189
+ return results(args)
162
190
  end
163
191
 
192
+ private
193
+
194
+ def results(args={}, fh=nil, type=nil, format=nil)
195
+ raise GatticaError::InvalidFileType, "Invalid file type" unless type.nil? ||[:csv,:xml].include?(type)
196
+ args = validate_and_clean(DEFAULT_ARGS.merge(args))
197
+
198
+ header = 0
199
+ results = nil
200
+ total_results = args[:max_results]
201
+ while(args[:start_index] < total_results)
202
+ query_string = build_query_string(args,@profile_id)
203
+ @logger.debug("Query String: " + query_string) if @debug
204
+
205
+ data = do_http_get("/analytics/feeds/data?#{query_string}")
206
+ result = DataSet.new(Hpricot.XML(data))
207
+
208
+ #handle returning results
209
+ results.points.concat(result.points) if !results.nil? && fh.nil?
210
+ #handle csv
211
+
212
+ if(!fh.nil? && type == :csv && header == 0)
213
+ fh.write result.to_csv_header(format)
214
+ header = 1
215
+ end
216
+
217
+ fh.write result.to_csv(:noheader) if !fh.nil? && type == :csv
218
+ fh.flush if !fh.nil?
219
+
220
+ results = result if results.nil?
221
+ total_results = result.total_results
222
+ args[:start_index] += args[:max_results]
223
+ break if !args[:page] # only continue while if we are suppose to page
224
+ end
225
+ return results if fh.nil?
226
+ end
164
227
 
165
228
  # Since google wants the token to appear in any HTTP call's header, we have to set that header
166
229
  # again any time @token is changed so we override the default writer (note that you need to set
@@ -172,9 +235,6 @@ module Gattica
172
235
  end
173
236
 
174
237
 
175
- private
176
-
177
-
178
238
  # Does the work of making HTTP calls and then going through a suite of tests on the response to make
179
239
  # sure it's valid and not an error
180
240
 
@@ -196,6 +256,7 @@ module Gattica
196
256
  return data
197
257
  end
198
258
 
259
+ private
199
260
 
200
261
  # Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
201
262
  # or manually by the user since the header must include the token)
@@ -206,19 +267,27 @@ module Gattica
206
267
 
207
268
  # Creates a valid query string for GA
208
269
  def build_query_string(args,profile)
209
- output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}"
210
- unless args[:dimensions].empty?
211
- output += '&dimensions=' + args[:dimensions].collect do |dimension|
270
+ query_params = args.clone
271
+ ga_start_date = query_params.delete(:start_date)
272
+ ga_end_date = query_params.delete(:end_date)
273
+ ga_dimensions = query_params.delete(:dimensions)
274
+ ga_metrics = query_params.delete(:metrics)
275
+ ga_sort = query_params.delete(:sort)
276
+ ga_filters = query_params.delete(:filters)
277
+
278
+ output = "ids=ga:#{profile}&start-date=#{ga_start_date}&end-date=#{ga_end_date}"
279
+ unless ga_dimensions.nil? || ga_dimensions.empty?
280
+ output += '&dimensions=' + ga_dimensions.collect do |dimension|
212
281
  "ga:#{dimension}"
213
282
  end.join(',')
214
283
  end
215
- unless args[:metrics].empty?
216
- output += '&metrics=' + args[:metrics].collect do |metric|
284
+ unless ga_metrics.nil? || ga_metrics.empty?
285
+ output += '&metrics=' + ga_metrics.collect do |metric|
217
286
  "ga:#{metric}"
218
287
  end.join(',')
219
288
  end
220
- unless args[:sort].empty?
221
- output += '&sort=' + args[:sort].collect do |sort|
289
+ unless ga_sort.nil? || ga_sort.empty?
290
+ output += '&sort=' + Array(ga_sort).collect do |sort|
222
291
  sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
223
292
  end.join(',')
224
293
  end
@@ -226,7 +295,7 @@ module Gattica
226
295
  # TODO: update so that in regular expression filters (=~ and !~), any initial special characters in the regular expression aren't also picked up as part of the operator (doesn't cause a problem, but just feels dirty)
227
296
  unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
228
297
  output += '&filters=' + args[:filters].collect do |filter|
229
- match, name, operator, expression = *filter.match(/^(\w*)(\W*)(.*)$/) # splat the resulting Match object to pull out the parts automatically
298
+ match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
230
299
  unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
231
300
  "ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
232
301
  else
@@ -234,6 +303,9 @@ module Gattica
234
303
  end
235
304
  end.join(';')
236
305
  end
306
+
307
+ query_params.inject(output) {|m,(key,value)| m << "&#{key}=#{value}"}
308
+
237
309
  return output
238
310
  end
239
311
 
@@ -1,4 +1,4 @@
1
- require 'csv'
1
+ require "csv"
2
2
 
3
3
  module Gattica
4
4
 
@@ -17,10 +17,10 @@ module Gattica
17
17
  @updated = DateTime.parse(xml.at('updated').inner_html)
18
18
  @title = xml.at('title').inner_html
19
19
  @dimensions = xml.search('dxp:dimension').collect do |dimension|
20
- { dimension.attributes['name'].split(':').last.to_sym => dimension.attributes['value'].split(':').last }
20
+ { dimension.attributes['name'].split(':').last.to_sym => dimension.attributes['value'].split(':', 1).last }
21
21
  end
22
22
  @metrics = xml.search('dxp:metric').collect do |metric|
23
- { metric.attributes['name'].split(':').last.to_sym => metric.attributes['value'].split(':').last.to_i }
23
+ { metric.attributes['name'].split(':').last.to_sym => metric.attributes['value'].split(':', 1).last.to_i }
24
24
  end
25
25
  end
26
26
 
@@ -28,19 +28,19 @@ module Gattica
28
28
  # Outputs in Comma Seperated Values format
29
29
  def to_csv(format = :long)
30
30
  output = ''
31
- columns = []
32
31
 
32
+ columns = []
33
33
  # only output
34
34
  case format
35
35
  when :long
36
- [@id, @updated, @title].each { |c| columns << c }
36
+ columns.concat([@id, @updated, @title])
37
37
  end
38
38
 
39
39
  # output all dimensions
40
- @dimensions.map {|d| d.value}.each { |c| columns << c }
40
+ columns.concat(@dimensions.map {|d| d.value})
41
41
 
42
42
  # output all metrics
43
- @metrics.map {|m| m.value}.each { |c| columns << c }
43
+ columns.concat(@metrics.map {|m| m.value})
44
44
 
45
45
  output = CSV.generate_line(columns)
46
46
  return output
@@ -57,4 +57,4 @@ module Gattica
57
57
 
58
58
  end
59
59
 
60
- end
60
+ end
@@ -18,27 +18,40 @@ module Gattica
18
18
  @points = xml.search(:entry).collect { |entry| DataPoint.new(entry) }
19
19
  end
20
20
 
21
-
22
- # output important data to CSV, ignoring all the specific data about this dataset
23
- # (total_results, start_date) and just output the data from the points
24
-
25
- def to_csv(format = :long)
21
+ def to_csv_header(format = :long)
26
22
  # build the headers
27
23
  output = ''
28
24
  columns = []
29
25
 
30
26
  # only show the nitty gritty details of id, updated_at and title if requested
31
- case format
27
+ case format #it would be nice if case statements in ruby worked differently
32
28
  when :long
33
- ["id", "updated", "title"].each { |c| columns << c }
29
+ columns.concat(["id", "updated", "title"])
30
+ unless @points.empty? # if there was at least one result
31
+ columns.concat(@points.first.dimensions.map {|d| d.key})
32
+ columns.concat(@points.first.metrics.map {|m| m.key})
33
+ end
34
+ when :short
35
+ unless @points.empty? # if there was at least one result
36
+ columns.concat(@points.first.dimensions.map {|d| d.key})
37
+ columns.concat(@points.first.metrics.map {|m| m.key})
38
+ end
39
+ when :noheader
34
40
  end
35
41
 
36
- unless @points.empty? # if there was at least one result
37
- @points.first.dimensions.map {|d| d.key}.each { |c| columns << c }
38
- @points.first.metrics.map {|m| m.key}.each { |c| columns << c }
39
- end
42
+ output = CSV.generate_line(columns) + "\n" if (columns.size > 0)
43
+
44
+ return output
45
+ end
46
+
47
+ # output important data to CSV, ignoring all the specific data about this dataset
48
+ # (total_results, start_date) and just output the data from the points
49
+
50
+ def to_csv(format = :long)
51
+ output = ''
40
52
 
41
- output = CSV.generate_line(columns) + "\n"
53
+ # build the headers
54
+ output = to_csv_header(format)
42
55
 
43
56
  # get the data from each point
44
57
  @points.each do |point|
@@ -60,4 +73,4 @@ module Gattica
60
73
 
61
74
  end
62
75
 
63
- end
76
+ end
@@ -1,4 +1,6 @@
1
1
  module GatticaError
2
+ # usage errors
3
+ class InvalidFileType < StandardError; end;
2
4
  # user errors
3
5
  class InvalidEmail < StandardError; end;
4
6
  class InvalidPassword < StandardError; end;
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestUser < Test::Unit::TestCase
4
+ def test_build_query_string
5
+ @gattica = Gattica.new(:token => 'ga-token', :profile_id => 'ga-profile_id')
6
+ expected = "ids=ga:ga-profile_id&start-date=2008-01-02&end-date=2008-01-03&dimensions=ga:pageTitle,ga:pagePath&metrics=ga:pageviews&sort=-ga:pageviews&max-results=3"
7
+ result = @gattica.send(:build_query_string, {
8
+ :start_date => Date.civil(2008,1,2),
9
+ :end_date => Date.civil(2008,1,3),
10
+ :dimensions => ['pageTitle','pagePath'],
11
+ :metrics => ['pageviews'],
12
+ :sort => '-pageviews',
13
+ 'max-results' => '3'}, 'ga-profile_id')
14
+ assert_equal expected, result
15
+ end
16
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cannikin-gattica
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Cameron
@@ -9,10 +9,19 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-18 00:00:00 -07:00
12
+ date: 2009-09-04 00:00:00 -07:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.6.164
24
+ version:
16
25
  description: Gattica is a Ruby library for extracting data from the Google Analytics API.
17
26
  email: cannikinn@gmail.com
18
27
  executables: []
@@ -23,12 +32,14 @@ extra_rdoc_files:
23
32
  - LICENSE
24
33
  - README.rdoc
25
34
  files:
35
+ - .gitignore
26
36
  - History.txt
27
37
  - LICENSE
28
38
  - README.rdoc
29
39
  - Rakefile
30
40
  - VERSION.yml
31
41
  - examples/example.rb
42
+ - gattica.gemspec
32
43
  - lib/gattica.rb
33
44
  - lib/gattica/account.rb
34
45
  - lib/gattica/auth.rb
@@ -42,6 +53,7 @@ files:
42
53
  - test/suite.rb
43
54
  - test/test_auth.rb
44
55
  - test/test_engine.rb
56
+ - test/test_gattica.rb
45
57
  - test/test_user.rb
46
58
  has_rdoc: true
47
59
  homepage: http://github.com/cannikin/gattica
@@ -74,5 +86,6 @@ test_files:
74
86
  - test/suite.rb
75
87
  - test/test_auth.rb
76
88
  - test/test_engine.rb
89
+ - test/test_gattica.rb
77
90
  - test/test_user.rb
78
91
  - examples/example.rb