skypager 0.0.2

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 (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