rpw 0.0.5 → 0.0.6

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
@@ -1,3 +1,9 @@
1
+ ## Installation Requirements
2
+
3
+ This client assumes you have `tar` installed.
4
+
5
+ ## Important Commands
6
+
1
7
  Here are some important commands for you to know:
2
8
 
3
9
  $ rpw lesson next | Proceed to the next part of the workshop.
@@ -8,4 +14,9 @@ $ rpw lesson show | Show any particular workshop lesson.
8
14
  $ rpw progress | Show where you're currently at in the workshop.
9
15
  $ rpw help | Help! You can also ask in Slack.
10
16
 
11
- Generally, you'll just be doing a lot of $ rpw lesson next
17
+ Generally, you'll just be doing a lot of $ rpw lesson next
18
+
19
+ ## Data Size Notice
20
+
21
+ Videos in this workshop are generally about 100MB each, which means the entire
22
+ course is about a 3 to 4GB download.
@@ -0,0 +1,96 @@
1
+ require "thor"
2
+ require "thor/hollaback"
3
+ require "rpw"
4
+ require "rpw/cli/bannerlord"
5
+ require "rpw/cli/sub_command_base"
6
+ require "rpw/cli/key"
7
+ require "rpw/cli/lesson"
8
+ require "rpw/cli/progress"
9
+
10
+ module RPW
11
+ class CLI < Thor
12
+ class_before :check_version
13
+ class_before :check_setup
14
+
15
+ desc "key register [EMAIL_ADDRESS]", "Change email registered w/Speedshop"
16
+ subcommand "key", Key
17
+ desc "lesson [SUBCOMMAND]", "View and download lessons"
18
+ subcommand "lesson", Lesson
19
+ desc "progress [SUBCOMMAND]", "View and set progress"
20
+ subcommand "progress", Progress
21
+
22
+ def self.exit_on_failure?
23
+ true
24
+ end
25
+
26
+ desc "start", "Tutorial and onboarding"
27
+ def start
28
+ warn_if_already_started
29
+
30
+ print_banner
31
+ say "Welcome to the Rails Performance Workshop."
32
+ say ""
33
+ say "This is rpw, the command line client for this workshop."
34
+ say ""
35
+ say "This client will download files from the internet into the current"
36
+ say "working directory, so it's best to run this client from a new directory"
37
+ say "that you'll use as your 'scratch space' for working on the Workshop."
38
+ say ""
39
+ say "We will create a handful of new files and folders in the current directory."
40
+ return unless yes? "Is this OK? (y/N) (N will quit)"
41
+ puts ""
42
+ say "We'll also create a .rpw_info file at #{File.expand_path("~/.rpw")} to save your purchase key."
43
+ home_dir_ok = yes?("Is this OK? (y/N) (N will create it in the current directory)")
44
+ client.directory_setup(home_dir_ok)
45
+
46
+ key = ask("Your Purchase Key: ")
47
+
48
+ unless client.setup(key)
49
+ say "That is not a valid key. Please try again."
50
+ exit(0)
51
+ end
52
+
53
+ puts ""
54
+ say "Successfully authenticated with the RPW server and saved your key."
55
+ puts ""
56
+ say "Setup complete!"
57
+ puts ""
58
+ say "To learn how to use this command-line client, consult README.md."
59
+ say "Remember to ask on Slack for help if you get stuck or encounter bugs."
60
+ say "Once you're ready to get going: $ rpw lesson next"
61
+ end
62
+
63
+ no_commands do
64
+ def print_banner
65
+ RPW::Bannerlord.print_banner
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def client
72
+ @client ||= RPW::Client.new
73
+ end
74
+
75
+ def warn_if_already_started
76
+ return unless client.setup?
77
+ exit(0) unless yes? "You have already started the workshop. Continuing "\
78
+ "this command will wipe all of your current progress. Continue? (y/N)"
79
+ end
80
+
81
+ def check_version
82
+ unless client.latest_version?
83
+ say "WARNING: You are running an old version of rpw."
84
+ say "WARNING: Please run `$ gem install rpw`"
85
+ exit(0)
86
+ end
87
+ end
88
+
89
+ def check_setup
90
+ unless client.setup? || current_command_chain == [:start]
91
+ say "WARNING: You do not have a purchase key set. Run `$ rpw start`"
92
+ exit(0)
93
+ end
94
+ end
95
+ end
96
+ end