zillabyte-cli 0.0.24 → 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.
Files changed (40) hide show
  1. checksums.yaml +6 -14
  2. data/lib/#zillabyte-cli.rb# +5 -0
  3. data/lib/zillabyte/api/apps.rb +16 -132
  4. data/lib/zillabyte/api/components.rb +115 -0
  5. data/lib/zillabyte/api/flows.rb +121 -0
  6. data/lib/zillabyte/api/keys.rb +70 -0
  7. data/lib/zillabyte/api.rb +15 -2
  8. data/lib/zillabyte/auth.rb +43 -16
  9. data/lib/zillabyte/cli/#logs.rb# +12 -0
  10. data/lib/zillabyte/cli/#repl.rb# +43 -0
  11. data/lib/zillabyte/cli/apps.rb +52 -893
  12. data/lib/zillabyte/cli/auth.rb +3 -8
  13. data/lib/zillabyte/cli/base.rb +28 -7
  14. data/lib/zillabyte/cli/components.rb +245 -0
  15. data/lib/zillabyte/cli/flows.rb +549 -0
  16. data/lib/zillabyte/cli/git.rb +38 -0
  17. data/lib/zillabyte/cli/help.rb +11 -4
  18. data/lib/zillabyte/cli/keys.rb +177 -0
  19. data/lib/zillabyte/cli/query.rb +0 -1
  20. data/lib/zillabyte/cli/relations.rb +2 -1
  21. data/lib/zillabyte/cli/templates/{js → apps/js}/simple_function.js +0 -0
  22. data/lib/zillabyte/cli/templates/{js → apps/js}/zillabyte.conf.yaml +0 -0
  23. data/lib/zillabyte/cli/templates/apps/python/app.py +17 -0
  24. data/lib/zillabyte/cli/templates/{python → apps/python}/requirements.txt +0 -0
  25. data/lib/zillabyte/cli/templates/{python → apps/python}/zillabyte.conf.yaml +1 -1
  26. data/lib/zillabyte/cli/templates/{ruby → apps/ruby}/Gemfile +0 -0
  27. data/lib/zillabyte/cli/templates/{ruby → apps/ruby}/app.rb +1 -1
  28. data/lib/zillabyte/cli/templates/{ruby → apps/ruby}/zillabyte.conf.yaml +0 -0
  29. data/lib/zillabyte/cli/templates/python/{simple_function.py → #simple_function.py#} +3 -6
  30. data/lib/zillabyte/common/session.rb +3 -1
  31. data/lib/zillabyte/helpers.rb +64 -1
  32. data/lib/zillabyte/runner/app_runner.rb +226 -0
  33. data/lib/zillabyte/runner/component_operation.rb +529 -0
  34. data/lib/zillabyte/runner/component_runner.rb +244 -0
  35. data/lib/zillabyte/runner/multilang_operation.rb +1133 -0
  36. data/lib/zillabyte/runner/operation.rb +11 -0
  37. data/lib/zillabyte/runner.rb +6 -0
  38. data/lib/zillabyte-cli/version.rb +1 -1
  39. data/zillabyte-cli.gemspec +1 -0
  40. metadata +83 -52
@@ -0,0 +1,177 @@
1
+ require "zillabyte/cli/base"
2
+ require "zillabyte/cli/helpers/table_output_builder"
3
+ require "cgi"
4
+ require "csv"
5
+ require "open-uri"
6
+ require "json"
7
+
8
+
9
+ # manage authentication keys
10
+ #
11
+ class Zillabyte::Command::Keys < Zillabyte::Command::Base
12
+
13
+ SHORT_KEY_SPLIT = 18
14
+ SHORT_KEY_MAX = 64
15
+
16
+ # keys:add NAME [KEY]
17
+ #
18
+ # add a key for the current user
19
+ #
20
+ # if no KEY is specified, will try to find ~/.ssh/id_rsa.pub
21
+ #
22
+ # --output_type OUTPUT_TYPE # The output format type
23
+ #
24
+ def add()
25
+ type = options[:output_type] || nil
26
+ name = shift_argument()
27
+ if name.nil?
28
+ error("no name given", type)
29
+ end
30
+ keypath = shift_argument() || "~/.ssh/id_rsa.pub"
31
+ begin
32
+ key = File.binread(File.expand_path(keypath))
33
+ rescue => e
34
+ error(e.message, type)
35
+ end
36
+ message = api.keys.add(name, key)
37
+ if type == "json"
38
+ display("{}")
39
+ else
40
+ display(message)
41
+ end
42
+ end
43
+
44
+ # keys:show NAME
45
+ #
46
+ # show a key for the current user
47
+ #
48
+ # --output_type OUTPUT_TYPE # The output format type
49
+ #
50
+ def show()
51
+ type = options[:output_type] || nil
52
+ name = shift_argument()
53
+ if name.nil?()
54
+ error("no name given", type)
55
+ end
56
+
57
+ key = api.keys.show(name)
58
+ if type == "json"
59
+ display({"key" => key}.to_json())
60
+ else
61
+ display(key)
62
+ end
63
+ end
64
+
65
+ # keys:remove [NAME]
66
+ #
67
+ # remove a key from the current user
68
+ #
69
+ # --output_type OUTPUT_TYPE # The output format type
70
+ # -l, --long # display extended information for each key
71
+ # -f, --force # Force -- remove key without prompting
72
+ #
73
+ def remove()
74
+ type = options[:output_type] || nil
75
+ long = options[:long]
76
+ force = options[:force]
77
+ name = shift_argument()
78
+ key_data = if name.nil?()
79
+ if force
80
+ error("no name given", type)
81
+ end
82
+ keys = api.keys.list()
83
+ display("=== Keys")
84
+ display(format_keys(keys, long, nil))
85
+ # NOTE: it may not be the most efficient thing to use a hash here, unless
86
+ # we expect users to routinely try to remove invalid keys.
87
+ keymap = Hash[*keys.flatten()]
88
+ loop do
89
+ display("Please enter the name of the key you wish to remove (case-sensitive):")
90
+ name = ask().chomp()
91
+ if name.empty?()
92
+ error("Aborting.", type)
93
+ end
94
+ if keymap.has_key?(name)
95
+ break(keymap[name])
96
+ end
97
+ display("Key not found: `#{name}`")
98
+ end
99
+ else
100
+ key = api.keys.show(name)
101
+ if force
102
+ key
103
+ else
104
+ display(format_keys([[name, key]], long, nil))
105
+ loop do
106
+ display("Proceed with removal? (yes or no)")
107
+ choice = ask().chomp()
108
+ if choice == "y" || choice == "yes"
109
+ break(key)
110
+ elsif choice == "n" || choice == "no"
111
+ error("Aborting.", type)
112
+ else
113
+ display("Invalid response.")
114
+ end
115
+ end
116
+ end
117
+ key
118
+ end
119
+
120
+ message = api.keys.remove(name, key_data)
121
+ if type == "json"
122
+ display("{}")
123
+ else
124
+ display(message)
125
+ end
126
+ end
127
+
128
+ # keys
129
+ #
130
+ # display keys for the current user
131
+ #
132
+ # --output_type OUTPUT_TYPE # The output format type
133
+ # -l, --long # display extended information for each key
134
+ def index()
135
+ type = options[:output_type] || nil
136
+ long = options[:long]
137
+
138
+ keys = api.keys.list()
139
+ if type.nil?()
140
+ display("=== Keys")
141
+ end
142
+ display(format_keys(keys, long, type))
143
+ end
144
+
145
+ # keys:clear
146
+ #
147
+ # remove all authentication keys from the current user
148
+ #
149
+ # --output_type OUTPUT_TYPE # The output format type
150
+ #
151
+ def clear()
152
+ type = options[:output_type] || nil
153
+
154
+ message = api.keys.clear()
155
+ if type == "json"
156
+ display("{}")
157
+ else
158
+ display(message)
159
+ end
160
+ end
161
+
162
+ private
163
+ def format_keys(keys, long, type)
164
+ outlong = long || type == "json"
165
+ rows = keys.map() { |row|
166
+ name = row[0]
167
+ key = row[1]
168
+ if outlong || key.length() < SHORT_KEY_MAX
169
+ [key, name]
170
+ else
171
+ ["#{key[0...SHORT_KEY_SPLIT]}...#{key[SHORT_KEY_SPLIT - SHORT_KEY_MAX + 2..-1]}", name]
172
+ end
173
+ }
174
+ headings = ["key", "name"]
175
+ TableOutputBuilder.build_table(headings, rows, type)
176
+ end
177
+ end
@@ -264,7 +264,6 @@ class Zillabyte::Command::Query < Zillabyte::Command::Base
264
264
  )
