rpw 0.0.3 → 1.0.1
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 +21 -0
- data/exe/rpw +1 -252
- data/lib/rpw.rb +4 -374
- data/lib/rpw/README.md +58 -0
- data/lib/rpw/cli.rb +94 -0
- data/lib/rpw/cli/bannerlord.rb +59 -0
- data/lib/rpw/cli/key.rb +15 -0
- data/lib/rpw/cli/lesson.rb +101 -0
- data/lib/rpw/cli/progress.rb +34 -0
- data/lib/rpw/cli/quiz.rb +28 -0
- data/lib/rpw/cli/sub_command_base.rb +30 -0
- data/lib/rpw/client.rb +178 -0
- data/lib/rpw/client_data.rb +73 -0
- data/lib/rpw/gateway.rb +67 -0
- data/lib/rpw/version.rb +1 -1
- data/rpw.gemspec +1 -1
- metadata +15 -3
@@ -0,0 +1,59 @@
|
|
1
|
+
module RPW
|
2
|
+
class Bannerlord
|
3
|
+
class << self
|
4
|
+
def print_banner
|
5
|
+
puts r
|
6
|
+
if `tput cols 80`.to_i < 80
|
7
|
+
puts small_banner
|
8
|
+
else
|
9
|
+
puts banner
|
10
|
+
end
|
11
|
+
puts reset
|
12
|
+
end
|
13
|
+
|
14
|
+
def r
|
15
|
+
"\e[31m"
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset
|
19
|
+
"\e[0m"
|
20
|
+
end
|
21
|
+
|
22
|
+
def small_banner
|
23
|
+
%(
|
24
|
+
_____ _ _____ _ _
|
25
|
+
|_ _| |_ ___ | __ |___|_| |___
|
26
|
+
| | | | -_| | -| .'| | |_ -|
|
27
|
+
|_| |_|_|___| |__|__|__,|_|_|___|
|
28
|
+
_____ ___
|
29
|
+
| _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
|
30
|
+
| __| -_| _| _| . | _| | .'| | _| -_|
|
31
|
+
|__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
|
32
|
+
_ _ _ _ _
|
33
|
+
| | | |___ ___| |_ ___| |_ ___ ___
|
34
|
+
| | | | . | _| '_|_ -| | . | . |
|
35
|
+
|_____|___|_| |_,_|___|_|_|___| _|
|
36
|
+
|_|
|
37
|
+
#{reset})
|
38
|
+
end
|
39
|
+
|
40
|
+
def banner
|
41
|
+
%(
|
42
|
+
_____ _ _____ _ _
|
43
|
+
+hmNMMMMMm/` -ymMMNh/ |_ _| |_ ___ | __ |___|_| |___
|
44
|
+
sMMMMMMMMMy +MMMMMMMMy | | | | -_| | -| .'| | |_ -|
|
45
|
+
yMMMMMMMMMMy` yMMMMMMMMN |_| |_|_|___| |__|__|__,|_|_|___|
|
46
|
+
`dMMMMMMMMMMm:-dMMMMMMm: _____ ___
|
47
|
+
`sNMMMMMMMMMMs.:+sso:` | _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
|
48
|
+
:dMMMMMMMMMMm/ | __| -_| _| _| . | _| | .'| | _| -_|
|
49
|
+
:oss+:.sNMMMMMMMMMMy` |__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
|
50
|
+
/mMMMMMMd-:mMMMMMMMMMMd. _ _ _ _ _
|
51
|
+
NMMMMMMMMy `hMMMMMMMMMMh | | | |___ ___| |_ ___| |_ ___ ___
|
52
|
+
yMMMMMMMM+ `dMMMMMMMMMy | | | | . | _| '_|_ -| | . | . |
|
53
|
+
/hNMMmy- `/mMMMMMNmy/ |_____|___|_| |_,_|___|_|_|___| _|
|
54
|
+
|_|
|
55
|
+
#{reset})
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/rpw/cli/key.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module RPW
|
2
|
+
class Key < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "register [EMAIL_ADDRESS]", "Change email registered with Speedshop. One-time only."
|
6
|
+
def register(email)
|
7
|
+
if client.register_email(email)
|
8
|
+
say "Key registered with #{email}. You should receive a Slack invite soon."
|
9
|
+
else
|
10
|
+
say "Key has already been registered. If you believe this is in error,"\
|
11
|
+
" please email support@speedshop.co"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module RPW
|
2
|
+
class Lesson < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "next", "Proceed to the next lesson of the workshop"
|
6
|
+
option :"no-open"
|
7
|
+
def next
|
8
|
+
say "Proceeding to next lesson..."
|
9
|
+
content = client.next
|
10
|
+
|
11
|
+
if content.nil?
|
12
|
+
RPW::CLI.new.print_banner
|
13
|
+
say "Congratulations!"
|
14
|
+
say "You have completed the Rails Performance Workshop."
|
15
|
+
exit(0)
|
16
|
+
end
|
17
|
+
|
18
|
+
client.download_and_extract(content)
|
19
|
+
client.complete(content["position"])
|
20
|
+
display_content(content, !options[:"no-open"])
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "complete", "Mark the current lesson as complete"
|
24
|
+
def complete
|
25
|
+
say "Marked current lesson as complete"
|
26
|
+
client.complete(nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "list", "Show all available workshop lessons"
|
30
|
+
def list
|
31
|
+
say "All available workshop lessons:"
|
32
|
+
say "Use [ID] for the show/download command"
|
33
|
+
say "[ID]: Lesson Name"
|
34
|
+
client.list.each do |lesson|
|
35
|
+
puts "[#{lesson["position"]}]:#{" " * lesson["indent"]} #{lesson["title"]}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "download [CONTENT | all]", "Download one or all workshop contents"
|
40
|
+
def download(content_pos)
|
41
|
+
to_download = if content_pos.downcase == "all"
|
42
|
+
client.list
|
43
|
+
else
|
44
|
+
[client.show(content_pos)]
|
45
|
+
end
|
46
|
+
to_download.each { |content| client.download_and_extract(content) }
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "show [CONTENT]", "Show any workshop lesson, shows current lesson w/no arguments"
|
50
|
+
option :"no-open"
|
51
|
+
def show(content_order = :current)
|
52
|
+
content = client.show(content_order)
|
53
|
+
client.download_and_extract(content)
|
54
|
+
display_content(content, !options[:"no-open"])
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def display_content(content, open_after)
|
60
|
+
say "Current Lesson: #{content["title"]}"
|
61
|
+
openable = false
|
62
|
+
case content["style"]
|
63
|
+
when "video"
|
64
|
+
location = "video/#{content["s3_key"]}"
|
65
|
+
openable = true
|
66
|
+
when "quiz"
|
67
|
+
Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
|
68
|
+
when "lab"
|
69
|
+
location = "lab/#{content["s3_key"][0..-8]}"
|
70
|
+
openable = true
|
71
|
+
when "text"
|
72
|
+
location = "text/#{content["s3_key"]}"
|
73
|
+
openable = true
|
74
|
+
when "cgrp"
|
75
|
+
say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
|
76
|
+
say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
|
77
|
+
say "You can check it out now, or to continue: $ rpw lesson next "
|
78
|
+
end
|
79
|
+
if location
|
80
|
+
if openable && !open_after
|
81
|
+
say "Download complete. Open with: $ #{open_command} #{location}"
|
82
|
+
elsif open_after && openable
|
83
|
+
exec "#{open_command} #{location}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
require "rbconfig"
|
89
|
+
def open_command
|
90
|
+
host_os = RbConfig::CONFIG["host_os"]
|
91
|
+
case host_os
|
92
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
93
|
+
"start"
|
94
|
+
when /darwin|mac os/
|
95
|
+
"open"
|
96
|
+
else
|
97
|
+
"xdg-open"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RPW
|
2
|
+
class Progress < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "set [LESSON]", "Set current lesson to a particular lesson"
|
6
|
+
def set(pos)
|
7
|
+
lesson = client.set_progress(pos.to_i)
|
8
|
+
say "Set current progress to #{lesson["title"]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "reset", "Erase all progress and start over"
|
12
|
+
def reset
|
13
|
+
return unless yes? "Are you sure you want to reset your progress? (Y/N)"
|
14
|
+
say "Resetting progress."
|
15
|
+
client.set_progress(nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "show", "Show current workshop progress"
|
19
|
+
def show
|
20
|
+
data = client.progress
|
21
|
+
say "The Rails Performance Workshop"
|
22
|
+
say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
|
23
|
+
say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
|
24
|
+
say "Progress by Section (X == completed, O == current):"
|
25
|
+
data[:sections].each do |section|
|
26
|
+
say "#{section[:title]}: #{section[:progress]}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
default_task :show
|
33
|
+
end
|
34
|
+
end
|
data/lib/rpw/cli/quiz.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "thor"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class Quiz < Thor
|
6
|
+
desc "give_quiz FILENAME", ""
|
7
|
+
def give_quiz(filename)
|
8
|
+
@quiz_data = YAML.safe_load(File.read(filename))
|
9
|
+
@quiz_data["questions"].each { |q| question(q) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def question(data)
|
15
|
+
puts data["prompt"]
|
16
|
+
data["answer_choices"].each { |ac| puts ac }
|
17
|
+
provided_answer = ask("Your answer?")
|
18
|
+
answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer.upcase)
|
19
|
+
if answer_digest == data["answer_digest"]
|
20
|
+
say "Correct!"
|
21
|
+
else
|
22
|
+
say "Incorrect."
|
23
|
+
say "I encourage you to try reviewing the material to see what the correct answer is."
|
24
|
+
end
|
25
|
+
say ""
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RPW
|
2
|
+
class SubCommandBase < Thor
|
3
|
+
def self.banner(command, namespace = nil, subcommand = false)
|
4
|
+
"#{basename} #{subcommand_prefix} #{command.usage}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.subcommand_prefix
|
8
|
+
name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }
|
9
|
+
.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
|
10
|
+
end
|
11
|
+
|
12
|
+
no_commands do
|
13
|
+
def client
|
14
|
+
@client ||= RPW::Client.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def exit_with_no_key
|
18
|
+
unless client.setup?
|
19
|
+
say "You have not yet set up the client. Run $ rpw start"
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
unless client.directories_ready?
|
23
|
+
say "You are not in your workshop scratch directory, or you have not yet"
|
24
|
+
say "set up the client. Change directory or run $ rpw start"
|
25
|
+
exit(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/rpw/client.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "rpw/cli/quiz"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class Client
|
6
|
+
RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
|
7
|
+
attr_reader :gateway
|
8
|
+
|
9
|
+
def initialize(gateway = nil)
|
10
|
+
@gateway = gateway || Gateway.new(RPW_SERVER_DOMAIN, client_data["key"])
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup(key)
|
14
|
+
success = gateway.authenticate_key(key)
|
15
|
+
client_data["key"] = key if success
|
16
|
+
success
|
17
|
+
end
|
18
|
+
|
19
|
+
def register_email(email)
|
20
|
+
gateway.register_email(email)
|
21
|
+
end
|
22
|
+
|
23
|
+
def next
|
24
|
+
return list.first unless client_data["completed"]
|
25
|
+
list.sort_by { |c| c["position"] }.find { |c| c["position"] > current_position }
|
26
|
+
end
|
27
|
+
|
28
|
+
def list
|
29
|
+
@list ||= begin
|
30
|
+
if client_data["content_cache_generated"] &&
|
31
|
+
client_data["content_cache_generated"] >= Time.now - 60 * 60
|
32
|
+
|
33
|
+
client_data["content_cache"]
|
34
|
+
else
|
35
|
+
begin
|
36
|
+
client_data["content_cache"] = gateway.list_content
|
37
|
+
client_data["content_cache_generated"] = Time.now
|
38
|
+
client_data["content_cache"]
|
39
|
+
rescue
|
40
|
+
client_data["content_cache"] || (raise Error.new("No internet connection"))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def show(content_pos)
|
47
|
+
content_pos = current_position if content_pos == :current
|
48
|
+
gateway.get_content_by_position(content_pos)
|
49
|
+
end
|
50
|
+
|
51
|
+
def directory_setup(home_dir_ok = true)
|
52
|
+
["video", "quiz", "lab", "text", "cgrp"].each do |path|
|
53
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
if home_dir_ok
|
57
|
+
ClientData.create_in_home!
|
58
|
+
else
|
59
|
+
ClientData.create_in_pwd!
|
60
|
+
end
|
61
|
+
|
62
|
+
unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
|
63
|
+
File.open(".gitignore", "a") do |f|
|
64
|
+
f.puts "\n"
|
65
|
+
f.puts ".rpw_info\n"
|
66
|
+
f.puts "video\n"
|
67
|
+
f.puts "quiz\n"
|
68
|
+
f.puts "lab\n"
|
69
|
+
f.puts "text\n"
|
70
|
+
f.puts "cgrp\n"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
File.open("README.md", "w+") do |f|
|
75
|
+
f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def progress
|
80
|
+
completed_lessons = client_data["completed"] || []
|
81
|
+
{
|
82
|
+
completed: completed_lessons.size,
|
83
|
+
total: list.size,
|
84
|
+
current_lesson: list.find { |c| c["position"] == current_position },
|
85
|
+
sections: chart_section_progress(list, completed_lessons)
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_progress(pos)
|
90
|
+
client_data["completed"] = [] && return if pos.nil?
|
91
|
+
lesson = list.find { |l| l["position"] == pos }
|
92
|
+
raise Error.new("No such lesson - use the IDs in $ rpw lesson list") unless lesson
|
93
|
+
client_data["completed"] += [pos]
|
94
|
+
lesson
|
95
|
+
end
|
96
|
+
|
97
|
+
def latest_version?
|
98
|
+
return true unless ClientData.exists?
|
99
|
+
return true if client_data["last_version_check"] &&
|
100
|
+
client_data["last_version_check"] >= Time.now - (60 * 60)
|
101
|
+
|
102
|
+
begin
|
103
|
+
latest = gateway.latest_version?
|
104
|
+
rescue
|
105
|
+
return true
|
106
|
+
end
|
107
|
+
|
108
|
+
client_data["last_version_check"] = if latest
|
109
|
+
Time.now
|
110
|
+
else
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def setup?
|
116
|
+
return false unless ClientData.exists?
|
117
|
+
client_data["key"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def directories_ready?
|
121
|
+
["video", "quiz", "lab", "text", "cgrp"].all? do |path|
|
122
|
+
File.directory?(path)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def download_and_extract(content)
|
127
|
+
location = content["style"] + "/" + content["s3_key"]
|
128
|
+
unless File.exist?(location)
|
129
|
+
gateway.download_content(content, folder: content["style"])
|
130
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def complete(position)
|
135
|
+
if client_data["completed"]
|
136
|
+
# we actually have to put the _next_ lesson on the completed stack
|
137
|
+
set_progress(self.next["position"])
|
138
|
+
else
|
139
|
+
client_data["completed"] = []
|
140
|
+
set_progress(position)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def current_position
|
147
|
+
@current_position ||= client_data["completed"]&.last || 0
|
148
|
+
end
|
149
|
+
|
150
|
+
def chart_section_progress(contents, completed)
|
151
|
+
contents.group_by { |c| c["position"] / 100 }
|
152
|
+
.each_with_object([]) do |(_, c), memo|
|
153
|
+
completed_str = c.map { |l|
|
154
|
+
if l["position"] == current_position
|
155
|
+
"O"
|
156
|
+
elsif completed.include?(l["position"])
|
157
|
+
"X"
|
158
|
+
else
|
159
|
+
"."
|
160
|
+
end
|
161
|
+
}.join
|
162
|
+
memo << {
|
163
|
+
title: c[0]["title"],
|
164
|
+
progress: completed_str
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def client_data
|
170
|
+
@client_data ||= ClientData.new
|
171
|
+
end
|
172
|
+
|
173
|
+
def extract_content(content)
|
174
|
+
folder = content["style"]
|
175
|
+
`tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|