gattica 0.4.1 → 0.4.3

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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  (The MIT License)
2
2
 
3
- Copyright (c) 2009 Rob Cameron
3
+ Copyright (c) 2009 The Active Network
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :minor: 4
3
- :patch: 1
3
+ :patch: 2
4
4
  :major: 0
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{gattica}
8
- s.version = "0.4.1"
8
+ s.version = "0.4.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["The Active Network"]
@@ -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.0'
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.debug("Query String: " + query_string) if @debug
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
- results = result if results.nil?
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::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
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 = args[:dimensions] + args[:metrics]
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
@@ -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
@@ -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
- 'max-results' => '3'}, 'ga-profile_id')
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
- version: 0.4.1
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
- type: :runtime
18
- version_requirement:
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
- version:
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.5
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.