skp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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