265
265
  res = res.body
266
266
 
267
- p res
268
267
  if(res["uri"])
269
268
  display "Waiting for download." if type.nil?
270
269
  File.open(file, "w") do |f|
@@ -520,6 +520,7 @@ class Zillabyte::Command::Relations < Zillabyte::Command::Base
520
520
  case filetype
521
521
  when "csv"
522
522
  begin
523
+
523
524
  CSV.foreach(file) do |row|
524
525
  if row.size != n_columns
525
526
  error("relation expect #{n_columns} column(s). Found a row with #{row.size}::\n #{row}", type)
@@ -539,7 +540,7 @@ class Zillabyte::Command::Relations < Zillabyte::Command::Base
539
540
  rows << row
540
541
  end
541
542
  rescue Exception => e
542
- error("unable to parse csv", type)
543
+ error("error when parsing csv : #{e}", type)
543
544
  end
544
545
 
545
546
  else
@@ -0,0 +1,17 @@
1
+ import zillabyte
2
+
3
+ def prep(controller):
4
+ return
5
+
6
+ # This is the heart of your algorithm. It's processed on every
7
+ # web page. This algorithm is run in parallel on possibly hundreds
8
+ # of machines.
9
+ def execute(controller, tup):
10
+ if("hello world" in tup["html"]):
11
+ controller.emit("has_hello_world",{"url":tup["url"]})
12
+ return
13
+
14
+ app = zillabyte.app(name = "hello_world")
15
+ app.source(matches = "select * from web_pages")\
16
+ .each(execute = execute)\
17
+ .sink(name = "has_hello_world", columns = [{"url":"string"}])
@@ -1,4 +1,4 @@
1
1
  language: python
