skypager 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE.txt +22 -0
  5. data/Rakefile +21 -0
  6. data/bin/skypager +17 -0
  7. data/examples/.gitignore +4 -0
  8. data/examples/blog-site/.gitignore +18 -0
  9. data/examples/blog-site/.pryrc +4 -0
  10. data/examples/blog-site/Gemfile +8 -0
  11. data/examples/blog-site/config.rb +17 -0
  12. data/examples/blog-site/data/dropbox.json +1 -0
  13. data/examples/blog-site/source/images/background.png +0 -0
  14. data/examples/blog-site/source/images/middleman.png +0 -0
  15. data/examples/blog-site/source/index.html.erb +10 -0
  16. data/examples/blog-site/source/javascripts/all.js +1 -0
  17. data/examples/blog-site/source/layouts/layout.erb +19 -0
  18. data/examples/blog-site/source/posts/introduction-to-skypager.html.md +23 -0
  19. data/examples/blog-site/source/posts/skypager-and-dnsimple-and-amazon-web-services-combo.html.md +9 -0
  20. data/examples/blog-site/source/stylesheets/all.css +55 -0
  21. data/examples/blog-site/source/stylesheets/normalize.css +375 -0
  22. data/examples/gallery-site/.gitignore +18 -0
  23. data/examples/gallery-site/.pryrc +4 -0
  24. data/examples/gallery-site/Gemfile +11 -0
  25. data/examples/gallery-site/config.rb +38 -0
  26. data/examples/gallery-site/data/dropbox.json +1 -0
  27. data/examples/gallery-site/data/galleries.json +1 -0
  28. data/examples/gallery-site/source/gallery.html.erb +7 -0
  29. data/examples/gallery-site/source/images/background.png +0 -0
  30. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/001.jpg +0 -0
  31. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/002.jpg +0 -0
  32. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/003.jpg +0 -0
  33. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/004.jpg +0 -0
  34. data/examples/gallery-site/source/images/galleries/luca-gallery-1/001.jpg +0 -0
  35. data/examples/gallery-site/source/images/galleries/luca-gallery-1/002.JPG +0 -0
  36. data/examples/gallery-site/source/images/galleries/luca-gallery-1/003.jpg +0 -0
  37. data/examples/gallery-site/source/images/galleries/luca-gallery-1/004.JPG +0 -0
  38. data/examples/gallery-site/source/images/middleman.png +0 -0
  39. data/examples/gallery-site/source/index.html.erb +10 -0
  40. data/examples/gallery-site/source/javascripts/all.js +1 -0
  41. data/examples/gallery-site/source/layouts/layout.erb +20 -0
  42. data/examples/gallery-site/source/stylesheets/all.css +0 -0
  43. data/examples/gallery-site/source/stylesheets/normalize.css +375 -0
  44. data/examples/gallery-site/source/tutorial.md +151 -0
  45. data/lib/skypager.rb +92 -0
  46. data/lib/skypager/build_server.rb +17 -0
  47. data/lib/skypager/cli/commands/config.rb +58 -0
  48. data/lib/skypager/cli/commands/create.rb +98 -0
  49. data/lib/skypager/cli/commands/deploy.rb +30 -0
  50. data/lib/skypager/cli/commands/edit.rb +32 -0
  51. data/lib/skypager/cli/commands/list.rb +12 -0
  52. data/lib/skypager/cli/commands/setup.rb +124 -0
  53. data/lib/skypager/cli/commands/sync.rb +18 -0
  54. data/lib/skypager/configuration.rb +173 -0
  55. data/lib/skypager/data.rb +8 -0
  56. data/lib/skypager/data/excel_spreadsheet.rb +8 -0
  57. data/lib/skypager/data/google_spreadsheet.rb +225 -0
  58. data/lib/skypager/data/request.rb +12 -0
  59. data/lib/skypager/data/source.rb +171 -0
  60. data/lib/skypager/data/source_routes_proxy.rb +30 -0
  61. data/lib/skypager/dns.rb +65 -0
  62. data/lib/skypager/extension.rb +203 -0
  63. data/lib/skypager/middleman/commands/data.rb +0 -0
  64. data/lib/skypager/middleman/commands/deploy.rb +0 -0
  65. data/lib/skypager/middleman/commands/sync.rb +0 -0
  66. data/lib/skypager/site.rb +208 -0
  67. data/lib/skypager/sync.rb +23 -0
  68. data/lib/skypager/sync/amazon.rb +171 -0
  69. data/lib/skypager/sync/dropbox.rb +173 -0
  70. data/lib/skypager/sync/dropbox/delta.rb +67 -0
  71. data/lib/skypager/sync/folder.rb +235 -0
  72. data/lib/skypager/sync/google.rb +143 -0
  73. data/lib/skypager/tar.rb +77 -0
  74. data/lib/skypager/version.rb +3 -0
  75. data/skypager.gemspec +40 -0
  76. data/spec/lib/skypager/configuration_spec.rb +5 -0
  77. data/spec/lib/skypager/data_spec.rb +5 -0
  78. data/spec/lib/skypager/site_spec.rb +5 -0
  79. data/spec/spec_helper.rb +14 -0
  80. data/spec/support/json_helper.rb +7 -0
  81. metadata +383 -0
