wolframarnold-google-spreadsheet-ruby 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Hiroshi Ichikawa, Wolfram Arnold
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ This is a Ruby 1.8/1.9 library to read/write Google Spreadsheet.
2
+
3
+
4
+ = How to install
5
+
6
+ $ gem sources -a http://gemcutter.org
7
+ $ sudo gem install google-spreadsheet-ruby
8
+
9
+ Note that gimite-google-spreadsheet-ruby at gems.github.com is no longer updated, because github stopped hosting it.
10
+
11
+
12
+ = How to use
13
+
14
+ Example:
15
+
16
+ require "rubygems"
17
+ require "google_spreadsheet"
18
+
19
+ # Logs in.
20
+ # You can also use OAuth. See document of GoogleSpreadsheet.login_with_oauth for details.
21
+ session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")
22
+
23
+ # First worksheet of http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en
24
+ ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]
25
+
26
+ # Gets content of A2 cell.
27
+ p ws[2, 1] #==> "hoge"
28
+
29
+ # Changes content of cells. Changes are not sent to the server until you call ws.save().
30
+ ws[2, 1] = "foo"
31
+ ws[2, 2] = "bar"
32
+ ws.save()
33
+
34
+ # Dumps all cells.
35
+ for row in 1..ws.num_rows
36
+ for col in 1..ws.num_cols
37
+ p ws[row, col]
38
+ end
39
+ end
40
+
41
+ # Yet another way to do so.
42
+ p ws.rows #==> [["fuga", ""], ["foo", "bar]]
43
+
44
+ # Reloads the worksheet to get changes by other clients.
45
+ ws.reload()
46
+
47
+ API document: http://gimite.net/gimite/rubymess/google-spreadsheet-ruby/
48
+
49
+
50
+ = Source code
51
+
52
+ http://github.com/gimite/google-spreadsheet-ruby/tree/master
53
+
54
+ The license of this source is "New BSD Licence"
55
+
56
+
57
+ = Supported environments
58
+
59
+ Ruby 1.8.x and Ruby 1.9.x. Checked with Ruby 1.8.7 and Ruby 1.9.1.
60
+
61
+
62
+ = Author
63
+
64
+ Hiroshi Ichikawa - http://gimite.net/en/index.php?Contact
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'google_spreadsheet'
2
+
@@ -0,0 +1,817 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "enumerator"
5
+ require "set"
6
+ require "net/https"
7
+ require "open-uri"
8
+ require "cgi"
9
+ require "uri"
10
+ require "rubygems"
11
+ require "hpricot"
12
+ require "oauth"
13
+ Net::HTTP.version_1_2
14
+
15
+ module GoogleSpreadsheet
16
+
17
+ # Authenticates with given +mail+ and +password+, and returns GoogleSpreadsheet::Session
18
+ # if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
19
+ # Google Apps account is supported.
20
+ def self.login(mail, password)
21
+ return Session.login(mail, password)
22
+ end
23
+
24
+ # Authenticates with given OAuth token.
25
+ #
26
+ # For generating oauth_token, you can proceed as follow:
27
+ #
28
+ # 1) First generate OAuth consumer object with key and secret for your site by registering site with google
29
+ # @consumer = OAuth::Consumer.new( "key","secret", {:site=>"https://agree2"})
30
+ # 2) Request token with OAuth
31
+ # @request_token = @consumer.get_request_token
32
+ # session[:request_token] = @request_token
33
+ # redirect_to @request_token.authorize_url
34
+ # 3) Create an oauth access token
35
+ # @oauth_access_token = @request_token.get_access_token
36
+ # @access_token = OAuth::AccessToken.new(@consumer, @oauth_access_token.token, @oauth_access_token.secret)
37
+ #
38
+ # See these documents for details:
39
+ #
40
+ # - http://oauth.rubyforge.org/
41
+ # - http://code.google.com/apis/accounts/docs/OAuth.html
42
+ def self.login_with_oauth(oauth_token)
43
+ return Session.login_with_oauth(oauth_token)
44
+ end
45
+
46
+ # Restores GoogleSpreadsheet::Session from +path+ and returns it.
47
+ # If +path+ doesn't exist or authentication has failed, prompts mail and password on console,
48
+ # authenticates with them, stores the session to +path+ and returns it.
49
+ #
50
+ # This method requires Highline library: http://rubyforge.org/projects/highline/
51
+ def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
52
+ tokens = {}
53
+ if File.exist?(path)
54
+ open(path) do |f|
55
+ for auth in [:wise, :writely]
56
+ line = f.gets()
57
+ tokens[auth] = line && line.chomp()
58
+ end
59
+ end
60
+ end
61
+ session = Session.new(tokens)
62
+ session.on_auth_fail = proc() do
63
+ begin
64
+ require "highline"
65
+ rescue LoadError
66
+ raise(LoadError,
67
+ "GoogleSpreadsheet.saved_session requires Highline library.\n" +
68
+ "Run\n" +
69
+ " \$ sudo gem install highline\n" +
70
+ "to install it.")
71
+ end
72
+ highline = HighLine.new()
73
+ mail = highline.ask("Mail: ")
74
+ password = highline.ask("Password: "){ |q| q.echo = false }
75
+ session.login(mail, password)
76
+ open(path, "w", 0600) do |f|
77
+ f.puts(session.auth_token(:wise))
78
+ f.puts(session.auth_token(:writely))
79
+ end
80
+ true
81
+ end
82
+ if !session.auth_token
83
+ session.on_auth_fail.call()
84
+ end
85
+ return session
86
+ end
87
+
88
+
89
+ module Util #:nodoc:
90
+
91
+ module_function
92
+
93
+ def encode_query(params)
94
+ return params.map(){ |k, v| CGI.escape(k) + "=" + CGI.escape(v) }.join("&")
95
+ end
96
+
97
+ def h(str)
98
+ return CGI.escapeHTML(str.to_s())
99
+ end
100
+
101
+ def as_utf8(str)
102
+ if str.respond_to?(:force_encoding)
103
+ str.force_encoding("UTF-8")
104
+ else
105
+ str
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+
112
+ # Raised when spreadsheets.google.com has returned error.
113
+ class Error < RuntimeError
114
+
115
+ end
116
+
117
+
118
+ # Raised when GoogleSpreadsheet.login has failed.
119
+ class AuthenticationError < GoogleSpreadsheet::Error
120
+
121
+ end
122
+
123
+
124
+ # Use GoogleSpreadsheet.login or GoogleSpreadsheet.saved_session to get
125
+ # GoogleSpreadsheet::Session object.
126
+ class Session
127
+
128
+ include(Util)
129
+ extend(Util)
130
+
131
+ # The same as GoogleSpreadsheet.login.
132
+ def self.login(mail, password)
133
+ session = Session.new()
134
+ session.login(mail, password)
135
+ return session
136
+ end
137
+
138
+ # The same as GoogleSpreadsheet.login_with_oauth.
139
+ def self.login_with_oauth(oauth_token)
140
+ session = Session.new(nil, oauth_token)
141
+ end
142
+
143
+ # Restores session using return value of auth_tokens method of previous session.
144
+ def initialize(auth_tokens = nil, oauth_token = nil)
145
+ if oauth_token
146
+ @oauth_token = oauth_token
147
+ else
148
+ @auth_tokens = auth_tokens
149
+ end
150
+ end
151
+
152
+ # Authenticates with given +mail+ and +password+, and updates current session object
153
+ # if succeeds. Raises GoogleSpreadsheet::AuthenticationError if fails.
154
+ # Google Apps account is supported.
155
+ def login(mail, password)
156
+ begin
157
+ @auth_tokens = {}
158
+ authenticate(mail, password, :wise)
159
+ authenticate(mail, password, :writely)
160
+ rescue GoogleSpreadsheet::Error => ex
161
+ return true if @on_auth_fail && @on_auth_fail.call()
162
+ raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
163
+ end
164
+ end
165
+
166
+ # Authentication tokens.
167
+ attr_reader(:auth_tokens)
168
+
169
+ # Authentication token.
170
+ def auth_token(auth = :wise)
171
+ return @auth_tokens[auth]
172
+ end
173
+
174
+ # Proc or Method called when authentication has failed.
175
+ # When this function returns +true+, it tries again.
176
+ attr_accessor :on_auth_fail
177
+
178
+ def auth_header(auth) #:nodoc:
179
+ if auth == :none
180
+ return {}
181
+ else
182
+ return {"Authorization" => "GoogleLogin auth=#{@auth_tokens[auth]}"}
183
+ end
184
+ end
185
+
186
+ # Returns list of spreadsheets for the user as array of GoogleSpreadsheet::Spreadsheet.
187
+ # You can specify query parameters described at
188
+ # http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
189
+ #
190
+ # e.g.
191
+ # session.spreadsheets
192
+ # session.spreadsheets("title" => "hoge")
193
+ def spreadsheets(params = {})
194
+ query = encode_query(params)
195
+ doc = request(:get, "https://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
196
+ result = []
197
+ for entry in doc.search("entry")
198
+ title = as_utf8(entry.search("title").text)
199
+ url = as_utf8(entry.search(
200
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
201
+ result.push(Spreadsheet.new(self, url, title))
202
+ end
203
+ return result
204
+ end
205
+
206
+ # Returns GoogleSpreadsheet::Spreadsheet with given +key+.
207
+ #
208
+ # e.g.
209
+ # # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
210
+ # session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
211
+ def spreadsheet_by_key(key)
212
+ url = "https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
213
+ return Spreadsheet.new(self, url)
214
+ end
215
+
216
+ # Returns GoogleSpreadsheet::Spreadsheet with given +url+. You must specify either of:
217
+ # - URL of the page you open to access the spreadsheet in your browser
218
+ # - URL of worksheet-based feed of the spreadseet
219
+ #
220
+ # e.g.
221
+ # session.spreadsheet_by_url(
222
+ # "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en")
223
+ # session.spreadsheet_by_url(
224
+ # "https://spreadsheets.google.com/feeds/worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
225
+ def spreadsheet_by_url(url)
226
+ # Tries to parse it as URL of human-readable spreadsheet.
227
+ uri = URI.parse(url)
228
+ if uri.host == "spreadsheets.google.com" && uri.path =~ /\/ccc$/
229
+ if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
230
+ return spreadsheet_by_key($1)
231
+ end
232
+ end
233
+ # Assumes the URL is worksheets feed URL.
234
+ return Spreadsheet.new(self, url)
235
+ end
236
+
237
+ # Returns GoogleSpreadsheet::Worksheet with given +url+.
238
+ # You must specify URL of cell-based feed of the worksheet.
239
+ #
240
+ # e.g.
241
+ # session.worksheet_by_url(
242
+ # "http://spreadsheets.google.com/feeds/cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
243
+ def worksheet_by_url(url)
244
+ return Worksheet.new(self, nil, url)
245
+ end
246
+
247
+ # Creates new spreadsheet and returns the new GoogleSpreadsheet::Spreadsheet.
248
+ #
249
+ # e.g.
250
+ # session.create_spreadsheet("My new sheet")
251
+ def create_spreadsheet(
252
+ title = "Untitled",
253
+ feed_url = "https://docs.google.com/feeds/documents/private/full")
254
+ xml = <<-"EOS"
255
+ <atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
256
+ <atom:category scheme="http://schemas.google.com/g/2005#kind"
257
+ term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
258
+ <atom:title>#{h(title)}</atom:title>
259
+ </atom:entry>
260
+ EOS
261
+
262
+ doc = request(:post, feed_url, :data => xml, :auth => :writely)
263
+ ss_url = as_utf8(doc.search(
264
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
265
+ return Spreadsheet.new(self, ss_url, title)
266
+ end
267
+
268
+ def request(method, url, params = {}) #:nodoc:
269
+ # Always uses HTTPS.
270
+ uri = URI.parse(url.gsub(%r{^http://}, "https://"))
271
+ data = params[:data]
272
+ auth = params[:auth] || :wise
273
+ if params[:header]
274
+ add_header = params[:header]
275
+ else
276
+ add_header = data ? {"Content-Type" => "application/atom+xml"} : {}
277
+ end
278
+ response_type = params[:response_type] || :xml
279
+
280
+ if @oauth_token
281
+
282
+ if method == :delete || method == :get
283
+ response = @oauth_token.__send__(method, url, add_header)
284
+ else
285
+ response = @oauth_token.__send__(method, url, data, add_header)
286
+ end
287
+ return convert_response(response, response_type)
288
+
289
+ else
290
+
291
+ http = Net::HTTP.new(uri.host, uri.port)
292
+ http.use_ssl = true
293
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
294
+ http.start() do
295
+ while true
296
+ path = uri.path + (uri.query ? "?#{uri.query}" : "")
297
+ header = auth_header(auth).merge(add_header)
298
+ if method == :delete || method == :get
299
+ response = http.__send__(method, path, header)
300
+ else
301
+ response = http.__send__(method, path, data, header)
302
+ end
303
+ if response.code == "401" && @on_auth_fail && @on_auth_fail.call()
304
+ next
305
+ end
306
+ if !(response.code =~ /^2/)
307
+ raise(
308
+ response.code == "401" ? AuthenticationError : GoogleSpreadsheet::Error,
309
+ "Response code #{response.code} for #{method} #{url}: " +
310
+ CGI.unescapeHTML(response.body))
311
+ end
312
+ return convert_response(response, response_type)
313
+ end
314
+ end
315
+
316
+ end
317
+ end
318
+
319
+ private
320
+
321
+ def convert_response(response, response_type)
322
+ case response_type
323
+ when :xml
324
+ return Hpricot.XML(response.body)
325
+ when :raw
326
+ return response.body
327
+ else
328
+ raise("unknown params[:response_type]: %s" % response_type)
329
+ end
330
+ end
331
+
332
+ def authenticate(mail, password, auth)
333
+ params = {
334
+ "accountType" => "HOSTED_OR_GOOGLE",
335
+ "Email" => mail,
336
+ "Passwd" => password,
337
+ "service" => auth.to_s(),
338
+ "source" => "Gimite-RubyGoogleSpreadsheet-1.00",
339
+ }
340
+ response = request(:post,
341
+ "https://www.google.com/accounts/ClientLogin",
342
+ :data => encode_query(params), :auth => :none, :header => {}, :response_type => :raw)
343
+ @auth_tokens[auth] = response.slice(/^Auth=(.*)$/, 1)
344
+ end
345
+
346
+ end
347
+
348
+
349
+ # Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
350
+ class Spreadsheet
351
+
352
+ include(Util)
353
+
354
+ def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
355
+ @session = session
356
+ @worksheets_feed_url = worksheets_feed_url
357
+ @title = title
358
+ end
359
+
360
+ # URL of worksheet-based feed of the spreadsheet.
361
+ attr_reader(:worksheets_feed_url)
362
+
363
+ # Title of the spreadsheet. So far only available if you get this object by
364
+ # GoogleSpreadsheet::Session#spreadsheets.
365
+ attr_reader(:title)
366
+
367
+ # Key of the spreadsheet.
368
+ def key
369
+ if !(@worksheets_feed_url =~
370
+ %r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/private/full$})
371
+ raise(GoogleSpreadsheet::Error,
372
+ "worksheets feed URL is in unknown format: #{@worksheets_feed_url}")
373
+ end
374
+ return $1
375
+ end
376
+
377
+ # Tables feed URL of the spreadsheet.
378
+ def tables_feed_url
379
+ return "https://spreadsheets.google.com/feeds/#{self.key}/tables"
380
+ end
381
+
382
+ # URL of feed used in document list feed API.
383
+ def document_feed_url
384
+ return "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A#{self.key}"
385
+ end
386
+
387
+ # Creates copy of this spreadsheet with the given name.
388
+ def duplicate(new_name = nil)
389
+ new_name ||= (@title ? "Copy of " + @title : "Untitled")
390
+ get_url = "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=#{key}&exportFormat=ods"
391
+ ods = @session.request(:get, get_url, :response_type => :raw)
392
+
393
+ url = "https://docs.google.com/feeds/documents/private/full"
394
+ header = {
395
+ "Content-Type" => "application/x-vnd.oasis.opendocument.spreadsheet",
396
+ "Slug" => URI.encode(new_name),
397
+ }
398
+ doc = @session.request(:post, url, :data => ods, :auth => :writely, :header => header)
399
+ ss_url = as_utf8(doc.search(
400
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"])
401
+ return Spreadsheet.new(@session, ss_url, title)
402
+ end
403
+
404
+ # If +permanent+ is +false+, moves the spreadsheet to the trash.
405
+ # If +permanent+ is +true+, deletes the spreadsheet permanently.
406
+ def delete(permanent = false)
407
+ @session.request(:delete,
408
+ self.document_feed_url + (permanent ? "?delete=true" : ""),
409
+ :auth => :writely, :header => {"If-Match" => "*"})
410
+ end
411
+
412
+ # Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
413
+ def worksheets
414
+ doc = @session.request(:get, @worksheets_feed_url)
415
+ result = []
416
+ for entry in doc.search("entry")
417
+ title = as_utf8(entry.search("title").text)
418
+ url = as_utf8(entry.search(
419
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"])
420
+ result.push(Worksheet.new(@session, self, url, title))
421
+ end
422
+ return result.freeze()
423
+ end
424
+
425
+ # Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
426
+ def add_worksheet(title, max_rows = 100, max_cols = 20)
427
+ xml = <<-"EOS"
428
+ <entry xmlns='http://www.w3.org/2005/Atom'
429
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
430
+ <title>#{h(title)}</title>
431
+ <gs:rowCount>#{h(max_rows)}</gs:rowCount>
432
+ <gs:colCount>#{h(max_cols)}</gs:colCount>
433
+ </entry>
434
+ EOS
435
+ doc = @session.request(:post, @worksheets_feed_url, :data => xml)
436
+ url = as_utf8(doc.search(
437
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"])
438
+ return Worksheet.new(@session, self, url, title)
439
+ end
440
+
441
+ # Returns list of tables in the spreadsheet.
442
+ def tables
443
+ doc = @session.request(:get, self.tables_feed_url)
444
+ return doc.search("entry").map(){ |e| Table.new(@session, e) }.freeze()
445
+ end
446
+
447
+ end
448
+
449
+ # Use GoogleSpreadsheet::Worksheet#add_table to create table.
450
+ # Use GoogleSpreadsheet::Worksheet#tables to get GoogleSpreadsheet::Table objects.
451
+ class Table
452
+
453
+ include(Util)
454
+
455
+ def initialize(session, entry) #:nodoc:
456
+ @columns = {}
457
+ @worksheet_title = as_utf8(entry.search("gs:worksheet")[0]["name"])
458
+ @records_url = as_utf8(entry.search("content")[0]["src"])
459
+ @session = session
460
+ end
461
+
462
+ # Title of the worksheet the table belongs to.
463
+ attr_reader(:worksheet_title)
464
+
465
+ # Adds a record.
466
+ def add_record(values)
467
+ fields = ""
468
+ values.each do |name, value|
469
+ fields += "<gs:field name='#{h(name)}'>#{h(value)}</gs:field>"
470
+ end
471
+ xml =<<-EOS
472
+ <entry
473
+ xmlns="http://www.w3.org/2005/Atom"
474
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
475
+ #{fields}
476
+ </entry>
477
+ EOS
478
+ @session.request(:post, @records_url, :data => xml)
479
+ end
480
+
481
+ # Returns records in the table.
482
+ def records
483
+ doc = @session.request(:get, @records_url)
484
+ return doc.search("entry").map(){ |e| Record.new(@session, e) }
485
+ end
486
+
487
+ end
488
+
489
+ # Use GoogleSpreadsheet::Table#records to get GoogleSpreadsheet::Record objects.
490
+ class Record < Hash
491
+
492
+ def initialize(session, entry) #:nodoc:
493
+ @session = session
494
+ for field in entry.search("gs:field")
495
+ self[as_utf8(field["name"])] = as_utf8(field.inner_text)
496
+ end
497
+ end
498
+
499
+ def inspect #:nodoc:
500
+ content = self.map(){ |k, v| "%p => %p" % [k, v] }.join(", ")
501
+ return "\#<%p:{%s}>" % [self.class, content]
502
+ end
503
+
504
+ end
505
+
506
+ # Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::Worksheet object.
507
+ class Worksheet
508
+
509
+ include(Util)
510
+
511
+ def initialize(session, spreadsheet, cells_feed_url, title = nil) #:nodoc:
512
+ @session = session
513
+ @spreadsheet = spreadsheet
514
+ @cells_feed_url = cells_feed_url
515
+ @title = title
516
+
517
+ @cells = nil
518
+ @input_values = nil
519
+ @modified = Set.new()
520
+ end
521
+
522
+ # URL of cell-based feed of the worksheet.
523
+ attr_reader(:cells_feed_url)
524
+
525
+ # URL of worksheet feed URL of the worksheet.
526
+ def worksheet_feed_url
527
+ # I don't know good way to get worksheet feed URL from cells feed URL.
528
+ # Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
529
+ # from it.
530
+ if !(@cells_feed_url =~
531
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
532
+ raise(GoogleSpreadsheet::Error,
533
+ "cells feed URL is in unknown format: #{@cells_feed_url}")
534
+ end
535
+ return "https://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}"
536
+ end
537
+
538
+ # GoogleSpreadsheet::Spreadsheet which this worksheet belongs to.
539
+ def spreadsheet
540
+ if !@spreadsheet
541
+ if !(@cells_feed_url =~
542
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full$})
543
+ raise(GoogleSpreadsheet::Error,
544
+ "cells feed URL is in unknown format: #{@cells_feed_url}")
545
+ end
546
+ @spreadsheet = @session.spreadsheet_by_key($1)
547
+ end
548
+ return @spreadsheet
549
+ end
550
+
551
+ # Returns content of the cell as String. Top-left cell is [1, 1].
552
+ def [](row, col)
553
+ return self.cells[[row, col]] || ""
554
+ end
555
+
556
+ # Updates content of the cell.
557
+ # Note that update is not sent to the server until you call save().
558
+ # Top-left cell is [1, 1].
559
+ #
560
+ # e.g.
561
+ # worksheet[2, 1] = "hoge"
562
+ # worksheet[1, 3] = "=A1+B1"
563
+ def []=(row, col, value)
564
+ reload() if !@cells
565
+ @cells[[row, col]] = value
566
+ @input_values[[row, col]] = value
567
+ @modified.add([row, col])
568
+ self.max_rows = row if row > @max_rows
569
+ self.max_cols = col if col > @max_cols
570
+ end
571
+
572
+ # Returns the value or the formula of the cell. Top-left cell is [1, 1].
573
+ #
574
+ # If user input "=A1+B1" to cell [1, 3], worksheet[1, 3] is "3" for example and
575
+ # worksheet.input_value(1, 3) is "=RC[-2]+RC[-1]".
576
+ def input_value(row, col)
577
+ reload() if !@cells
578
+ return @input_values[[row, col]] || ""
579
+ end
580
+
581
+ # Row number of the bottom-most non-empty row.
582
+ def num_rows
583
+ reload() if !@cells
584
+ return @cells.keys.map(){ |r, c| r }.max || 0
585
+ end
586
+
587
+ # Column number of the right-most non-empty column.
588
+ def num_cols
589
+ reload() if !@cells
590
+ return @cells.keys.map(){ |r, c| c }.max || 0
591
+ end
592
+
593
+ # Number of rows including empty rows.
594
+ def max_rows
595
+ reload() if !@cells
596
+ return @max_rows
597
+ end
598
+
599
+ # Updates number of rows.
600
+ # Note that update is not sent to the server until you call save().
601
+ def max_rows=(rows)
602
+ reload() if !@cells
603
+ @max_rows = rows
604
+ @meta_modified = true
605
+ end
606
+
607
+ # Number of columns including empty columns.
608
+ def max_cols
609
+ reload() if !@cells
610
+ return @max_cols
611
+ end
612
+
613
+ # Updates number of columns.
614
+ # Note that update is not sent to the server until you call save().
615
+ def max_cols=(cols)
616
+ reload() if !@cells
617
+ @max_cols = cols
618
+ @meta_modified = true
619
+ end
620
+
621
+ # Title of the worksheet (shown as tab label in Web interface).
622
+ def title
623
+ reload() if !@title
624
+ return @title
625
+ end
626
+
627
+ # Updates title of the worksheet.
628
+ # Note that update is not sent to the server until you call save().
629
+ def title=(title)
630
+ reload() if !@cells
631
+ @title = title
632
+ @meta_modified = true
633
+ end
634
+
635
+ def cells #:nodoc:
636
+ reload() if !@cells
637
+ return @cells
638
+ end
639
+
640
+ # An array of spreadsheet rows. Each row contains an array of
641
+ # columns. Note that resulting array is 0-origin so
642
+ # worksheet.rows[0][0] == worksheet[1, 1].
643
+ def rows(skip = 0)
644
+ nc = self.num_cols
645
+ result = ((1 + skip)..self.num_rows).map() do |row|
646
+ (1..nc).map(){ |col| self[row, col] }.freeze()
647
+ end
648
+ return result.freeze()
649
+ end
650
+
651
+ # Reloads content of the worksheets from the server.
652
+ # Note that changes you made by []= is discarded if you haven't called save().
653
+ def reload()
654
+ doc = @session.request(:get, @cells_feed_url)
655
+ @max_rows = doc.search("gs:rowCount").text.to_i()
656
+ @max_cols = doc.search("gs:colCount").text.to_i()
657
+ @title = as_utf8(doc.search("/feed/title").text)
658
+
659
+ @cells = {}
660
+ @input_values = {}
661
+ for entry in doc.search("entry")
662
+ cell = entry.search("gs:cell")[0]
663
+ row = cell["row"].to_i()
664
+ col = cell["col"].to_i()
665
+ @cells[[row, col]] = as_utf8(cell.inner_text)
666
+ @input_values[[row, col]] = as_utf8(cell["inputValue"])
667
+ end
668
+ @modified.clear()
669
+ @meta_modified = false
670
+ return true
671
+ end
672
+
673
+ # Saves your changes made by []=, etc. to the server.
674
+ def save()
675
+ sent = false
676
+
677
+ if @meta_modified
678
+
679
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
680
+ edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
681
+ xml = <<-"EOS"
682
+ <entry xmlns='http://www.w3.org/2005/Atom'
683
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
684
+ <title>#{h(self.title)}</title>
685
+ <gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
686
+ <gs:colCount>#{h(self.max_cols)}</gs:colCount>
687
+ </entry>
688
+ EOS
689
+
690
+ @session.request(:put, edit_url, :data => xml)
691
+
692
+ @meta_modified = false
693
+ sent = true
694
+
695
+ end
696
+
697
+ if !@modified.empty?
698
+
699
+ # Gets id and edit URL for each cell.
700
+ # Note that return-empty=true is required to get those info for empty cells.
701
+ cell_entries = {}
702
+ rows = @modified.map(){ |r, c| r }
703
+ cols = @modified.map(){ |r, c| c }
704
+ url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
705
+ "&min-col=#{cols.min}&max-col=#{cols.max}"
706
+ doc = @session.request(:get, url)
707
+ for entry in doc.search("entry")
708
+ row = entry.search("gs:cell")[0]["row"].to_i()
709
+ col = entry.search("gs:cell")[0]["col"].to_i()
710
+ cell_entries[[row, col]] = entry
711
+ end
712
+
713
+ # Updates cell values using batch operation.
714
+ # If the data is large, we split it into multiple operations, otherwise batch may fail.
715
+ @modified.each_slice(250) do |chunk|
716
+
717
+ xml = <<-EOS
718
+ <feed xmlns="http://www.w3.org/2005/Atom"
719
+ xmlns:batch="http://schemas.google.com/gdata/batch"
720
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
721
+ <id>#{h(@cells_feed_url)}</id>
722
+ EOS
723
+ for row, col in chunk
724
+ value = @cells[[row, col]]
725
+ entry = cell_entries[[row, col]]
726
+ id = entry.search("id").text
727
+ edit_url = entry.search("link[@rel='edit']")[0]["href"]
728
+ xml << <<-EOS
729
+ <entry>
730
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
731
+ <batch:operation type="update"/>
732
+ <id>#{h(id)}</id>
733
+ <link rel="edit" type="application/atom+xml"
734
+ href="#{h(edit_url)}"/>
735
+ <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
736
+ </entry>
737
+ EOS
738
+ end
739
+ xml << <<-"EOS"
740
+ </feed>
741
+ EOS
742
+
743
+ result = @session.request(:post, "#{@cells_feed_url}/batch", :data => xml)
744
+ for entry in result.search("atom:entry")
745
+ interrupted = entry.search("batch:interrupted")[0]
746
+ if interrupted
747
+ raise(GoogleSpreadsheet::Error, "Update has failed: %s" %
748
+ interrupted["reason"])
749
+ end
750
+ if !(entry.search("batch:status")[0]["code"] =~ /^2/)
751
+ raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
752
+ [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
753
+ end
754
+ end
755
+
756
+ end
757
+
758
+ @modified.clear()
759
+ sent = true
760
+
761
+ end
762
+ return sent
763
+ end
764
+
765
+ # Calls save() and reload().
766
+ def synchronize()
767
+ save()
768
+ reload()
769
+ end
770
+
771
+ # Deletes this worksheet. Deletion takes effect right away without calling save().
772
+ def delete()
773
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
774
+ edit_url = ws_doc.search("link[@rel='edit']")[0]["href"]
775
+ @session.request(:delete, edit_url)
776
+ end
777
+
778
+ # Returns true if you have changes made by []= which haven't been saved.
779
+ def dirty?
780
+ return !@modified.empty?
781
+ end
782
+
783
+ # Creates table for the worksheet and returns GoogleSpreadsheet::Table.
784
+ # See this document for details:
785
+ # http://code.google.com/intl/en/apis/spreadsheets/docs/3.0/developers_guide_protocol.html#TableFeeds
786
+ def add_table(table_title, summary, columns)
787
+ column_xml = ""
788
+ columns.each do |index, name|
789
+ column_xml += "<gs:column index='#{h(index)}' name='#{h(name)}'/>\n"
790
+ end
791
+
792
+ xml = <<-"EOS"
793
+ <entry xmlns="http://www.w3.org/2005/Atom"
794
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
795
+ <title type='text'>#{h(table_title)}</title>
796
+ <summary type='text'>#{h(summary)}</summary>
797
+ <gs:worksheet name='#{h(self.title)}' />
798
+ <gs:header row='1' />
799
+ <gs:data numRows='0' startRow='2'>
800
+ #{column_xml}
801
+ </gs:data>
802
+ </entry>
803
+ EOS
804
+
805
+ result = @session.request(:post, self.spreadsheet.tables_feed_url, :data => xml)
806
+ return Table.new(@session, result)
807
+ end
808
+
809
+ # Returns list of tables for the workwheet.
810
+ def tables
811
+ return self.spreadsheet.tables.select(){ |t| t.worksheet_title == self.title }
812
+ end
813
+
814
+ end
815
+
816
+
817
+ end
data/script/gen-rdoc ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ rdoc --main=README.rdoc --title="Google Spreadsheet Ruby" --force-update README.rdoc lib/google_spreadsheet.rb
@@ -0,0 +1,85 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "/../lib")
2
+ require "test/unit"
3
+ require "google_spreadsheet"
4
+ require "rubygems"
5
+ require "highline"
6
+
7
+
8
+ class TC_GoogleSpreadsheet < Test::Unit::TestCase
9
+
10
+ USE_SAVED_SESSION = false
11
+
12
+ def test_all()
13
+ puts("This test will create spreadsheets with your account, read/write them")
14
+ puts("and finally delete them (if everything goes well).")
15
+ if USE_SAVED_SESSION
16
+ session = GoogleSpreadsheet.saved_session
17
+ else
18
+ highline = HighLine.new()
19
+ mail = highline.ask("Mail: ")
20
+ password = highline.ask("Password: "){ |q| q.echo = false }
21
+ session = GoogleSpreadsheet.login(mail, password)
22
+ end
23
+
24
+ ss_title = "google-spreadsheet-ruby test " + Time.now.strftime("%Y-%m-%d-%H-%M-%S")
25
+ ss = session.create_spreadsheet(ss_title)
26
+ assert_equal(ss_title, ss.title)
27
+
28
+ ws = ss.worksheets[0]
29
+ assert_equal(ss.worksheets_feed_url, ws.spreadsheet.worksheets_feed_url)
30
+ ws.title = "hoge"
31
+ ws.max_rows = 20
32
+ ws.max_cols = 10
33
+ ws[1, 1] = "3"
34
+ ws[1, 2] = "5"
35
+ ws[1, 3] = "=A1+B1"
36
+ assert_equal(20, ws.max_rows)
37
+ assert_equal(10, ws.max_cols)
38
+ assert_equal(1, ws.num_rows)
39
+ assert_equal(3, ws.num_cols)
40
+ assert_equal("3", ws[1, 1])
41
+ assert_equal("5", ws[1, 2])
42
+ ws.save()
43
+
44
+ ws.reload()
45
+ assert_equal(20, ws.max_rows)
46
+ assert_equal(10, ws.max_cols)
47
+ assert_equal(1, ws.num_rows)
48
+ assert_equal(3, ws.num_cols)
49
+ assert_equal("3", ws[1, 1])
50
+ assert_equal("5", ws[1, 2])
51
+ assert_equal("8", ws[1, 3])
52
+
53
+ ss2 = session.spreadsheet_by_key(ss.key)
54
+ assert_equal(ss.worksheets_feed_url, ss2.worksheets_feed_url)
55
+ assert_equal("hoge", ss2.worksheets[0].title)
56
+ assert_equal("3", ss2.worksheets[0][1, 1])
57
+ ss3 = session.spreadsheet_by_url("http://spreadsheets.google.com/ccc?key=#{ss.key}&hl=en")
58
+ assert_equal(ss.worksheets_feed_url, ss3.worksheets_feed_url)
59
+ ss4 = session.spreadsheet_by_url(ss.worksheets_feed_url)
60
+ assert_equal(ss.worksheets_feed_url, ss4.worksheets_feed_url)
61
+
62
+ assert_not_nil(session.spreadsheets.find(){ |s| s.title == ss_title })
63
+ assert_not_nil(session.spreadsheets("title" => ss_title).
64
+ find(){ |s| s.title == ss_title })
65
+
66
+ ws2 = session.worksheet_by_url(ws.cells_feed_url)
67
+ assert_equal(ws.cells_feed_url, ws2.cells_feed_url)
68
+ assert_equal("hoge", ws2.title)
69
+
70
+ ss_copy_title = "google-spreadsheet-ruby test copy " + Time.now.strftime("%Y-%m-%d-%H-%M-%S")
71
+ ss_copy = ss.duplicate(ss_copy_title)
72
+ assert_not_nil(session.spreadsheets("title" => ss_copy_title).
73
+ find(){ |s| s.title == ss_copy_title })
74
+ assert_equal("3", ss_copy.worksheets[0][1, 1])
75
+
76
+ ss.delete()
77
+ assert_nil(session.spreadsheets("title" => ss_title).
78
+ find(){ |s| s.title == ss_title })
79
+ ss_copy.delete(true)
80
+ assert_nil(session.spreadsheets("title" => ss_copy_title).
81
+ find(){ |s| s.title == ss_copy_title })
82
+ ss.delete(true)
83
+ end
84
+
85
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wolframarnold-google-spreadsheet-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Hiroshi Ichikawa
8
+ - Wolfram Arnold
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2010-02-02 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: hpricot
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0.3"
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: oauth
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.3.6
35
+ version:
36
+ description: This is a library to read/write Google Spreadsheet.
37
+ email:
38
+ - gimite+github@gmail.com
39
+ - wolfram@rubyfocus.biz
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - README.rdoc
46
+ files:
47
+ - README.rdoc
48
+ - LICENSE
49
+ - init.rb
50
+ - lib/google_spreadsheet.rb
51
+ - script/gen-rdoc
52
+ - test/test_google_spreadsheet.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/wolframarnold/google-spreadsheet-ruby/tree/master
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --main
60
+ - README.rdoc
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.5
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: This is a library to read/write Google Spreadsheet.
82
+ test_files:
83
+ - test/test_google_spreadsheet.rb