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.
@@ -0,0 +1,268 @@
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 "cli/ui"
8
+
9
+ CLI::UI::StdoutRouter.enable
10
+
11
+ module RPW
12
+ class CLI < Thor
13
+ class_before :check_version
14
+
15
+ desc "key register [EMAIL_ADDRESS]", "Change email registered w/Speedshop"
16
+ subcommand "key", Key
17
+
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
22
+ desc "start", "Tutorial and onboarding"
23
+ def start
24
+ warn_if_already_started
25
+
26
+ print_banner
27
+ say "\u{1F48E} Welcome to the Rails Performance Workshop. \u{1F48E}"
28
+ say ""
29
+ say "This is rpw, the command line client for this workshop."
30
+ say ""
31
+ say "This client will download files from the internet into the current"
32
+ say "working directory, so it's best to run this client from a new directory"
33
+ say "that you'll use as your 'scratch space' for working on the Workshop."
34
+ say ""
35
+
36
+ ans = ::CLI::UI.confirm "Create files and folders in this directory? (no will quit)"
37
+
38
+ exit(1) unless ans
39
+
40
+ say ""
41
+
42
+ ans = ::CLI::UI::Prompt.ask("Where should we save your course progress?",
43
+ options: [
44
+ "here",
45
+ "my home directory (~/.rpw)"
46
+ ])
47
+
48
+ client.directory_setup((ans == "my home directory (~/.rpw)"))
49
+
50
+ key = ::CLI::UI::Prompt.ask("Your Purchase Key: ")
51
+
52
+ unless client.setup(key)
53
+ say "That is not a valid key. Please try again."
54
+ exit(0)
55
+ end
56
+
57
+ say ""
58
+ say "Successfully authenticated with the RPW server and saved your key."
59
+ say ""
60
+ say "Setup complete!"
61
+ say ""
62
+ say "To learn how to use this command-line client, consult ./README.md,"
63
+ say "which we just created."
64
+ say ""
65
+ say "Once you've read that and you're ready to get going: $ rpw next"
66
+ end
67
+
68
+ desc "next", "Proceed to the next lesson of the workshop"
69
+ option :"no-open", type: :boolean
70
+ def next
71
+ exit_with_no_key
72
+ content = client.next
73
+
74
+ if content.nil?
75
+ RPW::CLI.new.print_banner
76
+ say "Congratulations!"
77
+ say "You have completed the Rails Performance Workshop."
78
+ exit(0)
79
+ end
80
+
81
+ say "Proceeding to next lesson: #{content["title"]}"
82
+ client.download_and_extract(content)
83
+ client.complete(content["position"])
84
+ display_content(content, !options[:"no-open"])
85
+ end
86
+
87
+ desc "current", "Open the current lesson"
88
+ option :"no-open", type: :boolean
89
+ def current
90
+ exit_with_no_key
91
+ content = client.current
92
+ say "Opening: #{content["title"]}"
93
+ client.download_and_extract(content)
94
+ display_content(content, !options[:"no-open"])
95
+ end
96
+
97
+ desc "complete", "Mark the current lesson as complete"
98
+ def complete
99
+ say "Marked current lesson as complete"
100
+ client.complete(nil)
101
+ end
102
+
103
+ desc "list", "Show all available workshop lessons"
104
+ def list
105
+ ::CLI::UI::Frame.open("{{*}} {{bold:All Lessons}}", color: :green)
106
+
107
+ frame_open = false
108
+ client.list.each do |lesson|
109
+ if lesson["title"].start_with?("Section")
110
+ ::CLI::UI::Frame.close(nil) if frame_open
111
+ ::CLI::UI::Frame.open(lesson["title"])
112
+ frame_open = true
113
+ next
114
+ end
115
+
116
+ no_data = client.send(:client_data)["completed"].nil?
117
+ completed = client.send(:client_data)["completed"]&.include?(lesson["position"])
118
+
119
+ str = if no_data
120
+ ""
121
+ elsif completed
122
+ "\u{2705} "
123
+ else
124
+ "\u{274C} "
125
+ end
126
+
127
+ case lesson["style"]
128
+ when "video"
129
+ puts str + ::CLI::UI.fmt("{{red:#{lesson["title"]}}}")
130
+ when "quiz"
131
+ # puts ::CLI::UI.fmt "{{green:#{" " + lesson["title"]}}}"
132
+ when "lab"
133
+ puts str + ::CLI::UI.fmt("{{yellow:#{" " + lesson["title"]}}}")
134
+ when "text"
135
+ puts str + ::CLI::UI.fmt("{{magenta:#{" " + lesson["title"]}}}")
136
+ else
137
+ puts str + ::CLI::UI.fmt("{{magenta:#{" " + lesson["title"]}}}")
138
+ end
139
+ end
140
+
141
+ ::CLI::UI::Frame.close(nil)
142
+ ::CLI::UI::Frame.close(nil, color: :green)
143
+ end
144
+
145
+ desc "download", "Download all workshop contents"
146
+ def download
147
+ exit_with_no_key
148
+ total = client.list.size
149
+ client.list.each do |content|
150
+ current = client.list.index(content) + 1
151
+ puts "Downloading #{content["title"]} (#{current}/#{total})"
152
+ client.download_and_extract(content)
153
+ end
154
+ end
155
+
156
+ desc "show", "Show any individal workshop lesson"
157
+ option :"no-open", type: :boolean
158
+ def show
159
+ exit_with_no_key
160
+ title = ::CLI::UI::Prompt.ask(
161
+ "Which lesson would you like to view?",
162
+ options: client.list.reject { |l| l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
163
+ )
164
+ title.strip!
165
+ content_order = client.list.find { |l| l["title"] == title }["position"]
166
+ content = client.show(content_order)
167
+ client.download_and_extract(content)
168
+ display_content(content, !options[:"no-open"])
169
+ end
170
+
171
+ desc "set_progress", "Set current lesson to a particular lesson"
172
+ def set_progress
173
+ title = ::CLI::UI::Prompt.ask(
174
+ "Which lesson would you like to set your progress to? All prior lessons will be marked complete",
175
+ options: client.list.reject { |l| l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
176
+ )
177
+ title.strip!
178
+ content_order = client.list.find { |l| l["title"] == title }["position"]
179
+ content = client.set_progress(content_order, all_prior: true)
180
+ say "Setting current progress to #{content.last["title"]}"
181
+ end
182
+
183
+ desc "reset", "Erase all progress and start over"
184
+ def reset
185
+ return unless ::CLI::UI.confirm("Are you sure you want to erase all of your progress?", default: false)
186
+ say "Resetting progress."
187
+ client.set_progress(nil)
188
+ end
189
+
190
+ no_commands do
191
+ def print_banner
192
+ RPW::Bannerlord.print_banner
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def exit_with_no_key
199
+ unless client.setup?
200
+ say "You have not yet set up the client. Run $ rpw start"
201
+ exit(1)
202
+ end
203
+ unless client.directories_ready?
204
+ say "You are not in your workshop scratch directory, or you have not yet"
205
+ say "set up the client. Change directory or run $ rpw start"
206
+ exit(1)
207
+ end
208
+ end
209
+
210
+ def client
211
+ @client ||= RPW::Client.new
212
+ end
213
+
214
+ def display_content(content, open_after)
215
+ openable = false
216
+ case content["style"]
217
+ when "video"
218
+ location = "video/#{content["s3_key"]}"
219
+ openable = true
220
+ when "quiz"
221
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
222
+ when "lab"
223
+ location = "lab/#{content["s3_key"][0..-8]}"
224
+ openable = true
225
+ when "text"
226
+ location = "text/#{content["s3_key"]}"
227
+ openable = true
228
+ when "cgrp"
229
+ say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
230
+ say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
231
+ say "You can check it out now, or to continue: $ rpw next "
232
+ end
233
+ if location
234
+ if openable && !open_after
235
+ say "Download complete. Open with: $ #{open_command} #{location}"
236
+ elsif open_after && openable
237
+ exec "#{open_command} #{location}"
238
+ end
239
+ end
240
+ end
241
+
242
+ require "rbconfig"
243
+ def open_command
244
+ host_os = RbConfig::CONFIG["host_os"]
245
+ case host_os
246
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
247
+ "start"
248
+ when /darwin|mac os/
249
+ "open"
250
+ else
251
+ "xdg-open"
252
+ end
253
+ end
254
+
255
+ def warn_if_already_started
256
+ return unless client.setup?
257
+ exit(0) unless ::CLI::UI.confirm "You have already started the workshop. Continuing "\
258
+ "this command will wipe all of your current progress. Continue?", default: false
259
+ end
260
+
261
+ def check_version
262
+ unless client.latest_version?
263
+ say "WARNING: You are running an old version of rpw."
264
+ say "WARNING: Please run `$ gem install rpw`"
265
+ end
266
+ end
267
+ end
268
+ end
@@ -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
@@ -0,0 +1,17 @@
1
+ module RPW
2
+ class Key < SubCommandBase
3
+ desc "register [EMAIL_ADDRESS]", "Change email registered with Speedshop. One-time only."
4
+ def register(email)
5
+ unless client.setup?
6
+ say "You have not yet set up the client. Run $ rpw start"
7
+ exit(1)
8
+ end
9
+ if client.register_email(email)
10
+ say "Key registered with #{email}. You should receive a Slack invite soon."
11
+ else
12
+ say "Key has already been registered. If you believe this is in error,"\
13
+ " please email support@speedshop.co"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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 = ::CLI::UI::Prompt.ask("Your answer?", options: %w[A B C D])
18
+ answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer)
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,18 @@
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
+ end
17
+ end
18
+ end
@@ -0,0 +1,158 @@
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 current
29
+ return list.first unless client_data["completed"]
30
+ list.sort_by { |c| c["position"] }.find { |c| c["position"] == current_position }
31
+ end
32
+
33
+ def list
34
+ @list ||= begin
35
+ if client_data["content_cache_generated"] &&
36
+ client_data["content_cache_generated"] >= Time.now - 60 * 60
37
+
38
+ client_data["content_cache"]
39
+ else
40
+ begin
41
+ client_data["content_cache"] = gateway.list_content
42
+ client_data["content_cache_generated"] = Time.now
43
+ client_data["content_cache"]
44
+ rescue
45
+ client_data["content_cache"] || (raise Error.new("No internet connection"))
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def show(content_pos)
52
+ list.find { |l| l["position"] == content_pos }
53
+ end
54
+
55
+ def directory_setup(home_dir_ok = true)
56
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
57
+ FileUtils.mkdir_p(path) unless File.directory?(path)
58
+ end
59
+
60
+ if home_dir_ok
61
+ ClientData.create_in_home!
62
+ else
63
+ ClientData.create_in_pwd!
64
+ end
65
+
66
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
67
+ File.open(".gitignore", "a") do |f|
68
+ f.puts "\n"
69
+ f.puts ".rpw_info\n"
70
+ f.puts "video\n"
71
+ f.puts "quiz\n"
72
+ f.puts "lab\n"
73
+ f.puts "text\n"
74
+ f.puts "cgrp\n"
75
+ end
76
+ end
77
+
78
+ File.open("README.md", "w+") do |f|
79
+ f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
80
+ end
81
+ end
82
+
83
+ def set_progress(pos, all_prior: false)
84
+ client_data["completed"] = [] && return if pos.nil?
85
+ if all_prior
86
+ lessons = list.select { |l| l["position"] <= pos }
87
+ client_data["completed"] = lessons.map { |l| l["position"] }
88
+ lessons
89
+ else
90
+ lesson = list.find { |l| l["position"] == pos }
91
+ client_data["completed"] += [pos]
92
+ lesson
93
+ end
94
+ end
95
+
96
+ def latest_version?
97
+ return true unless ClientData.exists?
98
+ return true if client_data["last_version_check"] &&
99
+ client_data["last_version_check"] >= Time.now - (60 * 60)
100
+
101
+ begin
102
+ latest = gateway.latest_version?
103
+ rescue
104
+ return true
105
+ end
106
+
107
+ client_data["last_version_check"] = if latest
108
+ Time.now
109
+ else
110
+ false
111
+ end
112
+ end
113
+
114
+ def setup?
115
+ return false unless ClientData.exists?
116
+ client_data["key"]
117
+ end
118
+
119
+ def directories_ready?
120
+ ["video", "quiz", "lab", "text", "cgrp"].all? do |path|
121
+ File.directory?(path)
122
+ end
123
+ end
124
+
125
+ def download_and_extract(content)
126
+ location = content["style"] + "/" + content["s3_key"]
127
+ unless File.exist?(location)
128
+ gateway.download_content(content, folder: content["style"])
129
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
130
+ end
131
+ end
132
+
133
+ def complete(position)
134
+ if client_data["completed"]
135
+ # we actually have to put the _next_ lesson on the completed stack
136
+ set_progress(self.next["position"])
137
+ else
138
+ client_data["completed"] = []
139
+ set_progress(position)
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def current_position
146
+ @current_position ||= client_data["completed"]&.last || 0
147
+ end
148
+
149
+ def client_data
150
+ @client_data ||= ClientData.new
151
+ end
152
+
153
+ def extract_content(content)
154
+ folder = content["style"]
155
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
156
+ end
157
+ end
158
+ end