mortar 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 (61) hide show
  1. data/README.md +36 -0
  2. data/bin/mortar +13 -0
  3. data/lib/mortar.rb +23 -0
  4. data/lib/mortar/auth.rb +312 -0
  5. data/lib/mortar/cli.rb +54 -0
  6. data/lib/mortar/command.rb +267 -0
  7. data/lib/mortar/command/auth.rb +96 -0
  8. data/lib/mortar/command/base.rb +319 -0
  9. data/lib/mortar/command/clusters.rb +41 -0
  10. data/lib/mortar/command/describe.rb +97 -0
  11. data/lib/mortar/command/generate.rb +121 -0
  12. data/lib/mortar/command/help.rb +166 -0
  13. data/lib/mortar/command/illustrate.rb +97 -0
  14. data/lib/mortar/command/jobs.rb +174 -0
  15. data/lib/mortar/command/pigscripts.rb +45 -0
  16. data/lib/mortar/command/projects.rb +128 -0
  17. data/lib/mortar/command/validate.rb +94 -0
  18. data/lib/mortar/command/version.rb +42 -0
  19. data/lib/mortar/errors.rb +24 -0
  20. data/lib/mortar/generators/generator_base.rb +107 -0
  21. data/lib/mortar/generators/macro_generator.rb +37 -0
  22. data/lib/mortar/generators/pigscript_generator.rb +40 -0
  23. data/lib/mortar/generators/project_generator.rb +67 -0
  24. data/lib/mortar/generators/udf_generator.rb +28 -0
  25. data/lib/mortar/git.rb +233 -0
  26. data/lib/mortar/helpers.rb +488 -0
  27. data/lib/mortar/project.rb +156 -0
  28. data/lib/mortar/snapshot.rb +39 -0
  29. data/lib/mortar/templates/macro/macro.pig +14 -0
  30. data/lib/mortar/templates/pigscript/pigscript.pig +38 -0
  31. data/lib/mortar/templates/pigscript/python_udf.py +13 -0
  32. data/lib/mortar/templates/project/Gemfile +3 -0
  33. data/lib/mortar/templates/project/README.md +8 -0
  34. data/lib/mortar/templates/project/gitignore +4 -0
  35. data/lib/mortar/templates/project/macros/gitkeep +0 -0
  36. data/lib/mortar/templates/project/pigscripts/pigscript.pig +35 -0
  37. data/lib/mortar/templates/project/udfs/python/python_udf.py +13 -0
  38. data/lib/mortar/templates/udf/python_udf.py +13 -0
  39. data/lib/mortar/version.rb +20 -0
  40. data/lib/vendor/mortar/okjson.rb +598 -0
  41. data/lib/vendor/mortar/uuid.rb +312 -0
  42. data/spec/mortar/auth_spec.rb +156 -0
  43. data/spec/mortar/command/auth_spec.rb +46 -0
  44. data/spec/mortar/command/base_spec.rb +82 -0
  45. data/spec/mortar/command/clusters_spec.rb +61 -0
  46. data/spec/mortar/command/describe_spec.rb +135 -0
  47. data/spec/mortar/command/generate_spec.rb +139 -0
  48. data/spec/mortar/command/illustrate_spec.rb +140 -0
  49. data/spec/mortar/command/jobs_spec.rb +364 -0
  50. data/spec/mortar/command/pigscripts_spec.rb +70 -0
  51. data/spec/mortar/command/projects_spec.rb +165 -0
  52. data/spec/mortar/command/validate_spec.rb +119 -0
  53. data/spec/mortar/command_spec.rb +122 -0
  54. data/spec/mortar/git_spec.rb +278 -0
  55. data/spec/mortar/helpers_spec.rb +82 -0
  56. data/spec/mortar/project_spec.rb +76 -0
  57. data/spec/mortar/snapshot_spec.rb +46 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +278 -0
  60. data/spec/support/display_message_matcher.rb +68 -0
  61. metadata +259 -0
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color --format doc
@@ -0,0 +1,278 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ $stdin = File.new("/dev/null")
21
+
22
+ require "rubygems"
23
+ require "vendor/mortar/uuid"
24
+
25
+ require "excon"
26
+ Excon.defaults[:mock] = true
27
+
28
+ # ensure these are around for errors
29
+ # as their require is generally deferred
30
+ require "mortar-api-ruby"
31
+
32
+ require "mortar/cli"
33
+ require "rspec"
34
+ require "rr"
35
+ require "fakefs/safe"
36
+ require 'tmpdir'
37
+
38
+ def execute(command_line, project=nil, git=nil)
39
+
40
+ args = command_line.split(" ")
41
+ command = args.shift
42
+
43
+ Mortar::Command.load
44
+ object, method = Mortar::Command.prepare_run(command, args)
45
+
46
+ # stub the project
47
+ if project
48
+ any_instance_of(Mortar::Command::Base) do |base|
49
+ stub(base).project.returns(project)
50
+ end
51
+ end
52
+
53
+ # stub git
54
+ if git
55
+ # stub out any operations that affect remote resources
56
+ stub(git).push
57
+
58
+ any_instance_of(Mortar::Command::Base) do |base|
59
+ stub(base).git.returns(git)
60
+ end
61
+ end
62
+
63
+ original_stdin, original_stderr, original_stdout = $stdin, $stderr, $stdout
64
+
65
+ $stdin = captured_stdin = StringIO.new
66
+ $stderr = captured_stderr = StringIO.new
67
+ $stdout = captured_stdout = StringIO.new
68
+
69
+ begin
70
+ object.send(method)
71
+ rescue SystemExit
72
+ ensure
73
+ $stdin, $stderr, $stdout = original_stdin, original_stderr, original_stdout
74
+ Mortar::Command.current_command = nil
75
+ end
76
+
77
+ [captured_stderr.string, captured_stdout.string]
78
+ end
79
+
80
+ def any_instance_of(klass, &block)
81
+ any_instance_of(klass, &block)
82
+ end
83
+
84
+ def run(command_line)
85
+ capture_stdout do
86
+ begin
87
+ Mortar::CLI.start(*command_line.split(" "))
88
+ rescue SystemExit
89
+ end
90
+ end
91
+ end
92
+
93
+ alias mortar run
94
+
95
+ def capture_stderr(&block)
96
+ original_stderr = $stderr
97
+ $stderr = captured_stderr = StringIO.new
98
+ begin
99
+ yield
100
+ ensure
101
+ $stderr = original_stderr
102
+ end
103
+ captured_stderr.string
104
+ end
105
+
106
+ def capture_stdout(&block)
107
+ original_stdout = $stdout
108
+ $stdout = captured_stdout = StringIO.new
109
+ begin
110
+ yield
111
+ ensure
112
+ $stdout = original_stdout
113
+ end
114
+ captured_stdout.string
115
+ end
116
+
117
+ def fail_command(message)
118
+ raise_error(Mortar::Command::CommandFailed, message)
119
+ end
120
+
121
+ def stub_core
122
+ @stubbed_core ||= begin
123
+ stubbed_core = nil
124
+ stub(Mortar::Auth).user.returns("email@example.com")
125
+ stub(Mortar::Auth).password.returns("pass")
126
+ stubbed_core
127
+ end
128
+ end
129
+
130
+ def with_no_git_directory(&block)
131
+ starting_dir = Dir.pwd
132
+ sandbox = File.join(Dir.tmpdir, "mortar", Mortar::UUID.create_random.to_s)
133
+ FileUtils.mkdir_p(sandbox)
134
+ Dir.chdir(sandbox)
135
+
136
+ begin
137
+ block.call()
138
+ ensure
139
+ # return to the original starting dir,
140
+ # if one is defined. If using FakeFS, it will not
141
+ # be defined
142
+ if starting_dir && (! starting_dir.empty?)
143
+ Dir.chdir(starting_dir)
144
+ end
145
+
146
+ FileUtils.rm_rf(sandbox)
147
+ end
148
+ end
149
+
150
+ def with_blank_project(&block)
151
+ # setup a sandbox directory
152
+ starting_dir = Dir.pwd
153
+ sandbox = File.join(Dir.tmpdir, "mortar", Mortar::UUID.create_random.to_s)
154
+ FileUtils.mkdir_p(sandbox)
155
+
156
+ # setup project directory
157
+ project_name = "myproject"
158
+ project_path = File.join(sandbox, project_name)
159
+ FileUtils.mkdir_p(project_path)
160
+
161
+ # setup project subdirectories
162
+ FileUtils.mkdir_p(File.join(project_path, "pigscripts"))
163
+ FileUtils.mkdir_p(File.join(project_path, "macros"))
164
+
165
+ Dir.chdir(project_path)
166
+
167
+ # initialize git repo
168
+ `git init`
169
+
170
+ project = Mortar::Project::Project.new(project_name, project_path, nil)
171
+
172
+ begin
173
+ block.call(project)
174
+ ensure
175
+ # return to the original starting dir,
176
+ # if one is defined. If using FakeFS, it will not
177
+ # be defined
178
+ if starting_dir && (! starting_dir.empty?)
179
+ Dir.chdir(starting_dir)
180
+ end
181
+
182
+ FileUtils.rm_rf(sandbox)
183
+ end
184
+ end
185
+
186
+ def with_git_initialized_project(&block)
187
+ # wrap block in a proc that does a commit
188
+ commit_proc = Proc.new do |project|
189
+ write_file(File.join(project.root_path, "README.txt"), "Some README text")
190
+ remote = "mortar"
191
+ `git add README.txt`
192
+ `git commit -a -m "First commit"`
193
+ `git remote add #{remote} git@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_#{project.name}.git`
194
+ project.remote = remote
195
+ block.call(project)
196
+ end
197
+
198
+ with_blank_project(&commit_proc)
199
+ end
200
+
201
+ def write_file(path, contents="")
202
+ FileUtils.mkdir_p File.dirname(path)
203
+ File.open(path, 'w') {|f| f.write(contents)}
204
+ end
205
+
206
+ def git_create_conflict(git, project)
207
+ filename = "conflict_file.txt"
208
+
209
+ # add to master
210
+ git.git("checkout master")
211
+ write_file(File.join(project.root_path, filename), Mortar::UUID.create_random.to_s)
212
+ git.add(filename)
213
+ git.git("commit -a -m \"initial\"")
214
+
215
+ # checkin change on branch
216
+ git.git("checkout -b conflict_branch")
217
+ write_file(File.join(project.root_path, filename), Mortar::UUID.create_random.to_s)
218
+ git.add(filename)
219
+ git.git("commit -a -m \"conflict from branch\"")
220
+
221
+ # checkin change on master
222
+ git.git("checkout master")
223
+ write_file(File.join(project.root_path, filename), Mortar::UUID.create_random.to_s)
224
+ git.add(filename)
225
+ git.git("commit -a -m \"conflict from master\"")
226
+
227
+ # merge
228
+ git.git("merge conflict_branch", check_success=false)
229
+
230
+ filename
231
+ end
232
+
233
+
234
+ def git_add_file(git, project)
235
+ # add a new file
236
+ added_file = "added_file.txt"
237
+ write_file(File.join(project.root_path, added_file))
238
+ git.add(added_file)
239
+ added_file
240
+ end
241
+
242
+ def git_create_untracked_file(project)
243
+ # add an untracked file
244
+ untracked_file = "untracked_file.txt"
245
+ write_file(File.join(project.root_path, untracked_file))
246
+ untracked_file
247
+ end
248
+
249
+ def post_validate_git_snapshot(git, starting_status, snapshot_branch)
250
+ snapshot_branch.should_not be_nil
251
+ snapshot_branch.should_not == "master"
252
+ git.current_branch.should == "master"
253
+ git.status.should == starting_status
254
+ git.has_conflicts?.should be_false
255
+
256
+ # ensure the snapshot branch exists
257
+ git.git("branch").include?(snapshot_branch).should be_true
258
+ end
259
+
260
+ require "mortar/helpers"
261
+ module Mortar::Helpers
262
+ @home_directory = Dir.mktmpdir
263
+ undef_method :home_directory
264
+ def home_directory
265
+ @home_directory
266
+ end
267
+ end
268
+
269
+ require "support/display_message_matcher"
270
+
271
+ RSpec.configure do |config|
272
+ config.mock_with :rr
273
+ config.color_enabled = true
274
+ config.include DisplayMessageMatcher
275
+ config.before { Mortar::Helpers.error_with_failure = false }
276
+ config.after { RR.verify; RR.reset }
277
+ end
278
+
@@ -0,0 +1,68 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ module DisplayMessageMatcher
21
+
22
+ def display_message(command, message)
23
+ DisplayMessageMatcher::DisplayMessage.new command, message
24
+ end
25
+
26
+ class DisplayMessage
27
+ def initialize(command, message)
28
+ @command = command
29
+ @message = message
30
+ end
31
+
32
+ def matches?(given_proc)
33
+ displayed_expected_message = false
34
+ @given_messages = []
35
+
36
+ @command.should_receive(:display).
37
+ any_number_of_times do |message, newline|
38
+ @given_messages << message
39
+ displayed_expected_message = displayed_expected_message ||
40
+ message == @message
41
+ end
42
+
43
+ given_proc.call
44
+
45
+ displayed_expected_message
46
+ end
47
+
48
+ def failure_message
49
+ "expected #{ @command } to display the message #{ @message.inspect } but #{ given_messages }"
50
+ end
51
+
52
+ def negative_failure_message
53
+ "expected #{ @command } to not display the message #{ @message.inspect } but it was displayed"
54
+ end
55
+
56
+ private
57
+
58
+ def given_messages
59
+ if @given_messages.empty?
60
+ 'no messages were displayed'
61
+ else
62
+ formatted_given_messages = @given_messages.map(&:inspect).join ', '
63
+ "the follow messages were displayed: #{ formatted_given_messages }"
64
+ end
65
+ end
66
+
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,259 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mortar
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Mortar Data
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-30 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mortar-api-ruby
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 27
29
+ segments:
30
+ - 0
31
+ - 1
32
+ - 0
33
+ version: 0.1.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: netrc
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 9
45
+ segments:
46
+ - 0
47
+ - 7
48
+ - 5
49
+ version: 0.7.5
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: launchy
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 9
61
+ segments:
62
+ - 2
63
+ - 1
64
+ - 1
65
+ version: 2.1.1
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: excon
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 43
77
+ segments:
78
+ - 0
79
+ - 15
80
+ - 4
81
+ version: 0.15.4
82
+ type: :development
83
+ version_requirements: *id004
84
+ - !ruby/object:Gem::Dependency
85
+ name: fakefs
86
+ prerelease: false
87
+ requirement: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ type: :development
97
+ version_requirements: *id005
98
+ - !ruby/object:Gem::Dependency
99
+ name: gem-release
100
+ prerelease: false
101
+ requirement: &id006 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ type: :development
111
+ version_requirements: *id006
112
+ - !ruby/object:Gem::Dependency
113
+ name: rake
114
+ prerelease: false
115
+ requirement: &id007 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ hash: 3
121
+ segments:
122
+ - 0
123
+ version: "0"
124
+ type: :development
125
+ version_requirements: *id007
126
+ - !ruby/object:Gem::Dependency
127
+ name: rr
128
+ prerelease: false
129
+ requirement: &id008 !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ hash: 3
135
+ segments:
136
+ - 0
137
+ version: "0"
138
+ type: :development
139
+ version_requirements: *id008
140
+ - !ruby/object:Gem::Dependency
141
+ name: rspec
142
+ prerelease: false
143
+ requirement: &id009 !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ hash: 3
149
+ segments:
150
+ - 0
151
+ version: "0"
152
+ type: :development
153
+ version_requirements: *id009
154
+ description: Client library and command-line tool to interact with the Mortar service.
155
+ email: support@mortardata.com
156
+ executables:
157
+ - mortar
158
+ extensions: []
159
+
160
+ extra_rdoc_files: []
161
+
162
+ files:
163
+ - README.md
164
+ - bin/mortar
165
+ - lib/mortar.rb
166
+ - lib/mortar/auth.rb
167
+ - lib/mortar/cli.rb
168
+ - lib/mortar/command.rb
169
+ - lib/mortar/command/auth.rb
170
+ - lib/mortar/command/base.rb
171
+ - lib/mortar/command/clusters.rb
172
+ - lib/mortar/command/describe.rb
173
+ - lib/mortar/command/generate.rb
174
+ - lib/mortar/command/help.rb
175
+ - lib/mortar/command/illustrate.rb
176
+ - lib/mortar/command/jobs.rb
177
+ - lib/mortar/command/pigscripts.rb
178
+ - lib/mortar/command/projects.rb
179
+ - lib/mortar/command/validate.rb
180
+ - lib/mortar/command/version.rb
181
+ - lib/mortar/errors.rb
182
+ - lib/mortar/generators/generator_base.rb
183
+ - lib/mortar/generators/macro_generator.rb
184
+ - lib/mortar/generators/pigscript_generator.rb
185
+ - lib/mortar/generators/project_generator.rb
186
+ - lib/mortar/generators/udf_generator.rb
187
+ - lib/mortar/git.rb
188
+ - lib/mortar/helpers.rb
189
+ - lib/mortar/project.rb
190
+ - lib/mortar/snapshot.rb
191
+ - lib/mortar/templates/macro/macro.pig
192
+ - lib/mortar/templates/pigscript/pigscript.pig
193
+ - lib/mortar/templates/pigscript/python_udf.py
194
+ - lib/mortar/templates/project/Gemfile
195
+ - lib/mortar/templates/project/README.md
196
+ - lib/mortar/templates/project/gitignore
197
+ - lib/mortar/templates/project/macros/gitkeep
198
+ - lib/mortar/templates/project/pigscripts/pigscript.pig
199
+ - lib/mortar/templates/project/udfs/python/python_udf.py
200
+ - lib/mortar/templates/udf/python_udf.py
201
+ - lib/mortar/version.rb
202
+ - lib/vendor/mortar/okjson.rb
203
+ - lib/vendor/mortar/uuid.rb
204
+ - spec/mortar/auth_spec.rb
205
+ - spec/mortar/command/auth_spec.rb
206
+ - spec/mortar/command/base_spec.rb
207
+ - spec/mortar/command/clusters_spec.rb
208
+ - spec/mortar/command/describe_spec.rb
209
+ - spec/mortar/command/generate_spec.rb
210
+ - spec/mortar/command/illustrate_spec.rb
211
+ - spec/mortar/command/jobs_spec.rb
212
+ - spec/mortar/command/pigscripts_spec.rb
213
+ - spec/mortar/command/projects_spec.rb
214
+ - spec/mortar/command/validate_spec.rb
215
+ - spec/mortar/command_spec.rb
216
+ - spec/mortar/git_spec.rb
217
+ - spec/mortar/helpers_spec.rb
218
+ - spec/mortar/project_spec.rb
219
+ - spec/mortar/snapshot_spec.rb
220
+ - spec/spec.opts
221
+ - spec/spec_helper.rb
222
+ - spec/support/display_message_matcher.rb
223
+ homepage: http://mortardata.com/
224
+ licenses: []
225
+
226
+ post_install_message:
227
+ rdoc_options: []
228
+
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ none: false
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ hash: 57
237
+ segments:
238
+ - 1
239
+ - 8
240
+ - 7
241
+ version: 1.8.7
242
+ required_rubygems_version: !ruby/object:Gem::Requirement
243
+ none: false
244
+ requirements:
245
+ - - ">="
246
+ - !ruby/object:Gem::Version
247
+ hash: 3
248
+ segments:
249
+ - 0
250
+ version: "0"
251
+ requirements: []
252
+
253
+ rubyforge_project:
254
+ rubygems_version: 1.8.24
255
+ signing_key:
256
+ specification_version: 3
257
+ summary: Client library and CLI to interact with the Mortar service.
258
+ test_files: []
259
+