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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +109 -0
- data/.gitignore +1 -0
- data/Gemfile.lock +12 -16
- data/HISTORY.md +5 -0
- data/exe/rpw +1 -243
- data/lib/rpw.rb +4 -391
- data/lib/{README.md → rpw/README.md} +12 -1
- data/lib/rpw/cli.rb +96 -0
- data/lib/rpw/cli/bannerlord.rb +59 -0
- data/lib/rpw/cli/key.rb +15 -0
- data/lib/rpw/cli/lesson.rb +99 -0
- data/lib/rpw/cli/progress.rb +32 -0
- data/lib/rpw/cli/quiz.rb +28 -0
- data/lib/rpw/cli/sub_command_base.rb +30 -0
- data/lib/rpw/client.rb +168 -0
- data/lib/rpw/client_data.rb +73 -0
- data/lib/rpw/gateway.rb +63 -0
- data/lib/rpw/version.rb +1 -1
- data/rpw.gemspec +1 -1
- metadata +15 -4
data/lib/rpw.rb
CHANGED
@@ -1,395 +1,8 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
|
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.
|
data/lib/rpw/cli.rb
ADDED
@@ -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
|