rpw 0.0.1 → 0.0.6
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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +109 -0
- data/.gitignore +5 -1
- data/Gemfile.lock +17 -16
- data/HISTORY.md +21 -0
- data/README.md +2 -0
- data/exe/rpw +1 -57
- data/lib/rpw.rb +5 -60
- data/lib/rpw/README.md +22 -0
- data/lib/rpw/cli.rb +96 -0
- data/lib/rpw/cli/bannerlord.rb +59 -0
- data/lib/rpw/cli/key.rb +15 -0
- data/lib/rpw/cli/lesson.rb +99 -0
- data/lib/rpw/cli/progress.rb +32 -0
- data/lib/rpw/cli/quiz.rb +28 -0
- data/lib/rpw/cli/sub_command_base.rb +30 -0
- data/lib/rpw/client.rb +168 -0
- data/lib/rpw/client_data.rb +73 -0
- data/lib/rpw/gateway.rb +63 -0
- data/lib/rpw/version.rb +3 -3
- data/rpw.gemspec +2 -1
- metadata +29 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 796740f7cd5cfebc0f46f72669da6d1b2941436c984d2a624a18200ae3466495
|
4
|
+
data.tar.gz: 3b986dbab69c20f2e840b2197175119e428d19b117378ef9192a5ce31178cbac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b5530f032ece3e44819374cb7001530359a913e5b025e04f19313c6afeb1ec2450667783c8d44e20a18b1d0c16220a19f60d29275b1f3e9852a5401a2f2b7ba
|
7
|
+
data.tar.gz: 1389deea5fe3705527d841abc84d9f6f33b112d5a8d02502bb8d8f8a5df90ffaedc520f8449da2891405d7702a31d65ac193087c12a5a3588afe53cf6f4dee14
|
@@ -0,0 +1,109 @@
|
|
1
|
+
name: Test
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
name: >-
|
8
|
+
${{ matrix.os }} ${{ matrix.ruby }}
|
9
|
+
env:
|
10
|
+
CI: true
|
11
|
+
TESTOPTS: -v
|
12
|
+
|
13
|
+
runs-on: ${{ matrix.os }}
|
14
|
+
if: |
|
15
|
+
!( contains(github.event.pull_request.title, '[ci skip]')
|
16
|
+
|| contains(github.event.pull_request.title, '[skip ci]')
|
17
|
+
|| contains(github.event.head_commit.message, '[ci skip]')
|
18
|
+
|| contains(github.event.head_commit.message, '[skip ci]'))
|
19
|
+
strategy:
|
20
|
+
fail-fast: true
|
21
|
+
matrix:
|
22
|
+
os: [ ubuntu-20.04, macos-10.15, windows-2019 ]
|
23
|
+
ruby: [ 2.6, 2.7, head ]
|
24
|
+
|
25
|
+
steps:
|
26
|
+
- name: repo checkout
|
27
|
+
uses: actions/checkout@v2
|
28
|
+
|
29
|
+
- name: load ruby
|
30
|
+
uses: MSP-Greg/setup-ruby-pkgs@v1
|
31
|
+
with:
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
33
|
+
|
34
|
+
- name: bundle install
|
35
|
+
run: |
|
36
|
+
bundle install --jobs 4 --retry 3
|
37
|
+
|
38
|
+
- name: standardrb
|
39
|
+
run: bundle exec standardrb
|
40
|
+
|
41
|
+
- name: test
|
42
|
+
run: bundle exec rake test
|
43
|
+
|
44
|
+
build-live:
|
45
|
+
services:
|
46
|
+
postgres:
|
47
|
+
image: postgres:alpine
|
48
|
+
env:
|
49
|
+
POSTGRES_PASSWORD: password
|
50
|
+
ports:
|
51
|
+
- 5432:5432
|
52
|
+
# needed because the postgres container does not provide a healthcheck
|
53
|
+
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
54
|
+
|
55
|
+
name: Test against a live server
|
56
|
+
env:
|
57
|
+
CI: true
|
58
|
+
TESTOPTS: -v
|
59
|
+
LIVE_SERVER: true
|
60
|
+
|
61
|
+
runs-on: ubuntu-latest
|
62
|
+
|
63
|
+
steps:
|
64
|
+
- name: repo checkout
|
65
|
+
uses: actions/checkout@v2
|
66
|
+
|
67
|
+
- name: load ruby
|
68
|
+
uses: MSP-Greg/setup-ruby-pkgs@v1
|
69
|
+
with:
|
70
|
+
ruby-version: 2.7.2
|
71
|
+
|
72
|
+
- name: bundle install
|
73
|
+
run: |
|
74
|
+
bundle install --jobs 4 --retry 3
|
75
|
+
|
76
|
+
- uses: actions/checkout@master
|
77
|
+
with:
|
78
|
+
repository: speedshop/licensor
|
79
|
+
path: server
|
80
|
+
|
81
|
+
- name: nuke server ruby requirement
|
82
|
+
run: grep -v "^ruby" Gemfile > temp && mv -f temp Gemfile
|
83
|
+
working-directory: ./server
|
84
|
+
|
85
|
+
- name: bundle install the server
|
86
|
+
run: |
|
87
|
+
bundle install --jobs 4 --retry 3
|
88
|
+
bundle exec rails db:setup
|
89
|
+
working-directory: ./server
|
90
|
+
env:
|
91
|
+
POSTGRES_USER: postgres
|
92
|
+
POSTGRES_PASSWORD: password
|
93
|
+
|
94
|
+
- name: start the server
|
95
|
+
run: rails server &
|
96
|
+
working-directory: ./server
|
97
|
+
env:
|
98
|
+
POSTGRES_USER: postgres
|
99
|
+
POSTGRES_PASSWORD: password
|
100
|
+
|
101
|
+
- name: wait for server
|
102
|
+
run: |
|
103
|
+
until $(curl --output /dev/null --silent --head --fail http://localhost:3000); do
|
104
|
+
printf '.'
|
105
|
+
sleep 1
|
106
|
+
done
|
107
|
+
|
108
|
+
- name: test
|
109
|
+
run: bundle exec rake test
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rpw (0.0.
|
4
|
+
rpw (0.0.6)
|
5
|
+
excon
|
5
6
|
thor
|
6
|
-
|
7
|
+
thor-hollaback
|
7
8
|
|
8
9
|
GEM
|
9
10
|
remote: https://rubygems.org/
|
10
11
|
specs:
|
11
12
|
ast (2.4.1)
|
12
|
-
|
13
|
-
|
14
|
-
ffi (1.13.1)
|
13
|
+
excon (0.78.0)
|
14
|
+
hollaback (0.1.0)
|
15
15
|
minitest (5.14.2)
|
16
|
-
parallel (1.
|
17
|
-
parser (2.7.
|
16
|
+
parallel (1.20.0)
|
17
|
+
parser (2.7.2.0)
|
18
18
|
ast (~> 2.4.1)
|
19
19
|
rainbow (3.0.0)
|
20
20
|
rake (13.0.1)
|
21
|
-
regexp_parser (1.8.
|
21
|
+
regexp_parser (1.8.2)
|
22
22
|
rexml (3.2.4)
|
23
|
-
rubocop (
|
23
|
+
rubocop (1.2.0)
|
24
24
|
parallel (~> 1.10)
|
25
25
|
parser (>= 2.7.1.5)
|
26
26
|
rainbow (>= 2.2.2, < 4.0)
|
27
|
-
regexp_parser (>= 1.
|
27
|
+
regexp_parser (>= 1.8)
|
28
28
|
rexml
|
29
|
-
rubocop-ast (>= 0.
|
29
|
+
rubocop-ast (>= 1.0.1)
|
30
30
|
ruby-progressbar (~> 1.7)
|
31
31
|
unicode-display_width (>= 1.4.0, < 2.0)
|
32
|
-
rubocop-ast (
|
32
|
+
rubocop-ast (1.1.1)
|
33
33
|
parser (>= 2.7.1.5)
|
34
34
|
rubocop-performance (1.8.1)
|
35
35
|
rubocop (>= 0.87.0)
|
36
36
|
rubocop-ast (>= 0.4.0)
|
37
37
|
ruby-progressbar (1.10.1)
|
38
|
-
standard (0.
|
39
|
-
rubocop (= 0
|
38
|
+
standard (0.9.0)
|
39
|
+
rubocop (= 1.2.0)
|
40
40
|
rubocop-performance (= 1.8.1)
|
41
41
|
thor (1.0.1)
|
42
|
-
|
43
|
-
|
42
|
+
thor-hollaback (0.2.0)
|
43
|
+
hollaback (~> 0.1.0)
|
44
|
+
thor (>= 0.19.1)
|
44
45
|
unicode-display_width (1.7.0)
|
45
46
|
|
46
47
|
PLATFORMS
|
data/HISTORY.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## 0.0.6
|
2
|
+
|
3
|
+
* more bugfixes from beta testers
|
4
|
+
* CI
|
5
|
+
|
6
|
+
## 0.0.5
|
7
|
+
|
8
|
+
* more bugfixes from beta testers
|
9
|
+
|
10
|
+
## 0.0.4
|
11
|
+
|
12
|
+
* bugfixes
|
13
|
+
|
14
|
+
## 0.0.3
|
15
|
+
|
16
|
+
* beta
|
17
|
+
|
18
|
+
## 0.0.2
|
19
|
+
|
20
|
+
* first command-complete version
|
21
|
+
|
1
22
|
## 0.0.1
|
2
23
|
|
3
24
|
* setup command works
|
data/README.md
CHANGED
data/exe/rpw
CHANGED
@@ -1,61 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require_relative "../lib/rpw"
|
5
|
-
|
6
|
-
module RPW
|
7
|
-
class CLI < Thor
|
8
|
-
class_option :verbose, type: :boolean, aliases: "-v"
|
9
|
-
|
10
|
-
desc "next", "Proceed to the next section of the workshop"
|
11
|
-
|
12
|
-
def next
|
13
|
-
# ...
|
14
|
-
end
|
15
|
-
|
16
|
-
desc "setup", "Set up your purchase key in order to download workshop sections"
|
17
|
-
|
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: ")
|
21
|
-
|
22
|
-
client.setup(key)
|
23
|
-
|
24
|
-
say "Successfully authenticated with the RPW server and saved your key."
|
25
|
-
end
|
26
|
-
|
27
|
-
desc "status", "Show your current workshop progression"
|
28
|
-
|
29
|
-
def status
|
30
|
-
# ...
|
31
|
-
end
|
32
|
-
|
33
|
-
desc "support", "Create a new support ticket, report a bug or issue"
|
34
|
-
|
35
|
-
def support
|
36
|
-
end
|
37
|
-
|
38
|
-
desc "show CONTENT", "Show any workshop section (use list to see all section names)"
|
39
|
-
|
40
|
-
def show(content)
|
41
|
-
end
|
42
|
-
|
43
|
-
desc "list", "Show all available workshop content"
|
44
|
-
|
45
|
-
def list
|
46
|
-
end
|
47
|
-
|
48
|
-
desc "download [CONTENT | all]", "Download one or all workshop contents"
|
49
|
-
|
50
|
-
def download(content)
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def client
|
56
|
-
@client ||= RPW::Client.new
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
3
|
+
require_relative "../lib/rpw/cli"
|
60
4
|
|
61
5
|
RPW::CLI.start(ARGV)
|
data/lib/rpw.rb
CHANGED
@@ -1,63 +1,8 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
1
|
+
require "rpw/version"
|
2
|
+
require "rpw/client"
|
3
|
+
require "rpw/client_data"
|
4
|
+
require "rpw/gateway"
|
3
5
|
|
4
6
|
module RPW
|
5
|
-
class
|
6
|
-
attr_accessor :domain
|
7
|
-
|
8
|
-
def initialize(domain)
|
9
|
-
@domain = domain
|
10
|
-
end
|
11
|
-
|
12
|
-
class Error < StandardError; end
|
13
|
-
|
14
|
-
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
|
27
|
-
end
|
28
|
-
|
29
|
-
request.run
|
30
|
-
end
|
31
|
-
|
32
|
-
def get_resource(resource)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
class Client
|
37
|
-
DOTFILE_NAME = ".rpw_key"
|
38
|
-
RPW_SERVER_DOMAIN = "https://rpw-licensor.speedshop.co"
|
39
|
-
|
40
|
-
class Error < StandardError; end
|
41
|
-
|
42
|
-
def setup(key)
|
43
|
-
# authenticate against server
|
44
|
-
gateway.authenticate_key(key)
|
45
|
-
|
46
|
-
# write authenticated key
|
47
|
-
begin
|
48
|
-
File.open(DOTFILE_NAME, "w") { |f| f.write(key) }
|
49
|
-
rescue
|
50
|
-
raise Error.new "Could not create dotfile in this directory \
|
51
|
-
to save your key. Check your file permissions."
|
52
|
-
end
|
53
|
-
|
54
|
-
key
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def gateway
|
60
|
-
@gateway ||= Gateway.new(RPW_SERVER_DOMAIN)
|
61
|
-
end
|
62
|
-
end
|
7
|
+
class Error < StandardError; end
|
63
8
|
end
|
data/lib/rpw/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
## Installation Requirements
|
2
|
+
|
3
|
+
This client assumes you have `tar` installed.
|
4
|
+
|
5
|
+
## Important Commands
|
6
|
+
|
7
|
+
Here are some important commands for you to know:
|
8
|
+
|
9
|
+
$ rpw lesson next | Proceed to the next part of the workshop.
|
10
|
+
$ rpw lesson complete | Mark current lesson as complete.
|
11
|
+
$ rpw lesson list | List all workshop lessons. Note each lesson is preceded with an ID.
|
12
|
+
$ rpw lesson download | Download any or all lessons.
|
13
|
+
$ rpw lesson show | Show any particular workshop lesson.
|
14
|
+
$ rpw progress | Show where you're currently at in the workshop.
|
15
|
+
$ rpw help | Help! You can also ask in Slack.
|
16
|
+
|
17
|
+
Generally, you'll just be doing a lot of $ rpw lesson next
|
18
|
+
|
19
|
+
## Data Size Notice
|
20
|
+
|
21
|
+
Videos in this workshop are generally about 100MB each, which means the entire
|
22
|
+
course is about a 3 to 4GB download.
|
data/lib/rpw/cli.rb
ADDED
@@ -0,0 +1,96 @@
|
|
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."
|
59
|
+
say "Remember to ask on Slack for help if you get stuck or encounter bugs."
|
60
|
+
say "Once you're ready to get going: $ rpw lesson next"
|
61
|
+
end
|
62
|
+
|
63
|
+
no_commands do
|
64
|
+
def print_banner
|
65
|
+
RPW::Bannerlord.print_banner
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def client
|
72
|
+
@client ||= RPW::Client.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def warn_if_already_started
|
76
|
+
return unless client.setup?
|
77
|
+
exit(0) unless yes? "You have already started the workshop. Continuing "\
|
78
|
+
"this command will wipe all of your current progress. Continue? (y/N)"
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_version
|
82
|
+
unless client.latest_version?
|
83
|
+
say "WARNING: You are running an old version of rpw."
|
84
|
+
say "WARNING: Please run `$ gem install rpw`"
|
85
|
+
exit(0)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def check_setup
|
90
|
+
unless client.setup? || current_command_chain == [:start]
|
91
|
+
say "WARNING: You do not have a purchase key set. Run `$ rpw start`"
|
92
|
+
exit(0)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module RPW
|
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
|
+
_____ _ _____ _ _
|
25
|
+
|_ _| |_ ___ | __ |___|_| |___
|
26
|
+
| | | | -_| | -| .'| | |_ -|
|
27
|
+
|_| |_|_|___| |__|__|__,|_|_|___|
|
28
|
+
_____ ___
|
29
|
+
| _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
|
30
|
+
| __| -_| _| _| . | _| | .'| | _| -_|
|
31
|
+
|__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
|
32
|
+
_ _ _ _ _
|
33
|
+
| | | |___ ___| |_ ___| |_ ___ ___
|
34
|
+
| | | | . | _| '_|_ -| | . | . |
|
35
|
+
|_____|___|_| |_,_|___|_|_|___| _|
|
36
|
+
|_|
|
37
|
+
#{reset})
|
38
|
+
end
|
39
|
+
|
40
|
+
def banner
|
41
|
+
%(
|
42
|
+
_____ _ _____ _ _
|
43
|
+
+hmNMMMMMm/` -ymMMNh/ |_ _| |_ ___ | __ |___|_| |___
|
44
|
+
sMMMMMMMMMy +MMMMMMMMy | | | | -_| | -| .'| | |_ -|
|
45
|
+
yMMMMMMMMMMy` yMMMMMMMMN |_| |_|_|___| |__|__|__,|_|_|___|
|
46
|
+
`dMMMMMMMMMMm:-dMMMMMMm: _____ ___
|
47
|
+
`sNMMMMMMMMMMs.:+sso:` | _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
|
48
|
+
:dMMMMMMMMMMm/ | __| -_| _| _| . | _| | .'| | _| -_|
|
49
|
+
:oss+:.sNMMMMMMMMMMy` |__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
|
50
|
+
/mMMMMMMd-:mMMMMMMMMMMd. _ _ _ _ _
|
51
|
+
NMMMMMMMMy `hMMMMMMMMMMh | | | |___ ___| |_ ___| |_ ___ ___
|
52
|
+
yMMMMMMMM+ `dMMMMMMMMMy | | | | . | _| '_|_ -| | . | . |
|
53
|
+
/hNMMmy- `/mMMMMMNmy/ |_____|___|_| |_,_|___|_|_|___| _|
|
54
|
+
|_|
|
55
|
+
#{reset})
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/rpw/cli/key.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module RPW
|
2
|
+
class Key < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "register [EMAIL_ADDRESS]", "Change email registered with Speedshop. One-time only."
|
6
|
+
def register(email)
|
7
|
+
if client.register_email(email)
|
8
|
+
say "Key registered with #{email}. You should receive a Slack invite soon."
|
9
|
+
else
|
10
|
+
say "Key has already been registered. If you believe this is in error,"\
|
11
|
+
" please email nate.berkopec@speedshop.co"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module RPW
|
2
|
+
class Lesson < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "next", "Proceed to the next lesson of the workshop"
|
6
|
+
option :open
|
7
|
+
def next
|
8
|
+
say "Proceeding to next lesson..."
|
9
|
+
content = client.next
|
10
|
+
|
11
|
+
if content.nil?
|
12
|
+
RPW::CLI.new.print_banner
|
13
|
+
say "Congratulations!"
|
14
|
+
say "You have completed the Rails Performance Workshop."
|
15
|
+
exit(0)
|
16
|
+
end
|
17
|
+
|
18
|
+
client.download_and_extract(content)
|
19
|
+
client.increment_current_lesson!(content["position"])
|
20
|
+
display_content(content, options[:open])
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "complete", "Mark the current lesson as complete"
|
24
|
+
def complete
|
25
|
+
say "Marked current lesson as complete"
|
26
|
+
client.complete
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "list", "Show all available workshop lessons"
|
30
|
+
def list
|
31
|
+
say "All available workshop lessons:"
|
32
|
+
client.list.each do |lesson|
|
33
|
+
puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "download [CONTENT | all]", "Download one or all workshop contents"
|
38
|
+
def download(content_pos)
|
39
|
+
to_download = if content_pos.downcase == "all"
|
40
|
+
client.list
|
41
|
+
else
|
42
|
+
[client.show(content_pos)]
|
43
|
+
end
|
44
|
+
to_download.each { |content| client.download_and_extract(content) }
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "show [CONTENT]", "Show any workshop lesson, shows current lesson w/no arguments"
|
48
|
+
option :open
|
49
|
+
def show(content_order = :current)
|
50
|
+
content = client.show(content_order)
|
51
|
+
client.download_and_extract(content)
|
52
|
+
display_content(content, options[:open])
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def display_content(content, open_after)
|
58
|
+
say "Current Lesson: #{content["title"]}"
|
59
|
+
openable = false
|
60
|
+
case content["style"]
|
61
|
+
when "video"
|
62
|
+
location = "video/#{content["s3_key"]}"
|
63
|
+
openable = true
|
64
|
+
when "quiz"
|
65
|
+
Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
|
66
|
+
when "lab"
|
67
|
+
location = "lab/#{content["s3_key"][0..-8]}"
|
68
|
+
when "text"
|
69
|
+
location = "lab/#{content["s3_key"]}"
|
70
|
+
openable = true
|
71
|
+
when "cgrp"
|
72
|
+
say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
|
73
|
+
say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
|
74
|
+
end
|
75
|
+
if location
|
76
|
+
if openable && !open_after
|
77
|
+
say "This file can be opened automatically if you use the --open flag next time."
|
78
|
+
say "e.g. $ rpw lesson next --open"
|
79
|
+
say "Download complete. Open with: $ #{open_command} #{location}"
|
80
|
+
elsif open_after && openable
|
81
|
+
exec "#{open_command} #{location}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
require "rbconfig"
|
87
|
+
def open_command
|
88
|
+
host_os = RbConfig::CONFIG["host_os"]
|
89
|
+
case host_os
|
90
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
91
|
+
"start"
|
92
|
+
when /darwin|mac os/
|
93
|
+
"open"
|
94
|
+
else
|
95
|
+
"xdg-open"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RPW
|
2
|
+
class Progress < SubCommandBase
|
3
|
+
class_before :exit_with_no_key
|
4
|
+
|
5
|
+
desc "set [LESSON]", "Set current lesson to a particular lesson"
|
6
|
+
def set(lesson)
|
7
|
+
client.set_progress(lesson)
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "reset", "Erase all progress and start over"
|
11
|
+
def reset
|
12
|
+
yes? "Are you sure you want to reset your progress? (Y/N)"
|
13
|
+
client.reset_progress
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "show", "Show current workshop progress"
|
17
|
+
def show
|
18
|
+
data = client.progress
|
19
|
+
say "The Rails Performance Workshop"
|
20
|
+
say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
|
21
|
+
say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
|
22
|
+
say "Progress by Section (X == completed, O == current):"
|
23
|
+
data[:sections].each do |section|
|
24
|
+
say "#{section[:title]}: #{section[:progress]}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
default_task :show
|
31
|
+
end
|
32
|
+
end
|
data/lib/rpw/cli/quiz.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "thor"
|
3
|
+
|
4
|
+
module RPW
|
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 = ask("Your answer?")
|
18
|
+
answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer.upcase)
|
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,30 @@
|
|
1
|
+
module RPW
|
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 ||= RPW::Client.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def exit_with_no_key
|
18
|
+
unless client.setup?
|
19
|
+
say "You have not yet set up the client. Run $ rpw start"
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
unless client.directories_ready?
|
23
|
+
say "You are not in your workshop scratch directory, or you have not yet"
|
24
|
+
say "set up the client. Change directory or run $ rpw start"
|
25
|
+
exit(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/rpw/client.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "rpw/cli/quiz"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class Client
|
6
|
+
RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
|
7
|
+
attr_reader :gateway
|
8
|
+
|
9
|
+
def initialize(gateway = nil)
|
10
|
+
@gateway = gateway || Gateway.new(RPW_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
|
+
contents = gateway.list_content
|
25
|
+
return contents.first unless client_data["completed"]
|
26
|
+
contents.delete_if { |c| client_data["completed"].include? c["position"] }
|
27
|
+
contents.sort_by { |c| c["position"] }[1] # 0 would be the current lesson
|
28
|
+
end
|
29
|
+
|
30
|
+
def list
|
31
|
+
gateway.list_content
|
32
|
+
end
|
33
|
+
|
34
|
+
def show(content_pos)
|
35
|
+
content_pos = client_data["current_lesson"] if content_pos == :current
|
36
|
+
gateway.get_content_by_position(content_pos)
|
37
|
+
end
|
38
|
+
|
39
|
+
def directory_setup(home_dir_ok = true)
|
40
|
+
["video", "quiz", "lab", "text", "cgrp"].each do |path|
|
41
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
if home_dir_ok
|
45
|
+
ClientData.create_in_home!
|
46
|
+
else
|
47
|
+
ClientData.create_in_pwd!
|
48
|
+
end
|
49
|
+
|
50
|
+
unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
|
51
|
+
File.open(".gitignore", "a") do |f|
|
52
|
+
f.puts "\n"
|
53
|
+
f.puts ".rpw_info\n"
|
54
|
+
f.puts "video\n"
|
55
|
+
f.puts "quiz\n"
|
56
|
+
f.puts "lab\n"
|
57
|
+
f.puts "text\n"
|
58
|
+
f.puts "cgrp\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
File.open("README.md", "w+") do |f|
|
63
|
+
f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def increment_current_lesson!(position)
|
68
|
+
mark_current_lesson_as_completed
|
69
|
+
client_data["current_lesson"] = position
|
70
|
+
end
|
71
|
+
|
72
|
+
def progress
|
73
|
+
contents = gateway.list_content
|
74
|
+
completed_lessons = client_data["completed"] || []
|
75
|
+
{
|
76
|
+
completed: completed_lessons.size,
|
77
|
+
total: contents.size,
|
78
|
+
current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
|
79
|
+
sections: chart_section_progress(contents, completed_lessons)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_progress(lesson)
|
84
|
+
client_data["current_lesson"] = lesson.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset_progress
|
88
|
+
client_data["current_lesson"] = 0
|
89
|
+
client_data["completed"] = []
|
90
|
+
end
|
91
|
+
|
92
|
+
def latest_version?
|
93
|
+
return true unless ClientData.exists?
|
94
|
+
|
95
|
+
if client_data["last_version_check"]
|
96
|
+
return true if client_data["last_version_check"] >= Time.now - (60 * 60 * 24)
|
97
|
+
return false if client_data["last_version_check"] == false
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
latest = gateway.latest_version?
|
102
|
+
rescue
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
|
106
|
+
client_data["last_version_check"] = if latest
|
107
|
+
Time.now
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def setup?
|
114
|
+
return false unless ClientData.exists?
|
115
|
+
client_data["key"]
|
116
|
+
end
|
117
|
+
|
118
|
+
def directories_ready?
|
119
|
+
["video", "quiz", "lab", "text", "cgrp"].all? do |path|
|
120
|
+
File.directory?(path)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def download_and_extract(content)
|
125
|
+
location = content["style"] + "/" + content["s3_key"]
|
126
|
+
unless File.exist?(location)
|
127
|
+
gateway.download_content(content, folder: content["style"])
|
128
|
+
extract_content(content) if content["s3_key"].end_with?(".tar.gz")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def mark_current_lesson_as_completed
|
135
|
+
reset_progress unless client_data["current_lesson"] && client_data["completed"]
|
136
|
+
client_data["completed"] ||= []
|
137
|
+
client_data["completed"] += [client_data["current_lesson"] || 0]
|
138
|
+
end
|
139
|
+
|
140
|
+
def chart_section_progress(contents, completed)
|
141
|
+
contents.group_by { |c| c["position"] / 100 }
|
142
|
+
.each_with_object([]) do |(_, c), memo|
|
143
|
+
completed_str = c.map { |l|
|
144
|
+
if l["position"] == client_data["current_lesson"]
|
145
|
+
"O"
|
146
|
+
elsif completed.include?(l["position"])
|
147
|
+
"X"
|
148
|
+
else
|
149
|
+
"."
|
150
|
+
end
|
151
|
+
}.join
|
152
|
+
memo << {
|
153
|
+
title: c[0]["title"],
|
154
|
+
progress: completed_str
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def client_data
|
160
|
+
@client_data ||= ClientData.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def extract_content(content)
|
164
|
+
folder = content["style"]
|
165
|
+
`tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class ClientData
|
6
|
+
DOTFILE_NAME = ".rpw_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 RPW 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("~/.rpw/"))
|
34
|
+
FileUtils.mkdir(File.expand_path("~/.rpw/"))
|
35
|
+
end
|
36
|
+
|
37
|
+
FileUtils.touch(File.expand_path("~/.rpw/" + 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("~/.rpw/" + 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
|
data/lib/rpw/gateway.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "excon"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module RPW
|
5
|
+
class Gateway
|
6
|
+
attr_accessor :domain
|
7
|
+
|
8
|
+
def initialize(domain, key)
|
9
|
+
@domain = domain
|
10
|
+
@key = key
|
11
|
+
end
|
12
|
+
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
def authenticate_key(key)
|
16
|
+
Excon.get(domain + "/license", user: key).status == 200
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_content_by_position(position)
|
20
|
+
response = Excon.get(domain + "/contents/positional?position=#{position}", user: @key)
|
21
|
+
if response.status == 200
|
22
|
+
JSON.parse(response.body)
|
23
|
+
else
|
24
|
+
puts response.inspect
|
25
|
+
raise Error, "There was a problem fetching this content."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def list_content
|
30
|
+
response = Excon.get(domain + "/contents", user: @key)
|
31
|
+
if response.status == 200
|
32
|
+
JSON.parse(response.body)
|
33
|
+
else
|
34
|
+
puts response.inspect
|
35
|
+
raise Error, "There was a problem fetching this content."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def download_content(content, folder:)
|
40
|
+
puts "Downloading #{content["title"]}..."
|
41
|
+
downloaded_file = File.open("#{folder}/#{content["s3_key"]}.partial", "w")
|
42
|
+
streamer = lambda do |chunk, remaining_bytes, total_bytes|
|
43
|
+
downloaded_file.write(chunk)
|
44
|
+
print 13.chr
|
45
|
+
print "Remaining: #{(remaining_bytes.to_f / total_bytes * 100).round(2).to_s.rjust(8)}%"
|
46
|
+
end
|
47
|
+
Excon.get(content["url"], response_block: streamer)
|
48
|
+
downloaded_file.close
|
49
|
+
print "\n"
|
50
|
+
File.rename(downloaded_file, "#{folder}/#{content["s3_key"]}")
|
51
|
+
end
|
52
|
+
|
53
|
+
def latest_version?
|
54
|
+
resp = Excon.get("https://rubygems.org/api/v1/gems/rpw.json")
|
55
|
+
data = JSON.parse resp.body
|
56
|
+
Gem::Version.new(RPW::VERSION) >= Gem::Version.new(data["version"])
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_email(email)
|
60
|
+
Excon.put(domain + "/license?email=#{email}&key=#{@key}").status == 200
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/rpw/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module RPW
|
2
|
-
VERSION = "0.0.
|
3
|
-
end
|
1
|
+
module RPW
|
2
|
+
VERSION = "0.0.6"
|
3
|
+
end
|
data/rpw.gemspec
CHANGED
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.
|
4
|
+
version: 0.0.6
|
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-
|
11
|
+
date: 2020-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -25,7 +25,21 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: thor-hollaback
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: excon
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
@@ -46,6 +60,7 @@ executables:
|
|
46
60
|
extensions: []
|
47
61
|
extra_rdoc_files: []
|
48
62
|
files:
|
63
|
+
- ".github/workflows/test.yml"
|
49
64
|
- ".gitignore"
|
50
65
|
- ".ruby_version"
|
51
66
|
- CODE_OF_CONDUCT.md
|
@@ -57,6 +72,17 @@ files:
|
|
57
72
|
- Rakefile
|
58
73
|
- exe/rpw
|
59
74
|
- lib/rpw.rb
|
75
|
+
- lib/rpw/README.md
|
76
|
+
- lib/rpw/cli.rb
|
77
|
+
- lib/rpw/cli/bannerlord.rb
|
78
|
+
- lib/rpw/cli/key.rb
|
79
|
+
- lib/rpw/cli/lesson.rb
|
80
|
+
- lib/rpw/cli/progress.rb
|
81
|
+
- lib/rpw/cli/quiz.rb
|
82
|
+
- lib/rpw/cli/sub_command_base.rb
|
83
|
+
- lib/rpw/client.rb
|
84
|
+
- lib/rpw/client_data.rb
|
85
|
+
- lib/rpw/gateway.rb
|
60
86
|
- lib/rpw/version.rb
|
61
87
|
- rpw.gemspec
|
62
88
|
homepage: https://speedshop.co
|