cloudalign-cli 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +122 -0
- data/bin/cloudalign +86 -0
- data/lib/cloudalign/artifact.rb +24 -0
- data/lib/cloudalign/base_entity.rb +12 -0
- data/lib/cloudalign/client.rb +74 -0
- data/lib/cloudalign/config.rb +46 -0
- data/lib/cloudalign/file.rb +34 -0
- data/lib/cloudalign/project.rb +24 -0
- data/lib/cloudalign.rb +28 -0
- metadata +119 -0
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# Introduction
|
2
|
+
This is a Command Line Interface (CLI) to be used for accessing a remote CloudAlign system and interacting with it. This
|
3
|
+
tool is useful for creating scripts or other automated processes that one would need to interface with CloudAlign.
|
4
|
+
|
5
|
+
Also included with this tool is a simple Ruby library for talking to CloudAlign directly.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
### Dependencies
|
10
|
+
- Ruby 1.9.3 or greater
|
11
|
+
- RubyGems (http://www.rubygems.org ships with 1.9.x)
|
12
|
+
- A Unix like OS (Tested with Ubuntu 12 and Mac OSX)
|
13
|
+
|
14
|
+
### Setup
|
15
|
+
Simply install the CloudAlign CLI using GEM
|
16
|
+
<pre>
|
17
|
+
$ gem install cloudalign-cli
|
18
|
+
</pre>
|
19
|
+
|
20
|
+
### Configuration
|
21
|
+
Create a config file
|
22
|
+
<pre>
|
23
|
+
$ mkdir -p ~/cloudalign
|
24
|
+
$ vi ~/cloudalign/config.yml
|
25
|
+
</pre>
|
26
|
+
|
27
|
+
Finally, populate the config file with your credentials
|
28
|
+
<pre>
|
29
|
+
# File ~/cloudalign/config.yml
|
30
|
+
api_url: http://web1.dev.cloudalign.accetia.com
|
31
|
+
api_user: jmccaffrey
|
32
|
+
api_password: shhhitssecret
|
33
|
+
</pre>
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
### Supported Commands
|
38
|
+
Here are the supported operations with the current version of the CloudAlign Command Line Interface (CLI). Also listed
|
39
|
+
with each operation is a sample of how to use it.
|
40
|
+
|
41
|
+
#### Access Help Documentation
|
42
|
+
<pre>
|
43
|
+
$ cloudalign
|
44
|
+
Tasks:
|
45
|
+
cloudalign artifact <command> # Manipulate artifacts in the CloudAlign system
|
46
|
+
cloudalign help [TASK] # Describe available tasks or one specific task
|
47
|
+
cloudalign project <command> # Manipulate projects in the CloudAlign system
|
48
|
+
|
49
|
+
$ cloudalign artifact
|
50
|
+
cloudalign artifact align ARTIFACT # ...
|
51
|
+
cloudalign artifact compare ARTIFACT REFERENCE # ...
|
52
|
+
cloudalign artifact download ARTIFACT FILE # Downloads file FILE ...
|
53
|
+
cloudalign artifact help [COMMAND] # Describe subcommands...
|
54
|
+
cloudalign artifact list_files ARTIFACT # Lists all files in ...
|
55
|
+
|
56
|
+
$ cloudalign project
|
57
|
+
cloudalign project help [COMMAND] # Describe subcommands or...
|
58
|
+
cloudalign project list # Lists all viewable proj...
|
59
|
+
cloudalign project list_artifacts PROJECT # Lists all viewable arti...
|
60
|
+
cloudalign project upload_file PROJECT PATH # Uploads a file located ...
|
61
|
+
|
62
|
+
$ cloudalign project help list
|
63
|
+
Usage:
|
64
|
+
cloudalign list
|
65
|
+
|
66
|
+
Lists all viewable projects for your user
|
67
|
+
</pre>
|
68
|
+
|
69
|
+
#### List Projects
|
70
|
+
<pre>
|
71
|
+
$ cloudalign project list
|
72
|
+
+----+-----------------+
|
73
|
+
| id | name |
|
74
|
+
+----+-----------------+
|
75
|
+
| 1 | My Test Project |
|
76
|
+
+----+-----------------+
|
77
|
+
| 2 | asdf |
|
78
|
+
+----+-----------------+
|
79
|
+
|
80
|
+
</pre>
|
81
|
+
|
82
|
+
#### List Artifacts in a Project
|
83
|
+
<pre>
|
84
|
+
$ cloudalign project list_artifacts 2
|
85
|
+
+----+-----------+
|
86
|
+
| id | name |
|
87
|
+
+----+-----------+
|
88
|
+
| 20 | 40mb_file |
|
89
|
+
+----+-----------+
|
90
|
+
</pre>
|
91
|
+
|
92
|
+
#### List Files within an Artifact
|
93
|
+
<pre>
|
94
|
+
$ cloudalign artifact list_files 20
|
95
|
+
+-----+---------------+----------+--------+
|
96
|
+
| id | name | size | status |
|
97
|
+
+-----+---------------+----------+--------+
|
98
|
+
| 137 | 40mb_file.bin | 41943040 | READY |
|
99
|
+
+-----+---------------+----------+--------+
|
100
|
+
</pre>
|
101
|
+
|
102
|
+
#### Download a File
|
103
|
+
<pre>
|
104
|
+
$ cloudalign file download 20 137
|
105
|
+
Downloading file 40mb_file.bin from Artifact 20 to ./
|
106
|
+
...
|
107
|
+
Done
|
108
|
+
</pre>
|
109
|
+
|
110
|
+
#### Upload a File and Create Artifact
|
111
|
+
<pre>
|
112
|
+
$ cloudalign project upload 2 test.fastq
|
113
|
+
Uploading file test.fastq to project 'asdf'
|
114
|
+
...
|
115
|
+
Done
|
116
|
+
</pre>
|
117
|
+
|
118
|
+
#### Run BWA Alignment
|
119
|
+
<pre>
|
120
|
+
$ cloudalign artifact 20 align
|
121
|
+
Running alignment on artifact 'Test Artifact 123', use artifact list_files to see the status.
|
122
|
+
</pre>
|
data/bin/cloudalign
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift 'lib'
|
3
|
+
require "cloudalign"
|
4
|
+
require "thor"
|
5
|
+
require "thor/group"
|
6
|
+
require "pp"
|
7
|
+
require "formatador"
|
8
|
+
|
9
|
+
class ProjectTask < Thor
|
10
|
+
desc "list", "Lists all viewable projects for your user"
|
11
|
+
def list
|
12
|
+
projects = CloudAlign::Project.find_all.map do |p|
|
13
|
+
{
|
14
|
+
:id => p.id,
|
15
|
+
:name => p.name
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
Formatador.display_table(projects)
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "list_artifacts PROJECT", "Lists all viewable artifacts in <PROJECT>"
|
23
|
+
def list_artifacts(project_id)
|
24
|
+
artifacts = CloudAlign::Artifact.find_by_project(project_id).map do |a|
|
25
|
+
{
|
26
|
+
:id => a.id,
|
27
|
+
:name => a.name,
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
Formatador.display_table(artifacts)
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "upload_file PROJECT PATH", "Uploads a file located at PATH to the PROJECT in CloudAlign"
|
35
|
+
def upload_file(project_id, path)
|
36
|
+
project = CloudAlign::Project.find(project_id)
|
37
|
+
puts "Uploading file #{File.basename(path)} to project '#{project.name}'"
|
38
|
+
puts "..."
|
39
|
+
project.upload_file(path)
|
40
|
+
puts "Done"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class ArtifactTask < Thor
|
46
|
+
desc "list_files ARTIFACT", "Lists all files in <ARTIFACT>"
|
47
|
+
def list_files(artifact_id)
|
48
|
+
files = CloudAlign::File.find_by_artifact(artifact_id).map do |f|
|
49
|
+
{
|
50
|
+
:id => f.id,
|
51
|
+
:name => f.name,
|
52
|
+
:size => f.size,
|
53
|
+
:status => f.status
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
Formatador.display_table(files)
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "download ARTIFACT FILE", "Downloads file FILE from the ARTIFACT in CloudAlign"
|
61
|
+
def download(artifact_id, file_id)
|
62
|
+
file = CloudAlign::File.find(artifact_id, file_id)
|
63
|
+
puts "Downloading file #{file.file_name} from Artifact #{file.artifact_id} to ./"
|
64
|
+
puts "..."
|
65
|
+
file.download
|
66
|
+
puts "Done"
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "align ARTIFACT", "..."
|
70
|
+
def align(artifact_id)
|
71
|
+
artifact = CloudAlign::Artifact.find(artifact_id)
|
72
|
+
artifact.analyze(:bwa_alignment)
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "compare ARTIFACT REFERENCE", "..."
|
76
|
+
def compare(artifact_id, reference)
|
77
|
+
puts "Not Implemented!"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class CLI < Thor
|
82
|
+
register(ProjectTask, 'project', 'project <command>', 'Manipulate projects in the CloudAlign system')
|
83
|
+
register(ArtifactTask, 'artifact', 'artifact <command>', 'Manipulate artifacts in the CloudAlign system')
|
84
|
+
end
|
85
|
+
|
86
|
+
CLI.start
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CloudAlign
|
2
|
+
class Artifact < BaseEntity
|
3
|
+
attr_accessor :name, :description
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def find_by_project(project_id)
|
8
|
+
Client.get_json("/projects/#{project_id}/artifacts").map do |row|
|
9
|
+
Artifact.new(row)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def analyze(analyzer, options = {})
|
16
|
+
options[:analyzer] = analyzer
|
17
|
+
Client.post("/artifacts/#{@id}/analyze", options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy
|
21
|
+
Client.delete("/artifacts/#{@id}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
class String
|
5
|
+
def starts_with?(prefix)
|
6
|
+
prefix = prefix.to_s
|
7
|
+
self[0, prefix.length] == prefix
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module CloudAlign
|
12
|
+
class Client
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def get(path)
|
16
|
+
RestClient.get(authorized_url(path))
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_json(path)
|
20
|
+
uri = URI.parse(path)
|
21
|
+
uri.path += '.json' unless uri.path.match(/\.json$|\/$/)
|
22
|
+
JSON.parse(get(uri.to_s))
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(path)
|
26
|
+
RestClient.delete(authorized_url(path))
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_file(path, output_path)
|
30
|
+
::File.open(output_path, 'w') do |out|
|
31
|
+
process_response = lambda do |response|
|
32
|
+
response.read_body do |chunk|
|
33
|
+
print "."
|
34
|
+
out.write chunk
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RestClient::Request.execute(:method => :get, :url => authorized_url(path), :block_response => process_response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def post(path, data)
|
43
|
+
RestClient.post(authorized_url(path), data)
|
44
|
+
end
|
45
|
+
|
46
|
+
def post_for_upload(path, data)
|
47
|
+
pp data
|
48
|
+
RestClient.post(authorized_url(path), data) do |response, request, result, &block|
|
49
|
+
if [301, 302, 303, 307].include? response.code
|
50
|
+
return get_json(response.headers[:location])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorized_url(path)
|
56
|
+
url = CloudAlign.config.get("api_url", "http://api.cloudalign.com").sub(/\/$/, '')
|
57
|
+
|
58
|
+
if path.starts_with?(url)
|
59
|
+
uri = URI.parse(path)
|
60
|
+
elsif path.starts_with?('/')
|
61
|
+
uri = URI.parse(url)
|
62
|
+
uri.path = path
|
63
|
+
else
|
64
|
+
return path
|
65
|
+
end
|
66
|
+
|
67
|
+
uri.user = CGI.escape(CloudAlign.config.get("api_user"))
|
68
|
+
uri.password = CGI.escape(CloudAlign.config.get("api_password"))
|
69
|
+
|
70
|
+
uri.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module CloudAlign
|
5
|
+
class Config
|
6
|
+
def initialize(path = nil)
|
7
|
+
@config = {}
|
8
|
+
load path
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(key, value)
|
12
|
+
set_recursive(key, value, @config)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key, default = nil)
|
16
|
+
p = @config
|
17
|
+
key.split('.').each do |part|
|
18
|
+
return default unless p.has_key? part
|
19
|
+
p = p[part]
|
20
|
+
end
|
21
|
+
|
22
|
+
p
|
23
|
+
end
|
24
|
+
|
25
|
+
def load(path = nil)
|
26
|
+
if path.nil?
|
27
|
+
config_paths = [Pathname.new(Dir.home).join('.cloudalign', 'config.yml').to_s]
|
28
|
+
path = config_paths.detect{|p| ::File.exists?(p)}
|
29
|
+
end
|
30
|
+
|
31
|
+
@config = YAML.load_file(path)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_recursive(key, value, array)
|
37
|
+
if key.include? '.'
|
38
|
+
(part, key) = key.split('.', 2)
|
39
|
+
array[part] = {} unless array.has_key? part
|
40
|
+
return set_recursive(key, value, array[part])
|
41
|
+
end
|
42
|
+
|
43
|
+
array[key] = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CloudAlign
|
2
|
+
class File < BaseEntity
|
3
|
+
attr_accessor :name, :storage_path, :size, :ready
|
4
|
+
|
5
|
+
def download(output_path = nil)
|
6
|
+
output_path = self.file_name if output_path.nil?
|
7
|
+
download_info = Client.get_json("/artifacts/#{@artifact_id}/files/#{@id}/download")
|
8
|
+
Client.get_file(download_info["download_url"], output_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def file_name
|
12
|
+
::File.basename(@storage_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
statuses = %w(READY BUILDING UPLOADING)
|
17
|
+
(@status >= 0 && @status < statuses.length) ? statuses[@status] : @status
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
def find(artifact_id, id)
|
23
|
+
File.new(Client.get_json("/artifacts/#{artifact_id}/files/#{id}"))
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_by_artifact(artifact_id)
|
27
|
+
Client.get_json("/artifacts/#{artifact_id}/files").map do |row|
|
28
|
+
File.new(row)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CloudAlign
|
2
|
+
class Project < BaseEntity
|
3
|
+
attr_accessor :name, :description
|
4
|
+
|
5
|
+
def self.find(id)
|
6
|
+
Project.new(Client.get_json("/projects/#{id}"))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.find_all
|
10
|
+
Client.get_json("/projects").map do |row|
|
11
|
+
Project.new(row)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def upload_file(path)
|
16
|
+
post_data = Client.get_json("/projects/#{@id}/upload_file")
|
17
|
+
upload_url = post_data.delete("url")
|
18
|
+
post_data[:file] = ::File.new(path, 'rb')
|
19
|
+
|
20
|
+
Client.post_for_upload(upload_url, post_data)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/cloudalign.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module CloudAlign
|
6
|
+
autoload :BaseEntity, 'cloudalign/base_entity'
|
7
|
+
autoload :Client, 'cloudalign/client'
|
8
|
+
autoload :Project, 'cloudalign/project'
|
9
|
+
autoload :Artifact, 'cloudalign/artifact'
|
10
|
+
autoload :File, 'cloudalign/file'
|
11
|
+
autoload :Config, 'cloudalign/config'
|
12
|
+
|
13
|
+
API_VERSION = 1
|
14
|
+
|
15
|
+
class Error < StandardError; end
|
16
|
+
|
17
|
+
def self.logger
|
18
|
+
@logger ||= Logger.new(STDOUT)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.logger=(logger)
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.config
|
26
|
+
@config ||= Config.new
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudalign-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan McCaffrey
|
9
|
+
- Jeffrey Biles
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-08-30 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thor
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rest-client
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: formatador
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: rspec
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
description: Cloudalign brings the power of the cloud straight to your lab
|
80
|
+
email:
|
81
|
+
executables:
|
82
|
+
- cloudalign
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- bin/cloudalign
|
87
|
+
- lib/cloudalign/artifact.rb
|
88
|
+
- lib/cloudalign/base_entity.rb
|
89
|
+
- lib/cloudalign/client.rb
|
90
|
+
- lib/cloudalign/config.rb
|
91
|
+
- lib/cloudalign/file.rb
|
92
|
+
- lib/cloudalign/project.rb
|
93
|
+
- lib/cloudalign.rb
|
94
|
+
- README.md
|
95
|
+
homepage: http://rubygems.org/gems/cloudalign-cli
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project: cloudalign-cli
|
115
|
+
rubygems_version: 1.8.24
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Access cloudalign from your command line
|
119
|
+
test_files: []
|