wolframarnold-google-spreadsheet-ruby 0.1.2

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