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