google-cells 0.3.0

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +2 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +73 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +75 -0
  10. data/Rakefile +4 -0
  11. data/examples/oauth2_service_account.rb +17 -0
  12. data/examples/oauth2_web_flow.rb +66 -0
  13. data/examples/sinatra/routes.rb +21 -0
  14. data/google_cells.gemspec +30 -0
  15. data/lib/google_cells.rb +80 -0
  16. data/lib/google_cells/author.rb +7 -0
  17. data/lib/google_cells/cell.rb +32 -0
  18. data/lib/google_cells/cell_selector.rb +18 -0
  19. data/lib/google_cells/cell_selector/row_selector.rb +70 -0
  20. data/lib/google_cells/fetcher.rb +27 -0
  21. data/lib/google_cells/folder.rb +7 -0
  22. data/lib/google_cells/google_object.rb +31 -0
  23. data/lib/google_cells/reader.rb +19 -0
  24. data/lib/google_cells/row.rb +7 -0
  25. data/lib/google_cells/spreadsheet.rb +179 -0
  26. data/lib/google_cells/url_helper.rb +37 -0
  27. data/lib/google_cells/util.rb +17 -0
  28. data/lib/google_cells/version.rb +3 -0
  29. data/lib/google_cells/worksheet.rb +63 -0
  30. data/spec/google_cells/cell_selector/row_selector_spec.rb +104 -0
  31. data/spec/google_cells/cell_selector_spec.rb +26 -0
  32. data/spec/google_cells/fetcher_spec.rb +21 -0
  33. data/spec/google_cells/google_object_spec.rb +18 -0
  34. data/spec/google_cells/reader_spec.rb +24 -0
  35. data/spec/google_cells/spreadsheet_spec.rb +171 -0
  36. data/spec/google_cells/url_helper_spec.rb +15 -0
  37. data/spec/google_cells/worksheet_spec.rb +69 -0
  38. data/spec/google_cells_spec.rb +13 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/vcr_cassettes/google_cells/cell_selector/each.yml +9147 -0
  41. data/spec/vcr_cassettes/google_cells/cell_selector/find_each.yml +10272 -0
  42. data/spec/vcr_cassettes/google_cells/cell_selector/find_each/selection.yml +947 -0
  43. data/spec/vcr_cassettes/google_cells/fetcher.yml +144 -0
  44. data/spec/vcr_cassettes/google_cells/reader.yml +144 -0
  45. data/spec/vcr_cassettes/google_cells/spreadsheet/cell_selector/worksheet.yml +276 -0
  46. data/spec/vcr_cassettes/google_cells/spreadsheet/copy.yml +447 -0
  47. data/spec/vcr_cassettes/google_cells/spreadsheet/copy/content.yml +18555 -0
  48. data/spec/vcr_cassettes/google_cells/spreadsheet/enfold.yml +259 -0
  49. data/spec/vcr_cassettes/google_cells/spreadsheet/folders.yml +319 -0
  50. data/spec/vcr_cassettes/google_cells/spreadsheet/get.yml +135 -0
  51. data/spec/vcr_cassettes/google_cells/spreadsheet/list.yml +276 -0
  52. data/spec/vcr_cassettes/google_cells/spreadsheet/worksheets.yml +276 -0
  53. data/spec/vcr_cassettes/google_cells/worksheet/save.yml +9555 -0
  54. data/spec/vcr_cassettes/google_cells/worksheets.yml +145 -0
  55. metadata +250 -0
