rpw 0.0.5 → 1.2.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.
data/lib/rpw.rb CHANGED
@@ -1,395 +1,8 @@
1
- require "typhoeus"
2
- require "json"
3
- require_relative "rpw/version"
1
+ require "rpw/version"
2
+ require "rpw/client"
3
+ require "rpw/client_data"
4
+ require "rpw/gateway"
4
5
 
5
6
  module RPW
6
7
  class Error < StandardError; end
7
-
8
- class Gateway
9
- attr_accessor :domain
10
-
11
- def initialize(domain, key)
12
- @domain = domain
13
- @key = key
14
- end
15
-
16
- class Error < StandardError; end
17
-
18
- def authenticate_key(key)
19
- Typhoeus.get(domain + "/license", userpwd: key + ":").success?
20
- end
21
-
22
- def get_content_by_position(position)
23
- response = Typhoeus.get(domain + "/contents/positional?position=#{position}", userpwd: @key + ":")
24
- if response.success?
25
- JSON.parse(response.body)
26
- else
27
- puts response.inspect
28
- raise Error, "There was a problem fetching this content."
29
- end
30
- end
31
-
32
- def list_content
33
- response = Typhoeus.get(domain + "/contents", userpwd: @key + ":")
34
- if response.success?
35
- JSON.parse(response.body)
36
- else
37
- puts response.inspect
38
- raise Error, "There was a problem fetching this content."
39
- end
40
- end
41
-
42
- def download_content(content, folder:)
43
- puts "Downloading #{content["title"]}..."
44
- downloaded_file = File.open("#{folder}/#{content["s3_key"]}", "w")
45
- request = Typhoeus::Request.new(content["url"])
46
- request.on_body do |chunk|
47
- downloaded_file.write(chunk)
48
- printf(".") if rand(500) == 0 # lol
49
- end
50
- request.on_complete { |response| downloaded_file.close }
51
- request
52
- end
53
-
54
- def latest_version?
55
- resp = Typhoeus.get("https://rubygems.org/api/v1/gems/rpw.json")
56
- data = JSON.parse resp.body
57
- Gem::Version.new(RPW::VERSION) >= Gem::Version.new(data["version"])
58
- end
59
-
60
- def register_email(email)
61
- Typhoeus.put(domain + "/license", params: {email: email, key: @key})
62
- end
63
- end
64
-
65
- class Client
66
- RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
67
-
68
- def setup(key)
69
- gateway.authenticate_key(key)
70
- client_data["key"] = key
71
- end
72
-
73
- def register_email(email)
74
- gateway.register_email(email)
75
- end
76
-
77
- def directory_setup(home_dir_ok = true)
78
- ["video", "quiz", "lab", "text", "cgrp"].each do |path|
79
- FileUtils.mkdir_p(path) unless File.directory?(path)
80
- end
81
-
82
- if home_dir_ok
83
- ClientData.create_in_home!
84
- else
85
- ClientData.create_in_pwd!
86
- end
87
-
88
- unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_key/)
89
- File.open(".gitignore", "a") do |f|
90
- f.puts "\n"
91
- f.puts ".rpw_key\n"
92
- f.puts ".rpw_info\n"
93
- f.puts "video\n"
94
- f.puts "quiz\n"
95
- f.puts "lab\n"
96
- f.puts "text\n"
97
- f.puts "cgrp\n"
98
- end
99
- end
100
-
101
- File.open("README.md", "w+") do |f|
102
- f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
103
- end
104
- end
105
-
106
- def next(open_after = false)
107
- complete
108
- content = next_content
109
- if content.nil?
110
- finished_workshop
111
- return
112
- end
113
-
114
- unless File.exist?(content["style"] + "/" + content["s3_key"])
115
- gateway.download_content(content, folder: content["style"]).run
116
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
117
- end
118
- client_data["current_lesson"] = content["position"]
119
- display_content(content, open_after)
120
- end
121
-
122
- def complete
123
- reset_progress unless client_data["current_lesson"] && client_data["completed"]
124
- client_data["completed"] ||= []
125
- client_data["completed"] += [client_data["current_lesson"] || 0]
126
- end
127
-
128
- def list
129
- gateway.list_content
130
- end
131
-
132
- def show(content_pos, open_after = false)
133
- content_pos = client_data["current_lesson"] if content_pos == :current
134
- content = gateway.get_content_by_position(content_pos)
135
- unless File.exist?(content["style"] + "/" + content["s3_key"])
136
- gateway.download_content(content, folder: content["style"]).run
137
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
138
- end
139
- client_data["current_lesson"] = content["position"]
140
- display_content(content, open_after)
141
- end
142
-
143
- def download(content_pos)
144
- if content_pos.downcase == "all"
145
- to_download = gateway.list_content
146
- hydra = Typhoeus::Hydra.new(max_concurrency: 5)
147
- to_download.each do |content|
148
- unless File.exist?(content["style"] + "/" + content["s3_key"])
149
- hydra.queue gateway.download_content(content, folder: content["style"])
150
- end
151
- end
152
- hydra.run
153
- to_download.each { |content| extract_content(content) if content["s3_key"].end_with?(".tar.gz") }
154
- else
155
- content = gateway.get_content_by_position(content_pos)
156
- unless File.exist?(content["style"] + "/" + content["s3_key"])
157
- gateway.download_content(content, folder: content["style"]).run
158
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
159
- end
160
- end
161
- end
162
-
163
- def progress
164
- contents = gateway.list_content
165
- {
166
- completed: client_data["completed"].size,
167
- total: contents.size,
168
- current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
169
- sections: chart_section_progress(contents)
170
- }
171
- end
172
-
173
- def set_progress(lesson)
174
- client_data["current_lesson"] = lesson.to_i
175
- end
176
-
177
- def reset_progress
178
- client_data["current_lesson"] = 0
179
- client_data["completed"] = []
180
- end
181
-
182
- def latest_version?
183
- return true unless ClientData.exists?
184
-
185
- if client_data["last_version_check"]
186
- return true if client_data["last_version_check"] >= Time.now - (60 * 60 * 24)
187
- return false if client_data["last_version_check"] == false
188
- end
189
-
190
- begin
191
- latest = gateway.latest_version?
192
- rescue
193
- return true
194
- end
195
-
196
- client_data["last_version_check"] = if latest
197
- Time.now
198
- else
199
- false
200
- end
201
- end
202
-
203
- def setup?
204
- return false unless ClientData.exists?
205
- client_data["key"]
206
- end
207
-
208
- def directories_ready?
209
- ["video", "quiz", "lab", "text", "cgrp"].all? do |path|
210
- File.directory?(path)
211
- end
212
- end
213
-
214
- private
215
-
216
- def finished_workshop
217
- RPW::CLI.new.print_banner
218
- puts "Congratulations!"
219
- puts "You have completed the Rails Performance Workshop."
220
- end
221
-
222
- def chart_section_progress(contents)
223
- contents.group_by { |c| c["position"] / 100 }
224
- .each_with_object([]) do |(_, c), memo|
225
- completed_str = c.map { |l|
226
- if l["position"] == client_data["current_lesson"]
227
- "O"
228
- elsif client_data["completed"].include?(l["position"])
229
- "X"
230
- else
231
- "."
232
- end
233
- }.join
234
- memo << {
235
- title: c[0]["title"],
236
- progress: completed_str
237
- }
238
- end
239
- end
240
-
241
- def next_content
242
- contents = gateway.list_content
243
- return contents.first unless client_data["completed"]
244
- contents.delete_if { |c| client_data["completed"].include? c["position"] }
245
- contents.min_by { |c| c["position"] }
246
- end
247
-
248
- def client_data
249
- @client_data ||= ClientData.new
250
- end
251
-
252
- def gateway
253
- @gateway ||= Gateway.new(RPW_SERVER_DOMAIN, client_data["key"])
254
- end
255
-
256
- def extract_content(content)
257
- folder = content["style"]
258
- `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
259
- end
260
-
261
- def display_content(content, open_after)
262
- puts "\nCurrent Lesson: #{content["title"]}"
263
- openable = false
264
- case content["style"]
265
- when "video"
266
- location = "video/#{content["s3_key"]}"
267
- openable = true
268
- when "quiz"
269
- Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
270
- when "lab"
271
- location = "lab/#{content["s3_key"][0..-8]}"
272
- when "text"
273
- location = "lab/#{content["s3_key"]}"
274
- openable = true
275
- when "cgrp"
276
- puts "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
277
- puts "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
278
- end
279
- if location
280
- puts "This file can be opened automatically if you add the --open flag." if openable && !open_after
281
- puts "Downloaded to:"
282
- puts location.to_s
283
- if open_after && openable
284
- exec "#{open_command} #{location}"
285
- end
286
- end
287
- end
288
-
289
- require "rbconfig"
290
- def open_command
291
- host_os = RbConfig::CONFIG["host_os"]
292
- case host_os
293
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
294
- "start"
295
- when /darwin|mac os/
296
- "open"
297
- else
298
- "xdg-open"
299
- end
300
- end
301
- end
302
-
303
- require "fileutils"
304
- require "yaml"
305
-
306
- class ClientData
307
- DOTFILE_NAME = ".rpw_info"
308
-
309
- def initialize
310
- data # access file to load
311
- end
312
-
313
- def [](key)
314
- data[key]
315
- end
316
-
317
- def []=(key, value)
318
- data
319
- data[key] = value
320
-
321
- begin
322
- File.open(filestore_location, "w") { |f| f.write(YAML.dump(data)) }
323
- rescue
324
- raise Error, "The RPW data at #{filestore_location} is not writable. \
325
- Check your file permissions."
326
- end
327
- end
328
-
329
- def self.create_in_pwd!
330
- FileUtils.touch(File.expand_path("./" + DOTFILE_NAME))
331
- end
332
-
333
- def self.create_in_home!
334
- FileUtils.mkdir_p("~/.rpw/") unless File.directory?("~/.rpw/")
335
- FileUtils.touch(File.expand_path("~/.rpw/" + DOTFILE_NAME))
336
- end
337
-
338
- def self.delete_filestore
339
- return unless File.exist?(filestore_location)
340
- FileUtils.remove(filestore_location)
341
- end
342
-
343
- def self.exists?
344
- File.exist? filestore_location
345
- end
346
-
347
- def self.filestore_location
348
- if File.exist?(File.expand_path("./" + DOTFILE_NAME))
349
- File.expand_path("./" + DOTFILE_NAME)
350
- else
351
- File.expand_path("~/.rpw/" + DOTFILE_NAME)
352
- end
353
- end
354
-
355
- private
356
-
357
- def filestore_location
358
- self.class.filestore_location
359
- end
360
-
361
- def data
362
- @data ||= begin
363
- yaml = YAML.safe_load(File.read(filestore_location), permitted_classes: [Time])
364
- yaml || {}
365
- end
366
- end
367
- end
368
-
369
- require "digest"
370
- require "thor"
371
-
372
- class Quiz < Thor
373
- desc "give_quiz FILENAME", ""
374
- def give_quiz(filename)
375
- @quiz_data = YAML.safe_load(File.read(filename))
376
- @quiz_data["questions"].each { |q| question(q) }
377
- end
378
-
379
- private
380
-
381
- def question(data)
382
- puts data["prompt"]
383
- data["answer_choices"].each { |ac| puts ac }
384
- provided_answer = ask("Your answer?")
385
- answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer.upcase)
386
- if answer_digest == data["answer_digest"]
387
- say "Correct!"
388
- else
389
- say "Incorrect."
390
- say "I encourage you to try reviewing the material to see what the correct answer is."
391
- end
392
- say ""
393
- end
394
- end
395
8
  end
@@ -0,0 +1,60 @@
1
+ ## Installation Requirements
2
+
3
+ This client assumes you're using Ruby 2.6 or later.
4
+
5
+ This client assumes you have `tar` installed and available on your PATH.
6
+
7
+ The way that the client opens files (using `open` or `xdg-open` depending on platform) assumes you have your default program for the following filetypes set correctly:
8
+
9
+ * .md for Markdown (XCode by default on Mac: you probably want to change that!)
10
+ * .mp4 for videos
11
+
12
+ ## Slack Invite
13
+
14
+ The Slack channel is your best resource for questions about Rails Performance
15
+ or other material in the workshop. Nate is almost always monitoring that channel.
16
+
17
+ If you encounter a **bug or other software problem**, please email support@speedshop.co.
18
+
19
+ If you purchased the Workshop yourself, you will receive a Slack channel invitation
20
+ shortly. If you are attending the Workshop as part of a group and your license key
21
+ was provided to you, you need to register your key to get an invite:
22
+
23
+ ```
24
+ $ rpw key register [YOUR_EMAIL_ADDRESS]
25
+ ```
26
+
27
+ Please note you can only register your key once.
28
+
29
+ ## Important Commands
30
+
31
+ Here are some important commands for you to know:
32
+
33
+ ```
34
+ $ rpw next | Proceed to the next part of the workshop.
35
+ $ rpw complete | Mark current lesson as complete.
36
+ $ rpw list | List all workshop lessons. Shows progress.
37
+ $ rpw download | Download all lessons. Useful for offline access.
38
+ $ rpw show | Show any particular workshop lesson.
39
+ $ rpw current | Opens the current lesson.
40
+ ```
41
+
42
+ Generally, you'll just be doing a lot of `$ rpw next`! It's basically the same thing as `$ rpw complete && rpw show`.
43
+
44
+ #### --no-open
45
+
46
+ By default, `$ rpw next` (and `$ rpw show` and `$ rpw current`) will try to open the content it downloads. If you
47
+ either don't like this, or for some reason it doesn't work, use `$ rpw next --no-open`.
48
+
49
+ ## Working Offline
50
+
51
+ By default, the course will download each piece of content as you progress through
52
+ the course. However, you can use `rpw download` to download all content
53
+ at once, and complete the workshop entirely offline.
54
+
55
+ Videos in this workshop are generally about 100MB each, which means the entire
56
+ course is about a 3 to 4GB download.
57
+
58
+ ## Bugs and Support
59
+
60
+ If you encounter any problems, please email support@speedshop.co for the fastest possible response.