gimite-google-spreadsheet-ruby 0.0.2 → 0.0.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.
Files changed (2) hide show
  1. data/lib/google_spreadsheet.rb +203 -86
  2. metadata +2 -2
@@ -20,22 +20,25 @@ module GoogleSpreadsheet
20
20
  end
21
21
 
22
22
  # Restores GoogleSpreadsheet::Session from +path+ and returns it.
23
- # If +path+ doesn't exist, prompts mail and password on console, authenticates with them,
24
- # stores the session to +path+ and returns it.
23
+ # If +path+ doesn't exist or authentication has failed, prompts mail and password on console,
24
+ # authenticates with them, stores the session to +path+ and returns it.
25
25
  #
26
26
  # This method requires Ruby/Password library: http://www.caliban.org/ruby/ruby-password.shtml
27
27
  def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
28
- if File.exist?(path)
29
- return Session.new(File.read(path))
30
- else
28
+ session = Session.new(File.exist?(path) ? File.read(path) : nil)
29
+ session.on_auth_fail = proc() do
31
30
  require "password"
32
31
  $stderr.print("Mail: ")
33
32
  mail = $stdin.gets().chomp()
34
33
  password = Password.get()
35
- session = Session.login(mail, password)
34
+ session.login(mail, password)
36
35
  open(path, "w", 0600){ |f| f.write(session.auth_token) }
37
- return session
36
+ true
38
37
  end
38
+ if !session.auth_token
39
+ session.on_auth_fail.call()
40
+ end
41
+ return session
39
42
  end
40
43
 
41
44
 
@@ -43,14 +46,14 @@ module GoogleSpreadsheet
43
46
 
44
47
  module_function
45
48
 
46
- def http_post(url, data, header = {})
49
+ def http_request(method, url, data, header = {})
47
50
  uri = URI.parse(url)
48
51
  http = Net::HTTP.new(uri.host, uri.port)
49
52
  http.use_ssl = uri.scheme == "https"
50
53
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
51
54
  http.start() do
52
55
  path = uri.path + (uri.query ? "?#{uri.query}" : "")
53
- response = http.post(path, data, header)
56
+ response = http.__send__(method, path, data, header)
54
57
  if !(response.code =~ /^2/)
55
58
  raise(GoogleSpreadsheet::Error, "Response code #{response.code} for POST #{url}: " +
56
59
  CGI.unescapeHTML(response.body))
@@ -95,7 +98,22 @@ module GoogleSpreadsheet
95
98
 
96
99
  # The same as GoogleSpreadsheet.login.
97
100
  def self.login(mail, password)
101
+ session = Session.new()
102
+ session.login(mail, password)
103
+ return session
104
+ end
105
+
106
+ # Creates session object with given authentication token.
107
+ def initialize(auth_token = nil)
108
+ @auth_token = auth_token
109
+ end
110
+
111
+ # Authenticates with given +mail+ and +password+, and updates current session object
112
+ # if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
113
+ # Google Apps account is supported.
114
+ def login(mail, password)
98
115
  begin
116
+ @auth_token = nil
99
117
  params = {
100
118
  "accountType" => "HOSTED_OR_GOOGLE",
101
119
  "Email" => mail,
@@ -103,34 +121,46 @@ module GoogleSpreadsheet
103
121
  "service" => "wise",
104
122
  "source" => "Gimite-RubyGoogleSpreadsheet-1.00",
105
123
  }
106
- response = http_post("https://www.google.com/accounts/ClientLogin", encode_query(params))
107
- return Session.new(response.slice(/^Auth=(.*)$/, 1))
124
+ response = http_request(:post,
125
+ "https://www.google.com/accounts/ClientLogin", encode_query(params))
126
+ @auth_token = response.slice(/^Auth=(.*)$/, 1)
108
127
  rescue GoogleSpreadsheet::Error => ex
128
+ return true if @on_auth_fail && @on_auth_fail.call()
109
129
  raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
110
130
  end
111
131
  end
112
132
 
113
- # Creates session object with given authentication token.
114
- def initialize(auth_token)
115
- @auth_token = auth_token
116
- end
117
-
118
133
  # Authentication token.
