rpw 0.0.3 → 1.0.1

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