transcriptic 0.1.0

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/bin/transcriptic ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # resolve bin path, ignoring symlinks
4
+ require "pathname"
5
+ bin_file = Pathname.new(__FILE__).realpath
6
+
7
+ # add self to libpath
8
+ $:.unshift File.expand_path("../../lib", bin_file)
9
+
10
+ # start up the CLI
11
+ require "transcriptic/cli"
12
+ Transcriptic::CLI.start(*ARGV)
@@ -0,0 +1,3 @@
1
+ module Transcriptic; end
2
+
3
+ require "transcriptic/client"
@@ -0,0 +1,73 @@
1
+ require "transcriptic/api/errors"
2
+ require "transcriptic/api/version"
3
+
4
+ require "transcriptic/api/sequences"
5
+
6
+ srand
7
+
8
+ module Transcriptic
9
+ class API
10
+
11
+ def initialize(options={})
12
+ @api_key = options.delete(:api_key) || ENV['TRANSCRIPTIC_API_KEY']
13
+ user_pass = ":#{@api_key}"
14
+ options = {
15
+ :headers => {},
16
+ :host => 'www.transcriptic.com',
17
+ :scheme => 'https'
18
+ }.merge(options)
19
+ options[:headers] = {
20
+ 'Accept' => 'application/json',
21
+ 'Accept-Encoding' => 'gzip',
22
+ #'Accept-Language' => 'en-US, en;q=0.8',
23
+ 'Authorization' => "Basic #{Base64.encode64(user_pass).gsub("\n", '')}",
24
+ 'User-Agent' => "transcriptic-rb/#{Transcriptic::API::VERSION}",
25
+ 'X-Transcriptic-API-Version' => '3',
26
+ 'X-Ruby-Version' => RUBY_VERSION,
27
+ 'X-Ruby-Platform' => RUBY_PLATFORM
28
+ }.merge(options[:headers])
29
+ @connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options)
30
+ end
31
+
32
+ def request(params, &block)
33
+ begin
34
+ response = @connection.request(params, &block)
35
+ rescue Excon::Errors::SocketError => error
36
+ raise error
37
+ rescue Excon::Errors::Error => error
38
+ klass = case error.response.status
39
+ when 401 then Transcriptic::API::Errors::Unauthorized
40
+ when 402 then Transcriptic::API::Errors::VerificationRequired
41
+ when 403 then Transcriptic::API::Errors::Forbidden
42
+ when 404 then Transcriptic::API::Errors::NotFound
43
+ when 408 then Transcriptic::API::Errors::Timeout
44
+ when 422 then Transcriptic::API::Errors::RequestFailed
45
+ when 423 then Transcriptic::API::Errors::Locked
46
+ when /50./ then Transcriptic::API::Errors::RequestFailed
47
+ else Transcriptic::API::Errors::ErrorWithResponse
48
+ end
49
+
50
+ reerror = klass.new(error.message, error.response)
51
+ reerror.set_backtrace(error.backtrace)
52
+ raise(reerror)
53
+ end
54
+
55
+ if response.body && !response.body.empty?
56
+ if response.headers['Content-Encoding'] == 'gzip'
57
+ response.body = Zlib::GzipReader.new(StringIO.new(response.body)).read
58
+ end
59
+ begin
60
+ response.body = Transcriptic::API::OkJson.decode(response.body)
61
+ rescue
62
+ # leave non-JSON body as is
63
+ end
64
+ end
65
+
66
+ # reset (non-persistent) connection
67
+ @connection.reset
68
+
69
+ response
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ module Transcriptic
2
+ class API
3
+ module Errors
4
+ class Error < StandardError; end
5
+
6
+ class ErrorWithResponse < Error
7
+ attr_reader :response
8
+
9
+ def initialize(message, response)
10
+ super message
11
+ @response = response
12
+ end
13
+ end
14
+
15
+ class Unauthorized < ErrorWithResponse; end
16
+ class VerificationRequired < ErrorWithResponse; end
17
+ class Forbidden < ErrorWithResponse; end
18
+ class NotFound < ErrorWithResponse; end
19
+ class Timeout < ErrorWithResponse; end
20
+ class Locked < ErrorWithResponse; end
21
+ class RequestFailed < ErrorWithResponse; end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Transcriptic
2
+ class API
3
+
4
+ # DELETE /:organization/sequences/:sequence
5
+ def delete_sequence(organization, sequence)
6
+ request(
7
+ :expects => 200,
8
+ :method => :delete,
9
+ :path => "/#{organization}/sequences/#{sequence}"
10
+ )
11
+ end
12
+
13
+ # GET /:organization/sequences
14
+ def get_sequences(organization)
15
+ request(
16
+ :expects => 200,
17
+ :method => :get,
18
+ :path => "/#{organization}/sequences"
19
+ )
20
+ end
21
+
22
+ # POST /:organization/sequences
23
+ def post_domain(organization, sequence)
24
+ request(
25
+ :expects => 201,
26
+ :method => :post,
27
+ :path => "/#{organization}/sequences",
28
+ :query => {'sequence' => sequence}
29
+ )
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Transcriptic
2
+ class API
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,223 @@
1
+ require "transcriptic"
2
+ require "transcriptic/client"
3
+ require "transcriptic/helpers"
4
+
5
+ class Transcriptic::Auth
6
+ class << self
7
+ attr_accessor :credentials
8
+
9
+ def client
10
+ @client ||= begin
11
+ client = Transcriptic::Client.new(user, password, host)
12
+ client.on_warning { |msg| self.display("\n#{msg}\n\n") }
13
+ client
14
+ end
15
+ end
16
+
17
+ def login
18
+ delete_credentials
19
+ get_credentials
20
+ end
21
+
22
+ def logout
23
+ delete_credentials
24
+ end
25
+
26
+ def clear
27
+ @credentials = nil
28
+ @client = nil
29
+ end
30
+
31
+ include Transcriptic::Helpers
32
+
33
+ # just a stub; will raise if not authenticated
34
+ def check
35
+ client.list
36
+ end
37
+
38
+ def default_host
39
+ "transcriptic.com"
40
+ end
41
+
42
+ def host
43
+ ENV['TRANSCRIPTIC_HOST'] || default_host
44
+ end
45
+
46
+ def reauthorize
47
+ @credentials = ask_for_and_save_credentials
48
+ end
49
+
50
+ def user # :nodoc:
51
+ get_credentials
52
+ @credentials[0]
53
+ end
54
+
55
+ def password # :nodoc:
56
+ get_credentials
57
+ @credentials[1]
58
+ end
59
+
60
+ def credentials_file
61
+ if host == default_host
62
+ "#{home_directory}/.transcriptic/credentials"
63
+ else
64
+ "#{home_directory}/.transcriptic/credentials.#{host}"
65
+ end
66
+ end
67
+
68
+ def get_credentials # :nodoc:
69
+ return if @credentials
70
+ unless @credentials = read_credentials
71
+ ask_for_and_save_credentials
72
+ end
73
+ @credentials
74
+ end
75
+
76
+ def read_credentials
77
+ File.exists?(credentials_file) and File.read(credentials_file).split("\n")
78
+ end
79
+
80
+ def echo_off
81
+ system "stty -echo"
82
+ end
83
+
84
+ def echo_on
85
+ system "stty echo"
86
+ end
87
+
88
+ def ask_for_credentials
89
+ puts "Enter your Transcriptic credentials."
90
+
91
+ print "Email: "
92
+ user = ask
93
+
94
+ print "Password: "
95
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
96
+ api_key = Transcriptic::Client.auth(user, password, host)['api_key']
97
+
98
+ [user, api_key]
99
+ end
100
+
101
+ def ask_for_password_on_windows
102
+ require "Win32API"
103
+ char = nil
104
+ password = ''
105
+
106
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
107
+ break if char == 10 || char == 13 # received carriage return or newline
108
+ if char == 127 || char == 8 # backspace and delete
109
+ password.slice!(-1, 1)
110
+ else
111
+ # windows might throw a -1 at us so make sure to handle RangeError
112
+ (password << char.chr) rescue RangeError
113
+ end
114
+ end
115
+ puts
116
+ return password
117
+ end
118
+
119
+ def ask_for_password
120
+ echo_off
121
+ password = ask
122
+ puts
123
+ echo_on
124
+ return password
125
+ end
126
+
127
+ def ask_for_and_save_credentials
128
+ begin
129
+ @credentials = ask_for_credentials
130
+ write_credentials
131
+ check
132
+ rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
133
+ delete_credentials
134
+ clear
135
+ display "Authentication failed."
136
+ retry if retry_login?
137
+ exit 1
138
+ rescue Exception => e
139
+ delete_credentials
140
+ raise e
141
+ end
142
+ check_for_associated_ssh_key unless Transcriptic::Command.current_command == "keys:add"
143
+ end
144
+
145
+ def check_for_associated_ssh_key
146
+ return unless client.keys.length.zero?
147
+ associate_or_generate_ssh_key
148
+ end
149
+
150
+ def associate_or_generate_ssh_key
151
+ public_keys = available_ssh_public_keys.sort
152
+
153
+ case public_keys.length
154
+ when 0 then
155
+ display "Could not find an existing public key."
156
+ display "Would you like to generate one? [Yn] ", false
157
+ unless ask.strip.downcase == "n"
158
+ display "Generating new SSH public key."
159
+ generate_ssh_key("id_rsa")
160
+ associate_key("#{home_directory}/.ssh/id_rsa.pub")
161
+ end
162
+ when 1 then
163
+ display "Found existing public key: #{public_keys.first}"
164
+ associate_key(public_keys.first)
165
+ else
166
+ display "Found the following SSH public keys:"
167
+ public_keys.each_with_index do |key, index|
168
+ display "#{index+1}) #{File.basename(key)}"
169
+ end
170
+ display "Which would you like to use with your Transcriptic account? ", false
171
+ chosen = public_keys[ask.to_i-1] rescue error("Invalid choice")
172
+ associate_key(chosen)
173
+ end
174
+ end
175
+
176
+ def generate_ssh_key(keyfile)
177
+ ssh_dir = File.join(home_directory, ".ssh")
178
+ unless File.exists?(ssh_dir)
179
+ FileUtils.mkdir_p ssh_dir
180
+ File.chmod(0700, ssh_dir)
181
+ end
182
+ `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
183
+ end
184
+
185
+ def associate_key(key)
186
+ display "Uploading ssh public key #{key}"
187
+ client.add_key(File.read(key))
188
+ end
189
+
190
+ def available_ssh_public_keys
191
+ keys = [
192
+ "#{home_directory}/.ssh/id_rsa.pub",
193
+ "#{home_directory}/.ssh/id_dsa.pub"
194
+ ]
195
+ keys.concat(Dir["#{home_directory}/.ssh/*.pub"])
196
+ keys.select { |d| File.exists?(d) }.uniq
197
+ end
198
+
199
+ def retry_login?
200
+ @login_attempts ||= 0
201
+ @login_attempts += 1
202
+ @login_attempts < 3
203
+ end
204
+
205
+ def write_credentials
206
+ FileUtils.mkdir_p(File.dirname(credentials_file))
207
+ f = File.open(credentials_file, 'w')
208
+ f.puts self.credentials
209
+ f.close
210
+ set_credentials_permissions
211
+ end
212
+
213
+ def set_credentials_permissions
214
+ FileUtils.chmod 0700, File.dirname(credentials_file)
215
+ FileUtils.chmod 0600, credentials_file
216
+ end
217
+
218
+ def delete_credentials
219
+ FileUtils.rm_f(credentials_file)
220
+ clear
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,12 @@
1
+ require "transcriptic"
2
+ require "transcriptic/command"
3
+
4
+ class Transcriptic::CLI
5
+
6
+ def self.start(*args)
7
+ command = args.shift.strip rescue "help"
8
+ Transcriptic::Command.load
9
+ Transcriptic::Command.run(command, args)
10
+ end
11
+
12
+ end
@@ -0,0 +1,332 @@
1
+ require 'rexml/document'
2
+ require 'rest-client'
3
+ require 'uri'
4
+ require 'time'
5
+ require 'transcriptic/auth'
6
+ require 'transcriptic/helpers'
7
+ require 'transcriptic/version'
8
+
9
+ # A Ruby class to call the Transcriptic REST API. You might use this if you want to
10
+ # manage your Transcriptic apps from within a Ruby program, such as Capistrano.
11
+ #
12
+ # Example:
13
+ #
14
+ # require 'transcriptic'
15
+ # transcriptic = Transcriptic::Client.new('me@example.com', 'mypass')
16
+ # transcriptic.create('myapp')
17
+ #
18
+ class Transcriptic::Client
19
+
20
+ include Transcriptic::Helpers
21
+ extend Transcriptic::Helpers
22
+
23
+ def self.version
24
+ Transcriptic::VERSION
25
+ end
26
+
27
+ def self.gem_version_string
28
+ "transcriptic-gem/#{version}"
29
+ end
30
+
31
+ attr_accessor :host, :user, :password
32
+
33
+ def self.auth(user, password, host = Transcriptic::Auth.default_host)
34
+ client = new(user, password, host)
35
+ json_decode client.post('/users/sign_in', { 'user[email]' => user, 'user[password]' => password }, :accept => 'json').to_s
36
+ end
37
+
38
+ def initialize(user, password, host=Transcriptic::Auth.default_host)
39
+ @user = user
40
+ @password = password
41
+ @host = host
42
+ end
43
+
44
+ # Show info such as mode, custom domain, and collaborators on an app.
45
+ def status(run_id)
46
+ doc = xml(get("/apps/#{name_or_domain}").to_s)
47
+ attrs = hash_from_xml_doc(doc)[:app]
48
+ attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
49
+ attrs.merge!(:addons => installed_addons(attrs[:name]))
50
+ end
51
+
52
+ # Add an ssh public key to the current user.
53
+ def add_key(key)
54
+ post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' }).to_s
55
+ end
56
+
57
+ # Remove an existing ssh public key from the current user.
58
+ def remove_key(key)
59
+ delete("/user/keys/#{escape(key)}").to_s
60
+ end
61
+
62
+ # Clear all keys on the current user.
63
+ def remove_all_keys
64
+ delete("/user/keys").to_s
65
+ end
66
+
67
+ # Get a list of stacks available to the app, with the current one marked.
68
+ def list_stacks(app_name, options={})
69
+ include_deprecated = options.delete(:include_deprecated) || false
70
+
71
+ json_decode resource("/apps/#{app_name}/stack").get(
72
+ :params => { :include_deprecated => include_deprecated },
73
+ :accept => 'application/json'
74
+ ).to_s
75
+ end
76
+
77
+ class AppCrashed < RuntimeError; end
78
+
79
+ # Show a list of projects which you are a collaborator on.
80
+ def list
81
+ doc = xml(get('/apps').to_s)
82
+ doc.elements.to_a("//apps/app").map do |a|
83
+ name = a.elements.to_a("name").first
84
+ owner = a.elements.to_a("owner").first
85
+ [name.text, owner.text]
86
+ end
87
+ end
88
+
89
+ # Show info such as mode, custom domain, and collaborators on an app.
90
+ def info(name_or_domain)
91
+ name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
92
+ end
93
+
94
+ def organizations
95
+ json_decode resource("/organizations").get(:accept => 'application/json').to_s
96
+ end
97
+
98
+ # Add an ssh public key to the current user.
99
+ def add_sequence(organization, sequence)
100
+ post("/#{organization}/sequences", sequence, { 'Content-Type' => 'text/ssh-authkey' }).to_s
101
+ end
102
+
103
+ # Remove an existing ssh public key from the current user.
104
+ def remove_sequence(organization, sequence)
105
+ delete("/#{organization}/sequences/#{escape(key)}").to_s
106
+ end
107
+
108
+ class Protocol
109
+ attr_accessor :attached, :upid
110
+
111
+ def initialize(client, protocol, upid=nil)
112
+ @client = client
113
+ @protocol = protocol
114
+ @upid = upid
115
+ end
116
+
117
+ # launch the protocol
118
+ def launch(command, attached=false)
119
+ @attached = attached
120
+ @response = @client.post(
121
+ "/runs/#{@app}/services",
122
+ command,
123
+ :content_type => 'text/plain'
124
+ )
125
+ @next_chunk = @response.to_s
126
+ @interval = 0
127
+ self
128
+ rescue RestClient::RequestFailed => e
129
+ raise AppCrashed, e.http_body if e.http_code == 502
130
+ raise
131
+ end
132
+
133
+ # Does the service have any remaining output?
134
+ def end_of_stream?
135
+ @next_chunk.nil?
136
+ end
137
+
138
+ # Read the next chunk of output.
139
+ def read
140
+ chunk = @client.get(@next_chunk)
141
+ if chunk.headers[:location].nil? && chunk.code != 204
142
+ # no more chunks
143
+ @next_chunk = nil
144
+ chunk.to_s
145
+ elsif chunk.to_s == ''
146
+ # assume no content and back off
147
+ @interval = 2
148
+ ''
149
+ elsif location = chunk.headers[:location]
150
+ # some data read and next chunk available
151
+ @next_chunk = location
152
+ @interval = 0
153
+ chunk.to_s
154
+ end
155
+ end
156
+
157
+ # Iterate over all output chunks until EOF is reached.
158
+ def each
159
+ until end_of_stream?
160
+ sleep(@interval)
161
+ output = read
162
+ yield output unless output.empty?
163
+ end
164
+ end
165
+
166
+ # All output as a string
167
+ def to_s
168
+ buf = []
169
+ each { |part| buf << part }
170
+ buf.join
171
+ end
172
+ end
173
+
174
+ def status(run_id)
175
+ json_decode resource("/runs/#{run_id}/status").get(:accept => 'application/json').to_s
176
+ end
177
+
178
+ # Get a Protocol instance to execute commands against.
179
+ def protocol(run_id, upid)
180
+ Protocol.new(self, run_id, upid)
181
+ end
182
+
183
+ def read_logs(run_id, options=[])
184
+ query = "&" + options.join("&") unless options.empty?
185
+ url = get("/runs/#{run_id}/logs?#{query}").to_s
186
+ uri = URI.parse(url);
187
+ http = Net::HTTP.new(uri.host, uri.port)
188
+
189
+ if uri.scheme == 'https'
190
+ http.use_ssl = true
191
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
192
+ end
193
+
194
+ http.read_timeout = 60 * 60 * 24
195
+
196
+ begin
197
+ http.start do
198
+ http.request_get(uri.path + (uri.query ? "?" + uri.query : "")) do |request|
199
+ request.read_body do |chunk|
200
+ yield chunk
201
+ end
202
+ end
203
+ end
204
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
205
+ abort(" ! Could not connect to logging service")
206
+ rescue Timeout::Error, EOFError
207
+ abort("\n ! Request timed out")
208
+ end
209
+ end
210
+
211
+ def on_warning(&blk)
212
+ @warning_callback = blk
213
+ end
214
+
215
+ ##################
216
+
217
+ def resource(uri, options={})
218
+ RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
219
+ resource = RestClient::Resource.new(realize_full_uri(uri), options.merge(:user => user, :password => password))
220
+ resource
221
+ end
222
+
223
+ def get(uri, extra_headers={}) # :nodoc:
224
+ process(:get, uri, extra_headers)
225
+ end
226
+
227
+ def post(uri, payload="", extra_headers={}) # :nodoc:
228
+ process(:post, uri, extra_headers, payload)
229
+ end
230
+
231
+ def put(uri, payload, extra_headers={}) # :nodoc:
232
+ process(:put, uri, extra_headers, payload)
233
+ end
234
+
235
+ def delete(uri, extra_headers={}) # :nodoc:
236
+ process(:delete, uri, extra_headers)
237
+ end
238
+
239
+ def process(method, uri, extra_headers={}, payload=nil)
240
+ headers = transcriptic_headers.merge(extra_headers)
241
+ args = [method, payload, headers].compact
242
+
243
+ resource_options = default_resource_options_for_uri(uri)
244
+
245
+ begin
246
+ response = resource(uri, resource_options).send(*args)
247
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
248
+ host = URI.parse(realize_full_uri(uri)).host
249
+ error " ! Unable to connect to #{host}"
250
+ rescue RestClient::SSLCertificateNotVerified => ex
251
+ host = URI.parse(realize_full_uri(uri)).host
252
+ #error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with TRANSCRIPTIC_SSL_VERIFY=disable"
253
+ end
254
+
255
+ extract_warning(response)
256
+ response
257
+ end
258
+
259
+ def extract_warning(response)
260
+ return unless response
261
+ if response.headers[:x_transcriptic_warning] && @warning_callback
262
+ warning = response.headers[:x_transcriptic_warning]
263
+ @displayed_warnings ||= {}
264
+ unless @displayed_warnings[warning]
265
+ @warning_callback.call(warning)
266
+ @displayed_warnings[warning] = true
267
+ end
268
+ end
269
+ end
270
+
271
+ def transcriptic_headers # :nodoc:
272
+ {
273
+ 'X-Transcriptic-API-Version' => '1',
274
+ 'User-Agent' => self.class.gem_version_string,
275
+ 'X-Ruby-Version' => RUBY_VERSION,
276
+ 'X-Ruby-Platform' => RUBY_PLATFORM
277
+ }
278
+ end
279
+
280
+ def xml(raw) # :nodoc:
281
+ REXML::Document.new(raw)
282
+ end
283
+
284
+ def escape(value) # :nodoc:
285
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
286
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
287
+ end
288
+
289
+ module JSON
290
+ def self.parse(json)
291
+ json_decode(json)
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def realize_full_uri(given)
298
+ full_host = (host =~ /^http/) ? host : "https://www.#{host}"
299
+ host = URI.parse(full_host)
300
+ uri = URI.parse(given)
301
+ uri.host ||= host.host
302
+ uri.scheme ||= host.scheme || "https"
303
+ uri.path = (uri.path[0..0] == "/") ? uri.path : "/#{uri.path}"
304
+ uri.port = host.port if full_host =~ /\:\d+/
305
+ uri.to_s
306
+ end
307
+
308
+ def default_resource_options_for_uri(uri)
309
+ if ENV["TRANSCRIPTIC_SSL_VERIFY"] == "disable"
310
+ {}
311
+ elsif realize_full_uri(uri) =~ %r|^https://www.transcriptic.com|
312
+ { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file }
313
+ else
314
+ {}
315
+ end
316
+ end
317
+
318
+ def local_ca_file
319
+ File.expand_path("../../data/cacert.pem", __FILE__)
320
+ end
321
+
322
+ def hash_from_xml_doc(elements)
323
+ elements.inject({}) do |hash, e|
324
+ next(hash) unless e.respond_to?(:children)
325
+ hash.update(e.name.gsub("-","_").to_sym => case e.children.length
326
+ when 0 then nil
327
+ when 1 then e.text
328
+ else hash_from_xml_doc(e.children)
329
+ end)
330
+ end
331
+ end
332
+ end