gimite-google-spreadsheet-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.txt +31 -0
  2. data/lib/google_spreadsheet.rb +390 -0
  3. metadata +64 -0
data/README.txt ADDED
@@ -0,0 +1,31 @@
1
+ This is a library to read/write Google Spreadsheet.
2
+
3
+
4
+ = How to install
5
+
6
+ TBD
7
+
8
+
9
+ = How to use
10
+
11
+ Example:
12
+
13
+ require "rubygems"
14
+ require "google_spreadsheet"
15
+
16
+ # Logs in.
17
+ session = GoogleSpreadsheet.login("username@gmail.com", "mypassword")
18
+
19
+ # First worksheet of http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en
20
+ ws = session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg").worksheets[0]
21
+
22
+ # Gets content of A2 cell.
23
+ p ws[2, 1] #==> "hoge"
24
+
25
+ # Changes content of cells. Changes are not sent to the server until you call ws.save().
26
+ ws[2, 1] = "foo"
27
+ ws[2, 2] = "bar"
28
+ ws.save()
29
+
30
+ # Reloads the worksheet to get changes by other clients.
31
+ ws.reload()
@@ -0,0 +1,390 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "set"
5
+ require "net/https"
6
+ require "open-uri"
7
+ require "cgi"
8
+ require "rubygems"
9
+ require "hpricot"
10
+ Net::HTTP.version_1_2
11
+
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, prompts mail and password on console, authenticates with them,
24
+ # stores the session to +path+ and returns it.
25
+ #
26
+ # This method requires Ruby/Password library: http://www.caliban.org/ruby/ruby-password.shtml
27
+ def self.saved_session(path = ENV["HOME"] + "/.ruby_google_spreadsheet.token")
28
+ if File.exist?(path)
29
+ return Session.new(File.read(path))
30
+ else
31
+ require "password"
32
+ $stderr.print("Mail: ")
33
+ mail = $stdin.gets().chomp()
34
+ password = Password.get()
35
+ session = Session.login(mail, password)
36
+ open(path, "w", 0600){ |f| f.write(session.auth_token) }
37
+ return session
38
+ end
39
+ end
40
+
41
+
42
+ module Util #:nodoc:
43
+
44
+ module_function
45
+
46
+ def http_post(url, data, header = {})
47
+ uri = URI.parse(url)
48
+ http = Net::HTTP.new(uri.host, uri.port)
49
+ http.use_ssl = uri.scheme == "https"
50
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
51
+ http.start() do
52
+ path = uri.path + (uri.query ? "?#{uri.query}" : "")
53
+ response = http.post(path, data, header)
54
+ if !(response.code =~ /^2/)
55
+ raise(GoogleSpreadsheet::Error, "Response code #{response.code} for POST #{url}: " +
56
+ CGI.unescapeHTML(response.body))
57
+ end
58
+ return response.body
59
+ end
60
+ end
61
+
62
+ def encode_query(params)
63
+ return params.map(){ |k, v| uri_encode(k) + "=" + uri_encode(v) }.join("&")
64
+ end
65
+
66
+ def uri_encode(str)
67
+ return URI.encode(str, /#{URI::UNSAFE}|&/n)
68
+ end
69
+
70
+ def h(str)
71
+ return CGI.escapeHTML(str.to_s())
72
+ end
73
+
74
+ end
75
+
76
+
77
+ # Raised when spreadsheets.google.com has returned error.
78
+ class Error < RuntimeError
79
+
80
+ end
81
+
82
+
83
+ # Raised when GoogleSpreadsheet.login has failed.
84
+ class AuthenticationError < GoogleSpreadsheet::Error
85
+
86
+ end
87
+
88
+
89
+ # Use GoogleSpreadsheet.login or GoogleSpreadsheet.saved_session to get
90
+ # GoogleSpreadsheet::Session object.
91
+ class Session
92
+
93
+ include(Util)
94
+ extend(Util)
95
+
96
+ # The same as GoogleSpreadsheet.login.
97
+ def self.login(mail, password)
98
+ begin
99
+ params = {
100
+ "accountType" => "HOSTED_OR_GOOGLE",
101
+ "Email" => mail,
102
+ "Passwd" => password,
103
+ "service" => "wise",
104
+ "source" => "Gimite-RubyGoogleSpreadsheet-1.00",
105
+ }
106
+ response = http_post("https://www.google.com/accounts/ClientLogin", encode_query(params))
107
+ return Session.new(response.slice(/^Auth=(.*)$/, 1))
108
+ rescue GoogleSpreadsheet::Error => ex
109
+ raise(AuthenticationError, "authentication failed for #{mail}: #{ex.message}")
110
+ end
111
+ end
112
+
113
+ # Creates session object with given authentication token.
114
+ def initialize(auth_token)
115
+ @auth_token = auth_token
116
+ end
117
+
118
+ # Authentication token.
119
+ attr_reader(:auth_token)
120
+
121
+ def get(url) #:nodoc:
122
+ response = open(url, self.http_header){ |f| f.read() }
123
+ return Hpricot.XML(response)
124
+ end
125
+
126
+ def post(url, data) #:nodoc:
127
+ header = self.http_header.merge({"Content-Type" => "application/atom+xml"})
128
+ response = http_post(url, data, header)
129
+ return Hpricot.XML(response)
130
+ end
131
+
132
+ def http_header #:nodoc:
133
+ return {"Authorization" => "GoogleLogin auth=#{@auth_token}"}
134
+ end
135
+
136
+ # Returns list of spreadsheets for the user as array of GoogleSpreadsheet::Spreadsheet.
137
+ # You can specify query parameters described at
138
+ # http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
139
+ #
140
+ # e.g.
141
+ # session.spreadsheets
142
+ # session.spreadsheets("title" => "hoge")
143
+ def spreadsheets(params = {})
144
+ query = encode_query(params)
145
+ doc = get("http://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
146
+ result = []
147
+ for entry in doc.search("entry")
148
+ title = entry.search("title").text
149
+ url = entry.search(
150
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
151
+ result.push(Spreadsheet.new(self, url, title))
152
+ end
153
+ return result
154
+ end
155
+
156
+ # Returns GoogleSpreadsheet::Spreadsheet with given +key+.
157
+ #
158
+ # e.g.
159
+ # # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
160
+ # session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
161
+ def spreadsheet_by_key(key)
162
+ url = "http://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
163
+ return Spreadsheet.new(self, url)
164
+ end
165
+
166
+ # Returns GoogleSpreadsheet::Spreadsheet with given +url+. You must specify either of:
167
+ # - URL of the page you open to access the spreadsheet in your browser
168
+ # - URL of worksheet-based feed of the spreadseet
169
+ #
170
+ # e.g.
171
+ # session.spreadsheet_by_url(
172
+ # "http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=en")
173
+ # session.spreadsheet_by_url(
174
+ # "http://spreadsheets.google.com/feeds/worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
175
+ def spreadsheet_by_url(url)
176
+ # Tries to parse it as URL of human-readable spreadsheet.
177
+ uri = URI.parse(url)
178
+ if uri.host == "spreadsheets.google.com" && uri.path =~ /\/ccc$/
179
+ if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
180
+ return spreadsheet_by_key($1)
181
+ end
182
+ end
183
+ # Assumes the URL is worksheets feed URL.
184
+ return Spreadsheet.new(self, url)
185
+ end
186
+
187
+ # Returns GoogleSpreadsheet::Worksheet with given +url+.
188
+ # You must specify URL of cell-based feed of the worksheet.
189
+ #
190
+ # e.g.
191
+ # session.worksheet_by_url(
192
+ # "http://spreadsheets.google.com/feeds/cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
193
+ def worksheet_by_url(url)
194
+ return Worksheet.new(self, url)
195
+ end
196
+
197
+ end
198
+
199
+
200
+ # Use methods in GoogleSpreadsheet::Session to get GoogleSpreadsheet::Spreadsheet object.
201
+ class Spreadsheet
202
+
203
+ include(Util)
204
+
205
+ def initialize(session, worksheets_feed_url, title = nil) #:nodoc:
206
+ @session = session
207
+ @worksheets_feed_url = worksheets_feed_url
208
+ @title = title
209
+ end
210
+
211
+ # URL of worksheet-based feed of the spreadsheet.
212
+ attr_reader(:worksheets_feed_url)
213
+
214
+ # Title of the spreadsheet. So far only available if you get this object by
215
+ # GoogleSpreadsheet::Session#spreadsheets.
216
+ attr_reader(:title)
217
+
218
+ # Returns worksheets of the spreadsheet as array of GoogleSpreadsheet::Worksheet.
219
+ def worksheets
220
+ doc = @session.get(@worksheets_feed_url)
221
+ result = []
222
+ for entry in doc.search("entry")
223
+ title = entry.search("title").text
224
+ url = entry.search(
225
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
226
+ result.push(Worksheet.new(@session, url, title))
227
+ end
228
+ return result.freeze()
229
+ end
230
+
231
+ # Adds a new worksheet to the spreadsheet. Returns added GoogleSpreadsheet::Worksheet.
232
+ def add_worksheet(title, max_rows = 100, max_cols = 20)
233
+ xml = <<-"EOS"
234
+ <entry xmlns='http://www.w3.org/2005/Atom'
235
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
236
+ <title>#{h(title)}</title>
237
+ <gs:rowCount>#{h(max_rows)}</gs:rowCount>
238
+ <gs:colCount>#{h(max_cols)}</gs:colCount>
239
+ </entry>
240
+ EOS
241
+ doc = @session.post(@worksheets_feed_url, xml)
242
+ url = doc.search(
243
+ "link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
244
+ return Worksheet.new(@session, url, title)
245
+ end
246
+
247
+ end
248
+
249
+
250
+ # Use GoogleSpreadsheet::Spreadsheet#worksheets to get GoogleSpreadsheet::Worksheet object.
251
+ class Worksheet
252
+
253
+ include(Util)
254
+
255
+ def initialize(session, cells_feed_url, title = nil) #:nodoc:
256
+ @session = session
257
+ @cells_feed_url = cells_feed_url
258
+ @title = title
259
+ @cells = nil
260
+ @modified = Set.new()
261
+ end
262
+
263
+ # URL of cell-based feed of the spreadsheet.
264
+ attr_reader(:cells_feed_url)
265
+
266
+ # Title of the spreadsheet. So far not available if you get this object by
267
+ # GoogleSpreadsheet::Spreadsheet#worksheet_by_url.
268
+ attr_reader(:title)
269
+
270
+ # Returns content of the cell as String. Top-left cell is [1, 1].
271
+ def [](row, col)
272
+ return self.cells[[row, col]] || ""
273
+ end
274
+
275
+ # Updates content of the cell.
276
+ # Note that update is not sent to the server until you call save().
277
+ # Top-left cell is [1, 1].
278
+ #
279
+ # e.g.
280
+ # worksheet[2, 1] = "hoge"
281
+ # worksheet[1, 3] = "=A1+B1"
282
+ def []=(row, col, value)
283
+ reload() if !@cells
284
+ @cells[[row, col]] = value
285
+ @modified.add([row, col])
286
+ end
287
+
288
+ # Number of the bottom-most non-empty row.
289
+ def num_rows
290
+ reload() if !@cells
291
+ return @cells.keys.map(){ |r, c| r }.max || 0
292
+ end
293
+
294
+ # Number of the right-most non-empty column.
295
+ def num_cols
296
+ reload() if !@cells
297
+ return @cells.keys.map(){ |r, c| c }.max || 0
298
+ end
299
+
300
+ def cells #:nodoc:
301
+ reload() if !@cells
302
+ return @cells
303
+ end
304
+
305
+ # Reloads content of the worksheets from the server.
306
+ # Note that changes you made by []= is discarded if you haven't called save().
307
+ def reload()
308
+ doc = @session.get(@cells_feed_url)
309
+ @cells = {}
310
+ for entry in doc.search("entry")
311
+ row = entry.search("gs:cell")[0]["row"].to_i()
312
+ col = entry.search("gs:cell")[0]["col"].to_i()
313
+ content = entry.search("content").text
314
+ @cells[[row, col]] = content
315
+ end
316
+ @modified.clear()
317
+ return true
318
+ end
319
+
320
+ # Saves your changes made by []= to the server.
321
+ def save()
322
+ return false if @modified.empty?
323
+
324
+ # Gets id and edit URL for each cell.
325
+ # Note that return-empty=true is required to get those info for empty cells.
326
+ cell_entries = {}
327
+ rows = @modified.map(){ |r, c| r }
328
+ cols = @modified.map(){ |r, c| c }
329
+ url = "#{@cells_feed_url}?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
330
+ "&min-col=#{cols.min}&max-col=#{cols.max}"
331
+ doc = @session.get(url)
332
+ for entry in doc.search("entry")
333
+ row = entry.search("gs:cell")[0]["row"].to_i()
334
+ col = entry.search("gs:cell")[0]["col"].to_i()
335
+ cell_entries[[row, col]] = entry
336
+ end
337
+
338
+ # Updates cell values using batch operation.
339
+ xml = <<-"EOS"
340
+ <feed xmlns='http://www.w3.org/2005/Atom'
341
+ xmlns:batch='http://schemas.google.com/gdata/batch'
342
+ xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
343
+ <id>#{h(@cells_feed_url)}</id>
344
+ EOS
345
+ for row, col in @modified
346
+ value = @cells[[row, col]]
347
+ entry = cell_entries[[row, col]]
348
+ id = entry.search("id").text
349
+ edit_url = entry.search("link[@rel='edit']")[0]["href"]
350
+ xml << <<-"EOS"
351
+ <entry>
352
+ <batch:id>#{h(row)},#{h(col)}</batch:id>
353
+ <batch:operation type='update'/>
354
+ <id>#{h(id)}</id>
355
+ <link rel='edit' type='application/atom+xml'
356
+ href='#{h(edit_url)}'/>
357
+ <gs:cell row='#{h(row)}' col='#{h(col)}' inputValue='#{h(value)}'/>
358
+ </entry>
359
+ EOS
360
+ end
361
+ xml << <<-"EOS"
362
+ </feed>
363
+ EOS
364
+ result = @session.post("#{@cells_feed_url}/batch", xml)
365
+ for entry in result.search("atom:entry")
366
+ if !(entry.search("batch:status")[0]["code"] =~ /^2/)
367
+ raise(GoogleSpreadsheet::Error, "Updating cell %s has failed: %s" %
368
+ [entry.search("atom:id").text, entry.search("batch:status")[0]["reason"]])
369
+ end
370
+ end
371
+
372
+ @modified.clear()
373
+ return true
374
+ end
375
+
376
+ # Calls save() and reload().
377
+ def synchronize()
378
+ save()
379
+ reload()
380
+ end
381
+
382
+ # Returns true if you have changes made by []= which haven't been saved.
383
+ def dirty?
384
+ return !@modified.empty?
385
+ end
386
+
387
+ end
388
+
389
+
390
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gimite-google-spreadsheet-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Hiroshi Ichikawa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-24 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0.3"
23
+ version:
24
+ description: This is a library to read/write Google Spreadsheet.
25
+ email:
26
+ - gimite+github@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.txt
33
+ files:
34
+ - README.txt
35
+ - lib/google_spreadsheet.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/gimite/google-spreadsheet-ruby/tree/master
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --main
41
+ - README.txt
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.2.0
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: This is a library to read/write Google Spreadsheet.
63
+ test_files: []
64
+