rpw 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b470a6daefa536118bc45ad650345ae904890cfdc21340f4241fe95585840ca
4
- data.tar.gz: 4055764a0110865d0bd1faa4616a269c16beeceee6868ad9f1b5a8188b172f30
3
+ metadata.gz: 7da44e397174a310bc8ba3ed4c2ecb28f2811cf30275a2cb7cd3ec85029ab99a
4
+ data.tar.gz: c096b4539f5570d6994eed51ec758007e08f75632e0e26f7d13e15b531633995
5
5
  SHA512:
6
- metadata.gz: 797f1c5528477c5befc5bed08a2dba98aabf2968be757a682a5f5dda81ae9815bbfa572f4974504eecb478a7bf106abef1562945f3865f09a8429badf68f61b4
7
- data.tar.gz: a948446a6b948793d63dad89880c1e958f21532eaa278b9e232248a2b298a59c8b103e21c4f0ec537fb003d066abf79da56d8d3e97fde3aad49c9c99fbfaf7f3
6
+ metadata.gz: 3557e0688d5141ae96bcfed87ced6539059bbbd0517a3f2bf85910c5b13b99e153d940fbf56d9bd86bf2cf23525f32a9c848d7fa3ee2d58ae664db806854e93b
7
+ data.tar.gz: 0e804e064e64cd66c703238e00d80f75a35871bf724c08203594d4eec1c94db5815d5ac2bacc11b743e008eb1aeec2c39f2b240fefac87aebe2c005bb194ae19
data/.gitignore CHANGED
@@ -1,2 +1,5 @@
1
1
  bundle
2
- *.gem
2
+ *.gem
3
+ .rpw_key
4
+ .rpw_info
5
+ smoketest
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rpw (0.0.1)
4
+ rpw (0.0.2)
5
5
  thor
6
6
  typhoeus
7
7
 
data/HISTORY.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.0.2
2
+
3
+ * first command-complete version
4
+
1
5
  ## 0.0.1
2
6
 
3
7
  * setup command works
data/exe/rpw CHANGED
@@ -4,56 +4,131 @@ require "thor"
4
4
  require_relative "../lib/rpw"
5
5
 
6
6
  module RPW
7
- class CLI < Thor
8
- class_option :verbose, type: :boolean, aliases: "-v"
7
+ class SubCommandBase < Thor
8
+ def self.banner(command, namespace = nil, subcommand = false)
9
+ "#{basename} #{subcommand_prefix} #{command.usage}"
10
+ end
9
11
 
10
- desc "next", "Proceed to the next section of the workshop"
12
+ def self.subcommand_prefix
13
+ name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
14
+ end
15
+ end
16
+
17
+ class Lesson < SubCommandBase
18
+ desc "next", "Proceed to the next lesson of the workshop"
11
19
 
12
20
  def next
13
- # ...
21
+ say "Proceeding to next lesson..."
22
+ client.next
14
23
  end
15
24
 
16
- desc "setup", "Set up your purchase key in order to download workshop sections"
25
+ desc "complete", "Mark the current lesson as complete"
17
26
 
18
- def setup
19
- say "We'll create a .rpw_key file in the current directory to save your key for future use."
20
- key = ask("Purchase Key: ")
27
+ def complete
28
+ say "Marked current lesson as complete"
29
+ client.complete
30
+ end
21
31
 
22
- client.setup(key)
32
+ desc "list", "Show all available workshop lessons"
23
33
 
24
- say "Successfully authenticated with the RPW server and saved your key."
34
+ def list
35
+ say "All available workshop lessons:"
36
+ client.list.each do |lesson|
37
+ puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
38
+ end
25
39
  end
26
40
 
27
- desc "status", "Show your current workshop progression"
41
+ desc "download [CONTENT | all]", "Download one or all workshop contents"
28
42
 
29
- def status
30
- # ...
43
+ def download(content)
44
+ client.download(content)
31
45
  end
32
46
 
33
- desc "support", "Create a new support ticket, report a bug or issue"
47
+ desc "show [CONTENT]", "Show any workshop lesson"
34
48
 
35
- def support
49
+ def show(content)
50
+ client.show(content)
36
51
  end
37
52
 
38
- desc "show CONTENT", "Show any workshop section (use list to see all section names)"
53
+ private
39
54
 
