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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +109 -0
- data/.gitignore +1 -0
- data/Gemfile.lock +14 -16
- data/HISTORY.md +22 -0
- data/exe/rpw +1 -243
- data/lib/rpw.rb +4 -391
- data/lib/rpw/README.md +60 -0
- data/lib/rpw/cli.rb +268 -0
- data/lib/rpw/cli/bannerlord.rb +59 -0
- data/lib/rpw/cli/key.rb +17 -0
- data/lib/rpw/cli/quiz.rb +28 -0
- data/lib/rpw/cli/sub_command_base.rb +18 -0
- data/lib/rpw/client.rb +158 -0
- data/lib/rpw/client_data.rb +73 -0
- data/lib/rpw/gateway.rb +57 -0
- data/lib/rpw/version.rb +1 -1
- data/rpw.gemspec +2 -1
- metadata +27 -4
- data/lib/README.md +0 -11
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
|
data/lib/rpw/README.md
ADDED
@@ -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.
|