skp 0.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.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "rake/testtask"
2
+ require "rubygems/package_task"
3
+ require "bundler/gem_tasks"
4
+ require "standard/rake"
5
+
6
+ gemspec = Gem::Specification.load("skp.gemspec")
7
+ Gem::PackageTask.new(gemspec).define
8
+
9
+ Rake::TestTask.new(:test)
10
+
11
+ task default: [:standard, :test]
data/exe/skp ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/skp/cli"
4
+
5
+ SKP::CLI.start(ARGV)
data/lib/skp/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 (requires HEVC/h265 support)
11
+
12
+ ## Slack Invite
13
+
14
+ The Slack channel is your best resource for questions about Ruby 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
+ $ skp 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
+ $ skp next | Proceed to the next part of the workshop.
35
+ $ skp complete | Mark current lesson as complete.
36
+ $ skp list | List all workshop lessons. Shows progress.
37
+ $ skp download | Download all lessons. Useful for offline access.
38
+ $ skp show | Show any particular workshop lesson.
39
+ $ skp current | Opens the current lesson.
40
+ ```
41
+
42
+ Generally, you'll just be doing a lot of `$ skp next`! It's the same thing as `$ skp complete && skp show`.
43
+
44
+ #### --no-open
45
+
46
+ By default, `$ skp next` (and `$ skp show` and `$ skp 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 `$ skp 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 `skp 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.
@@ -0,0 +1,53 @@
1
+ module SKP
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
+ ,---.o | | o o
25
+ `---..,---|,---.|__/ .,---. .,---.
26
+ ||| ||---'| \ || | || |
27
+ `---'``---'`---'` ```---| `` '
28
+ |
29
+ ,---. | o
30
+ |---',---.,---.,---.|--- .,---.,---.
31
+ | | ,---|| | || |---'
32
+ ` ` `---^`---'`---'``---'`---'
33
+ #{reset})
34
+ end
35
+
36
+ def banner
37
+ %(
38
+ +hmNMMMMMm/` -ymMMNh/
39
+ sMMMMMMMMMy +MMMMMMMMy ,---.o | | o o
40
+ yMMMMMMMMMMy` yMMMMMMMMN `---..,---|,---.|__/ .,---. .,---.
41
+ `dMMMMMMMMMMm:-dMMMMMMm: ||| ||---'| \ || | || |
42
+ `sNMMMMMMMMMMs.:+sso:` `---'``---'`---'` ```---| `` '
43
+ :dMMMMMMMMMMm/ |
44
+ :oss+:.sNMMMMMMMMMMy` ,---. | o
45
+ /mMMMMMMd-:mMMMMMMMMMMd. |---',---.,---.,---.|--- .,---.,---.
46
+ NMMMMMMMMy `hMMMMMMMMMMh | | ,---|| | || |---'
47
+ yMMMMMMMM+ `dMMMMMMMMMy ` ` `---^`---'`---'``---'`---'
48
+ /hNMMmy- `/mMMMMMNmy/
49
+ #{reset})
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ module SKP
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 $ skp 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 SKP
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 SKP
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 ||= SKP::Client.new
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/skp/cli.rb ADDED
@@ -0,0 +1,269 @@
1
+ require "thor"
2
+ require "thor/hollaback"
3
+ require "skp"
4
+ require "skp/cli/bannerlord"
5
+ require "skp/cli/sub_command_base"
6
+ require "skp/cli/key"
7
+ require "cli/ui"
8
+
9
+ CLI::UI::StdoutRouter.enable
10
+
11
+ module SKP
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 Sidekiq in Practice. \u{1F48E}"
28
+ say ""
29
+ say "This is skp, 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 (~/.skp)"
46
+ ])
47
+
48
+ client.directory_setup((ans == "my home directory (~/.skp)"))
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 SKP 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: $ skp 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
+ SKP::CLI.new.print_banner
76
+ say "Congratulations!"
77
+ say "You have completed Sidekiq in Practice."
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
+ option :quizzes, type: :boolean
159
+ def show
160
+ exit_with_no_key
161
+ title = ::CLI::UI::Prompt.ask(
162
+ "Which lesson would you like to view?",
163
+ options: client.list.reject { |l| !options[:quizzes] && l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
164
+ )
165
+ title.strip!
166
+ content_order = client.list.find { |l| l["title"] == title }["position"]
167
+ content = client.show(content_order)
168
+ client.download_and_extract(content)
169
+ display_content(content, !options[:"no-open"])
170
+ end
171
+
172
+ desc "set_progress", "Set current lesson to a particular lesson"
173
+ def set_progress
174
+ title = ::CLI::UI::Prompt.ask(
175
+ "Which lesson would you like to set your progress to? All prior lessons will be marked complete",
176
+ options: client.list.reject { |l| l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
177
+ )
178
+ title.strip!
179
+ content_order = client.list.find { |l| l["title"] == title }["position"]
180
+ content = client.set_progress(content_order, all_prior: true)
181
+ say "Setting current progress to #{content.last["title"]}"
182
+ end
183
+
184
+ desc "reset", "Erase all progress and start over"
185
+ def reset
186
+ return unless ::CLI::UI.confirm("Are you sure you want to erase all of your progress?", default: false)
187
+ say "Resetting progress."
188
+ client.set_progress(nil)
189
+ end
190
+
191
+ no_commands do
192
+ def print_banner
193
+ SKP::Bannerlord.print_banner
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ def exit_with_no_key
200
+ unless client.setup?
201
+ say "You have not yet set up the client. Run $ skp start"
202
+ exit(1)
203
+ end
204
+ unless client.directories_ready?
205
+ say "You are not in your workshop scratch directory, or you have not yet"
206
+ say "set up the client. Change directory or run $ skp start"
207
+ exit(1)
208
+ end
209
+ end
210
+
211
+ def client
212
+ @client ||= SKP::Client.new
213
+ end
214
+
215
+ def display_content(content, open_after)
216
+ openable = false
217
+ case content["style"]
218
+ when "video"
219
+ location = "video/#{content["s3_key"]}"
220
+ openable = true
221
+ when "quiz"
222
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
223
+ when "lab"
224
+ location = "lab/#{content["s3_key"][0..-8]}"
225
+ openable = true
226
+ when "text"
227
+ location = "text/#{content["s3_key"]}"
228
+ openable = true
229
+ when "cgrp"
230
+ say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
231
+ say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
232
+ say "You can check it out now, or to continue: $ skp next "
233
+ end
234
+ if location
235
+ if openable && !open_after
236
+ say "Download complete. Open with: $ #{open_command} #{location}"
237
+ elsif open_after && openable
238
+ exec "#{open_command} #{location}"
239
+ end
240
+ end
241
+ end
242
+
243
+ require "rbconfig"
244
+ def open_command
245
+ host_os = RbConfig::CONFIG["host_os"]
246
+ case host_os
247
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
248
+ "start"
249
+ when /darwin|mac os/
250
+ "open"
251
+ else
252
+ "xdg-open"
253
+ end
254
+ end
255
+
256
+ def warn_if_already_started
257
+ return unless client.setup?
258
+ exit(0) unless ::CLI::UI.confirm "You have already started the workshop. Continuing "\
259
+ "this command will wipe all of your current progress. Continue?", default: false
260
+ end
261
+
262
+ def check_version
263
+ unless client.latest_version?
264
+ say "WARNING: You are running an old version of skp."
265
+ say "WARNING: Please run `$ gem install skp`"
266
+ end
267
+ end
268
+ end
269
+ end
data/lib/skp/client.rb ADDED
@@ -0,0 +1,158 @@
1
+ require "fileutils"
2
+ require "skp/cli/quiz"
3
+
4
+ module SKP
5
+ class Client
6
+ SKP_SERVER_DOMAIN = ENV["SKP_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
7
+ attr_reader :gateway
8
+
9
+ def initialize(gateway = nil)
10
+ @gateway = gateway || Gateway.new(SKP_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(/skp_info/)
67
+ File.open(".gitignore", "a") do |f|
68
+ f.puts "\n"
69
+ f.puts ".skp_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} -xzf #{folder}/#{content["s3_key"]}`
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,73 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+
4
+ module SKP
5
+ class ClientData
6
+ DOTFILE_NAME = ".skp_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 SKP 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("~/.skp/"))
34
+ FileUtils.mkdir(File.expand_path("~/.skp/"))
35
+ end
36
+
37
+ FileUtils.touch(File.expand_path("~/.skp/" + 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("~/.skp/" + 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