40
- def show(content)
55
+ def client
56
+ @client ||= RPW::Client.new
41
57
  end
58
+ end
42
59
 
43
- desc "list", "Show all available workshop content"
60
+ class Progress < SubCommandBase
61
+ desc "set [LESSON]", "Set current lesson to a particular lesson"
44
62
 
45
- def list
63
+ def set(lesson)
64
+ client.set_progress(lesson)
46
65
  end
47
66
 
48
- desc "download [CONTENT | all]", "Download one or all workshop contents"
67
+ desc "reset", "Erase all progress and start over"
49
68
 
50
- def download(content)
69
+ def reset
70
+ yes? "Are you sure you want to reset your progress? (Y/N)"
71
+ client.reset_progress
72
+ end
73
+
74
+ desc "show", "Show current workshop progress"
75
+ def show
76
+ data = client.progress
77
+ say "The Rails Performance Workshop"
78
+ say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
79
+ say "Current lesson: #{data[:current_lesson]["title"]}"
80
+ say "Progress by Section (X == completed, O == current):"
81
+ data[:sections].each do |section|
82
+ say "#{section[:title]}: #{section[:progress]}"
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def client
89
+ @client ||= RPW::Client.new
90
+ end
91
+
92
+ default_task :show
93
+ end
94
+
95
+ class CLI < Thor
96
+ desc "lesson [SUBCOMMAND]", "View and download lessons"
97
+ subcommand "lesson", Lesson
98
+ desc "progress [SUBCOMMAND]", "View and set progress"
99
+ subcommand "progress", Progress
100
+
101
+ def self.exit_on_failure?
102
+ true
103
+ end
104
+
105
+ desc "setup", "Set up purchase key and directories"
106
+
107
+ def setup
108
+ return unless yes? "We're going to (idempotently!) create a few files and directories in the current working directory. Is that OK? (Y/N)"
109
+
110
+ say "We'll create a .rpw_key file in the current directory to save your purchase key and course data."
111
+ key = ask("Your Purchase Key: ")
112
+
113
+ client.setup(key)
114
+
115
+ say "Successfully authenticated with the RPW server and saved your key."
116
+ say "We'll also create a few directories for your course data and progress."
117
+
118
+ client.directory_setup
119
+
120
+ say "Setup complete!"
121
+ end
122
+
123
+ desc "support", "Create a new support ticket, report a bug or issue"
124
+
125
+ def support
51
126
  end
52
127
 
53
- private
128
+ private
54
129
 
55
130
  def client
56
- @client ||= RPW::Client.new
131
+ @client ||= RPW::Client.new
57
132
  end
58
133
  end
59
134
  end
data/lib/rpw.rb CHANGED
@@ -1,63 +1,289 @@
1
1
  require "typhoeus"
2
- require "base64"
2
+ require "json"
3
3
 
4
4
  module RPW
5
+ class Error < StandardError; end
6
+
5
7
  class Gateway
6
- attr_accessor :domain
8
+ attr_accessor :domain
7
9
 
8
- def initialize(domain)
10
+ def initialize(domain, key)
9
11
  @domain = domain
12
+ @key = key
10
13
  end
11
14
 
12
15
  class Error < StandardError; end
13
16
 
14
17
  def authenticate_key(key)
15
- request = Typhoeus::Request.new(
16
- domain + "/license",
17
- method: :get,
18
- headers: { Authorization: "Basic #{Base64.encode64(key + ':')}" }
19
- )
20
-
21
- request.on_complete do |response|
22
- if response.success?
23
- true
24
- else
25
- raise Error, "Server responded: #{response.code} #{response.response_body}"
26
- end
18
+ Typhoeus.get(domain + "/license", userpwd: key + ":").success?
19
+ end
20
+
21
+ def get_content_by_position(position)
22
+ response = Typhoeus.get(domain + "/contents/positional?position=#{position}", userpwd: @key + ":")
23
+ if response.success?
24
+ JSON.parse(response.body)
25
+ else
26
+ puts response.inspect
27
+ raise Error, "There was a problem fetching this content."
27
28
  end
29
+ end
28
30
 
29
- request.run
31
+ def list_content
32
+ response = Typhoeus.get(domain + "/contents", userpwd: @key + ":")
33
+ if response.success?
34
+ JSON.parse(response.body)
35
+ else
36
+ puts response.inspect
37
+ raise Error, "There was a problem fetching this content."
38
+ end
30
39
  end
