turbot 0.0.2
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 +15 -0
- data/README.md +36 -0
- data/bin/turbot +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/turbot/auth.rb +315 -0
- data/lib/turbot/cli.rb +38 -0
- data/lib/turbot/client/cisaurus.rb +25 -0
- data/lib/turbot/client/pgbackups.rb +113 -0
- data/lib/turbot/client/rendezvous.rb +111 -0
- data/lib/turbot/client/ssl_endpoint.rb +25 -0
- data/lib/turbot/client/turbot_postgresql.rb +148 -0
- data/lib/turbot/client.rb +757 -0
- data/lib/turbot/command/auth.rb +85 -0
- data/lib/turbot/command/base.rb +192 -0
- data/lib/turbot/command/bots.rb +326 -0
- data/lib/turbot/command/config.rb +123 -0
- data/lib/turbot/command/help.rb +179 -0
- data/lib/turbot/command/keys.rb +115 -0
- data/lib/turbot/command/logs.rb +34 -0
- data/lib/turbot/command/ssl.rb +43 -0
- data/lib/turbot/command/status.rb +51 -0
- data/lib/turbot/command/update.rb +47 -0
- data/lib/turbot/command/version.rb +23 -0
- data/lib/turbot/command.rb +304 -0
- data/lib/turbot/deprecated/help.rb +38 -0
- data/lib/turbot/deprecated.rb +5 -0
- data/lib/turbot/distribution.rb +9 -0
- data/lib/turbot/errors.rb +28 -0
- data/lib/turbot/excon.rb +11 -0
- data/lib/turbot/helpers/log_displayer.rb +70 -0
- data/lib/turbot/helpers/pg_dump_restore.rb +115 -0
- data/lib/turbot/helpers/turbot_postgresql.rb +213 -0
- data/lib/turbot/helpers.rb +521 -0
- data/lib/turbot/plugin.rb +165 -0
- data/lib/turbot/updater.rb +171 -0
- data/lib/turbot/version.rb +3 -0
- data/lib/turbot.rb +19 -0
- data/lib/vendor/turbot/okjson.rb +598 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/helper/pg_dump_restore_spec.rb +67 -0
- data/spec/schemas/dummy_schema.json +12 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +220 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/dummy_api.rb +120 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- data/spec/support/organizations_mock_helper.rb +11 -0
- data/spec/turbot/auth_spec.rb +214 -0
- data/spec/turbot/client/pgbackups_spec.rb +43 -0
- data/spec/turbot/client/rendezvous_spec.rb +62 -0
- data/spec/turbot/client/ssl_endpoint_spec.rb +48 -0
- data/spec/turbot/client/turbot_postgresql_spec.rb +71 -0
- data/spec/turbot/client_spec.rb +548 -0
- data/spec/turbot/command/auth_spec.rb +38 -0
- data/spec/turbot/command/base_spec.rb +66 -0
- data/spec/turbot/command/bots_spec.rb +54 -0
- data/spec/turbot/command/config_spec.rb +143 -0
- data/spec/turbot/command/help_spec.rb +90 -0
- data/spec/turbot/command/keys_spec.rb +117 -0
- data/spec/turbot/command/logs_spec.rb +60 -0
- data/spec/turbot/command/status_spec.rb +48 -0
- data/spec/turbot/command/version_spec.rb +16 -0
- data/spec/turbot/command_spec.rb +131 -0
- data/spec/turbot/helpers/turbot_postgresql_spec.rb +181 -0
- data/spec/turbot/helpers_spec.rb +48 -0
- data/spec/turbot/plugin_spec.rb +172 -0
- data/spec/turbot/updater_spec.rb +44 -0
- data/templates/manifest.json +7 -0
- data/templates/scraper.py +5 -0
- data/templates/scraper.rb +6 -0
- metadata +199 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
require "turbot/command/base"
|
2
|
+
|
3
|
+
# authentication (login, logout)
|
4
|
+
#
|
5
|
+
class Turbot::Command::Auth < Turbot::Command::Base
|
6
|
+
|
7
|
+
# auth
|
8
|
+
#
|
9
|
+
# Authenticate, display token and current user
|
10
|
+
def index
|
11
|
+
validate_arguments!
|
12
|
+
|
13
|
+
Turbot::Command::Help.new.send(:help_for_command, current_command)
|
14
|
+
end
|
15
|
+
|
16
|
+
# auth:login
|
17
|
+
#
|
18
|
+
# log in with your turbot credentials
|
19
|
+
#
|
20
|
+
#Example:
|
21
|
+
#
|
22
|
+
# $ turbot auth:login
|
23
|
+
# Enter your Turbot credentials:
|
24
|
+
# Email: email@example.com
|
25
|
+
# Password (typing will be hidden):
|
26
|
+
# Authentication successful.
|
27
|
+
#
|
28
|
+
def login
|
29
|
+
validate_arguments!
|
30
|
+
|
31
|
+
Turbot::Auth.login
|
32
|
+
display "Authentication successful."
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_command "login", "auth:login"
|
36
|
+
|
37
|
+
# auth:logout
|
38
|
+
#
|
39
|
+
# clear local authentication credentials
|
40
|
+
#
|
41
|
+
#Example:
|
42
|
+
#
|
43
|
+
# $ turbot auth:logout
|
44
|
+
# Local credentials cleared.
|
45
|
+
#
|
46
|
+
def logout
|
47
|
+
validate_arguments!
|
48
|
+
|
49
|
+
Turbot::Auth.logout
|
50
|
+
display "Local credentials cleared."
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_command "logout", "auth:logout"
|
54
|
+
|
55
|
+
# auth:token
|
56
|
+
#
|
57
|
+
# display your api token
|
58
|
+
#
|
59
|
+
#Example:
|
60
|
+
#
|
61
|
+
# $ turbot auth:token
|
62
|
+
# ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD
|
63
|
+
#
|
64
|
+
def token
|
65
|
+
validate_arguments!
|
66
|
+
|
67
|
+
display Turbot::Auth.api_key
|
68
|
+
end
|
69
|
+
|
70
|
+
# auth:whoami
|
71
|
+
#
|
72
|
+
# display your turbot email address
|
73
|
+
#
|
74
|
+
#Example:
|
75
|
+
#
|
76
|
+
# $ turbot auth:whoami
|
77
|
+
# email@example.com
|
78
|
+
#
|
79
|
+
def whoami
|
80
|
+
validate_arguments!
|
81
|
+
|
82
|
+
display Turbot::Auth.user
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "turbot/auth"
|
3
|
+
require "turbot/client/rendezvous"
|
4
|
+
require "turbot/command"
|
5
|
+
|
6
|
+
class Turbot::Command::Base
|
7
|
+
include Turbot::Helpers
|
8
|
+
|
9
|
+
def self.namespace
|
10
|
+
self.to_s.split("::").last.downcase
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :args
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def initialize(args=[], options={})
|
17
|
+
@args = args
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def bot
|
22
|
+
@bot ||= if options[:bot].is_a?(String)
|
23
|
+
options[:bot]
|
24
|
+
elsif ENV.has_key?('TURBOT_BOT')
|
25
|
+
ENV['TURBOT_BOT']
|
26
|
+
elsif bot_from_manifest = extract_bot_from_manifest(Dir.pwd)
|
27
|
+
bot_from_manifest
|
28
|
+
else
|
29
|
+
# raise instead of using error command to enable rescuing when bot is optional
|
30
|
+
raise Turbot::Command::CommandFailed.new("No bot specified.\nRun this command from a bot folder containing a `manifest.json`, or specify which bot to use with --bot BOT_ID.") unless options[:ignore_no_bot]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def api
|
35
|
+
Turbot::Auth.api
|
36
|
+
end
|
37
|
+
|
38
|
+
def turbot
|
39
|
+
Turbot::Auth.client
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def self.inherited(klass)
|
45
|
+
unless klass == Turbot::Command::Base
|
46
|
+
help = extract_help_from_caller(caller.first)
|
47
|
+
|
48
|
+
Turbot::Command.register_namespace(
|
49
|
+
:name => klass.namespace,
|
50
|
+
:description => help.first
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.method_added(method)
|
56
|
+
return if self == Turbot::Command::Base
|
57
|
+
return if private_method_defined?(method)
|
58
|
+
return if protected_method_defined?(method)
|
59
|
+
|
60
|
+
help = extract_help_from_caller(caller.first)
|
61
|
+
resolved_method = (method.to_s == "index") ? nil : method.to_s
|
62
|
+
command = [ self.namespace, resolved_method ].compact.join(":")
|
63
|
+
banner = extract_banner(help) || command
|
64
|
+
|
65
|
+
Turbot::Command.register_command(
|
66
|
+
:klass => self,
|
67
|
+
:method => method,
|
68
|
+
:namespace => self.namespace,
|
69
|
+
:command => command,
|
70
|
+
:banner => banner.strip,
|
71
|
+
:help => help.join("\n"),
|
72
|
+
:summary => extract_summary(help),
|
73
|
+
:description => extract_description(help),
|
74
|
+
:options => extract_options(help)
|
75
|
+
)
|
76
|
+
|
77
|
+
alias_command command.gsub(/_/, '-'), command if command =~ /_/
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.alias_command(new, old)
|
81
|
+
raise "no such command: #{old}" unless Turbot::Command.commands[old]
|
82
|
+
Turbot::Command.command_aliases[new] = old
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_bot
|
86
|
+
output_with_bang "Command::Base#extract_bot has been deprecated. Please use Command::Base#bot instead. #{caller.first}"
|
87
|
+
bot
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Parse the caller format and identify the file and line number as identified
|
92
|
+
# in : http://www.ruby-doc.org/core/classes/Kernel.html#M001397. This will
|
93
|
+
# look for a colon followed by a digit as the delimiter. The biggest
|
94
|
+
# complication is windows paths, which have a colon after the drive letter.
|
95
|
+
# This regex will match paths as anything from the beginning to a colon
|
96
|
+
# directly followed by a number (the line number).
|
97
|
+
#
|
98
|
+
# Examples of the caller format :
|
99
|
+
# * c:/Ruby192/lib/.../lib/turbot/command/addons.rb:8:in `<module:Command>'
|
100
|
+
# * c:/Ruby192/lib/.../turbot-2.0.1/lib/turbot/command/pg.rb:96:in `<class:Pg>'
|
101
|
+
# * /Users/ph7/...../xray-1.1/lib/xray/thread_dump_signal_handler.rb:9
|
102
|
+
#
|
103
|
+
def self.extract_help_from_caller(line)
|
104
|
+
# pull out of the caller the information for the file path and line number
|
105
|
+
if line =~ /^(.+?):(\d+)/
|
106
|
+
extract_help($1, $2)
|
107
|
+
else
|
108
|
+
raise("unable to extract help from caller: #{line}")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.extract_help(file, line_number)
|
113
|
+
buffer = []
|
114
|
+
lines = Turbot::Command.files[file]
|
115
|
+
|
116
|
+
(line_number.to_i-2).downto(0) do |i|
|
117
|
+
line = lines[i]
|
118
|
+
case line[0..0]
|
119
|
+
when ""
|
120
|
+
when "#"
|
121
|
+
buffer.unshift(line[1..-1])
|
122
|
+
else
|
123
|
+
break
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
buffer
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.extract_banner(help)
|
131
|
+
help.first
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.extract_summary(help)
|
135
|
+
extract_description(help).split("\n")[2].to_s.split("\n").first
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.extract_description(help)
|
139
|
+
help.reject do |line|
|
140
|
+
line =~ /^\s+-(.+)#(.+)/
|
141
|
+
end.join("\n")
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.extract_options(help)
|
145
|
+
help.select do |line|
|
146
|
+
line =~ /^\s+-(.+)#(.+)/
|
147
|
+
end.inject([]) do |options, line|
|
148
|
+
args = line.split('#', 2).first
|
149
|
+
args = args.split(/,\s*/).map {|arg| arg.strip}.sort.reverse
|
150
|
+
name = args.last.split(' ', 2).first[2..-1]
|
151
|
+
options << { :name => name, :args => args }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def current_command
|
156
|
+
Turbot::Command.current_command
|
157
|
+
end
|
158
|
+
|
159
|
+
def extract_option(key)
|
160
|
+
options[key.dup.gsub('-','_').to_sym]
|
161
|
+
end
|
162
|
+
|
163
|
+
def invalid_arguments
|
164
|
+
Turbot::Command.invalid_arguments
|
165
|
+
end
|
166
|
+
|
167
|
+
def shift_argument
|
168
|
+
Turbot::Command.shift_argument
|
169
|
+
end
|
170
|
+
|
171
|
+
def validate_arguments!
|
172
|
+
Turbot::Command.validate_arguments!
|
173
|
+
end
|
174
|
+
|
175
|
+
def extract_bot_from_manifest(dir)
|
176
|
+
begin
|
177
|
+
config = JSON.load(open("#{dir}/manifest.json").read)
|
178
|
+
config && config["bot_id"]
|
179
|
+
rescue Errno::ENOENT
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def escape(value)
|
184
|
+
turbot.escape(value)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
module Turbot::Command
|
189
|
+
unless const_defined?(:BaseWithApp)
|
190
|
+
BaseWithApp = Base
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
require "turbot/command/base"
|
2
|
+
require 'zip'
|
3
|
+
require 'json-schema'
|
4
|
+
require 'open3'
|
5
|
+
require 'base64'
|
6
|
+
require 'shellwords'
|
7
|
+
|
8
|
+
# manage bots (create, destroy)
|
9
|
+
#
|
10
|
+
class Turbot::Command::Bots < Turbot::Command::Base
|
11
|
+
|
12
|
+
# bots
|
13
|
+
#
|
14
|
+
# list your bots
|
15
|
+
#
|
16
|
+
#Example:
|
17
|
+
#
|
18
|
+
# $ turbot bots
|
19
|
+
# === My Bots
|
20
|
+
# example
|
21
|
+
# example2
|
22
|
+
#
|
23
|
+
def index
|
24
|
+
validate_arguments!
|
25
|
+
bots = api.get_bots
|
26
|
+
unless bots.empty?
|
27
|
+
styled_header("Bots")
|
28
|
+
styled_array(bots.map{|k,data| data['name']})
|
29
|
+
else
|
30
|
+
display("You have no bots.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_command "list", "bots"
|
35
|
+
|
36
|
+
# bots:info
|
37
|
+
#
|
38
|
+
# show detailed bot information
|
39
|
+
#
|
40
|
+
# -s, --shell # output more shell friendly key/value pairs
|
41
|
+
#
|
42
|
+
#Examples:
|
43
|
+
#
|
44
|
+
# $ turbot bots:info
|
45
|
+
# === example
|
46
|
+
# Last run status: OK
|
47
|
+
# Last run ended: 2001/01/01
|
48
|
+
# ...
|
49
|
+
#
|
50
|
+
# $ turbot bots:info --shell
|
51
|
+
# last_run_status: OK
|
52
|
+
# last_run_ended: 2001/01/01
|
53
|
+
# ...
|
54
|
+
#
|
55
|
+
def info
|
56
|
+
validate_arguments!
|
57
|
+
bot_data = api.get_bot(bot)
|
58
|
+
unless options[:shell]
|
59
|
+
styled_header(bot_data["name"])
|
60
|
+
end
|
61
|
+
|
62
|
+
if options[:shell]
|
63
|
+
bot_data.keys.sort_by { |a| a.to_s }.each do |key|
|
64
|
+
hputs("#{key}=#{bot_data[key]}")
|
65
|
+
end
|
66
|
+
else
|
67
|
+
data = {}
|
68
|
+
if bot_data["last_run_status"]
|
69
|
+
data["Last run status"] = bot_data["last_run_status"]
|
70
|
+
end
|
71
|
+
if bot_data["last_run_ended"]
|
72
|
+
data["Last run ended"] = format_date(bot_data["last_run_ended"]) if bot_data["last_run_ended"]
|
73
|
+
end
|
74
|
+
data["Git URL"] = bot_data["git_url"]
|
75
|
+
data["Repo Size"] = format_bytes(bot_data["repo_size"]) if bot_data["repo_size"]
|
76
|
+
styled_hash(data)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_command "info", "bots:info"
|
81
|
+
|
82
|
+
|
83
|
+
# bots:generate --bot name_of_bot
|
84
|
+
#
|
85
|
+
# Generate stub code for a bot in specified language
|
86
|
+
#
|
87
|
+
# -l, --language LANGUAGE # language to generate (currently `ruby` (default) or `python`)
|
88
|
+
|
89
|
+
# $ turbot bots:generate --language=ruby --bot my_amazing_bot
|
90
|
+
# Created new bot template at my_amazing_bot!
|
91
|
+
|
92
|
+
def generate
|
93
|
+
validate_arguments!
|
94
|
+
language = options[:language] || "ruby"
|
95
|
+
puts "Generating #{language} codes..."
|
96
|
+
FileUtils.mkdir(bot)
|
97
|
+
case language
|
98
|
+
when "ruby"
|
99
|
+
scraper = "scraper.rb"
|
100
|
+
when "python"
|
101
|
+
scraper = "scraper.py"
|
102
|
+
end
|
103
|
+
manifest_template = File.expand_path("../../../../templates/manifest.json", __FILE__)
|
104
|
+
scraper_template = File.expand_path("../../../../templates/#{scraper}", __FILE__)
|
105
|
+
manifest = open(manifest_template).read.sub(/{{bot_id}}/, bot)
|
106
|
+
FileUtils.cp(scraper_template, "#{bot}/#{scraper}")
|
107
|
+
open("#{bot}/manifest.json", "w") do |f|
|
108
|
+
f.write(manifest)
|
109
|
+
end
|
110
|
+
puts "Created new bot template at #{bot}!"
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# bots:push
|
115
|
+
#
|
116
|
+
# Push bot code to the turbot server. Must be run from a local bot checkout.
|
117
|
+
#
|
118
|
+
# $ turbot bots:push
|
119
|
+
# Creating example... done
|
120
|
+
|
121
|
+
def push
|
122
|
+
validate_arguments!
|
123
|
+
|
124
|
+
working_dir = Dir.pwd
|
125
|
+
manifest = parsed_manifest(working_dir)
|
126
|
+
#archive_file = File.join(working_dir, 'tmp', "#{manifest['bot_id']}.zip")
|
127
|
+
archive = Tempfile.new(bot)
|
128
|
+
archive_path = "#{archive.path}.zip"
|
129
|
+
|
130
|
+
Zip.continue_on_exists_proc = true
|
131
|
+
Zip::File.open(archive_path, Zip::File::CREATE) do |zipfile|
|
132
|
+
zipfile.add("manifest.json", manifest_path)
|
133
|
+
manifest['files'].each { |f| zipfile.add(f, File.join(working_dir,f)) }
|
134
|
+
end
|
135
|
+
|
136
|
+
File.open(archive_path) do |file|
|
137
|
+
params = {
|
138
|
+
"bot[archive]" => file,
|
139
|
+
"bot[manifest]" => manifest
|
140
|
+
}
|
141
|
+
api.post_bot(params)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
alias_command "push", "bots:push"
|
146
|
+
|
147
|
+
# bots:validate
|
148
|
+
#
|
149
|
+
# Validate bot output against its schema
|
150
|
+
#
|
151
|
+
# $ heroku bots:validate
|
152
|
+
# Validating example... done
|
153
|
+
|
154
|
+
def validate
|
155
|
+
scraper_path = shift_argument || scraper_file(Dir.pwd)
|
156
|
+
validate_arguments!
|
157
|
+
config = parsed_manifest(Dir.pwd)
|
158
|
+
type = config["data_type"]
|
159
|
+
|
160
|
+
schema = get_schema(type)
|
161
|
+
|
162
|
+
if !schema || !File.exists?(schema)
|
163
|
+
error("No schema found for data_type: #{type}")
|
164
|
+
end
|
165
|
+
|
166
|
+
count = 0
|
167
|
+
|
168
|
+
run_scraper_each_line("#{scraper_path} #{bot}") do |line|
|
169
|
+
errors = ""
|
170
|
+
errors = JSON::Validator.fully_validate(
|
171
|
+
schema,
|
172
|
+
line,
|
173
|
+
{:errors_as_objects => true})
|
174
|
+
|
175
|
+
if !errors.empty?
|
176
|
+
error("LINE WITH ERROR: #{line}\n\nERRORS: #{errors}")
|
177
|
+
end
|
178
|
+
count += 1
|
179
|
+
end
|
180
|
+
puts "Validated #{count} records successfully!"
|
181
|
+
end
|
182
|
+
|
183
|
+
# bots:dump
|
184
|
+
#
|
185
|
+
# Execute bot locally (writes to STDOUT)
|
186
|
+
#
|
187
|
+
# $ heroku bots:dump
|
188
|
+
# {'foo': 'bar'}
|
189
|
+
# {'foo2': 'bar2'}
|
190
|
+
|
191
|
+
def dump
|
192
|
+
# This will need to be language-aware, eventually
|
193
|
+
scraper_path = shift_argument || scraper_file(Dir.pwd)
|
194
|
+
validate_arguments!
|
195
|
+
count = 0
|
196
|
+
run_scraper_each_line("#{scraper_path} #{bot}") do |line|
|
197
|
+
puts line
|
198
|
+
count += 1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# bots:single
|
203
|
+
#
|
204
|
+
# Execute bot in same way as OpenCorporates single-record update
|
205
|
+
#
|
206
|
+
# $ heroku bots:single
|
207
|
+
# Enter argument (as JSON object):
|
208
|
+
# {"id": "frob123"}
|
209
|
+
# {"id": "frob123", "stuff": "updated-data-for-this-record"}
|
210
|
+
|
211
|
+
def single
|
212
|
+
# This will need to be language-aware, eventually
|
213
|
+
scraper_path = shift_argument || scraper_file(Dir.pwd)
|
214
|
+
validate_arguments!
|
215
|
+
print 'Arguments (as JSON object, e.g. {"id":"ABC123"}: '
|
216
|
+
arg = ask
|
217
|
+
count = 0
|
218
|
+
run_scraper_each_line("#{scraper_path} #{bot} #{Shellwords.shellescape(arg)}") do |line|
|
219
|
+
raise "Your scraper returned more than one value!" if count > 1
|
220
|
+
puts line
|
221
|
+
count += 1
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
# bots:preview
|
227
|
+
#
|
228
|
+
# Send bot data to Angler for remote previewing / sharing
|
229
|
+
#
|
230
|
+
# Sending example to Angler... done
|
231
|
+
def preview
|
232
|
+
scraper_path = shift_argument || scraper_file(Dir.pwd)
|
233
|
+
validate_arguments!
|
234
|
+
|
235
|
+
bots = api.get_bots
|
236
|
+
raise "You have not pushed your bot" unless bots.include?(bot)
|
237
|
+
|
238
|
+
batch = []
|
239
|
+
count = 0
|
240
|
+
puts "Sending to angler... "
|
241
|
+
result = ""
|
242
|
+
run_scraper_each_line("#{scraper_path} #{bot}") do |line|
|
243
|
+
batch << JSON.parse(line)
|
244
|
+
spinner(count)
|
245
|
+
if count % 20 == 0
|
246
|
+
result = api.send_drafts_to_angler(bot, batch.to_json)
|
247
|
+
batch = []
|
248
|
+
end
|
249
|
+
count += 1
|
250
|
+
end
|
251
|
+
if !batch.empty?
|
252
|
+
result = api.send_drafts_to_angler(bot, batch.to_json)
|
253
|
+
end
|
254
|
+
puts "Sent #{count} records."
|
255
|
+
puts "View your records at #{JSON.parse(result)['url']}"
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def spinner(p)
|
261
|
+
parts = "\|/-" * 2
|
262
|
+
print parts[p % parts.length] + "\r"
|
263
|
+
end
|
264
|
+
|
265
|
+
def run_scraper_each_line(scraper_path, options={})
|
266
|
+
case scraper_path
|
267
|
+
when /scraper.rb /
|
268
|
+
interpreter = "ruby"
|
269
|
+
when /scraper.py /
|
270
|
+
interpreter = "python"
|
271
|
+
else
|
272
|
+
raise "Unsupported file extension at #{scraper_path}"
|
273
|
+
end
|
274
|
+
|
275
|
+
command = "#{interpreter} #{scraper_path}"
|
276
|
+
Open3::popen3(command, options) do |_, stdout, stderr, wait_thread|
|
277
|
+
loop do
|
278
|
+
check_output_with_timeout(stdout)
|
279
|
+
|
280
|
+
begin
|
281
|
+
result = stdout.readline.strip
|
282
|
+
yield result unless result.empty?
|
283
|
+
# add run id and bot name
|
284
|
+
rescue EOFError
|
285
|
+
break
|
286
|
+
end
|
287
|
+
end
|
288
|
+
status = wait_thread.value.exitstatus
|
289
|
+
if status > 0
|
290
|
+
message = "Bot <#{command}> exited with status #{status}: #{stderr.read}"
|
291
|
+
raise RuntimeError.new(message)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def check_output_with_timeout(stdout, initial_interval = 10, timeout = 21600)
|
297
|
+
interval = initial_interval
|
298
|
+
loop do
|
299
|
+
reads, _, _ = IO.select([stdout], [], [], interval)
|
300
|
+
break if !reads.nil?
|
301
|
+
raise "Timeout! - could not read from external bot after #{timeout} seconds" if reads.nil? && interval > timeout
|
302
|
+
interval *= 2
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def parsed_manifest(dir)
|
307
|
+
begin
|
308
|
+
JSON.parse(open(manifest_path).read)
|
309
|
+
rescue Errno::ENOENT
|
310
|
+
raise "This command must be run from a directory including `manifest.json`"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def scraper_file(dir)
|
315
|
+
Dir.glob("scraper*").first
|
316
|
+
end
|
317
|
+
|
318
|
+
def manifest_path
|
319
|
+
File.join(Dir.pwd, 'manifest.json')
|
320
|
+
end
|
321
|
+
|
322
|
+
def get_schema(type)
|
323
|
+
hyphenated_name = type.to_s.gsub("_", "-").gsub(" ", "-")
|
324
|
+
schema = File.expand_path("../../../../schema/schemas/#{hyphenated_name}-schema.json", __FILE__)
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "turbot/command/base"
|
2
|
+
|
3
|
+
# manage bot config vars
|
4
|
+
#
|
5
|
+
class Turbot::Command::Config < Turbot::Command::Base
|
6
|
+
|
7
|
+
# config
|
8
|
+
#
|
9
|
+
# display the config vars for an bot
|
10
|
+
#
|
11
|
+
# -s, --shell # output config vars in shell format
|
12
|
+
#
|
13
|
+
#Examples:
|
14
|
+
#
|
15
|
+
# $ turbot config
|
16
|
+
# A: one
|
17
|
+
# B: two
|
18
|
+
#
|
19
|
+
# $ turbot config --shell
|
20
|
+
# A=one
|
21
|
+
# B=two
|
22
|
+
#
|
23
|
+
def index
|
24
|
+
validate_arguments!
|
25
|
+
|
26
|
+
vars = api.get_config_vars(bot)
|
27
|
+
if vars.empty?
|
28
|
+
display("#{bot} has no config vars.")
|
29
|
+
else
|
30
|
+
vars.each {|key, value| vars[key] = value.to_s.strip}
|
31
|
+
if options[:shell]
|
32
|
+
vars.keys.sort.each do |key|
|
33
|
+
display(%{#{key}=#{vars[key]}})
|
34
|
+
end
|
35
|
+
else
|
36
|
+
styled_header("#{bot} Config Vars")
|
37
|
+
styled_hash(vars)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# config:set KEY1=VALUE1 [KEY2=VALUE2 ...]
|
43
|
+
#
|
44
|
+
# set one or more config vars
|
45
|
+
#
|
46
|
+
#Example:
|
47
|
+
#
|
48
|
+
# $ turbot config:set A=one
|
49
|
+
# Setting config vars and restarting example... done, v123
|
50
|
+
# A: one
|
51
|
+
#
|
52
|
+
# $ turbot config:set A=one B=two
|
53
|
+
# Setting config vars and restarting example... done, v123
|
54
|
+
# A: one
|
55
|
+
# B: two
|
56
|
+
#
|
57
|
+
def set
|
58
|
+
unless args.size > 0 and args.all? { |a| a.include?('=') }
|
59
|
+
error("Usage: turbot config:set KEY1=VALUE1 [KEY2=VALUE2 ...]\nMust specify KEY and VALUE to set.")
|
60
|
+
end
|
61
|
+
|
62
|
+
vars = args.inject({}) do |vars, arg|
|
63
|
+
key, value = arg.split('=', 2)
|
64
|
+
vars[key] = value
|
65
|
+
vars
|
66
|
+
end
|
67
|
+
|
68
|
+
action("Setting config vars and restarting #{bot}") do
|
69
|
+
api.put_config_vars(bot, vars)
|
70
|
+
end
|
71
|
+
|
72
|
+
vars.each {|key, value| vars[key] = value.to_s}
|
73
|
+
styled_hash(vars)
|
74
|
+
end
|
75
|
+
|
76
|
+
alias_command "config:add", "config:set"
|
77
|
+
|
78
|
+
# config:get KEY
|
79
|
+
#
|
80
|
+
# display a config value for an bot
|
81
|
+
#
|
82
|
+
#Examples:
|
83
|
+
#
|
84
|
+
# $ turbot config:get A
|
85
|
+
# one
|
86
|
+
#
|
87
|
+
def get
|
88
|
+
unless key = shift_argument
|
89
|
+
error("Usage: turbot config:get KEY\nMust specify KEY.")
|
90
|
+
end
|
91
|
+
validate_arguments!
|
92
|
+
|
93
|
+
vars = api.get_config_vars(bot)
|
94
|
+
key, value = vars.detect {|k,v| k == key}
|
95
|
+
display(value.to_s)
|
96
|
+
end
|
97
|
+
|
98
|
+
# config:unset KEY1 [KEY2 ...]
|
99
|
+
#
|
100
|
+
# unset one or more config vars
|
101
|
+
#
|
102
|
+
# $ turbot config:unset A
|
103
|
+
# Unsetting A and restarting example... done, v123
|
104
|
+
#
|
105
|
+
# $ turbot config:unset A B
|
106
|
+
# Unsetting A and restarting example... done, v123
|
107
|
+
# Unsetting B and restarting example... done, v124
|
108
|
+
#
|
109
|
+
def unset
|
110
|
+
if args.empty?
|
111
|
+
error("Usage: turbot config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.")
|
112
|
+
end
|
113
|
+
|
114
|
+
args.each do |key|
|
115
|
+
action("Unsetting #{key} and restarting #{bot}") do
|
116
|
+
api.delete_config_var(bot, key)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
alias_command "config:remove", "config:unset"
|
122
|
+
|
123
|
+
end
|