@@ -0,0 +1,7 @@
1
+ module GoogleCells
2
+ class Author < GoogleCells::GoogleObject
3
+ @permanent_attributes = [ :name, :email ]
4
+ define_accessors
5
+ end
6
+ end
7
+
@@ -0,0 +1,32 @@
1
+ module GoogleCells
2
+ class Cell < GoogleCells::GoogleObject
3
+ include Util
4
+
5
+ @permanent_attributes = [:title, :id, :value, :numeric_value, :row, :col,
6
+ :edit_url, :worksheet]
7
+ define_accessors
8
+
9
+ attr_reader :input_value
10
+
11
+ def input_value=(v)
12
+ @input_value = v
13
+ worksheet.track_changes(self)
14
+ v
15
+ end
16
+
17
+ def to_xml
18
+ <<-EOS
19
+ <entry>
20
+ <batch:id>#{e(row)},#{e(col)}</batch:id>
21
+ <batch:operation type="update"/>
22
+ <id>#{e(id)}</id>
23
+ <link rel="edit" type="application/atom+xml"
24
+ href="#{e(edit_url)}"/>
25
+ <gs:cell row="#{e(row)}" col="#{e(col)}" inputValue="#{e(input_value)}"/>
26
+ </entry>
27
+ EOS
28
+ end
29
+ end
30
+ end
31
+
32
+
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/cell_selector/row_selector'
2
+
3
+ module GoogleCells
4
+ class CellSelector
5
+ include Reader
6
+
7
+ attr_accessor :min_row, :max_row, :min_col, :max_col, :worksheet
8
+
9
+ def initialize(ws)
10
+ @worksheet = ws
11
+ @min_row = 1
12
+ @max_row = worksheet.row_count
13
+ @min_col = 1
14
+ @max_col = worksheet.col_count
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,70 @@
1
+ module GoogleCells
2
+ class CellSelector
3
+
4
+ class RowSelector < CellSelector
5
+
6
+ DEFAULT_BATCH_SIZE = 10
7
+
8
+ def find_each(opts={}, &block)
9
+ size = (opts[:batch_size] || DEFAULT_BATCH_SIZE).to_i
10
+ rnum = @min_row
11
+ loop do
12
+ last = [rnum + size, @max_row].min
13
+ break if rnum > last
14
+ get_cells(rnum, last).each do |cells|
15
+ yield Row.new(cells:cells, number:rnum, worksheet:worksheet)
16
+ rnum += 1
17
+ end
18
+ end
19
+ end
20
+
21
+ def each
22
+ all.each{|c| yield c}
23
+ end
24
+
25
+ def all
26
+ @rows = []
27
+ self.find_each(batch_size:@max_row - @min_row){|r| @rows << r}
28
+ @rows
29
+ end
30
+
31
+ def first
32
+ all.first
33
+ end
34
+
35
+ def from(num)
36
+ @min_row = num.to_i
37
+ self
38
+ end
39
+
40
+ def to(num)
41
+ @max_row = num.to_i
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ def get_cells(start, last)
48
+ cells = []
49
+ each_entry(worksheet.cells_uri, 'return-empty' => 'true',
50
+ 'min-row' => start.to_s, 'max-row' => last.to_s) do |entry|
51
+ gscell = entry.css("gs|cell")[0]
52
+ cell = Cell.new(
53
+ id: entry.css("id").text,
54
+ title: entry.css("title").text,
55
+ value: gscell.inner_text,
56
+ row: gscell["row"].to_i,
57
+ col: gscell["col"].to_i,
58
+ edit_url: entry.css("link[rel='edit']")[0]["href"],
59
+ input_value: gscell["inputValue"],
60
+ numeric_value: gscell["numericValue"],
61
+ worksheet: self.worksheet
62
+ )
63
+ cells[cell.row - start] ||= []
64
+ cells[cell.row - start][cell.col - 1] = cell
65
+ end
66
+ cells
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ module GoogleCells
2
+
3
+ module Fetcher
4
+
5
+ BASE_URL = 'https://spreadsheets.google.com/feeds/spreadsheets/private/full'
6
+
7
+ def raw(url=nil, params={})
8
+ url ||= BASE_URL
9
+ res = request(:get, url, url_params: params)
10
+ res.body
11
+ end
12
+
13
+ def request(method, url, params={})
14
+ if params[:url_params] && !params[:url_params].empty?
15
+ url << '?' unless url[-1] == "?"
16
+ url << params[:url_params].to_a.map{|k,v| "#{k}=#{v}"}.join('&')
17
+ end
18
+ GoogleCells.client.authorization.fetch_access_token!
19
+ GoogleCells.client.execute!(
20
+ :http_method => method,
21
+ :uri => url,
22
+ :headers => params[:headers],
23
+ :body => params[:body]
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module GoogleCells
2
+ class Folder < GoogleCells::GoogleObject
3
+ @permanent_attributes = [ :key, :spreadsheet ]
4
+ define_accessors
5
+ end
6
+ end
7
+
@@ -0,0 +1,31 @@
1
+ module GoogleCells
2
+
3
+ class GoogleObject
4
+ class << self
5
+ attr_reader :permanent_attributes
6
+
7
+ def define_accessors
8
+ self.instance_eval do
9
+ @permanent_attributes.each do |k|
10
+ define_method(k){ @values[k] }
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize(attribs={})
17
+ @values = {}
18
+ self.class.permanent_attributes.each{|a| @values[a] = attribs[a]}
19
+
20
+ extra = attribs.keys - self.class.permanent_attributes
21
+ extra.each do |a|
22
+ if self.respond_to?("#{a}=")
23
+ instance_variable_set("@#{a}".to_sym, attribs[a])
24
+ next
25
+ end
26
+ raise ArgumentError, "invalid attribute #{a} passed to #{
27
+ self.class}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/fetcher'
2
+
3
+ module GoogleCells
4
+
5
+ module Reader
6
+ include GoogleCells::Fetcher
7
+
8
+ def each_entry(url=nil, params={}, &block)
9
+ doc = raw(url, params)
10
+ reader = Nokogiri::XML::Reader(doc)
11
+ reader.each do |node|
12
+ next unless node.name == 'entry' && node.node_type ==
13
+ Nokogiri::XML::Reader::TYPE_ELEMENT
14
+ block.call(Nokogiri.parse(node.outer_xml))
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,7 @@
1
+ module GoogleCells
2
+
3
+ class Row < GoogleCells::GoogleObject
4
+ @permanent_attributes = [:number, :cells, :worksheet]
5
+ define_accessors
6
+ end
7
+ end
@@ -0,0 +1,179 @@
1
+ require 'google_cells/worksheet'
2
+ require 'json'
3
+
4
+ module GoogleCells
5
+
6
+ class Spreadsheet < GoogleCells::GoogleObject
7
+ extend UrlHelper
8
+ extend Reader
9
+
10
+ @permanent_attributes = [ :title, :updated_at, :author, :key ]
11
+ define_accessors
12
+
13
+ class << self
14
+
15
+ def list
16
+ spreadsheets = []
17
+ each_entry do |entry|
18
+ args = parse_from_entry(entry)
19
+ spreadsheets << Spreadsheet.new(args)
20
+ end
21
+ spreadsheets
22
+ end
23
+
24
+ alias_method :all, :list
25
+
26
+ def get(key)
27
+ res = request(:get, worksheets_uri(key))
28
+ args = parse_from_entry(Nokogiri.parse(res.body), key)
29
+ Spreadsheet.new(args)
30
+ end
31
+
32
+ def copy(key, opts={})
33
+ params = {}
34
+ body = nil
35
+ { :writers_can_share => 'writersCanShare',
36
+ :title => 'title' }.each do |sym,str|
37
+ next unless opts[sym]
38
+ body ||= {}
39
+ body[str] = opts.delete(sym)
40
+ end
41
+ if body
42
+ params[:body] = body.to_json
43
+ params[:headers] = {'Content-Type' => 'application/json'}
44
+ end
45
+ res = request(:post, copy_uri(key), params)
46
+ s = get(res.data['id'])
47
+ end
48
+
49
+ def share(key, params)
50
+ body = {}
51
+ [:role, :type, :value].each do |sym|
52
+ body[sym.to_s] = params.delete(sym)
53
+ end
54
+ params[:body] = body.to_json
55
+
56
+ params[:url_params] = {}
57
+ params[:url_params]['sendNotificationEmails'] = params.delete(
58
+ :send_notification_emails) if !params[:send_notification_emails].
59
+ to_s.empty?
60
+ params[:url_params]['emailMessage'] = params.delete(
61
+ :email_message) if params[:email_message]
62
+
63
+ params[:headers] = {'Content-Type' => 'application/json'}
64
+
65
+ res = request(:post, permissions_uri(key), params)
66
+ true
67
+ end
68
+
69
+ def delete(key)
70
+ request(:delete, file_uri(key))
71
+ end
72
+
73
+ def unsubscribe(params)
74
+ body = {}
75
+ body['id'] = params.delete(:id) if params[:id]
76
+ body['resourceId'] = params.delete(:resource_id) if params[:resource_id]
77
+ drive = GoogleCells.client.discovered_api('drive', 'v2')
78
+ GoogleCells.client.execute!(
79
+ :api_method => drive.channels.stop,
80
+ :body_object => body )
81
+ true
82
+ end
83
+
84
+ def subscribe(key, params)
85
+ body = {"type" => "web_hook"}
86
+ [:id, :address, :token, :expiration, :type].each do |sym|
87
+ body[sym.to_s] = params.delete(sym) if params[sym]
88
+ end
89
+ drive = GoogleCells.client.discovered_api('drive', 'v2')
90
+ res = GoogleCells.client.execute!(
91
+ :api_method => drive.files.watch,
92
+ :body_object => body,
93
+ :parameters => { 'fileId' => key })
94
+ res.data['resourceId']
95
+ end
96
+ end
97
+
98
+ %w( subscribe share ).each do |m|
99
+ define_method(m){|args| self.class.send(m, self.key, args) }
100
+ end
101
+
102
+ def unsubscribe(params)
103
+ self.class.unsubscribe(params)
104
+ end
105
+
106
+ def delete
107
+ self.class.delete(self.key)
108
+ end
109
+
110
+ def copy(opts={})
111
+ self.class.copy(self.key, opts)
112
+ end
113
+
114
+ def enfold(folder_key)
115
+ return true if @folders && @folders.select{|f| f.key == folder_key}.first
116
+ body = {'id' => self.key}.to_json
117
+ res = self.class.request(:post, self.class.folder_uri(folder_key),
118
+ :body => body, :headers => {'Content-Type' => 'application/json'})
119
+ @folders << Folder.new(spreadsheet:self, key:folder_key) if @folders
120
+ true
121
+ end
122
+
123
+ def defold(folder_key)
124
+ klass = self.class
125
+ res = klass.request(:delete, klass.child_uri(folder_key, self.key))
126
+ @folders = nil
127
+ true
128
+ end
129
+
130
+ def folders
131
+ return @folders if @folders
132
+ res = self.class.request(:get, self.class.file_uri(key))
133
+ data = JSON.parse(res.body)
134
+ return @folders = [] if data['parents'].nil?
135
+ @folders = data['parents'].map do |f|
136
+ Folder.new(spreadsheet: self, key:f['id'])
137
+ end
138
+ end
139
+
140
+ def worksheets
141
+ return @worksheets if @worksheets
142
+ @worksheets = []
143
+ self.class.each_entry(worksheets_uri) do |entry|
144
+ args = {
145
+ title: entry.css("title").text,
146
+ updated_at: entry.css("updated").text,
147
+ cells_uri: entry.css(
148
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
149
+ )[0]["href"],
150
+ lists_uri: entry.css(
151
+ "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
152
+ )[0]["href"],
153
+ row_count: entry.css("gs|rowCount").text.to_i,
154
+ col_count: entry.css("gs|colCount").text.to_i,
155
+ spreadsheet: self
156
+ }
157
+ @worksheets << Worksheet.new(args)
158
+ end
159
+ return @worksheets
160
+ end
161
+
162
+ private
163
+
164
+ def self.parse_from_entry(entry, key=nil)
165
+ key ||= entry.css("link").select{|el| el['rel'] == 'alternate'}.
166
+ first['href'][/key=.+/][4..-1]
167
+ { title: entry.css("title").first.text,
168
+ key: key,
169
+ updated_at: entry.css("updated").first.text,
170
+ author: Author.new(
171
+ name: entry.css("author/name").first.text,
172
+ email: entry.css("author/email").first.text
173
+ )
174
+ }
175
+ end
176
+
177
+ def worksheets_uri; self.class.worksheets_uri(key); end
178
+ end
179
+ end
@@ -0,0 +1,37 @@
1
+ module GoogleCells
2
+
3
+ module UrlHelper
4
+
5
+ def worksheets_uri(key)
6
+ "https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
7
+ end
8
+
9
+ def copy_uri(key)
10
+ "https://www.googleapis.com/drive/v2/files/#{key}/copy"
11
+ end
12
+
13
+ def permissions_uri(key)
14
+ "https://www.googleapis.com/drive/v2/files/#{key}/permissions"
15
+ end
16
+
17
+ def folder_uri(key)
18
+ "https://www.googleapis.com/drive/v2/files/#{key}/children"
19
+ end
20
+
21
+ def child_uri(folder_key, child_key)
22
+ "https://www.googleapis.com/drive/v2/files/#{folder_key}/children/#{child_key}"
23
+ end
24
+
25
+ def file_uri(key)
26
+ "https://www.googleapis.com/drive/v2/files/#{key}"
27
+ end
28
+
29
+ def watch_uri(key)
30
+ "https://www.googleapis.com/drive/v2/files/#{key}/watch"
31
+ end
32
+
33
+ def unwatch_uri
34
+ "https://www.googleapis.com/drive/v2/channels/stop"
35
+ end
36
+ end
37
+ end