rpw 0.0.3 → 1.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/lib/rpw.rb CHANGED
@@ -1,378 +1,8 @@
1
- require "typhoeus"
2
- require "json"
3
- require_relative "rpw/version"
1
+ require "rpw/version"
2
+ require "rpw/client"
3
+ require "rpw/client_data"
4
+ require "rpw/gateway"
4
5
 
5
6
  module RPW
6
7
  class Error < StandardError; end
7
-
8
- class Gateway
9
- attr_accessor :domain
10
-
11
- def initialize(domain, key)
12
- @domain = domain
13
- @key = key
14
- end
15
-
16
- class Error < StandardError; end
17
-
18
- def authenticate_key(key)
19
- Typhoeus.get(domain + "/license", userpwd: key + ":").success?
20
- end
21
-
22
- def get_content_by_position(position)
23
- response = Typhoeus.get(domain + "/contents/positional?position=#{position}", userpwd: @key + ":")
24
- if response.success?
25
- JSON.parse(response.body)
26
- else
27
- puts response.inspect
28
- raise Error, "There was a problem fetching this content."
29
- end
30
- end
31
-
32
- def list_content
33
- response = Typhoeus.get(domain + "/contents", userpwd: @key + ":")
34
- if response.success?
35
- JSON.parse(response.body)
36
- else
37
- puts response.inspect
38
- raise Error, "There was a problem fetching this content."
39
- end
40
- end
41
-
42
- def download_content(content, folder:)
43
- puts "Downloading #{content["title"]}..."
44
- downloaded_file = File.open("#{folder}/#{content["s3_key"]}", "w")
45
- request = Typhoeus::Request.new(content["url"])
46
- request.on_body do |chunk|
47
- downloaded_file.write(chunk)
48
- printf(".") if rand(500) == 0 # lol
49
- end
50
- request.on_complete { |response| downloaded_file.close }
51
- request
52
- end
53
-
54
- def latest_version?
55
- resp = Typhoeus.get("https://rubygems.org/api/v1/gems/rpw.json")
56
- data = JSON.parse resp.body
57
- Gem::Version.new(RPW::VERSION) >= Gem::Version.new(data["version"])
58
- end
59
-
60
- def register_email(email)
61
- Typhoeus.put(domain + "/license", params: {email: email, key: @key})
62
- end
63
- end
64
-
65
- class Client
66
- RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
67
-
68
- def setup(key)
69
- gateway.authenticate_key(key)
70
- client_data["key"] = key
71
- end
72
-
73
- def register_email(email)
74
- gateway.register_email(email)
75
- end
76
-
77
- def directory_setup
78
- ["video", "quiz", "lab", "text", "cgrp"].each do |path|
79
- FileUtils.mkdir_p(path) unless File.directory?(path)
80
- end
81
-
82
- client_data["completed"] = [] # just to write the file
83
-
84
- unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_key/)
85
- File.open(".gitignore", "a") do |f|
86
- f.puts "\n"
87
- f.puts ".rpw_key\n"
88
- f.puts ".rpw_info\n"
89
- f.puts "video\n"
90
- f.puts "quiz\n"
91
- f.puts "lab\n"
92
- f.puts "text\n"
93
- f.puts "cgrp\n"
94
- end
95
- end
96
- end
97
-
98
- def next(open_after = false)
99
- complete
100
- content = next_content
101
- if content.nil?
102
- finished_workshop
103
- return
104
- end
105
-
106
- unless File.exist?(content["style"] + "/" + content["s3_key"])
107
- gateway.download_content(content, folder: content["style"]).run
108
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
109
- end
110
- client_data["current_lesson"] = content["position"]
111
- display_content(content, open_after)
112
- end
113
-
114
- def complete
115
- reset_progress unless client_data["current_lesson"] && client_data["completed"]
116
- client_data["completed"] ||= []
117
- client_data["completed"] += [client_data["current_lesson"] || 0]
118
- end
119
-
120
- def list
121
- gateway.list_content
122
- end
123
-
124
- def show(content_pos, open_after = false)
125
- content_pos = client_data["current_lesson"] if content_pos == :current
126
- content = gateway.get_content_by_position(content_pos)
127
- unless File.exist?(content["style"] + "/" + content["s3_key"])
128
- gateway.download_content(content, folder: content["style"]).run
129
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
130
- end
131
- client_data["current_lesson"] = content["position"]
132
- display_content(content, open_after)
133
- end
134
-
135
- def download(content_pos)
136
- if content_pos.downcase == "all"
137
- to_download = gateway.list_content
138
- hydra = Typhoeus::Hydra.new(max_concurrency: 5)
139
- to_download.each do |content|
140
- unless File.exist?(content["style"] + "/" + content["s3_key"])
141
- hydra.queue gateway.download_content(content, folder: content["style"])
142
- end
143
- end
144
- hydra.run
145
- to_download.each { |content| extract_content(content) if content["s3_key"].end_with?(".tar.gz") }
146
- else
147
- content = gateway.get_content_by_position(content_pos)
148
- unless File.exist?(content["style"] + "/" + content["s3_key"])
149
- gateway.download_content(content, folder: content["style"]).run
150
- extract_content(content) if content["s3_key"].end_with?(".tar.gz")
151
- end
152
- end
153
- end
154
-
155
- def progress
156
- contents = gateway.list_content
157
- {
158
- completed: client_data["completed"].size,
159
- total: contents.size,
160
- current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
161
- sections: chart_section_progress(contentsi)
162
- }
163
- end
164
-
165
- def set_progress(lesson)
166
- client_data["current_lesson"] = lesson.to_i
167
- end
168
-
169
- def reset_progress
170
- client_data["current_lesson"] = 0
171
- client_data["completed"] = []
172
- end
173
-
174
- def latest_version?
175
- if client_data["last_version_check"]
176
- return true if client_data["last_version_check"] >= Time.now - (60 * 60 * 24)
177
- return false if client_data["last_version_check"] == false
178
- end
179
-
180
- begin
181
- latest = gateway.latest_version?
182
- rescue
183
- return true
184
- end
185
-
186
- client_data["last_version_check"] = if latest
187
- Time.now
188
- else
189
- false
190
- end
191
- end
192
-
193
- def setup?
194
- client_data["key"]
195
- end
196
-
197
- private
198
-
199
- def finished_workshop
200
- RPW::CLI.new.print_banner
201
- puts "Congratulations!"
202
- puts "You have completed the Rails Performance Workshop."
203
- end
204
-
205
- def chart_section_progress(contents)
206
- contents.group_by { |c| c["position"] / 100 }
207
- .each_with_object([]) do |(_, c), memo|
208
- completed_str = c.map { |l|
209
- if l["position"] == client_data["current_lesson"]
210
- "O"
211
- elsif client_data["completed"].include?(l["position"])
212
- "X"
213
- else
214
- "."
215
- end
216
- }.join
217
- memo << {
218
- title: c[0]["title"],
219
- progress: completed_str
220
- }
221
- end
222
- end
223
-
224
- def next_content
225
- contents = gateway.list_content
226
- return contents.first unless client_data["completed"]
227
- contents.delete_if { |c| client_data["completed"].include? c["position"] }
228
- contents.min_by { |c| c["position"] }
229
- end
230
-
231
- def client_data
232
- @client_data ||= ClientData.new
233
- end
234
-
235
- def gateway
236
- @gateway ||= Gateway.new(RPW_SERVER_DOMAIN, client_data["key"])
237
- end
238
-
239
- def extract_content(content)
240
- folder = content["style"]
241
- `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
242
- end
243
-
244
- def display_content(content, open_after)
245
- puts "\nCurrent Lesson: #{content["title"]}"
246
- case content["style"]
247
- when "video"
248
- location = "video/#{content["s3_key"]}"
249
- when "quiz"
250
- Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
251
- when "lab"
252
- location = "lab/#{content["s3_key"]}"
253
- when "text"
254
- location = "lab/#{content["s3_key"]}"
255
- when "cgrp"
256
- puts "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
257
- puts "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
258
- end
259
- if location
260
- puts "Downloaded to:"
261
- puts location.to_s
262
- if open_after
263
- exec "#{open_command} #{location}"
264
- end
265
- end
266
- end
267
-
268
- require "rbconfig"
269
- def open_command
270
- host_os = RbConfig::CONFIG["host_os"]
271
- case host_os
272
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
273
- "start"
274
- when /darwin|mac os/
275
- "open"
276
- else
277
- "xdg-open"
278
- end
279
- end
280
- end
281
-
282
- require "fileutils"
283
- require "yaml"
284
-
285
- class ClientData
286
- DOTFILE_NAME = ".rpw_info"
287
-
288
- def initialize
289
- make_sure_dotfile_exists
290
- data # access file to load
291
- end
292
-
293
- def [](key)
294
- data[key]
295
- end
296
-
297
- def []=(key, value)
298
- data
299
- data[key] = value
300
-
301
- begin
302
- File.open(filestore_location, "w") { |f| f.write(YAML.dump(data)) }
303
- rescue
304
- raise Error, "The RPW data at #{filestore_location} is not writable. \
305
- Check your file permissions."
306
- end
307
- end
308
-
309
- def self.delete_filestore
310
- return unless File.exist?(filestore_location)
311
- FileUtils.remove(filestore_location)
312
- end
313
-
314
- def self.filestore_location
315
- if File.exist?(File.expand_path("./" + self::DOTFILE_NAME))
316
- File.expand_path("./" + + self::DOTFILE_NAME)
317
- else
318
- File.expand_path("~/.rpw/" + self::DOTFILE_NAME)
319
- end
320
- end
321
-
322
- private
323
-
324
- def filestore_location
325
- self.class.filestore_location
326
- end
327
-
328
- def create_client_data_directory(path)
329
- dirname = File.dirname(path)
330
- FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
331
- end
332
-
333
- def data
334
- @data ||= begin
335
- yaml = YAML.safe_load(File.read(filestore_location), permitted_classes: [Time])
336
- yaml || {}
337
- end
338
- end
339
-
340
- def make_sure_dotfile_exists
341
- return true if File.exist?(filestore_location)
342
- create_client_data_directory(filestore_location)
343
- begin
344
- FileUtils.touch(filestore_location)
345
- rescue
346
- raise Error, "Could not create the RPW data file at ~/.rpw/ \
347
- Check your file permissions."
348
- end
349
- end
350
- end
351
-
352
- require "digest"
353
- require "thor"
354
-
355
- class Quiz < Thor
356
- desc "give_quiz FILENAME", ""
357
- def give_quiz(filename)
358
- @quiz_data = YAML.safe_load(File.read(filename))
359
- @quiz_data["questions"].each { |q| question(q) }
360
- end
361
-
362
- private
363
-
364
- def question(data)
365
- puts data["prompt"]
366
- data["answer_choices"].each { |ac| puts ac }
367
- provided_answer = ask("Your answer?")
368
- answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer.upcase)
369
- if answer_digest == data["answer_digest"]
370
- say "Correct!"
371
- else
372
- say "Incorrect."
373
- say "I encourage you to try reviewing the material to see what the correct answer is."
374
- end
375
- say ""
376
- end
377
- end
378
8
  end
@@ -0,0 +1,58 @@
1
+ ## Installation Requirements
2
+
3
+ This client assumes you're using Ruby 2.3 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
11
+
12
+ ## Slack Invite
13
+
14
+ If you purchased the Workshop yourself, you will receive a Slack channel invitation
15
+ shortly. If you are attending the Workshop as part of a group and your license key
16
+ was provided to you, you need to register your key to get an invite:
17
+
18
+ ```
19
+ $ rpw key register [YOUR_EMAIL_ADDRESS]
20
+ ```
21
+
22
+ Please note you can only register your key once.
23
+
24
+ The Slack channel is your best resource for questions about Rails Performance
25
+ or other material in the workshop. Nate is almost always monitoring that channel.
26
+
27
+ If you encounter a **bug or other software problem**, please email support@speedshop.co.
28
+
29
+ ## Important Commands
30
+
31
+ Here are some important commands for you to know:
32
+
33
+ ```
34
+ $ rpw lesson next | Proceed to the next part of the workshop.
35
+ $ rpw lesson complete | Mark current lesson as complete.
36
+ $ rpw lesson list | List all workshop lessons. Note each lesson is preceded with an ID.
37
+ $ rpw lesson download | Download any or all lessons. Use the IDs from "list".
38
+ $ rpw lesson show | Show any particular workshop lesson. Use the IDs from "list".
39
+ $ rpw progress | Show where you're currently at in the workshop.
40
+ ```
41
+
42
+ Generally, you'll just be doing a lot of `$ rpw lesson next`!
43
+
44
+ By default, `$ rpw lesson next` will try to open the content it downloads. If you
45
+ either don't like this, or for some reason it doesn't work, use `$ rpw lesson next --no-open`.
46
+
47
+ ## Working Offline
48
+
49
+ By default, the course will download each piece of content as you progress through
50
+ the course. However, you can use `rpw lesson download all` to download all content
51
+ at once, and complete the workshop entirely offline.
52
+
53
+ Videos in this workshop are generally about 100MB each, which means the entire
54
+ course is about a 3 to 4GB download.
55
+
56
+ ## Bugs and Support
57
+
58
+ If you encounter any problems, please email support@speedshop.co for the fastest possible response.
@@ -0,0 +1,94 @@
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 "rpw/cli/lesson"
8
+ require "rpw/cli/progress"
9
+
10
+ module RPW
11
+ class CLI < Thor
12
+ class_before :check_version
13
+ class_before :check_setup
14
+
15
+ desc "key register [EMAIL_ADDRESS]", "Change email registered w/Speedshop"
16
+ subcommand "key", Key
17
+ desc "lesson [SUBCOMMAND]", "View and download lessons"
18
+ subcommand "lesson", Lesson
19
+ desc "progress [SUBCOMMAND]", "View and set progress"
20
+ subcommand "progress", Progress
21
+
22
+ def self.exit_on_failure?
23
+ true
24
+ end
25
+
26
+ desc "start", "Tutorial and onboarding"
27
+ def start
28
+ warn_if_already_started
29
+
30
+ print_banner
31
+ say "Welcome to the Rails Performance Workshop."
32
+ say ""
33
+ say "This is rpw, the command line client for this workshop."
34
+ say ""
35
+ say "This client will download files from the internet into the current"
36
+ say "working directory, so it's best to run this client from a new directory"
37
+ say "that you'll use as your 'scratch space' for working on the Workshop."
38
+ say ""
39
+ say "We will create a handful of new files and folders in the current directory."
40
+ return unless yes? "Is this OK? (y/N) (N will quit)"
41
+ puts ""
42
+ say "We'll also create a .rpw_info file at #{File.expand_path("~/.rpw")} to save your purchase key."
43
+ home_dir_ok = yes?("Is this OK? (y/N) (N will create it in the current directory)")
44
+ client.directory_setup(home_dir_ok)
45
+
46
+ key = ask("Your Purchase Key: ")
47
+
48
+ unless client.setup(key)
49
+ say "That is not a valid key. Please try again."
50
+ exit(0)
51
+ end
52
+
53
+ puts ""
54
+ say "Successfully authenticated with the RPW server and saved your key."
55
+ puts ""
56
+ say "Setup complete!"
57
+ puts ""
58
+ say "To learn how to use this command-line client, consult ./README.md, which we just created."
59
+ say "Once you've read that and you're ready to get going: $ rpw lesson next"
60
+ end
61
+
62
+ no_commands do
63
+ def print_banner
64
+ RPW::Bannerlord.print_banner
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def client
71
+ @client ||= RPW::Client.new
72
+ end
73
+
74
+ def warn_if_already_started
75
+ return unless client.setup?
76
+ exit(0) unless yes? "You have already started the workshop. Continuing "\
77
+ "this command will wipe all of your current progress. Continue? (y/N)"
78
+ end
79
+
80
+ def check_version
81
+ unless client.latest_version?
82
+ say "WARNING: You are running an old version of rpw."
83
+ say "WARNING: Please run `$ gem install rpw`"
84
+ end
85
+ end
86
+
87
+ def check_setup
88
+ unless client.setup? || current_command_chain == [:start]
89
+ say "WARNING: You do not have a purchase key set. Run `$ rpw start`"
90
+ exit(0)
91
+ end
92
+ end
93
+ end
94
+ end