31
40
 
32
- def get_resource(resource)
41
+ def download_content(content, folder:)
42
+ puts "Downloading #{content["title"]}..."
43
+ downloaded_file = File.open("#{folder}/#{content["s3_key"]}", "w")
44
+ request = Typhoeus::Request.new(content["url"])
45
+ request.on_body do |chunk|
46
+ downloaded_file.write(chunk)
47
+ printf(".") if rand(10) == 0 # lol
48
+ end
49
+ request.on_complete { |response| downloaded_file.close }
50
+ request
33
51
  end
34
52
  end
35
53
 
36
54
  class Client
37
- DOTFILE_NAME = ".rpw_key"
38
- RPW_SERVER_DOMAIN = "https://rpw-licensor.speedshop.co"
39
-
40
- class Error < StandardError; end
55
+ RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
41
56
 
42
57
  def setup(key)
43
- # authenticate against server
44
58
  gateway.authenticate_key(key)
59
+ keyfile["key"] = key
60
+ end
61
+
62
+ def directory_setup
63
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
64
+ FileUtils.mkdir_p(path) unless File.directory?(path)
65
+ end
66
+
67
+ client_data["completed"] = [] # just to write the file
68
+
69
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_key/)
70
+ File.open(".gitignore", "a") do |f|
71
+ f.puts "\n"
72
+ f.puts ".rpw_key\n"
73
+ f.puts ".rpw_info\n"
74
+ f.puts "video\n"
75
+ f.puts "quiz\n"
76
+ f.puts "lab\n"
77
+ f.puts "text\n"
78
+ f.puts "cgrp\n"
79
+ end
80
+ end
81
+ end
82
+
83
+ def next
84
+ content = next_content
85
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
86
+ gateway.download_content(content, folder: content["style"]).run
87
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
88
+ end
89
+ client_data["current_lesson"] = content["position"]
90
+ display_content(content)
91
+ end
92
+
93
+ def complete
94
+ client_data["completed"] ||= []
95
+ client_data["completed"] += [client_data["current_lesson"]]
96
+ end
97
+
98
+ def list
99
+ gateway.list_content
100
+ end
101
+
102
+ def show(content_pos)
103
+ content = gateway.get_content_by_position(content_pos)
104
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
105
+ gateway.download_content(content, folder: content["style"]).run
106
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
107
+ end
108
+ client_data["current_lesson"] = content["position"]
109
+ display_content(content)
110
+ end
111
+
112
+ def download(content_pos)
113
+ if content_pos.downcase == "all"
114
+ to_download = gateway.list_content
115
+ hydra = Typhoeus::Hydra.new(max_concurrency: 5)
116
+ to_download.each do |content|
117
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
118
+ hydra.queue gateway.download_content(content, folder: content["style"])
119
+ end
120
+ end
121
+ hydra.run
122
+ to_download.each { |content| extract_content(content) if content["s3_key"].end_with?(".tar.gz") }
123
+ else
124
+ content = gateway.get_content_by_position(content_pos)
125
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
126
+ gateway.download_content(content, folder: content["style"]).run
127
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
128
+ end
129
+ end
130
+ end
131
+
132
+ def progress
133
+ contents = gateway.list_content
134
+ {
135
+ completed: client_data["completed"].size,
136
+ total: contents.size,
137
+ current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
138
+ sections: chart_section_progress(contents)
139
+ }
140
+ end
141
+
142
+ def set_progress(lesson)
143
+ client_data["current_lesson"] = lesson.to_i
144
+ end
145
+
146
+ def reset_progress
147
+ client_data["current_lesson"] = 0
148
+ client_data["completed"] = []
149
+ end
150
+
151
+ private
45
152
 
