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
@@ -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 nate.berkopec@speedshop.co"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,99 @@
|
|
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 :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.increment_current_lesson!(content["position"])
|
20
|
+
display_content(content, options[: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
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "list", "Show all available workshop lessons"
|
30
|
+
def list
|
31
|
+
say "All available workshop lessons:"
|
32
|
+
client.list.each do |lesson|
|
33
|
+
puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "download [CONTENT | all]", "Download one or all workshop contents"
|
38
|
+
def download(content_pos)
|
39
|
+
to_download = if content_pos.downcase == "all"
|
40
|
+
client.list
|
41
|
+
else
|
42
|
+
[client.show(content_pos)]
|
43
|
+
end
|
44
|
+
to_download.each { |content| client.download_and_extract(content) }
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "show [CONTENT]", "Show any workshop lesson, shows current lesson w/no arguments"
|
48
|
+
option :open
|
49
|
+
def show(content_order = :current)
|
50
|
+
content = client.show(content_order)
|
51
|
+
client.download_and_extract(content)
|
52
|
+
display_content(content, options[:open])
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def display_content(content, open_after)
|
58
|
+
say "Current Lesson: #{content["title"]}"
|
59
|
+
openable = false
|
60
|
+
case content["style"]
|
61
|
+
when "video"
|
62
|
+
location = "video/#{content["s3_key"]}"
|
63
|
+
openable = true
|
64
|
+
when "quiz"
|
65
|
+
Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
|
66
|
+
when "lab"
|
67
|
+
location = "lab/#{content["s3_key"][0..-8]}"
|
68
|
+
when "text"
|
69
|
+
location = "lab/#{content["s3_key"]}"
|
70
|
+
openable = true
|
71
|
+
when "cgrp"
|
72
|
+
say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
|
73
|
+
say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
|
74
|
+
end
|
75
|
+
if location
|
76
|
+
if openable && !open_after
|
77
|
+
say "This file can be opened automatically if you use the --open flag next time."
|
78
|
+
say "e.g. $ rpw lesson next --open"
|
79
|
+
say "Download complete. Open with: $ #{open_command} #{location}"
|
80
|
+
elsif open_after && openable
|
81
|
+
exec "#{open_command} #{location}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
require "rbconfig"
|
87
|
+
def open_command
|
88
|
+
host_os = RbConfig::CONFIG["host_os"]
|
89
|
+
case host_os
|
90
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
91
|
+
"start"
|
92
|
+
when /darwin|mac os/
|
93
|
+
"open"
|
94
|
+
else
|
95
|
+
"xdg-open"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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(lesson)
|
7
|
+
client.set_progress(lesson)
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "reset", "Erase all progress and start over"
|
11
|
+
def reset
|
12
|
+
yes? "Are you sure you want to reset your progress? (Y/N)"
|
13
|
+
client.reset_progress
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "show", "Show current workshop progress"
|
17
|
+
def show
|
18
|
+
data = client.progress
|
19
|
+
say "The Rails Performance Workshop"
|
20
|
+
say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
|
21
|
+
say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
|
22
|
+
say "Progress by Section (X == completed, O == current):"
|
23
|
+
data[:sections].each do |section|
|
24
|
+
say "#{section[:title]}: #{section[:progress]}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
default_task :show
|
31
|
+
end
|
32
|
+
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,168 @@
|
|
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
|
+
contents = gateway.list_content
|
25
|
+
return contents.first unless client_data["completed"]
|
26
|
+
contents.delete_if { |c| client_data["completed"].include? c["position"] }
|
27
|
+
contents.sort_by { |c| c["position"] }[1] # 0 would be the current lesson
|
28
|
+
end
|
29
|
+
|
30
|
+
def list
|
31
|
+
gateway.list_content
|
32
|
+
end
|
33
|
+
|
34
|
+
def show(content_pos)
|
35
|
+
content_pos = client_data["current_lesson"] if content_pos == :current
|
36
|
+
gateway.get_content_by_position(content_pos)
|
37
|
+
end
|
38
|
+
|
39
|
+
def directory_setup(home_dir_ok = true)
|
40
|
+
["video", "quiz", "lab", "text", "cgrp"].each do |path|
|
41
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
if home_dir_ok
|
45
|
+
ClientData.create_in_home!
|
46
|
+
else
|
47
|
+
ClientData.create_in_pwd!
|
48
|
+
end
|
49
|
+
|
50
|
+
unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
|
51
|
+
File.open(".gitignore", "a") do |f|
|
52
|
+
f.puts "\n"
|
53
|
+
f.puts ".rpw_info\n"
|
54
|
+
f.puts "video\n"
|
55
|
+
f.puts "quiz\n"
|
56
|
+
f.puts "lab\n"
|
57
|
+
f.puts "text\n"
|
58
|
+
f.puts "cgrp\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
File.open("README.md", "w+") do |f|
|
63
|
+
f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def increment_current_lesson!(position)
|
68
|
+
mark_current_lesson_as_completed
|
69
|
+
client_data["current_lesson"] = position
|
70
|
+
end
|
71
|
+
|
72
|
+
def progress
|
73
|
+
contents = gateway.list_content
|
74
|
+
completed_lessons = client_data["completed"] || []
|
75
|
+
{
|
76
|
+
completed: completed_lessons.size,
|
77
|
+
total: contents.size,
|
78
|
+
current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
|
79
|
+
sections: chart_section_progress(contents, completed_lessons)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_progress(lesson)
|
84
|
+
client_data["current_lesson"] = lesson.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset_progress
|
88
|
+
client_data["current_lesson"] = 0
|
89
|
+
client_data["completed"] = []
|
90
|
+
end
|
91
|
+
|
92
|
+
def latest_version?
|
93
|
+
return true unless ClientData.exists?
|
94
|
+
|
95
|
+
if client_data["last_version_check"]
|
96
|
+
return true if client_data["last_version_check"] >= Time.now - (60 * 60 * 24)
|
97
|
+
return false if client_data["last_version_check"] == false
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
latest = gateway.latest_version?
|
102
|
+
rescue
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
|
106
|
+
client_data["last_version_check"] = if latest
|
107
|
+
Time.now
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def setup?
|
114
|
+
return false unless ClientData.exists?
|
115
|
+
client_data["key"]
|
116
|
+
end
|
117
|
+
|
118
|
+
def directories_ready?
|
119
|
+
["video", "quiz", "lab", "text", "cgrp"].all? do |path|
|
120
|
+
File.directory?(path)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def download_and_extract(content)
|
125
|
+
location = content["style"] + "/" + content["s3_key"]
|
126
|
+
unless File.exist?(location)
|
127
|
+
gateway.download_content(content, folder: content["style"])
|
128
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def mark_current_lesson_as_completed
|
135
|
+
reset_progress unless client_data["current_lesson"] && client_data["completed"]
|
136
|
+
client_data["completed"] ||= []
|
137
|
+
client_data["completed"] += [client_data["current_lesson"] || 0]
|
138
|
+
end
|
139
|
+
|
140
|
+
def chart_section_progress(contents, completed)
|
141
|
+
contents.group_by { |c| c["position"] / 100 }
|
142
|
+
.each_with_object([]) do |(_, c), memo|
|
143
|
+
completed_str = c.map { |l|
|
144
|
+
if l["position"] == client_data["current_lesson"]
|
145
|
+
"O"
|
146
|
+
elsif completed.include?(l["position"])
|
147
|
+
"X"
|
148
|
+
else
|
149
|
+
"."
|
150
|
+
end
|
151
|
+
}.join
|
152
|
+
memo << {
|
153
|
+
title: c[0]["title"],
|
154
|
+
progress: completed_str
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def client_data
|
160
|
+
@client_data ||= ClientData.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def extract_content(content)
|
164
|
+
folder = content["style"]
|
165
|
+
`tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class ClientData
|
6
|
+
DOTFILE_NAME = ".rpw_info"
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
data # access file to load
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
data[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
data
|
18
|
+
data[key] = value
|
19
|
+
|
20
|
+
begin
|
21
|
+
File.open(filestore_location, "w") { |f| f.write(YAML.dump(data)) }
|
22
|
+
rescue
|
23
|
+
raise Error, "The RPW data at #{filestore_location} is not writable. \
|
24
|
+
Check your file permissions."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.create_in_pwd!
|
29
|
+
FileUtils.touch(File.expand_path("./" + DOTFILE_NAME))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.create_in_home!
|
33
|
+
unless File.directory?(File.expand_path("~/.rpw/"))
|
34
|
+
FileUtils.mkdir(File.expand_path("~/.rpw/"))
|
35
|
+
end
|
36
|
+
|
37
|
+
FileUtils.touch(File.expand_path("~/.rpw/" + DOTFILE_NAME))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.delete_filestore
|
41
|
+
return unless File.exist?(filestore_location)
|
42
|
+
FileUtils.remove(filestore_location)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.exists?
|
46
|
+
File.exist? filestore_location
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.filestore_location
|
50
|
+
if File.exist?(File.expand_path("./" + DOTFILE_NAME))
|
51
|
+
File.expand_path("./" + DOTFILE_NAME)
|
52
|
+
else
|
53
|
+
File.expand_path("~/.rpw/" + DOTFILE_NAME)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def filestore_location
|
60
|
+
self.class.filestore_location
|
61
|
+
end
|
62
|
+
|
63
|
+
def data
|
64
|
+
@data ||= begin
|
65
|
+
begin
|
66
|
+
YAML.safe_load(File.read(filestore_location), permitted_classes: [Time]) || {}
|
67
|
+
rescue Errno::ENOENT
|
68
|
+
{}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|