gattica 0.4.1 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.