119
- attr_reader(:auth_token)
134
+ attr_accessor(:auth_token)
135
+
136
+ # Proc or Method called when authentication has failed.
137
+ # When this function returns +true+, it tries again.
138
+ attr_accessor(:on_auth_fail)
120
139
 
121
140
  def get(url) #:nodoc:
122
- begin
123
- response = open(url, self.http_header){ |f| f.read() }
124
- rescue OpenURI::HTTPError => ex
125
- raise(GoogleSpreadsheet::Error, "Error #{ex.message} for GET #{url}: " +
126
- ex.io.read())
141
+ while true
142
+ begin
143
+ response = open(url, self.http_header){ |f| f.read() }
144
+ rescue OpenURI::HTTPError => ex
145
+ if ex.message =~ /^401/ && @on_auth_fail && @on_auth_fail.call()
146
+ next
147
+ end
148
+ raise(ex.message =~ /^401/ ? AuthenticationError : GoogleSpreadsheet::Error,
149
+ "Error #{ex.message} for GET #{url}: " + ex.io.read())
150
+ end
151
+ return Hpricot.XML(response)
127
152
  end
128
- return Hpricot.XML(response)
129
153
  end
130
154
 
131
155
  def post(url, data) #:nodoc:
132
156
  header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
133
- response = http_post(url, data, header)
157
+ response = http_request(:post, url, data, header)
158
+ return Hpricot.XML(response)
159
+ end
160
+
161
+ def put(url, data) #:nodoc:
162
+ header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
163
+ response = http_request(:put, url, data, header)
134
164
  return Hpricot.XML(response)
135
165
  end
136
166
 
@@ -262,16 +292,13 @@ module GoogleSpreadsheet
262
292
  @cells_feed_url = cells_feed_url
263
293
  @title = title
264
294
  @cells = nil
295
+ @input_values = nil
265
296
  @modified = Set.new()
266
297
  end
267
298
 
268
299
  # URL of cell-based feed of the spreadsheet.
269
300
  attr_reader(:cells_feed_url)
270
301
 
271
- # Title of the spreadsheet. So far not available if you get this object by
272
- # GoogleSpreadsheet::Spreadsheet#worksheet_by_url.
273
- attr_reader(:title)
274
-
275
302
  # Returns content of the cell as String. Top-left cell is [1, 1].
276
303
  def [](row, col)
277
304
  return self.cells[[row, col]] || ""
@@ -287,21 +314,72 @@ module GoogleSpreadsheet
287
314
  def []=(row, col, value)
288
315
  reload() if !@cells
289
316
  @cells[[row, col]] = value
317
+ @input_values[[row, col]] = value
290
318
  @modified.add([row, col])
319
+ self.max_rows = row if row > @max_rows
320
+ self.max_cols = col if col > @max_cols
291
321
  end
292
322
 
293
- # Number of the bottom-most non-empty row.
323
+ # Returns the value or the formula of the cell. Top-left cell is [1, 1].
324
+ #
325
+ # If user input "=A1+B1" to cell [1, 3], worksheet[1, 3] is "3" for example and
326
+ # worksheet.input_value(1, 3) is "=RC[-2]+RC[-1]".
327
+ def input_value(row, col)
328
+ reload() if !@cells
329
+ return @input_values[[row, col]] || ""
330
+ end
331
+
332
+ # Row number of the bottom-most non-empty row.
294
333
  def num_rows
295
334
  reload() if !@cells
296
335
  return @cells.keys.map(){ |r, c| r }.max || 0
297
336
  end
298
337
 
299
- # Number of the right-most non-empty column.
338
+ # Column number of the right-most non-empty column.
300
339
  def num_cols
301
340
  reload() if !@cells
302
341
  return @cells.keys.map(){ |r, c| c }.max || 0
303
342
  end
304
343
 