46
- # write authenticated key
153
+ def chart_section_progress(contents)
154
+ contents.group_by { |c| c["position"] / 100 }
155
+ .each_with_object([]) do |(_, c), memo|
156
+ completed_str = c.map { |l|
157
+ if l["position"] == client_data["current_lesson"]
158
+ "O"
159
+ elsif client_data["completed"].include?(l["position"])
160
+ "X"
161
+ else
162
+ "."
163
+ end
164
+ }.join
165
+ memo << {
166
+ title: c[0]["title"],
167
+ progress: completed_str
168
+ }
169
+ end
170
+ end
171
+
172
+ def next_content
173
+ contents = gateway.list_content
174
+ return contents.first unless client_data["completed"]
175
+ contents.delete_if { |c| client_data["completed"].include? c["position"] }
176
+ contents.min_by { |c| c["position"] }
177
+ end
178
+
179
+ def client_data
180
+ @client_data ||= ClientData.new
181
+ end
182
+
183
+ def keyfile
184
+ @keyfile ||= Keyfile.new
185
+ end
186
+
187
+ def gateway
188
+ @gateway ||= Gateway.new(RPW_SERVER_DOMAIN, keyfile["key"])
189
+ end
190
+
191
+ def extract_content(content)
192
+ folder = content["style"]
193
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
194
+ end
195
+
196
+ def display_content(content)
197
+ case content["style"]
198
+ when "video"
199
+ puts "Opening video: #{content["title"]}"
200
+ exec("open video/#{content["s3_key"]}")
201
+ when "quiz"
202
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
203
+ when "lab"
204
+ # extract and rm archive
205
+ puts "Lab downloaded to lab/#{content["s3_key"]}, navigate there and look at the README to continue"
206
+ when "text"
207
+ puts "Opening in your editor: #{content["title"]}"
208
+ exec("$EDITOR text/#{content["s3_key"]}")
209
+ when "cgrp"
210
+ puts "The Complete Guide to Rails Performance has been downloaded and extracted to the cgrp directory."
211
+ puts "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
212
+ end
213
+ end
214
+ end
215
+
216
+ require "fileutils"
217
+ require "yaml"
218
+
219
+ class ClientData
220
+ DOTFILE_NAME = ".rpw_info"
221
+
222
+ def [](key)
223
+ make_sure_dotfile_exists
224
+ data[key]
225
+ end
226
+
227
+ def []=(key, value)
228
+ data[key] = value
229
+ begin
230
+ File.open(self.class::DOTFILE_NAME, "w") { |f| f.write(YAML.dump(data)) }
231
+ rescue
232
+ raise Error, "The RPW data file in this directory is not writable. \
233
+ Check your file permissions."
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def data
240
+ @data ||= begin
241
+ yaml = begin
242
+ YAML.safe_load(File.read(self.class::DOTFILE_NAME))
243
+ rescue
244
+ nil
245
+ end
246
+ yaml || {}
247
+ end
248
+ end
249
+
250
+ def make_sure_dotfile_exists
251
+ return true if File.exist?(self.class::DOTFILE_NAME)
47
252
  begin
48
- File.open(DOTFILE_NAME, "w") { |f| f.write(key) }
253
+ FileUtils.touch(self.class::DOTFILE_NAME)
49
254
  rescue
50
- raise Error.new "Could not create dotfile in this directory \
51
- to save your key. Check your file permissions."
255
+ raise Error, "Could not create the RPW data file in this directory \
256
+ Check your file permissions."
52
257
  end
258
+ end
259
+ end
260
+
261
+ class Keyfile < ClientData
262
+ DOTFILE_NAME = ".rpw_key"
263
+ end
264
+
265
+ require "digest"
53
266
 
54
- key
267
+ class Quiz < Thor
268
+ desc "give_quiz FILENAME", ""
269
+ def give_quiz(filename)
270
+ @quiz_data = YAML.safe_load(File.read(filename))
271
+ @quiz_data["questions"].each { |q| question(q) }
55
272
  end
56
273
 
57
274
  private
58
275
 
59
- def gateway
60
- @gateway ||= Gateway.new(RPW_SERVER_DOMAIN)
276
+ def question(data)
277
+ puts data["prompt"]
278
+ data["answer_choices"].each { |ac| puts ac }
279
+ provided_answer = ask("Your answer?")
280
+ answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer)
281
+ if answer_digest == data["answer_digest"]
282
+ say "Correct!"
283
+ else
284
+ say "Incorrect."
285
+ say "I encourage you to try reviewing the material to see what the correct answer is."
286
+ end
61
287
  end
62
288
  end
63
289
  end
@@ -1,3 +1,3 @@
1
- module RPW
2
- VERSION = "0.0.1"
3
- end
1
+ module RPW
2
+ VERSION = "0.0.2"
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rpw
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Berkopec
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-05 00:00:00.000000000 Z
11
+ date: 2020-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor