gimite-google-spreadsheet-ruby 0.0.1

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.
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
+