344
+ # Number of rows including empty rows.
345
+ def max_rows
346
+ reload() if !@cells
347
+ return @max_rows
348
+ end
349
+
350
+ # Updates number of rows.
351
+ # Note that update is not sent to the server until you call save().
352
+ def max_rows=(rows)
353
+ @max_rows = rows
354
+ @meta_modified = true
355
+ end
356
+
357
+ # Number of columns including empty columns.
358
+ def max_cols
359
+ reload() if !@cells
360
+ return @max_cols
361
+ end
362
+
363
+ # Updates number of columns.
364
+ # Note that update is not sent to the server until you call save().
365
+ def max_cols=(cols)
366
+ @max_cols = cols
367
+ @meta_modified = true
368
+ end
369
+
370
+ # Title of the worksheet (shown as tab label in Web interface).
371
+ def title
372
+ reload() if !@title
373
+ return @title
374
+ end
375
+
376
+ # Updates title of the worksheet.
377
+ # Note that update is not sent to the server until you call save().
378
+ def title=(title)
379
+ @title = title
380
+ @meta_modified = true
381
+ end
382
+
305
383
  def cells #:nodoc:
306
384
  reload() if !@cells
307
385
  return @cells
@@ -310,9 +388,9 @@ module GoogleSpreadsheet
310
388
  # An array of spreadsheet rows. Each row contains an array of
311
389
  # columns. Note that resulting array is 0-origin so
312
390
  # worksheet.rows[0][0] == worksheet[1, 1].
313
- def rows
391
+ def rows(skip = 0)
314
392
  nc = self.num_cols
315
- result = (1..self.num_rows).map() do |row|
393
+ result = ((1 + skip)..self.num_rows).map() do |row|
316
394
  (1..nc).map(){ |col| self[row, col] }.freeze()
317
395
  end
318
396
  return result.freeze()
@@ -322,76 +400,115 @@ module GoogleSpreadsheet
322
400
  # Note that changes you made by []= is discarded if you haven't called save().
323
401
  def reload()
324
402
  doc = @session.get(@cells_feed_url)
403
+ @max_rows = doc.search("gs:rowCount").text.to_i()
404
+ @max_cols = doc.search("gs:colCount").text.to_i()
405
+ @title = doc.search("title").text
325
406
  @cells = {}
407
+ @input_values = {}
326
408
  for entry in doc.search("entry")
327
- row = entry.search("gs:cell")[0]["row"].to_i()
328
- col = entry.search("gs:cell")[0]["col"].to_i()
329
- content = entry.search("content").text
330
- @cells[[row, col]] = content
409
+ cell = entry.search("gs:cell")[0]
410
+ row = cell["row"].to_i()
411
+ col = cell["col"].to_i()
412
+ @cells[[row, col]] = cell.inner_text
413
+ @input_values[[row, col]] = cell["inputValue"]
331
414
  end
332
415
  @modified.clear()
416
+ @meta_modified = false
333
417
  return true
334
418
  end
335
419
 
336
420
  # Saves your changes made by []= to the server.
337
421
  def save()
338
- return false if @modified.empty?
422
+ sent = false
339
423
 
