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.
- data/lib/google_spreadsheet.rb +203 -86
- metadata +2 -2
data/lib/google_spreadsheet.rb
CHANGED
@@ -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,
|
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
|
-
|
29
|
-
|
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
|
34
|
+
session.login(mail, password)
|
36
35
|
open(path, "w", 0600){ |f| f.write(session.auth_token) }
|
37
|
-
|
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
|
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.
|
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 =
|
107
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
ex.
|
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 =
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
328
|
-
|
329
|
-
|
330
|
-
@cells[[row, col]] =
|
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
|
-
|
422
|
+
sent = false
|
339
423
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
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
|
-
|
388
|
-
|
389
|
-
|
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.
|
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:
|
12
|
+
date: 2009-07-13 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|