2
- script: simple_function.py
2
+ script: app.py
3
3
  ignore_files:
4
4
  - ./vEnv
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'zillabyte'
5
5
 
6
- Zillabyte.app("hello_world_app")
6
+ Zillabyte.app("<%= name %>")
7
7
  .source("select * from web_pages")
8
8
  .each{ |page|
9
9
  if page['html'].include? "hello world"
@@ -7,18 +7,15 @@ def prep(controller):
7
7
  # web page. This algorithm is run in parallel on possibly hundreds
8
8
  # of machines.
9
9
  def execute(controller, tup):
10
- if("hello world" in tup["html"]):
11
- controller.emit("has_hello_world",{"url":tup["url"]})
10
+ if("hello world" in tup.values["html"]):
11
+ controller.emit("has_hello_world",{"url":tup.values["url"]})
12
12
  return
13
13
 
14
14
  zillabyte.simple_function(\
15
- # This specifies the function's name and is mandatory.
16
- name = "simple_function", \
17
-
18
15
  # This directive instructs zillabyte to give your function every
19
16
  # web page in our known universe. Your function will have access
20
17
  # to two fields: URL and HTML
21
- matches = "select * from web_pages", \
18
+ matches = "select * from web_pa", \
22
19
 
23
20
  # This directive tells Zillabyte what kind of data your function
24
21
  # produces. In this case, we're saying we will emit a tuple that
@@ -6,7 +6,9 @@ module Zillabyte::Common
6
6
  end
7
7
 
8
8
  def display(*args)
9
- puts *args
9
+ args.each do |arg|
10
+ STDOUT.syswrite(arg + "\n")
11
+ end
10
12
  end
11
13
 
12
14
  def error(*args)
@@ -7,7 +7,7 @@ module Zillabyte
7
7
 
8
8
  def display(msg="", new_line=true)
9
9
  if new_line
10
- puts(msg)
10
+ puts(msg)
11
11
  else
12
12
  print(msg)
13
13
  $stdout.flush
@@ -50,6 +50,69 @@ module Zillabyte
50
50
  # fails on windows
51
51
  end
52
52
  end
53
+
54
+ def has_git?
55
+ %x{ git --version }
56
+ $?.success?
57
+ end
58
+
59
+ def git(args)
60
+ return "" unless has_git?
61
+ flattened_args = [args].flatten.compact.join(" ")
62
+ %x{ git #{flattened_args} 2>&1 }.strip
63
+ end
64
+
65
+ def create_git_remote(remote, url)
66
+ return if git('remote').split("\n").include?(remote)
67
+ return unless File.exists?(".git")
68
+ git "remote add #{remote} #{url}"
69
+ display "Git remote #{remote} added"
70
+ end
71
+
72
+
73
+
74
+ def app
75
+ @app ||= if options[:confirm].is_a?(String)
76
+ if options[:app] && (options[:app] != options[:confirm])
77
+ error("Mismatch between --app and --confirm")
78
+ end
79
+ options[:confirm]
80
+ elsif options[:app].is_a?(String)
81
+ options[:app]
82
+ elsif ENV.has_key?('ZILLABYTE_APP')
83
+ ENV['ZILLABYTE_APP']
84
+ elsif app_from_dir = extract_app_in_dir(Dir.pwd)
85
+ app_from_dir
86
+ else
87
+ # raise instead of using error command to enable rescuing when app is optional
88
+ raise Zillabyte::Command::CommandFailed.new("No app specified.\nRun this command from an app folder or specify which app to use with --app APP.") unless options[:ignore_no_app]
89
+ end
90
+ end
91
+
92
+
93
+
94
+ def extract_app_in_dir(dir)
95
+ return unless remotes = git_remotes(dir)
96
+
97
+ if remote = options[:remote]
98
+ remotes[remote]
99
+ elsif remote = extract_app_from_git_config
100
+ remotes[remote]
101
+ else
102
+ apps = remotes.values.uniq
103
+ if apps.size == 1
104
+ apps.first
105
+ else
106
+ raise(Heroku::Command::CommandFailed, "Multiple apps in folder and no app specified.\nSpecify app with --app APP.") unless options[:ignore_no_app]
107
+ end
108
+ end
109
+ end
110
+
111
+ def extract_app_from_git_config
112
+ remote = git("config zillabyte.remote")
113
+ remote == "" ? nil : remote
114
+ end
115
+
53
116
 
54
117
  end
55
118
  end
@@ -0,0 +1,226 @@
1
+ require "zillabyte/cli/base"
2
+ require "zillabyte/runner"
3
+ require "zillabyte/runner/multilang_operation"
4
+ require 'thread'
5
+
6
+
7
+ # HIDDEN:
8
+ class Zillabyte::Runner::AppRunner < Zillabyte::Command::Base
9
+ include Zillabyte::Helpers
10
+
11
+ def run (dir = Dir.pwd, session = nil, options = {})
12
+
13
+ if session.nil?
14
+ return
15
+ end
16
+
17
+ @session = session
18
+ @colors = {}
19
+ output = options[:output]
20
+ otype = options[:output_type]
21
+ interactive = options[:interactive]
22
+ cycles = (options[:cycles] || "1").to_i
23
+
24
+ # Get app metadata
25
+ meta = Zillabyte::API::Flows.get_rich_meta_info_from_script(dir, @session, {:test => true})
26
+ if meta.nil? || meta["nodes"].nil?
27
+ error "this is not a valid zillabyte app directory"
28
+ exit
29
+ end
30
+
31
+ # Check that multilang version is atleast 0.1.0
32
+ version = meta["multilang_version"] || "0.0.0"
33
+ version_arr = version.split('.').map {|v| v.to_i}
34
+ if version_arr.empty? || (version_arr[0] == 0 && version_arr[1] < 1)
35
+ display "The version of zillabyte used in your application is outdated."
36
+ display "Please use upgrade your zillabyte gem via 'bundle update zillabyte; gem cleanup zillabyte'"
37
+ return
38
+ end
39
+
40
+ # Show the user what we know about their app...
41
+ display "inferring your app details..."
42
+ describe_app(meta)
43
+
44
+ # Setup streams
45
+ @nodes = meta["nodes"]
46
+
47
+ # Index stream consummers and emitters by stream name
48
+ @arcs = meta["arcs"]
49
+
50
+ # Organize component pipes
51
+ @operations = {}
52
+ @operation_pipes = {}
53
+
54
+ # On each cycle, setup and tear down a test harness
55
+ (1..cycles).each do |cycle|
56
+
57
+ display "starting cycle #{cycle}" unless cycles == 1
58
+
59
+ begin
60
+
61
+ # Setup component pipes
62
+ @nodes.each do |n|
63
+
64
+ name = n["name"]
65
+ type = n["type"]
66
+ emits = n["emits"]
67
+ if n["type"] == "source"
68
+ options[:end_cycle_policy] = n["end_cycle_policy"]
69
+ end
70
+
71
+ # Create two new pipes in the parent.
72
+ rd_child, wr_parent = IO.pipe()
73
+ rd_parent, wr_child = IO.pipe()
74
+
75
+ @operation_pipes[name] = {
76
+ :rd_child => rd_child,
77
+ :wr_child => wr_child,
78
+ :rd_parent => rd_parent,
79
+ :wr_parent => wr_parent
80
+ }
81
+ end
82
+
83
+ # Maps origin => {stream => [destinations]}
84
+ @arc_map = {}
85
+ @arcs.each do |a|
86
+ origin = a["origin"]
87
+ name = a["name"]
88
+ dest = a["dest"]
89
+ @arc_map[origin] ||= {}
90
+ @arc_map[origin][name] ||= []
91
+ @arc_map[origin][name] << a["dest"]
92
+ end
93
+
94
+ # Spawn component threads
95
+ @nodes.each do |n|
96
+
97
+ name = n["name"]
98
+ type = n["type"]
99
+ emits = n["emits"]
100
+
101
+ pipes = @operation_pipes[name]
102
+ rd_child = pipes[:rd_child]
103
+ wr_child = pipes[:wr_child]
104
+ rd_parent = pipes[:rd_parent]
105
+ wr_parent = pipes[:wr_parent]
106
+
107
+ # Fork.
108
+ pid = fork()
109
+ if pid # In parent
110
+ # Close the reading end of the child so we can write to the child.
111
+ rd_child.close()
112
+ # Close the writing end of the child so we can read from the child.
113
+ wr_child.close()
114
+
115
+ else # in child
116
+ # Close the writing end of the parent so we can read from the parent.
117
+ wr_parent.close()
118
+ # Close the reading end of the parent so we can write to the parent.
119
+ rd_parent.close()
120
+ begin
121
+
122
+
123
+ # Setup reading and writing pipes for communicating with consumee component
124
+ in_pipe = {:rd_child => @operation_pipes[name][:rd_child], :wr_child => @operation_pipes[name][:wr_child]}
125
+
126
+ # Index consumer pipes by stream name, consumer_name
127
+ out_pipes = {}
128
+
129
+
130
+ if type != "sink"
131
+ @arc_map[name].each_pair do |stream, destinations|
132
+ out_pipes[stream] ||= {}
133
+ destinations.each do |dest|
134
+ out_pipes[stream][dest] = {:wr_parent => @operation_pipes[dest][:wr_parent], :rd_parent => @operation_pipes[dest][:rd_parent] }
135
+ end
136
+ end
137
+ end
138
+
139
+ # Run the child process
140
+ Zillabyte::Runner::MultilangOperation.run(n, dir, in_pipe, out_pipes, self, meta, options)
141
+
142
+ ensure
143
+ # Close the reading end of the child
144
+ rd_child.close()
145
+ # Close the writing end of the child
146
+ wr_child.close()
147
+ exit!(-1)
148
+ end
149
+
150
+ end #end child
151
+ end
152
+
153
+ if interactive
154
+ display ""
155
+ while true
156
+ display "Enter an input tuple in JSON format i.e.{ \"url\" : \"foo.com\", \"html\" : \"bar.html\" }"
157
+ msg = ask
158
+
159
+ begin
160
+ JSON.parse(msg)
161
+ rescue JSON::ParserError
162
+ display "Received invalid JSON object"
163
+ next
164
+ end
165
+ # Send tuple to source
166
+ @operation_pipes["source_1"][:wr_parent].puts msg
167
+ end
168
+ end
169
+
170
+ ensure
171
+ Process.waitall()
172
+ @operation_pipes.each do |name, pipes|
173
+ #Close the writing end of the parent
174
+ pipes[:wr_parent].close()
175
+ # Close the reading end of the parent
176
+ pipes[:rd_parent].close()
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+
183
+ def session
184
+ @session
185
+ end
186
+
187
+
188
+ def cdisplay(name, message)
189
+
190
+ color = @colors[name] || :default
191
+ if message == ""
192
+ display ""
193
+ else
194
+ display "#{name} - #{message}".colorize(color)
195
+ end
196
+ end
197
+
198
+
199
+ def query_agnostic(query)
200
+ @session.api.query.agnostic(query)
201
+ end
202
+
203
+ def display(message, newline = true)
204
+ @session.display(message, newline)
205
+ end
206
+
207
+
208
+ def describe_app(meta)
209
+ colors ||= [:green, :yellow, :magenta, :cyan, :white, :blue, :light_yellow, :light_blue, :red, :light_magenta, :light_cyan]
210
+ rjust = 20
211
+ display "#{'app name'.rjust(rjust)}: #{meta['name']}"
212
+ display "#{'app language'.rjust(rjust)}: #{meta['language']}"
213
+ meta['nodes'].each_with_index do |node, index|
214
+ @colors[node['name']] ||= colors[index % colors.length]
215
+ color = @colors[node['name']]
216
+ display (("="*rjust + " operation ##{index}").colorize(color))
217
+ display "#{"name".rjust(rjust)}: #{node['name'].to_s.colorize(color)}"
218
+ display "#{"type".rjust(rjust)}: #{node['type'].to_s.colorize(color)}"
219
+ display "#{"matches".rjust(rjust)}: #{JSON.pretty_generate(node['matches']).indent(rjust+2).lstrip.colorize(color)}" if node['matches']
220
+ display "#{"consumes".rjust(rjust)}: #{node['consumes'].to_s.colorize(color)}" if node['consumes']
221
+ display "#{"emits".rjust(rjust)}: #{JSON.pretty_generate(node['emits']).indent(rjust+2).lstrip.colorize(color)}" if node['emits']
222
+ end
223
+ end
224
+
225
+
226
+ end