parallel588_google_drive 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,475 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "set"
5
+
6
+ require "google_drive/util"
7
+ require "google_drive/error"
8
+ require "google_drive/table"
9
+ require "google_drive/list"
10
+
11
+
12
+ module GoogleDrive
13
+
14
+ # A worksheet (i.e. a tab) in a spreadsheet.
15
+ # Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet object.
16
+ class Worksheet
17
+
18
+ include(Util)
19
+
20
+ def initialize(session, spreadsheet, cells_feed_url, title = nil) #:nodoc:
21
+
22
+ @session = session
23
+ @spreadsheet = spreadsheet
24
+ @cells_feed_url = cells_feed_url
25
+ @title = title
26
+
27
+ @cells = nil
28
+ @input_values = nil
29
+ @numeric_values = nil
30
+ @modified = Set.new()
31
+ @list = nil
32
+
33
+ end
34
+
35
+ # URL of cell-based feed of the worksheet.
36
+ attr_reader(:cells_feed_url)
37
+
38
+ # URL of worksheet feed URL of the worksheet.
39
+ def worksheet_feed_url
40
+ # I don't know good way to get worksheet feed URL from cells feed URL.
41
+ # Probably it would be cleaner to keep worksheet feed URL and get cells feed URL
42
+ # from it.
43
+ if !(@cells_feed_url =~
44
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full((\?.*)?)$})
45
+ raise(GoogleDrive::Error,
46
+ "Cells feed URL is in unknown format: #{@cells_feed_url}")
47
+ end
48
+ return "https://spreadsheets.google.com/feeds/worksheets/#{$1}/private/full/#{$2}#{$3}"
49
+ end
50
+
51
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
52
+ def spreadsheet
53
+ if !@spreadsheet
54
+ if !(@cells_feed_url =~
55
+ %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$})
56
+ raise(GoogleDrive::Error,
57
+ "Cells feed URL is in unknown format: #{@cells_feed_url}")
58
+ end
59
+ @spreadsheet = @session.spreadsheet_by_key($1)
60
+ end
61
+ return @spreadsheet
62
+ end
63
+
64
+ # Returns content of the cell as String. Arguments must be either
65
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
66
+ #
67
+ # e.g.
68
+ # worksheet[2, 1] #=> "hoge"
69
+ # worksheet["A2"] #=> "hoge"
70
+ def [](*args)
71
+ (row, col) = parse_cell_args(args)
72
+ return self.cells[[row, col]] || ""
73
+ end
74
+
75
+ # Updates content of the cell.
76
+ # Arguments in the bracket must be either (row number, column number) or cell name.
77
+ # Note that update is not sent to the server until you call save().
78
+ # Top-left cell is [1, 1].
79
+ #
80
+ # e.g.
81
+ # worksheet[2, 1] = "hoge"
82
+ # worksheet["A2"] = "hoge"
83
+ # worksheet[1, 3] = "=A1+B1"
84
+ def []=(*args)
85
+ (row, col) = parse_cell_args(args[0...-1])
86
+ value = args[-1].to_s()
87
+ reload() if !@cells
88
+ @cells[[row, col]] = value
89
+ @input_values[[row, col]] = value
90
+ @numeric_values[[row, col]] = nil
91
+ @modified.add([row, col])
92
+ self.max_rows = row if row > @max_rows
93
+ self.max_cols = col if col > @max_cols
94
+ end
95
+
96
+ # Updates cells in a rectangle area by a two-dimensional Array.
97
+ # +top_row+ and +left_col+ specifies the top-left corner of the area.
98
+ #
99
+ # e.g.
100
+ # worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
101
+ def update_cells(top_row, left_col, darray)
102
+ darray.each_with_index() do |array, y|
103
+ array.each_with_index() do |value, x|
104
+ self[top_row + y, left_col + x] = value
105
+ end
106
+ end
107
+ end
108
+
109
+ # Returns the value or the formula of the cell. Arguments must be either
110
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
111
+ #
112
+ # If user input "=A1+B1" to cell [1, 3]:
113
+ # worksheet[1, 3] #=> "3" for example
114
+ # worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
115
+ def input_value(*args)
116
+ (row, col) = parse_cell_args(args)
117
+ reload() if !@cells
118
+ return @input_values[[row, col]] || ""
119
+ end
120
+
121
+ # Returns the numeric value of the cell. Arguments must be either
122
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
123
+ #
124
+ # e.g.
125
+ # worksheet[1, 3] #=> "3,0" # it depends on locale, currency...
126
+ # worksheet.numeric_value(1, 3) #=> 3.0
127
+ #
128
+ # Returns nil if the cell is empty or contains non-number.
129
+ #
130
+ # If you modify the cell, its numeric_value is nil until you call save() and reload().
131
+ #
132
+ # For details, see:
133
+ # https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
134
+ def numeric_value(*args)
135
+ (row, col) = parse_cell_args(args)
136
+ reload() if !@cells
137
+ return @numeric_values[[row, col]]
138
+ end
139
+
140
+ # Row number of the bottom-most non-empty row.
141
+ def num_rows
142
+ reload() if !@cells
143
+ return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| r }.max || 0
144
+ end
145
+
146
+ # Column number of the right-most non-empty column.
147
+ def num_cols
148
+ reload() if !@cells
149
+ return @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| c }.max || 0
150
+ end
151
+
152
+ # Number of rows including empty rows.
153
+ def max_rows
154
+ reload() if !@cells
155
+ return @max_rows
156
+ end
157
+
158
+ # Updates number of rows.
159
+ # Note that update is not sent to the server until you call save().
160
+ def max_rows=(rows)
161
+ reload() if !@cells
162
+ @max_rows = rows
163
+ @meta_modified = true
164
+ end
165
+
166
+ # Number of columns including empty columns.
167
+ def max_cols
168
+ reload() if !@cells
169
+ return @max_cols
170
+ end
171
+
172
+ # Updates number of columns.
173
+ # Note that update is not sent to the server until you call save().
174
+ def max_cols=(cols)
175
+ reload() if !@cells
176
+ @max_cols = cols
177
+ @meta_modified = true
178
+ end
179
+
180
+ # Title of the worksheet (shown as tab label in Web interface).
181
+ def title
182
+ reload() if !@title
183
+ return @title
184
+ end
185
+
186
+ # Updates title of the worksheet.
187
+ # Note that update is not sent to the server until you call save().
188
+ def title=(title)
189
+ reload() if !@cells
190
+ @title = title
191
+ @meta_modified = true
192
+ end
193
+
194
+ def cells #:nodoc:
195
+ reload() if !@cells
196
+ return @cells
197
+ end
198
+
199
+ # An array of spreadsheet rows. Each row contains an array of
200
+ # columns. Note that resulting array is 0-origin so:
201
+ #
202
+ # worksheet.rows[0][0] == worksheet[1, 1]
203
+ def rows(skip = 0)
204
+ nc = self.num_cols
205
+ result = ((1 + skip)..self.num_rows).map() do |row|
206
+ (1..nc).map(){ |col| self[row, col] }.freeze()
207
+ end
208
+ return result.freeze()
209
+ end
210
+
211
+ # Reloads content of the worksheets from the server.
212
+ # Note that changes you made by []= etc. is discarded if you haven't called save().
213
+ def reload()
214
+
215
+ doc = @session.request(:get, @cells_feed_url)
216
+ @max_rows = doc.css("gs|rowCount").text.to_i()
217
+ @max_cols = doc.css("gs|colCount").text.to_i()
218
+ @title = doc.css("feed > title")[0].text
219
+
220
+ @cells = {}
221
+ @input_values = {}
222
+ @numeric_values = {}
223
+ doc.css("feed > entry").each() do |entry|
224
+ cell = entry.css("gs|cell")[0]
225
+ row = cell["row"].to_i()
226
+ col = cell["col"].to_i()
227
+ @cells[[row, col]] = cell.inner_text
228
+ @input_values[[row, col]] = cell["inputValue"]
229
+ numeric_value = cell["numericValue"]
230
+ @numeric_values[[row, col]] = numeric_value ? numeric_value.to_f() : nil
231
+ end
232
+ @modified.clear()
233
+ @meta_modified = false
234
+ return true
235
+
236
+ end
237
+
238
+ # Saves your changes made by []=, etc. to the server.
239
+ def save()
240
+
241
+ sent = false
242
+
243
+ if @meta_modified
244
+
245
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
246
+ edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
247
+ xml = <<-"EOS"
248
+ <entry xmlns='http://www.w3.org/2005/Atom'
249
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
250
+ <title>#{h(self.title)}</title>
251
+ <gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
252
+ <gs:colCount>#{h(self.max_cols)}</gs:colCount>
253
+ </entry>
254
+ EOS
255
+
256
+ @session.request(:put, edit_url, :data => xml)
257
+
258
+ @meta_modified = false
259
+ sent = true
260
+
261
+ end
262
+
263
+ if !@modified.empty?
264
+
265
+ # Gets id and edit URL for each cell.
266
+ # Note that return-empty=true is required to get those info for empty cells.
267
+ cell_entries = {}
268
+ rows = @modified.map(){ |r, c| r }
269
+ cols = @modified.map(){ |r, c| c }
270
+ url = concat_url(@cells_feed_url,
271
+ "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
272
+ "&min-col=#{cols.min}&max-col=#{cols.max}")
273
+ doc = @session.request(:get, url)
274
+
275
+ doc.css("entry").each() do |entry|
276
+ row = entry.css("gs|cell")[0]["row"].to_i()
277
+ col = entry.css("gs|cell")[0]["col"].to_i()
278
+ cell_entries[[row, col]] = entry
279
+ end
280
+
281
+ # Updates cell values using batch operation.
282
+ # If the data is large, we split it into multiple operations, otherwise batch may fail.
283
+ @modified.each_slice(250) do |chunk|
284
+
285
+ xml = <<-EOS
286
+ <feed xmlns="http://www.w3.org/2005/Atom"
287
+ xmlns:batch="http://schemas.google.com/gdata/batch"
288
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
289
+ <id>#{h(@cells_feed_url)}</id>
290
+ EOS
291
+ for row, col in chunk
292
+ value = @cells[[row, col]]
293
+ entry = cell_entries[[row, col]]
294
+ id = entry.css("id").text
295
+ edit_url = entry.css("link[rel='edit']")[0]["href"]
296
+ xml << <<-EOS
297
+ <entry>
298
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
299
+ <batch:operation type="update"/>
300
+ <id>#{h(id)}</id>
301
+ <link rel="edit" type="application/atom+xml"
302
+ href="#{h(edit_url)}"/>
303
+ <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
304
+ </entry>
305
+ EOS
306
+ end
307
+ xml << <<-"EOS"
308
+ </feed>
309
+ EOS
310
+
311
+ batch_url = concat_url(@cells_feed_url, "/batch")
312
+ result = @session.request(:post, batch_url, :data => xml)
313
+ result.css("atom|entry").each() do |entry|
314
+ interrupted = entry.css("batch|interrupted")[0]
315
+ if interrupted
316
+ raise(GoogleDrive::Error, "Update has failed: %s" %
317
+ interrupted["reason"])
318
+ end
319
+ if !(entry.css("batch|status").first["code"] =~ /^2/)
320
+ raise(GoogleDrive::Error, "Updating cell %s has failed: %s" %
321
+ [entry.css("atom|id").text, entry.css("batch|status")[0]["reason"]])
322
+ end
323
+ end
324
+
325
+ end
326
+
327
+ @modified.clear()
328
+ sent = true
329
+
330
+ end
331
+
332
+ return sent
333
+
334
+ end
335
+
336
+ # Calls save() and reload().
337
+ def synchronize()
338
+ save()
339
+ reload()
340
+ end
341
+
342
+ # Deletes this worksheet. Deletion takes effect right away without calling save().
343
+ def delete()
344
+ ws_doc = @session.request(:get, self.worksheet_feed_url)
345
+ edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
346
+ @session.request(:delete, edit_url)
347
+ end
348
+
349
+ # Returns true if you have changes made by []= which haven't been saved.
350
+ def dirty?
351
+ return !@modified.empty?
352
+ end
353
+
354
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
355
+ # March 2012.
356
+ #
357
+ # Creates table for the worksheet and returns GoogleDrive::Table.
358
+ # See this document for details:
359
+ # http://code.google.com/intl/en/apis/spreadsheets/docs/3.0/developers_guide_protocol.html#TableFeeds
360
+ def add_table(table_title, summary, columns, options)
361
+
362
+ warn(
363
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
364
+ "will not be available after March 2012.")
365
+ default_options = { :header_row => 1, :num_rows => 0, :start_row => 2}
366
+ options = default_options.merge(options)
367
+
368
+ column_xml = ""
369
+ columns.each() do |index, name|
370
+ column_xml += "<gs:column index='#{h(index)}' name='#{h(name)}'/>\n"
371
+ end
372
+
373
+ xml = <<-"EOS"
374
+ <entry xmlns="http://www.w3.org/2005/Atom"
375
+ xmlns:gs="http://schemas.google.com/spreadsheets/2006">
376
+ <title type='text'>#{h(table_title)}</title>
377
+ <summary type='text'>#{h(summary)}</summary>
378
+ <gs:worksheet name='#{h(self.title)}' />
379
+ <gs:header row='#{options[:header_row]}' />
380
+ <gs:data numRows='#{options[:num_rows]}' startRow='#{options[:start_row]}'>
381
+ #{column_xml}
382
+ </gs:data>
383
+ </entry>
384
+ EOS
385
+
386
+ result = @session.request(:post, self.spreadsheet.tables_feed_url, :data => xml)
387
+ return Table.new(@session, result)
388
+
389
+ end
390
+
391
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
392
+ # March 2012.
393
+ #
394
+ # Returns list of tables for the workwheet.
395
+ def tables
396
+ warn(
397
+ "DEPRECATED: Google Spreadsheet Table and Record feeds are deprecated and they " +
398
+ "will not be available after March 2012.")
399
+ return self.spreadsheet.tables.select(){ |t| t.worksheet_title == self.title }
400
+ end
401
+
402
+ # List feed URL of the worksheet.
403
+ def list_feed_url
404
+ # Gets the worksheets metafeed.
405
+ entry = @session.request(:get, self.worksheet_feed_url)
406
+
407
+ # Gets the URL of list-based feed for the given spreadsheet.
408
+ return entry.css(
409
+ "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']")[0]["href"]
410
+ end
411
+
412
+ # Provides access to cells using column names, assuming the first row contains column
413
+ # names. Returned object is GoogleDrive::List which you can use mostly as
414
+ # Array of Hash.
415
+ #
416
+ # e.g. Assuming the first row is ["x", "y"]:
417
+ # worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
418
+ # worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
419
+ # worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
420
+ # worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
421
+ # worksheet.list.push({"x" => "5", "y" => "6"})
422
+ #
423
+ # Note that update is not sent to the server until you call save().
424
+ def list
425
+ return @list ||= List.new(self)
426
+ end
427
+
428
+ # Returns a [row, col] pair for a cell name string.
429
+ # e.g.
430
+ # worksheet.cell_name_to_row_col("C2") #=> [2, 3]
431
+ def cell_name_to_row_col(cell_name)
432
+ if !cell_name.is_a?(String)
433
+ raise(ArgumentError, "Cell name must be a string: %p" % cell_name)
434
+ end
435
+ if !(cell_name.upcase =~ /^([A-Z]+)(\d+)$/)
436
+ raise(ArgumentError,
437
+ "Cell name must be only letters followed by digits with no spaces in between: %p" %
438
+ cell_name)
439
+ end
440
+ col = 0
441
+ $1.each_byte() do |b|
442
+ # 0x41: "A"
443
+ col = col * 26 + (b - 0x41 + 1)
444
+ end
445
+ row = $2.to_i()
446
+ return [row, col]
447
+ end
448
+
449
+ def inspect
450
+ fields = {:worksheet_feed_url => self.worksheet_feed_url}
451
+ fields[:title] = @title if @title
452
+ return "\#<%p %s>" % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
453
+ end
454
+
455
+ private
456
+
457
+ def parse_cell_args(args)
458
+ if args.size == 1 && args[0].is_a?(String)
459
+ return cell_name_to_row_col(args[0])
460
+ elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
461
+ if args[0] >= 1 && args[1] >= 1
462
+ return args
463
+ else
464
+ raise(ArgumentError,
465
+ "Row/col must be >= 1 (1-origin), but are %d/%d" % [args[0], args[1]])
466
+ end
467
+ else
468
+ raise(ArgumentError,
469
+ "Arguments must be either one String or two Integer's, but are %p" % [args])
470
+ end
471
+ end
472
+
473
+ end
474
+
475
+ end
@@ -0,0 +1,126 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "google_drive/session"
5
+
6
+
7
+ module GoogleDrive
8
+
9
+ # Authenticates with given +mail+ and +password+, and returns GoogleDrive::Session
10
+ # if succeeds. Raises GoogleDrive::AuthenticationError if fails.
11
+ # Google Apps account is supported.
12
+ #
13
+ # +proxy+ can be nil or return value of Net::HTTP.Proxy. If +proxy+ is specified, all
14
+ # HTTP access in the session uses the proxy. If +proxy+ is nil, it uses the proxy
15
+ # specified by http_proxy environment variable if available. Otherwise it performs direct
16
+ # access.
17
+ def self.login(mail, password, proxy = nil)
18
+ return Session.login(mail, password, proxy)
19
+ end
20
+
21
+ # Authenticates with given OAuth1 or OAuth2 token.
22
+ #
23
+ # OAuth2 code example:
24
+ #
25
+ # client = OAuth2::Client.new(
26
+ # your_client_id, your_client_secret,
27
+ # :site => "https://accounts.google.com",
28
+ # :token_url => "/o/oauth2/token",
29
+ # :authorize_url => "/o/oauth2/auth")
30
+ # auth_url = client.auth_code.authorize_url(
31
+ # :redirect_uri => "http://example.com/",
32
+ # :scope =>
33
+ # "https://docs.google.com/feeds/ " +
34
+ # "https://docs.googleusercontent.com/ " +
35
+ # "https://spreadsheets.google.com/feeds/")
36
+ # # Redirect the user to auth_url and get authorization code from redirect URL.
37
+ # auth_token = client.auth_code.get_token(
38
+ # authorization_code, :redirect_uri => "http://example.com/")
39
+ # session = GoogleDrive.login_with_oauth(auth_token)
40
+ #
41
+ # Or, from existing refresh token:
42
+ #
43
+ # access_token = OAuth2::AccessToken.from_hash(client,
44
+ # {:refresh_token => refresh_token, :expires_at => expires_at})
45
+ # access_token = access_token.refresh!
46
+ # session = GoogleDrive.login_with_oauth(access_token)
47
+ #
48
+ # If your app is not a Web app, use "urn:ietf:wg:oauth:2.0:oob" as redirect_url. Then
49
+ # authorization code is shown after authorization.
50
+ #
51
+ # OAuth1 code example:
52
+ #
53
+ # 1) First generate OAuth consumer object with key and secret for your site by registering site
54
+ # with Google.
55
+ # @consumer = OAuth::Consumer.new( "key","secret", {:site=>"https://agree2"})
56
+ # 2) Request token with OAuth.
57
+ # @request_token = @consumer.get_request_token
58
+ # session[:request_token] = @request_token
59
+ # redirect_to @request_token.authorize_url
60
+ # 3) Create an oauth access token.
61
+ # @oauth_access_token = @request_token.get_access_token
62
+ # @access_token = OAuth::AccessToken.new(
63
+ # @consumer, @oauth_access_token.token, @oauth_access_token.secret)
64
+ #
65
+ # See these documents for details:
66
+ #
67
+ # - https://github.com/intridea/oauth2
68
+ # - http://code.google.com/apis/accounts/docs/OAuth2.html
69
+ # - http://oauth.rubyforge.org/
70
+ # - http://code.google.com/apis/accounts/docs/OAuth.html
71
+ def self.login_with_oauth(oauth_token)
72
+ return Session.login_with_oauth(oauth_token)
73
+ end
74
+
75
+ # Restores session using return value of auth_tokens method of previous session.
76
+ #
77
+ # See GoogleDrive.login for description of parameter +proxy+.
78
+ def self.restore_session(auth_tokens, proxy = nil)
79
+ return Session.restore_session(auth_tokens, proxy)
80
+ end
81
+
82
+ # Restores GoogleDrive::Session from +path+ and returns it.
83
+ # If +path+ doesn't exist or authentication has failed, prompts mail and password on console,
84
+ # authenticates with them, stores the session to +path+ and returns it.
85
+ #
86
+ # See login for description of parameter +proxy+.
87
+ #
88
+ # This method requires Highline library: http://rubyforge.org/projects/highline/
89
+ def self.saved_session(path = ENV["HOME"] + "/.ruby_google_drive.token", proxy = nil)
90
+ tokens = {}
91
+ if ::File.exist?(path)
92
+ open(path) do |f|
93
+ for auth in [:wise, :writely]
94
+ line = f.gets()
95
+ tokens[auth] = line && line.chomp()
96
+ end
97
+ end
98
+ end
99
+ session = Session.new(tokens, nil, proxy)
100
+ session.on_auth_fail = proc() do
101
+ begin
102
+ require "highline"
103
+ rescue LoadError
104
+ raise(LoadError,
105
+ "GoogleDrive.saved_session requires Highline library.\n" +
106
+ "Run\n" +
107
+ " \$ sudo gem install highline\n" +
108
+ "to install it.")
109
+ end
110
+ highline = HighLine.new()
111
+ mail = highline.ask("Mail: ")
112
+ password = highline.ask("Password: "){ |q| q.echo = false }
113
+ session.login(mail, password)
114
+ open(path, "w", 0600) do |f|
115
+ f.puts(session.auth_token(:wise))
116
+ f.puts(session.auth_token(:writely))
117
+ end
118
+ true
119
+ end
120
+ if !session.auth_token
121
+ session.on_auth_fail.call()
122
+ end
123
+ return session
124
+ end
125
+
126
+ end