340
- # Gets id and edit URL for each cell.
341
- # Note that return-empty=true is required to get those info for empty cells.
342
- cell_entries = {}
343
- rows = @modified.map(){ |r, c| r }
344
- cols = @modified.map(){ |r, c| c }
345
- url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
346
- "&min-col=#{cols.min}&max-col=#{cols.max}"
347
- doc = @session.get(url)
348
- for entry in doc.search("entry")
349
- row = entry.search("gs:cell")[0]["row"].to_i()
350
- col = entry.search("gs:cell")[0]["col"].to_i()
351
- cell_entries[[row, col]] = entry
352
- end
353
-
354
- # Updates cell values using batch operation.
355
- xml = <<-"EOS"
356
- <feed xmlns="http://www.w3.org/2005/Atom"
357
- xmlns:batch="http://schemas.google.com/gdata/batch"
358
- xmlns:gs="http://schemas.google.com/spreadsheets/2006">
359
- <id>#{h(@cells_feed_url)}</id>
360
- EOS
361
- for row, col in @modified
362
- value = @cells[[row, col]]
363
- entry = cell_entries[[row, col]]
364
- id = entry.search("id").text
365
- edit_url = entry.search("link[@rel='edit']")[0]["href"]
366
- xml << <<-"EOS"
367
- <entry>
368
- <batch:id>#{h(row)},#{h(col)}</batch:id>
369
- <batch:operation type="update"/>
370
- <id>#{h(id)}</id>
371
- <link rel="edit" type="application/atom+xml"
372
- href="#{h(edit_url)}"/>
373
- <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
424
+ if @meta_modified
425
+
426
+ # I don't know good way to get worksheet feed URL from cells feed URL.
427
+ # Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
428
+ # from it.
429
+ if !(@cells_feed_url =~
430
+ %r{^http://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
431
+ raise(GoogleSpreadsheet::Error,
432
+ "cells feed URL is in unknown format: #{@cells_feed_url}")
433
+ end
434
+ ws_doc = @session.get(
435
+ "http://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}")
436
+ edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
437
+ xml = <<-"EOS"
438
+ <entry xmlns='http://www.w3.org/2005/Atom'
439
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
440
+ <title>#{h(@title)}</title>
441
+ <gs:rowCount>#{h(@max_rows)}</gs:rowCount>
442
+ <gs:colCount>#{h(@max_cols)}</gs:colCount>
374
443
  </entry>
375
444
  EOS
445
+ @session.put(edit_url, xml)
446
+
447
+ @meta_modified = false
448
+ sent = true
449
+
376
450
  end
377
- xml << <<-"EOS"
378
- </feed>
379
- EOS
380
- result = @session.post("#{@cells_feed_url}/batch", xml)
381
- for entry in result.search("atom:entry")
382
- interrupted = entry.search("batch:interrupted")[0]
383
- if interrupted
384
- raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
385
- interrupted["reason"])
451
+
452
+ if !@modified.empty?
453
+
454
+ # Gets id and edit URL for each cell.
455
+ # Note that return-empty=true is required to get those info for empty cells.
456
+ cell_entries = {}
457
+ rows = @modified.map(){ |r, c| r }
458
+ cols = @modified.map(){ |r, c| c }
459
+ url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
460
+ "&min-col=#{cols.min}&max-col=#{cols.max}"
461
+ doc = @session.get(url)
462
+ for entry in doc.search("entry")
463
+ row = entry.search("gs:cell")[0]["row"].to_i()
464
+ col = entry.search("gs:cell")[0]["col"].to_i()
465
+ cell_entries[[row, col]] = entry
386
466
  end
387
- if !(entry.search("batch:status")[0]["code"] =~ /^2/)
388
- raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
389
- [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
467
+
468
+ # Updates cell values using batch operation.
469
+ xml = <<-"EOS"
470
+ <feed xmlns="http://www.w3.org/2005/Atom"
471
+ xmlns:batch="http://schemas.google.com/gdata/batch"
472
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
473
+ <id>#{h(@cells_feed_url)}</id>
474
+ EOS
475
+ for row, col in @modified
476
+ value = @cells[[row, col]]
477
+ entry = cell_entries[[row, col]]
478
+ id = entry.search("id").text
479
+ edit_url = entry.search("link[@rel='edit']")[0]["href"]
480
+ xml << <<-"EOS"
481
+ <entry>
482
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
483
+ <batch:operation type="update"/>
484
+ <id>#{h(id)}</id>
485
+ <link rel="edit" type="application/atom+xml"
486
+ href="#{h(edit_url)}"/>
487
+ <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
488
+ </entry>
489
+ EOS
490
+ end
491
+ xml << <<-"EOS"
492
+ </feed>
493
+ EOS
494
+ result = @session.post("#{@cells_feed_url}/batch", xml)
495
+ for entry in result.search("atom:entry")
496
+ interrupted = entry.search("batch:interrupted")[0]
497
+ if interrupted
498
+ raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
499
+ interrupted["reason"])
500
+ end
501
+ if !(entry.search("batch:status")[0]["code"] =~ /^2/)
502
+ raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
503
+ [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
504
+ end
390
505
  end
506
+
507
+ @modified.clear()
508
+ sent = true
509
+
391
510
  end
392
-
393
- @modified.clear()
394
- return true
511
+ return sent
395
512
  end
396
513
 
397
514
  # Calls save() and reload().
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gimite-google-spreadsheet-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi Ichikawa
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-12-24 00:00:00 -08:00
12
+ date: 2009-07-13 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency