parallel588_google_drive 0.3.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.
@@ -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