gattica 0.4.1 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/LICENSE +1 -1
- data/VERSION.yml +1 -1
- data/gattica.gemspec +1 -1
- data/lib/gattica.rb +88 -68
- data/lib/gattica/user.rb +9 -9
- data/test/test_gattica.rb +5 -5
- metadata +19 -8
data/History.txt
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
== 0.4.3
|
2
|
+
* er1c fixed start-index
|
3
|
+
* rpanachi fixed max_results
|
4
|
+
|
5
|
+
== 0.4.2
|
6
|
+
* rpanachi Updates for max_results
|
7
|
+
* Fixed various nil references from multiple people
|
8
|
+
* howcast Updated Email Regex to allow "username+suffix@gmail.com"
|
9
|
+
* scottpersinger Added V2 API Parameter for Segment filter
|
10
|
+
|
1
11
|
== 0.4.0
|
2
12
|
* er1c added start_index and max_results
|
3
13
|
* er1c added paging for all results
|
data/LICENSE
CHANGED
data/VERSION.yml
CHANGED
data/gattica.gemspec
CHANGED
data/lib/gattica.rb
CHANGED
@@ -25,22 +25,22 @@ require 'gattica/data_point'
|
|
25
25
|
# Please see the README for usage docs.
|
26
26
|
|
27
27
|
module Gattica
|
28
|
-
|
29
|
-
VERSION = '0.4.
|
30
|
-
|
28
|
+
|
29
|
+
VERSION = '0.4.3'
|
30
|
+
|
31
31
|
# Creates a new instance of Gattica::Engine and gets us going. Please see the README for usage docs.
|
32
32
|
#
|
33
33
|
# ga = Gattica.new({:email => 'anonymous@anon.com', :password => 'password, :profile_id => 123456 })
|
34
|
-
|
34
|
+
|
35
35
|
def self.new(*args)
|
36
36
|
Engine.new(*args)
|
37
37
|
end
|
38
|
-
|
39
|
-
# The real meat of Gattica, deals with talking to GA, returning and parsing results. You actually get
|
38
|
+
|
39
|
+
# The real meat of Gattica, deals with talking to GA, returning and parsing results. You actually get
|
40
40
|
# an instance of this when you go Gattica.new()
|
41
|
-
|
41
|
+
|
42
42
|
class Engine
|
43
|
-
|
43
|
+
|
44
44
|
SERVER = 'www.google.com'
|
45
45
|
PORT = 443
|
46
46
|
SECURE = true
|
@@ -48,10 +48,10 @@ module Gattica
|
|
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{ == != =~ !~ =@ ~@ }
|
51
|
-
|
51
|
+
|
52
52
|
attr_reader :user
|
53
53
|
attr_accessor :profile_id, :token
|
54
|
-
|
54
|
+
|
55
55
|
# Create a user, and get them authorized.
|
56
56
|
# If you're making a web app you're going to want to save the token that's retrieved by Gattica
|
57
57
|
# so that you can use it later (Google recommends not re-authenticating the user for each and every request)
|
@@ -62,15 +62,17 @@ module Gattica
|
|
62
62
|
# Or if you already have the token (because you authenticated previously and now want to reuse that session):
|
63
63
|
#
|
64
64
|
# ga = Gattica.new({:token => '23ohda09hw...', :profile_id => 123456})
|
65
|
-
|
65
|
+
|
66
66
|
def initialize(options={})
|
67
67
|
@options = DEFAULT_OPTIONS.merge(options)
|
68
68
|
@logger = @options[:logger]
|
69
|
-
|
69
|
+
@logger.level = Logger::INFO
|
70
|
+
|
71
|
+
|
70
72
|
@profile_id = @options[:profile_id] # if you don't include the profile_id now, you'll have to set it manually later via Gattica::Engine#profile_id=
|
71
73
|
@user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
|
72
74
|
@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
|
-
|
75
|
+
|
74
76
|
# save a proxy-aware http connection for everyone to use
|
75
77
|
proxy_host = nil
|
76
78
|
proxy_port = nil
|
@@ -85,7 +87,7 @@ module Gattica
|
|
85
87
|
@http = Net::HTTP::Proxy(proxy_host,proxy_port).new(SERVER, PORT)
|
86
88
|
@http.use_ssl = SECURE
|
87
89
|
@http.set_debug_output $stdout if @options[:debug]
|
88
|
-
|
90
|
+
|
89
91
|
# authenticate
|
90
92
|
if @options[:email] && @options[:password] # email and password: authenticate, get a token from Google's ClientLogin, save it for later
|
91
93
|
@user = User.new(@options[:email], @options[:password])
|
@@ -96,11 +98,11 @@ module Gattica
|
|
96
98
|
else # no login or token, you can't do anything
|
97
99
|
raise GatticaError::NoLoginOrToken, 'You must provide an email and password, or authentication token'
|
98
100
|
end
|
99
|
-
|
101
|
+
|
100
102
|
# TODO: check that the user has access to the specified profile and show an error here rather than wait for Google to respond with a message
|
101
103
|
end
|
102
|
-
|
103
|
-
|
104
|
+
|
105
|
+
|
104
106
|
# Returns the list of accounts the user has access to. A user may have multiple accounts on Google Analytics
|
105
107
|
# and each account may have multiple profiles. You need the profile_id in order to get info from GA. If you
|
106
108
|
# don't know the profile_id then use this method to get a list of all them. Then set the profile_id of your
|
@@ -116,7 +118,7 @@ module Gattica
|
|
116
118
|
# get the accounts and find a profile_id - you apparently already know it!
|
117
119
|
#
|
118
120
|
# See Gattica::Engine#get to see how to get some data.
|
119
|
-
|
121
|
+
|
120
122
|
def accounts
|
121
123
|
# if we haven't retrieved the user's accounts yet, get them now and save them
|
122
124
|
if @user_accounts.nil?
|
@@ -133,35 +135,35 @@ module Gattica
|
|
133
135
|
#
|
134
136
|
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
135
137
|
# 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',
|
138
|
+
# gs.get_to_csv({ :start_date => '2008-01-01',
|
139
|
+
# :end_date => '2008-02-01',
|
140
|
+
# :dimensions => 'browser',
|
141
|
+
# :metrics => 'pageviews',
|
140
142
|
# :sort => 'pageviews',
|
141
143
|
# :filters => ['browser == Firefox']}, fh, :short)
|
142
144
|
#
|
143
145
|
# See Gattica::Engine#get to see details of arguments
|
144
146
|
|
145
|
-
def get_to_csv(args={}, fh = nil, format = :long)
|
147
|
+
def get_to_csv(args={}, fh = nil, format = :long)
|
146
148
|
raise GatticaError::InvalidFileType, "Invalid file handle" unless !fh.nil?
|
147
149
|
results(args, fh, :csv, format)
|
148
150
|
end
|
149
|
-
|
151
|
+
|
150
152
|
# This is the method that performs the actual request to get data.
|
151
153
|
#
|
152
154
|
# == Usage
|
153
155
|
#
|
154
156
|
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
155
|
-
# gs.get({ :start_date => '2008-01-01',
|
156
|
-
# :end_date => '2008-02-01',
|
157
|
-
# :dimensions => 'browser',
|
158
|
-
# :metrics => 'pageviews',
|
157
|
+
# gs.get({ :start_date => '2008-01-01',
|
158
|
+
# :end_date => '2008-02-01',
|
159
|
+
# :dimensions => 'browser',
|
160
|
+
# :metrics => 'pageviews',
|
159
161
|
# :sort => 'pageviews',
|
160
162
|
# :filters => ['browser == Firefox']})
|
161
163
|
#
|
162
164
|
# == Input
|
163
165
|
#
|
164
|
-
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
|
166
|
+
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
|
165
167
|
# Google Analytics, see http://code.google.com/apis/analytics/docs
|
166
168
|
#
|
167
169
|
# Required values are:
|
@@ -184,13 +186,13 @@ module Gattica
|
|
184
186
|
# If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
|
185
187
|
# Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
|
186
188
|
# error back from Google Analytics telling you so.
|
187
|
-
|
189
|
+
|
188
190
|
def get(args={})
|
189
191
|
return results(args)
|
190
192
|
end
|
191
|
-
|
193
|
+
|
192
194
|
private
|
193
|
-
|
195
|
+
|
194
196
|
def results(args={}, fh=nil, type=nil, format=nil)
|
195
197
|
raise GatticaError::InvalidFileType, "Invalid file type" unless type.nil? ||[:csv,:xml].include?(type)
|
196
198
|
args = validate_and_clean(DEFAULT_ARGS.merge(args))
|
@@ -200,47 +202,48 @@ module Gattica
|
|
200
202
|
total_results = args[:max_results]
|
201
203
|
while(args[:start_index] < total_results)
|
202
204
|
query_string = build_query_string(args,@profile_id)
|
203
|
-
@logger.
|
205
|
+
@logger.info("Start Index: #{args[:start_index]}, Total Results: #{total_results}, Query String: " + query_string) if @options[:debug]
|
204
206
|
|
205
207
|
data = do_http_get("/analytics/feeds/data?#{query_string}")
|
206
208
|
result = DataSet.new(Hpricot.XML(data))
|
207
|
-
|
209
|
+
|
208
210
|
#handle returning results
|
209
211
|
results.points.concat(result.points) if !results.nil? && fh.nil?
|
212
|
+
results = result if results.nil?
|
213
|
+
|
210
214
|
#handle csv
|
211
|
-
|
212
215
|
if(!fh.nil? && type == :csv && header == 0)
|
213
216
|
fh.write result.to_csv_header(format)
|
214
|
-
header = 1
|
217
|
+
header = 1
|
215
218
|
end
|
216
|
-
|
219
|
+
|
217
220
|
fh.write result.to_csv(:noheader) if !fh.nil? && type == :csv
|
218
221
|
fh.flush if !fh.nil?
|
219
|
-
|
220
|
-
|
222
|
+
|
223
|
+
# Update Loop Counters
|
221
224
|
total_results = result.total_results
|
222
225
|
args[:start_index] += args[:max_results]
|
223
226
|
break if !args[:page] # only continue while if we are suppose to page
|
224
|
-
end
|
227
|
+
end
|
225
228
|
return results if fh.nil?
|
226
229
|
end
|
227
|
-
|
230
|
+
|
228
231
|
# Since google wants the token to appear in any HTTP call's header, we have to set that header
|
229
232
|
# again any time @token is changed so we override the default writer (note that you need to set
|
230
233
|
# @token with self.token= instead of @token=)
|
231
|
-
|
234
|
+
|
232
235
|
def token=(token)
|
233
236
|
@token = token
|
234
237
|
set_http_headers
|
235
238
|
end
|
236
|
-
|
237
|
-
|
239
|
+
|
240
|
+
|
238
241
|
# Does the work of making HTTP calls and then going through a suite of tests on the response to make
|
239
242
|
# sure it's valid and not an error
|
240
|
-
|
243
|
+
|
241
244
|
def do_http_get(query_string)
|
242
245
|
response, data = @http.get(query_string, @headers)
|
243
|
-
|
246
|
+
|
244
247
|
# error checking
|
245
248
|
if response.code != '200'
|
246
249
|
case response.code
|
@@ -252,29 +255,38 @@ module Gattica
|
|
252
255
|
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
253
256
|
end
|
254
257
|
end
|
255
|
-
|
258
|
+
|
256
259
|
return data
|
257
260
|
end
|
258
|
-
|
261
|
+
|
259
262
|
private
|
260
|
-
|
263
|
+
|
261
264
|
# Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
|
262
265
|
# or manually by the user since the header must include the token)
|
263
266
|
def set_http_headers
|
264
267
|
@headers['Authorization'] = "GoogleLogin auth=#{@token}"
|
268
|
+
@headers['GData-Version']= '2'
|
265
269
|
end
|
266
|
-
|
267
|
-
|
270
|
+
|
271
|
+
|
268
272
|
# Creates a valid query string for GA
|
269
273
|
def build_query_string(args,profile)
|
270
274
|
query_params = args.clone
|
275
|
+
|
276
|
+
# Internal Parameters, don't pass to google
|
277
|
+
query_params.delete(:debug)
|
278
|
+
query_params.delete(:page)
|
279
|
+
|
271
280
|
ga_start_date = query_params.delete(:start_date)
|
272
281
|
ga_end_date = query_params.delete(:end_date)
|
273
282
|
ga_dimensions = query_params.delete(:dimensions)
|
274
283
|
ga_metrics = query_params.delete(:metrics)
|
275
284
|
ga_sort = query_params.delete(:sort)
|
276
285
|
ga_filters = query_params.delete(:filters)
|
277
|
-
|
286
|
+
ga_segment = query_params.delete(:segment)
|
287
|
+
ga_start_index = query_params.delete(:start_index) || query_params.delete(:'start-index')
|
288
|
+
ga_max_results = query_params.delete(:max_results) || query_params.delete(:'max-results')
|
289
|
+
|
278
290
|
output = "ids=ga:#{profile}&start-date=#{ga_start_date}&end-date=#{ga_end_date}"
|
279
291
|
unless ga_dimensions.nil? || ga_dimensions.empty?
|
280
292
|
output += '&dimensions=' + ga_dimensions.collect do |dimension|
|
@@ -291,9 +303,13 @@ module Gattica
|
|
291
303
|
sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
|
292
304
|
end.join(',')
|
293
305
|
end
|
294
|
-
|
306
|
+
|
307
|
+
unless ga_segment.nil? || ga_segment.empty?
|
308
|
+
output += "&segment=#{ga_segment}"
|
309
|
+
end
|
310
|
+
|
295
311
|
# 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)
|
296
|
-
unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
|
312
|
+
unless args[:filters].nil? || args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
|
297
313
|
output += '&filters=' + args[:filters].collect do |filter|
|
298
314
|
match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
|
299
315
|
unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
|
@@ -303,23 +319,27 @@ module Gattica
|
|
303
319
|
end
|
304
320
|
end.join(';')
|
305
321
|
end
|
306
|
-
|
322
|
+
|
323
|
+
output += "&start-index=#{ga_start_index}" unless ga_start_index.nil? || ga_start_index.to_s.empty?
|
324
|
+
output += "&max-results=#{ga_max_results}" unless ga_max_results.nil? || ga_max_results.to_s.empty?
|
325
|
+
|
307
326
|
query_params.inject(output) {|m,(key,value)| m << "&#{key}=#{value}"}
|
308
|
-
|
327
|
+
|
309
328
|
return output
|
310
329
|
end
|
311
|
-
|
312
|
-
|
330
|
+
|
331
|
+
|
313
332
|
# Validates that the args passed to +get+ are valid
|
314
333
|
def validate_and_clean(args)
|
315
|
-
|
316
|
-
raise GatticaError::
|
317
|
-
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
|
334
|
+
raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].to_s.empty?
|
335
|
+
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].to_s.empty?
|
318
336
|
raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
|
319
337
|
raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
|
320
|
-
|
321
|
-
possible =
|
322
|
-
|
338
|
+
|
339
|
+
possible = []
|
340
|
+
possible << args[:dimensions] << args[:metrics]
|
341
|
+
possible.flatten!
|
342
|
+
|
323
343
|
# make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
|
324
344
|
if args[:sort]
|
325
345
|
missing = args[:sort].find_all do |arg|
|
@@ -329,7 +349,7 @@ module Gattica
|
|
329
349
|
raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
330
350
|
end
|
331
351
|
end
|
332
|
-
|
352
|
+
|
333
353
|
# make sure that the user is only trying to filter fields that are in dimensions or metrics
|
334
354
|
if args[:filters]
|
335
355
|
missing = args[:filters].find_all do |arg|
|
@@ -339,10 +359,10 @@ module Gattica
|
|
339
359
|
raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
340
360
|
end
|
341
361
|
end
|
342
|
-
|
362
|
+
|
343
363
|
return args
|
344
364
|
end
|
345
|
-
|
346
|
-
|
365
|
+
|
366
|
+
|
347
367
|
end
|
348
368
|
end
|
data/lib/gattica/user.rb
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
module Gattica
|
2
|
-
|
2
|
+
|
3
3
|
# Represents a user to be authenticated by GA
|
4
|
-
|
4
|
+
|
5
5
|
class User
|
6
|
-
|
6
|
+
|
7
7
|
include Convertible
|
8
|
-
|
8
|
+
|
9
9
|
attr_accessor :email, :password
|
10
|
-
|
10
|
+
|
11
11
|
def initialize(email,password)
|
12
12
|
@email = email
|
13
13
|
@password = password
|
14
14
|
validate
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
# User gets a special +to_h+ because Google expects +Email+ and +Passwd+ instead of our nicer internal names
|
18
18
|
def to_h
|
19
19
|
{ :Email => @email,
|
20
20
|
:Passwd => @password }
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
private
|
24
24
|
# Determine whether or not this is a valid user
|
25
25
|
def validate
|
26
|
-
raise GatticaError::InvalidEmail, "The email address '#{@email}' is not valid" if not @email.match(/^(?:[_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-zA-Z0-9\-\.]+)*(\.[a-z]{2,4})$/i)
|
26
|
+
raise GatticaError::InvalidEmail, "The email address '#{@email}' is not valid" if not @email.match(/^(?:[\+_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-zA-Z0-9\-\.]+)*(\.[a-z]{2,4})$/i)
|
27
27
|
raise GatticaError::InvalidPassword, "The password cannot be blank" if @password.empty? || @password.nil?
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
end
|
31
31
|
end
|
data/test/test_gattica.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/helper'
|
2
|
-
|
2
|
+
|
3
3
|
class TestUser < Test::Unit::TestCase
|
4
4
|
def test_build_query_string
|
5
5
|
@gattica = Gattica.new(:token => 'ga-token', :profile_id => 'ga-profile_id')
|
6
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
7
|
result = @gattica.send(:build_query_string, {
|
8
|
-
:start_date => Date.civil(2008,1,2),
|
8
|
+
:start_date => Date.civil(2008,1,2),
|
9
9
|
:end_date => Date.civil(2008,1,3),
|
10
|
-
:dimensions => ['pageTitle','pagePath'],
|
11
|
-
:metrics => ['pageviews'],
|
10
|
+
:dimensions => ['pageTitle','pagePath'],
|
11
|
+
:metrics => ['pageviews'],
|
12
12
|
:sort => '-pageviews',
|
13
|
-
|
13
|
+
:max_results => '3'}, 'ga-profile_id')
|
14
14
|
assert_equal expected, result
|
15
15
|
end
|
16
16
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gattica
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 4
|
8
|
+
- 3
|
9
|
+
version: 0.4.3
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- The Active Network
|
@@ -14,14 +19,18 @@ default_executable:
|
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: hpricot
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
24
|
requirements:
|
21
25
|
- - ">="
|
22
26
|
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 6
|
30
|
+
- 164
|
23
31
|
version: 0.6.164
|
24
|
-
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
25
34
|
description: Gattica is a Ruby library for extracting data from the Google Analytics API.
|
26
35
|
email: rob.cameron@active.com
|
27
36
|
executables: []
|
@@ -68,18 +77,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
77
|
requirements:
|
69
78
|
- - ">="
|
70
79
|
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 0
|
71
82
|
version: "0"
|
72
|
-
version:
|
73
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
84
|
requirements:
|
75
85
|
- - ">="
|
76
86
|
- !ruby/object:Gem::Version
|
87
|
+
segments:
|
88
|
+
- 0
|
77
89
|
version: "0"
|
78
|
-
version:
|
79
90
|
requirements: []
|
80
91
|
|
81
92
|
rubyforge_project:
|
82
|
-
rubygems_version: 1.3.
|
93
|
+
rubygems_version: 1.3.6
|
83
94
|
signing_key:
|
84
95
|
specification_version: 3
|
85
96
|
summary: Gattica is a Ruby library for extracting data from the Google Analytics API.
|