rpw 0.0.1 → 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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/Gemfile.lock +1 -1
- data/HISTORY.md +4 -0
- data/exe/rpw +98 -23
- data/lib/rpw.rb +255 -29
- data/lib/rpw/version.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7da44e397174a310bc8ba3ed4c2ecb28f2811cf30275a2cb7cd3ec85029ab99a
|
4
|
+
data.tar.gz: c096b4539f5570d6994eed51ec758007e08f75632e0e26f7d13e15b531633995
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3557e0688d5141ae96bcfed87ced6539059bbbd0517a3f2bf85910c5b13b99e153d940fbf56d9bd86bf2cf23525f32a9c848d7fa3ee2d58ae664db806854e93b
|
7
|
+
data.tar.gz: 0e804e064e64cd66c703238e00d80f75a35871bf724c08203594d4eec1c94db5815d5ac2bacc11b743e008eb1aeec2c39f2b240fefac87aebe2c005bb194ae19
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/HISTORY.md
CHANGED
data/exe/rpw
CHANGED
@@ -4,56 +4,131 @@ require "thor"
|
|
4
4
|
require_relative "../lib/rpw"
|
5
5
|
|
6
6
|
module RPW
|
7
|
-
class
|
8
|
-
|
7
|
+
class SubCommandBase < Thor
|
8
|
+
def self.banner(command, namespace = nil, subcommand = false)
|
9
|
+
"#{basename} #{subcommand_prefix} #{command.usage}"
|
10
|
+
end
|
9
11
|
|
10
|
-
|
12
|
+
def self.subcommand_prefix
|
13
|
+
name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Lesson < SubCommandBase
|
18
|
+
desc "next", "Proceed to the next lesson of the workshop"
|
11
19
|
|
12
20
|
def next
|
13
|
-
|
21
|
+
say "Proceeding to next lesson..."
|
22
|
+
client.next
|
14
23
|
end
|
15
24
|
|
16
|
-
desc "
|
25
|
+
desc "complete", "Mark the current lesson as complete"
|
17
26
|
|
18
|
-
def
|
19
|
-
say "
|
20
|
-
|
27
|
+
def complete
|
28
|
+
say "Marked current lesson as complete"
|
29
|
+
client.complete
|
30
|
+
end
|
21
31
|
|
22
|
-
|
32
|
+
desc "list", "Show all available workshop lessons"
|
23
33
|
|
24
|
-
|
34
|
+
def list
|
35
|
+
say "All available workshop lessons:"
|
36
|
+
client.list.each do |lesson|
|
37
|
+
puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
|
38
|
+
end
|
25
39
|
end
|
26
40
|
|
27
|
-
desc "
|
41
|
+
desc "download [CONTENT | all]", "Download one or all workshop contents"
|
28
42
|
|
29
|
-
def
|
30
|
-
|
43
|
+
def download(content)
|
44
|
+
client.download(content)
|
31
45
|
end
|
32
46
|
|
33
|
-
desc "
|
47
|
+
desc "show [CONTENT]", "Show any workshop lesson"
|
34
48
|
|
35
|
-
def
|
49
|
+
def show(content)
|
50
|
+
client.show(content)
|
36
51
|
end
|
37
52
|
|
38
|
-
|
53
|
+
private
|
39
54
|
|
40
|
-
def
|
55
|
+
def client
|
56
|
+
@client ||= RPW::Client.new
|
41
57
|
end
|
58
|
+
end
|
42
59
|
|
43
|
-
|
60
|
+
class Progress < SubCommandBase
|
61
|
+
desc "set [LESSON]", "Set current lesson to a particular lesson"
|
44
62
|
|
45
|
-
def
|
63
|
+
def set(lesson)
|
64
|
+
client.set_progress(lesson)
|
46
65
|
end
|
47
66
|
|
48
|
-
desc "
|
67
|
+
desc "reset", "Erase all progress and start over"
|
49
68
|
|
50
|
-
def
|
69
|
+
def reset
|
70
|
+
yes? "Are you sure you want to reset your progress? (Y/N)"
|
71
|
+
client.reset_progress
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "show", "Show current workshop progress"
|
75
|
+
def show
|
76
|
+
data = client.progress
|
77
|
+
say "The Rails Performance Workshop"
|
78
|
+
say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
|
79
|
+
say "Current lesson: #{data[:current_lesson]["title"]}"
|
80
|
+
say "Progress by Section (X == completed, O == current):"
|
81
|
+
data[:sections].each do |section|
|
82
|
+
say "#{section[:title]}: #{section[:progress]}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def client
|
89
|
+
@client ||= RPW::Client.new
|
90
|
+
end
|
91
|
+
|
92
|
+
default_task :show
|
93
|
+
end
|
94
|
+
|
95
|
+
class CLI < Thor
|
96
|
+
desc "lesson [SUBCOMMAND]", "View and download lessons"
|
97
|
+
subcommand "lesson", Lesson
|
98
|
+
desc "progress [SUBCOMMAND]", "View and set progress"
|
99
|
+
subcommand "progress", Progress
|
100
|
+
|
101
|
+
def self.exit_on_failure?
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "setup", "Set up purchase key and directories"
|
106
|
+
|
107
|
+
def setup
|
108
|
+
return unless yes? "We're going to (idempotently!) create a few files and directories in the current working directory. Is that OK? (Y/N)"
|
109
|
+
|
110
|
+
say "We'll create a .rpw_key file in the current directory to save your purchase key and course data."
|
111
|
+
key = ask("Your Purchase Key: ")
|
112
|
+
|
113
|
+
client.setup(key)
|
114
|
+
|
115
|
+
say "Successfully authenticated with the RPW server and saved your key."
|
116
|
+
say "We'll also create a few directories for your course data and progress."
|
117
|
+
|
118
|
+
client.directory_setup
|
119
|
+
|
120
|
+
say "Setup complete!"
|
121
|
+
end
|
122
|
+
|
123
|
+
desc "support", "Create a new support ticket, report a bug or issue"
|
124
|
+
|
125
|
+
def support
|
51
126
|
end
|
52
127
|
|
53
|
-
private
|
128
|
+
private
|
54
129
|
|
55
130
|
def client
|
56
|
-
@client ||= RPW::Client.new
|
131
|
+
@client ||= RPW::Client.new
|
57
132
|
end
|
58
133
|
end
|
59
134
|
end
|
data/lib/rpw.rb
CHANGED
@@ -1,63 +1,289 @@
|
|
1
1
|
require "typhoeus"
|
2
|
-
require "
|
2
|
+
require "json"
|
3
3
|
|
4
4
|
module RPW
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
5
7
|
class Gateway
|
6
|
-
attr_accessor :domain
|
8
|
+
attr_accessor :domain
|
7
9
|
|
8
|
-
def initialize(domain)
|
10
|
+
def initialize(domain, key)
|
9
11
|
@domain = domain
|
12
|
+
@key = key
|
10
13
|
end
|
11
14
|
|
12
15
|
class Error < StandardError; end
|
13
16
|
|
14
17
|
def authenticate_key(key)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
raise Error, "Server responded: #{response.code} #{response.response_body}"
|
26
|
-
end
|
18
|
+
Typhoeus.get(domain + "/license", userpwd: key + ":").success?
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_content_by_position(position)
|
22
|
+
response = Typhoeus.get(domain + "/contents/positional?position=#{position}", userpwd: @key + ":")
|
23
|
+
if response.success?
|
24
|
+
JSON.parse(response.body)
|
25
|
+
else
|
26
|
+
puts response.inspect
|
27
|
+
raise Error, "There was a problem fetching this content."
|
27
28
|
end
|
29
|
+
end
|
28
30
|
|
29
|
-
|
31
|
+
def list_content
|
32
|
+
response = Typhoeus.get(domain + "/contents", userpwd: @key + ":")
|
33
|
+
if response.success?
|
34
|
+
JSON.parse(response.body)
|
35
|
+
else
|
36
|
+
puts response.inspect
|
37
|
+
raise Error, "There was a problem fetching this content."
|
38
|
+
end
|
30
39
|
end
|
31
40
|
|
32
|
-
def
|
41
|
+
def download_content(content, folder:)
|
42
|
+
puts "Downloading #{content["title"]}..."
|
43
|
+
downloaded_file = File.open("#{folder}/#{content["s3_key"]}", "w")
|
44
|
+
request = Typhoeus::Request.new(content["url"])
|
45
|
+
request.on_body do |chunk|
|
46
|
+
downloaded_file.write(chunk)
|
47
|
+
printf(".") if rand(10) == 0 # lol
|
48
|
+
end
|
49
|
+
request.on_complete { |response| downloaded_file.close }
|
50
|
+
request
|
33
51
|
end
|
34
52
|
end
|
35
53
|
|
36
54
|
class Client
|
37
|
-
|
38
|
-
RPW_SERVER_DOMAIN = "https://rpw-licensor.speedshop.co"
|
39
|
-
|
40
|
-
class Error < StandardError; end
|
55
|
+
RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
|
41
56
|
|
42
57
|
def setup(key)
|
43
|
-
# authenticate against server
|
44
58
|
gateway.authenticate_key(key)
|
59
|
+
keyfile["key"] = key
|
60
|
+
end
|
61
|
+
|
62
|
+
def directory_setup
|
63
|
+
["video", "quiz", "lab", "text", "cgrp"].each do |path|
|
64
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
client_data["completed"] = [] # just to write the file
|
68
|
+
|
69
|
+
unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_key/)
|
70
|
+
File.open(".gitignore", "a") do |f|
|
71
|
+
f.puts "\n"
|
72
|
+
f.puts ".rpw_key\n"
|
73
|
+
f.puts ".rpw_info\n"
|
74
|
+
f.puts "video\n"
|
75
|
+
f.puts "quiz\n"
|
76
|
+
f.puts "lab\n"
|
77
|
+
f.puts "text\n"
|
78
|
+
f.puts "cgrp\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def next
|
84
|
+
content = next_content
|
85
|
+
unless File.exist?(content["style"] + "/" + content["s3_key"])
|
86
|
+
gateway.download_content(content, folder: content["style"]).run
|
87
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
88
|
+
end
|
89
|
+
client_data["current_lesson"] = content["position"]
|
90
|
+
display_content(content)
|
91
|
+
end
|
92
|
+
|
93
|
+
def complete
|
94
|
+
client_data["completed"] ||= []
|
95
|
+
client_data["completed"] += [client_data["current_lesson"]]
|
96
|
+
end
|
97
|
+
|
98
|
+
def list
|
99
|
+
gateway.list_content
|
100
|
+
end
|
101
|
+
|
102
|
+
def show(content_pos)
|
103
|
+
content = gateway.get_content_by_position(content_pos)
|
104
|
+
unless File.exist?(content["style"] + "/" + content["s3_key"])
|
105
|
+
gateway.download_content(content, folder: content["style"]).run
|
106
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
107
|
+
end
|
108
|
+
client_data["current_lesson"] = content["position"]
|
109
|
+
display_content(content)
|
110
|
+
end
|
111
|
+
|
112
|
+
def download(content_pos)
|
113
|
+
if content_pos.downcase == "all"
|
114
|
+
to_download = gateway.list_content
|
115
|
+
hydra = Typhoeus::Hydra.new(max_concurrency: 5)
|
116
|
+
to_download.each do |content|
|
117
|
+
unless File.exist?(content["style"] + "/" + content["s3_key"])
|
118
|
+
hydra.queue gateway.download_content(content, folder: content["style"])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
hydra.run
|
122
|
+
to_download.each { |content| extract_content(content) if content["s3_key"].end_with?(".tar.gz") }
|
123
|
+
else
|
124
|
+
content = gateway.get_content_by_position(content_pos)
|
125
|
+
unless File.exist?(content["style"] + "/" + content["s3_key"])
|
126
|
+
gateway.download_content(content, folder: content["style"]).run
|
127
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def progress
|
133
|
+
contents = gateway.list_content
|
134
|
+
{
|
135
|
+
completed: client_data["completed"].size,
|
136
|
+
total: contents.size,
|
137
|
+
current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
|
138
|
+
sections: chart_section_progress(contents)
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_progress(lesson)
|
143
|
+
client_data["current_lesson"] = lesson.to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
def reset_progress
|
147
|
+
client_data["current_lesson"] = 0
|
148
|
+
client_data["completed"] = []
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
45
152
|
|
46
|
-
|
153
|
+
def chart_section_progress(contents)
|
154
|
+
contents.group_by { |c| c["position"] / 100 }
|
155
|
+
.each_with_object([]) do |(_, c), memo|
|
156
|
+
completed_str = c.map { |l|
|
157
|
+
if l["position"] == client_data["current_lesson"]
|
158
|
+
"O"
|
159
|
+
elsif client_data["completed"].include?(l["position"])
|
160
|
+
"X"
|
161
|
+
else
|
162
|
+
"."
|
163
|
+
end
|
164
|
+
}.join
|
165
|
+
memo << {
|
166
|
+
title: c[0]["title"],
|
167
|
+
progress: completed_str
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def next_content
|
173
|
+
contents = gateway.list_content
|
174
|
+
return contents.first unless client_data["completed"]
|
175
|
+
contents.delete_if { |c| client_data["completed"].include? c["position"] }
|
176
|
+
contents.min_by { |c| c["position"] }
|
177
|
+
end
|
178
|
+
|
179
|
+
def client_data
|
180
|
+
@client_data ||= ClientData.new
|
181
|
+
end
|
182
|
+
|
183
|
+
def keyfile
|
184
|
+
@keyfile ||= Keyfile.new
|
185
|
+
end
|
186
|
+
|
187
|
+
def gateway
|
188
|
+
@gateway ||= Gateway.new(RPW_SERVER_DOMAIN, keyfile["key"])
|
189
|
+
end
|
190
|
+
|
191
|
+
def extract_content(content)
|
192
|
+
folder = content["style"]
|
193
|
+
`tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
|
194
|
+
end
|
195
|
+
|
196
|
+
def display_content(content)
|
197
|
+
case content["style"]
|
198
|
+
when "video"
|
199
|
+
puts "Opening video: #{content["title"]}"
|
200
|
+
exec("open video/#{content["s3_key"]}")
|
201
|
+
when "quiz"
|
202
|
+
Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
|
203
|
+
when "lab"
|
204
|
+
# extract and rm archive
|
205
|
+
puts "Lab downloaded to lab/#{content["s3_key"]}, navigate there and look at the README to continue"
|
206
|
+
when "text"
|
207
|
+
puts "Opening in your editor: #{content["title"]}"
|
208
|
+
exec("$EDITOR text/#{content["s3_key"]}")
|
209
|
+
when "cgrp"
|
210
|
+
puts "The Complete Guide to Rails Performance has been downloaded and extracted to the cgrp directory."
|
211
|
+
puts "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
require "fileutils"
|
217
|
+
require "yaml"
|
218
|
+
|
219
|
+
class ClientData
|
220
|
+
DOTFILE_NAME = ".rpw_info"
|
221
|
+
|
222
|
+
def [](key)
|
223
|
+
make_sure_dotfile_exists
|
224
|
+
data[key]
|
225
|
+
end
|
226
|
+
|
227
|
+
def []=(key, value)
|
228
|
+
data[key] = value
|
229
|
+
begin
|
230
|
+
File.open(self.class::DOTFILE_NAME, "w") { |f| f.write(YAML.dump(data)) }
|
231
|
+
rescue
|
232
|
+
raise Error, "The RPW data file in this directory is not writable. \
|
233
|
+
Check your file permissions."
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def data
|
240
|
+
@data ||= begin
|
241
|
+
yaml = begin
|
242
|
+
YAML.safe_load(File.read(self.class::DOTFILE_NAME))
|
243
|
+
rescue
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
yaml || {}
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def make_sure_dotfile_exists
|
251
|
+
return true if File.exist?(self.class::DOTFILE_NAME)
|
47
252
|
begin
|
48
|
-
|
253
|
+
FileUtils.touch(self.class::DOTFILE_NAME)
|
49
254
|
rescue
|
50
|
-
raise Error
|
51
|
-
|
255
|
+
raise Error, "Could not create the RPW data file in this directory \
|
256
|
+
Check your file permissions."
|
52
257
|
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class Keyfile < ClientData
|
262
|
+
DOTFILE_NAME = ".rpw_key"
|
263
|
+
end
|
264
|
+
|
265
|
+
require "digest"
|
53
266
|
|
54
|
-
|
267
|
+
class Quiz < Thor
|
268
|
+
desc "give_quiz FILENAME", ""
|
269
|
+
def give_quiz(filename)
|
270
|
+
@quiz_data = YAML.safe_load(File.read(filename))
|
271
|
+
@quiz_data["questions"].each { |q| question(q) }
|
55
272
|
end
|
56
273
|
|
57
274
|
private
|
58
275
|
|
59
|
-
def
|
60
|
-
|
276
|
+
def question(data)
|
277
|
+
puts data["prompt"]
|
278
|
+
data["answer_choices"].each { |ac| puts ac }
|
279
|
+
provided_answer = ask("Your answer?")
|
280
|
+
answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer)
|
281
|
+
if answer_digest == data["answer_digest"]
|
282
|
+
say "Correct!"
|
283
|
+
else
|
284
|
+
say "Incorrect."
|
285
|
+
say "I encourage you to try reviewing the material to see what the correct answer is."
|
286
|
+
end
|
61
287
|
end
|
62
288
|
end
|
63
289
|
end
|
data/lib/rpw/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module RPW
|
2
|
-
VERSION = "0.0.
|
3
|
-
end
|
1
|
+
module RPW
|
2
|
+
VERSION = "0.0.2"
|
3
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rpw
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Berkopec
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-10-
|
11
|
+
date: 2020-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|