@@ -0,0 +1,32 @@
1
+ edit_spreadsheet_command = lambda do |c|
2
+ c.syntax = "skypager edit datasource NAME"
3
+ c.description = "Open up the data source editor"
4
+
5
+ c.action do |args, options|
6
+ require 'launchy'
7
+ require 'middleman-core'
8
+
9
+ name = args.first
10
+
11
+ if defined?(::Middleman)
12
+ app = ::Middleman::Application.server.inst do
13
+ set :environment, 'development'
14
+ end
15
+
16
+ if !name
17
+ names = app.data_sources.keys.map(&:to_s)
18
+ name = choose("Which data source?", *names)
19
+ end
20
+
21
+ source = app.data_sources[name.to_sym]
22
+
23
+ if source
24
+ Launchy.open(source.edit_url)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ command "edit data source" do |c|
31
+ edit_spreadsheet_command.call(c)
32
+ end
@@ -0,0 +1,12 @@
1
+ command 'list spreadsheets' do |c|
2
+ c.syntax = 'skypager list:spreadsheets [options]'
3
+ c.description = 'list available spreadsheets to use with skypager '
4
+
5
+ c.action do |_args, _options|
6
+
7
+ Skypager::Sync::Google.setup
8
+ Skypager::Sync.google.spreadsheets.map {|s| puts [s.key,s.title].join("\t\t") }
9
+ end
10
+ end
11
+
12
+
@@ -0,0 +1,124 @@
1
+ command 'setup' do |c|
2
+ c.syntax = 'skypager setup [options]'
3
+ c.description = 'setup the integrations for skypager to work'
4
+
5
+ c.option '--skip-dropbox', nil, 'Skip dropbox setup'
6
+ c.option '--skip-google', nil, 'Skip google setup'
7
+ c.option '--skip-amazon', nil, 'Skip amazon setup'
8
+ c.option '--skip-dns', nil, 'Skip dns setup'
9
+
10
+ c.action do |args, options|
11
+ Skypager::Sync::Dropbox.setup unless options.skip_dropbox || Skypager.config.dropbox_setup?
12
+ Skypager::Sync::Google.setup unless options.skip_google || Skypager.config.google_setup?
13
+ Skypager::Sync::Amazon.setup unless options.skip_amazon || Skypager.config.amazon_setup?
14
+
15
+ require 'dnsimple'
16
+ Skypager.dns.setup unless options.skip_dns || Skypager.config.dnsimple_setup?
17
+ end
18
+ end
19
+
20
+ command 'setup amazon' do |c|
21
+ c.syntax = 'skypager setup amazon [options]'
22
+ c.description = 'Setup amazon integration'
23
+
24
+ c.option '--access-key-id STRING', String, 'What is the AWS access key id?'
25
+ c.option '--secret-access-key STRING', String, 'What is the AWS secret access key?'
26
+ c.option '--create-bucket STRING', String, 'What is the bucket name?'
27
+
28
+ c.action do |args, options|
29
+ Skypager::Sync::Amazon.setup(access_key_id: options.access_key_id,
30
+ secret_access_key: options.secret_access_key)
31
+ end
32
+
33
+ end
34
+
35
+ command 'setup deployment' do |c|
36
+ c.syntax = 'skypager setup deployment [options]'
37
+ c.description = 'Setup AWS deployment / hosting'
38
+
39
+ c.option '--access-key-id STRING', String, 'What is the AWS access key id?'
40
+ c.option '--secret-access-key STRING', String, 'What is the AWS secret access key?'
41
+ c.option '--create-bucket STRING', String, 'What is the bucket name?'
42
+
43
+ c.action do |args, options|
44
+ Skypager::Sync::Amazon.setup(access_key_id: options.access_key_id,
45
+ secret_access_key: options.secret_access_key)
46
+
47
+
48
+ app = Skypager.app
49
+
50
+ if !Skypager.config.dnsimple_setup?
51
+ if agree("Do you want to setup DNSimple integration?")
52
+ Skypager.dns.setup()
53
+ end
54
+ end
55
+
56
+ custom_domain = app.deploy_options[:custom_domain] || options.custom_domain || ask("Enter a custom domain name to deploy to. Leave blank if you don't want to use this.")
57
+
58
+ if custom_domain.to_s.length > 0
59
+ app.site.set :custom_domain, custom_domain
60
+ app.site.set :use_cdn, true
61
+ @custom_domain = custom_domain
62
+ end
63
+
64
+ unless @custom_domain || app.deploy_options[:use_cdn] == true
65
+ if agree("Do you want to use a CDN? Answer yes if you want to use a custom domain, or SSL for this site")
66
+ app.site.set :use_cdn, true
67
+ end
68
+ end
69
+
70
+ bucket_name = app.deploy_options[:bucket_name] || options.create_bucket || ask("Enter a name for this bucket. Leave blank to use #{ app.site.name }.#{ Skypager.config.domain}")
71
+ bucket_name = app.site_name + "." + Skypager.config.domain if bucket_name.to_s.length == 0
72
+
73
+ if bucket_name
74
+ if bucket = Skypager.amazon.find_or_create_bucket(bucket_name)
75
+ app.site.set :bucket_key, bucket.key
76
+ app.site.set :bucket_name, bucket.key
77
+ end
78
+ end
79
+
80
+ config_path = Pathname(app.root).join("config.rb")
81
+ config_text = config_path.read rescue ""
82
+
83
+ app.site.after_setup
84
+
85
+ unless config_text.match(/deploy_to\(\:amazon/)
86
+ puts "Adding the following to your config.rb:\n\n\t#{ line = app.site.deploy_options_config_string }"
87
+ config_path.open("a+") {|fh| fh.write(line) }
88
+ end
89
+
90
+ site = app.site
91
+
92
+ puts "Site Info:"
93
+ puts "Bucket Name: #{ site.bucket_name }"
94
+ puts "Bucket URL: #{ site.bucket_url }"
95
+ puts "CDN URL: #{ site.cname_value }" if site.has_cdn?
96
+ puts "Custom Domain: #{ site.custom_domain }" if site.custom_domain?
97
+ end
98
+ end
99
+
100
+ command 'setup dropbox' do |c|
101
+ c.syntax = 'skypager setup dropbox [options]'
102
+ c.description = 'Setup dropbox sync'
103
+
104
+ c.option '--app-key STRING', String, 'What is the dropbox app key?'
105
+ c.option '--app-secret STRING', String, 'What is the dropbox app secret?'
106
+
107
+ c.action do |args, options|
108
+ Skypager::Sync::Dropbox.setup(dropbox_app_key: options.app_key,
109
+ dropbox_app_secret: options.app_secret)
110
+ end
111
+ end
112
+
113
+ command 'setup google' do |c|
114
+ c.syntax = 'skypager setup google [options]'
115
+ c.description = 'Setup google sync'
116
+
117
+ c.option '--client-id STRING', String, 'What is the google app client id?'
118
+ c.option '--client-secret STRING', String, 'What is the google app client secret?'
119
+
120
+ c.action do |args, options|
121
+ Skypager::Sync::Google.setup(client_id: options.client_id,
122
+ client_secret: options.client_secret)
123
+ end
124
+ end
@@ -0,0 +1,18 @@
1
+ command 'sync' do |c|
2
+ c.action do |args, options|
3
+ app = ::Middleman::Application.server.inst do
4
+ set :environment, 'development'
5
+ end
6
+
7
+ app.synced_folders.each do |name, folder|
8
+ puts "Syncing #{ name }"
9
+ folder.sync()
10
+ end
11
+
12
+ app.data_sources.each do |name, source|
13
+ source.refresh
14
+ source.save_to_disk unless source.persisted?
15
+ data.callbacks(name, -> { source.data })
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,173 @@
1
+ require 'singleton'
2
+ require 'json'
3
+
4
+ module Skypager
5
+ class Configuration
6
+ include Singleton
7
+
8
+ DefaultSettings = {
9
+ github_username: '',
10
+ github_api_token: '',
11
+
12
+ dnsimple_api_token: '',
13
+ dnsimple_username: '',
14
+
15
+ dropbox_app_key: '',
16
+ dropbox_app_secret: '',
17
+ dropbox_app_type: 'sandbox',
18
+ dropbox_client_token: '',
19
+ dropbox_client_secret: '',
20
+
21
+ aws_access_key_id: '',
22
+ aws_secret_access_key: '',
23
+
24
+ google_client_id: '',
25
+ google_client_secret: '',
26
+ google_refresh_token: '',
27
+ google_access_token: '',
28
+
29
+ domain: "skypager.io",
30
+
31
+ sites_directory: { }
32
+ }
33
+
34
+ def self.method_missing(meth, *args, &block)
35
+ if instance.respond_to?(meth)
36
+ return instance.send meth, *args, &block
37
+ end
38
+
39
+ nil
40
+ end
41
+
42
+ def method_missing(meth, *args, &block)
43
+ if current.key?(meth.to_s)
44
+ return current.fetch(meth)
45
+ end
46
+
47
+ super
48
+ end
49
+
50
+ def initialize!
51
+ FileUtils.mkdir_p home_config_path.dirname
52
+ end
53
+
54
+ def dnsimple_setup?
55
+ dnsimple_api_token.to_s.length > 0 && dnsimple_username.to_s.length > 0
56
+ end
57
+
58
+ def dropbox_setup?
59
+ dropbox_app_key.to_s.length > 0 && dropbox_app_secret.to_s.length > 0
60
+ end
61
+
62
+ def google_setup?
63
+ google_client_secret.to_s.length > 0 && google_client_id.to_s.length > 0
64
+ end
65
+
66
+ def amazon_setup?
67
+ aws_access_key_id.to_s.length > 0 && aws_secret_access_key.to_s.length > 0
68
+ end
69
+
70
+ def show
71
+ current.each do |p|
72
+ key, value = p
73
+
74
+ unless key == 'sites_directory'
75
+ puts "#{key}: #{ value.inspect }"
76
+ end
77
+ end
78
+ end
79
+
80
+ def current
81
+ @current ||= begin
82
+ home_config.merge(cwd_config)
83
+ end
84
+ end
85
+
86
+ def current_config
87
+ cwd_config_path.exist? ? cwd_config : home_config
88
+ end
89
+
90
+ def get(setting)
91
+ setting = setting.to_s.downcase
92
+ current_config[setting]
93
+ end
94
+
95
+ def set(setting, value, persist = true, options={})
96
+ setting = setting.to_s.downcase
97
+ cfg = options[:global] ? home_config : current_config
98
+ cfg[setting] = value
99
+ save! if persist == true
100
+ value
101
+ end
102
+
103
+ def unset(setting, persist = true)
104
+ current_config.delete(setting)
105
+ save! if persist == true
106
+ end
107
+
108
+ def current
109
+ defaults = DefaultSettings.dup
110
+ Hashie::Mash.new(defaults.merge(home_config.merge(cwd_config).merge(applied_config)))
111
+ end
112
+
113
+ def apply_config_from_path(path)
114
+ path = Pathname(path)
115
+ parsed = JSON.parse(path.read) rescue {}
116
+ applied_config.merge!(parsed)
117
+ nil
118
+ end
119
+
120
+ def save!
121
+ save_home_config
122
+ save_cwd_config
123
+ end
124
+
125
+ def save_cwd_config
126
+ return nil unless cwd_config_path.exist?
127
+
128
+ File.open(cwd_config_path, 'w+') do |fh|
129
+ fh.write JSON.generate(cwd_config.to_hash)
130
+ end
131
+ end
132
+
133
+ def save_home_config
134
+ File.open(home_config_path, 'w+') do |fh|
135
+ fh.write JSON.generate(home_config.to_hash)
136
+ end
137
+ end
138
+
139
+ # Applied config is configuration values passed in context
140
+ # usually from the cli, but also in the unit tests
141
+ def applied_config
142
+ @applied_config ||= {}
143
+ end
144
+
145
+ def cwd_config
146
+ @cwd_config ||= begin
147
+ (cwd_config_path.exist? rescue false) ? JSON.parse(cwd_config_path.read) : {}
148
+ rescue
149
+ {}
150
+ end
151
+ end
152
+
153
+ def home_config
154
+ @home_config ||= begin
155
+ (home_config_path.exist? rescue false) ? JSON.parse(home_config_path.read) : {}
156
+ rescue
157
+ {}
158
+ end
159
+ end
160
+
161
+ def home_folder
162
+ Pathname(ENV['HOME']).join('.skypager')
163
+ end
164
+
165
+ def home_config_path
166
+ home_folder.join('config.json')
167
+ end
168
+
169
+ def cwd_config_path
170
+ Pathname(Dir.pwd).join('skypager.json')
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,8 @@
1
+ module Skypager
2
+ module Data
3
+ def self.sync_all
4
+ end
5
+ end
6
+ end
7
+
8
+
@@ -0,0 +1,8 @@
1
+ module Skypager
2
+ module Data
3
+ class ExcelSpreadsheet < Source
4
+ requires :host,
5
+ :path
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,225 @@
1
+ module Skypager
2
+ module Data
3
+ class GoogleSpreadsheet < Source
4
+
5
+ requires :key
6
+
7
+ attr_accessor :key,
8
+ :session,
9
+ :name
10
+
11
+ def initialize name, options={}
12
+ @options = options
13
+
14
+ if name.is_a?(GoogleDrive::Spreadsheet)
15
+ @spreadsheet = name
16
+ @name = @spreadsheet.title
17
+ @key = @spreadsheet.key
18
+ end
19
+
20
+ @key ||= options[:key]
21
+ @session ||= options.fetch(:session) { Skypager::Sync.google.api }
22
+
23
+ ensure_valid_options!
24
+ end
25
+
26
+ def self.create_from_file(path, title)
27
+ if find_by_title(title)
28
+ raise 'Spreadsheet with this title already exists'
29
+ end
30
+
31
+ session.upload_from_file(path, title, :content_type => "text/csv")
32
+
33
+ find_by_title(title)
34
+ end
35
+
36
+ def self.[](key_or_title)
37
+ find_by_key(key_or_title) || find_by_title(key_or_title)
38
+ end
39
+
40
+ def self.find_by_key(key)
41
+ sheet = session_spreadsheets.detect do |spreadsheet|
42
+ spreadsheet.key == key
43
+ end
44
+
45
+ sheet && new(sheet, session: Skypager.google.session)
46
+ end
47
+
48
+ def self.find_by_title title
49
+ sheet = session_spreadsheets.detect do |spreadsheet|
50
+ spreadsheet.title.match(title)
51
+ end
52
+
53
+ sheet && new(sheet, session: Skypager.google.session)
54
+ end
55
+
56
+ def self.session_spreadsheets
57
+ @session_spreadsheets ||= Skypager.google.api.spreadsheets
58
+ end
59
+
60
+ def self.create_from_data(data, options={})
61
+ require 'csv'
62
+
63
+ headers = Array(options[:headers]).map(&:to_s)
64
+
65
+ tmpfile = "tmp-csv.csv"
66
+
67
+ CSV.open(tmpfile, "wb") do |csv|
68
+ csv << headers
69
+
70
+ data.each do |row|
71
+ csv << headers.map do |header|
72
+ row = row.stringify_keys
73
+ row[header.to_s]
74
+ end
75
+ end
76
+ end
77
+
78
+ spreadsheet = Skypager::Sync.google.api.upload_from_file(tmpfile, options[:title], :content_type => "text/csv")
79
+
80
+ if options[:skypager_config_info]
81
+ config_line = "data_source :#{ spreadsheet.title.to_s.downcase.underscore }, :type => 'google', :key => '#{ spreadsheet.key }'\n"
82
+
83
+ puts "Adding the following line to your config.rb: \n #{config_line}"
84
+
85
+ File.open("config.rb", "a+") do |fh|
86
+ fh.write("\n")
87
+ fh.write(config_line)
88
+ end
89
+ end
90
+
91
+ new(spreadsheet.title, key: spreadsheet.key)
92
+ end
93
+
94
+
95
+ def title
96
+ @name ||= spreadsheet.try(:title)
97
+ end
98
+
99
+ def edit_url
100
+ spreadsheet.human_url
101
+ end
102
+
103
+ def share_write_access_with *emails
104
+ acl = spreadsheet.acl
105
+
106
+ Array(emails).flatten.each do |email|
107
+ acl.push scope_type: "user",
108
+ with_key: false,
109
+ role: "writer",
110
+ scope: email
111
+ end
112
+ end
113
+
114
+ def share_read_access_with *emails
115
+ acl = spreadsheet.acl
116
+
117
+ Array(emails).flatten.each do |email|
118
+ acl.push scope_type: "user",
119
+ with_key: false,
120
+ role: "reader",
121
+ scope: email
122
+ end
123
+ end
124
+
125
+ def add_to_collection collection_title
126
+ collection = if collection_title.is_a?(GoogleDrive::Collection)
127
+ collection_title
128
+ else
129
+ session.collections.find do |c|
130
+ c.title == collection_title
131
+ end
132
+ end
133
+
134
+ if !collection
135
+ collection_names = session.collections.map(&:title)
136
+ raise 'Could not find collection in Google drive. Maybe you mean: ' + collection_names.join(', ')
137
+ end
138
+ end
139
+
140
+ def spreadsheet_key
141
+ key
142
+ end
143
+
144
+ def stale?
145
+ (!need_to_refresh? && (age > max_age)) || fresh_on_server?
146
+ end
147
+
148
+ def fresh_on_server?
149
+ refreshed_at.to_i > 0 && (last_updated_at > refreshed_at)
150
+ end
151
+
152
+ def last_updated_at
153
+ if value = spreadsheet.document_feed_entry.css('updated').try(:text) rescue nil
154
+ DateTime.parse(value).to_i
155
+ end
156
+ end
157
+
158
+ def fetch
159
+ self.raw = process_worksheets
160
+ end
161
+
162
+ def preprocess
163
+ single? ? raw.values.flatten : raw
164
+ end
165
+
166
+ protected
167
+
168
+ def process_worksheets
169
+ worksheets.inject(Hashie::Mash.new) do |memo, parts|
170
+ k, ws = parts
171
+ header_row = Array(ws.rows[0])
172
+ column_names = header_row.map {|cell| "#{ cell }".parameterize.underscore }
173
+ rows = ws.rows.slice(1, ws.rows.length)
174
+
175
+ row_index = 1
176
+ memo[k] = rows.map do |row|
177
+ col_index = 0
178
+
179
+ _record = column_names.inject({}) do |record, field|
180
+ record[field] = "#{ row[col_index] }".strip
181
+ record["_id"] = row_index
182
+ col_index += 1
183
+ record
184
+ end
185
+
186
+ row_index += 1
187
+
188
+ _record
189
+ end
190
+
191
+ memo
192
+ end
193
+ end
194
+
195
+ def single?
196
+ worksheets.length == 1
197
+ end
198
+
199
+ def header_rows_for_worksheet key
200
+ if key.is_a?(Fixnum)
201
+ _worksheets[key]
202
+ else
203
+ worksheets.fetch(key)
204
+ end
205
+ end
206
+
207
+ def worksheets
208
+ @worksheets ||= _worksheets.inject(Hashie::Mash.new) do |memo,ws|
209
+ key = ws.title.strip.downcase.underscore.gsub(/\s+/,'_')
210
+ memo[key] = ws
211
+ memo
212
+ end
213
+ end
214
+
215
+ def _worksheets
216
+ @_worksheets ||= spreadsheet.worksheets
217
+ end
218
+
219
+ def spreadsheet
220
+ @spreadsheet ||= session.spreadsheet_by_key(spreadsheet_key)
221
+ end
222
+
223
+ end
224
+ end
225
+ end