unimatrix-cli 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +12 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/unimatrix +11 -0
- data/lib/unimatrix_cli.rb +74 -0
- data/lib/unimatrix_cli/alchemist/rendition/list_command.rb +72 -0
- data/lib/unimatrix_cli/alchemist/rendition_profile/assign_command.rb +59 -0
- data/lib/unimatrix_cli/alchemist/rendition_profile/list_command.rb +36 -0
- data/lib/unimatrix_cli/alchemist/video/create_command.rb +40 -0
- data/lib/unimatrix_cli/alchemist/video/describe_command.rb +40 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/create_command.rb +37 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/describe_command.rb +39 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/encode_command.rb +102 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/list_command.rb +35 -0
- data/lib/unimatrix_cli/archivist/blueprint/create_command.rb +94 -0
- data/lib/unimatrix_cli/citadel/app/build_command.rb +28 -0
- data/lib/unimatrix_cli/citadel/app/console_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/db_setup_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/environment_command.rb +30 -0
- data/lib/unimatrix_cli/citadel/app/logs_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/migrate_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/migrate_status_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/rake_command.rb +18 -0
- data/lib/unimatrix_cli/citadel/app/routes_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/citadel_command.rb +285 -0
- data/lib/unimatrix_cli/citadel/instance/details_command.rb +18 -0
- data/lib/unimatrix_cli/citadel/instance/scp_command.rb +48 -0
- data/lib/unimatrix_cli/citadel/instance/ssh_command.rb +29 -0
- data/lib/unimatrix_cli/citadel/instance/status_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/instance/user_data_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/instance/user_data_logs_command.rb +17 -0
- data/lib/unimatrix_cli/cli.rb +30 -0
- data/lib/unimatrix_cli/command.rb +138 -0
- data/lib/unimatrix_cli/config/acceptance.yml +23 -0
- data/lib/unimatrix_cli/config/configuration.rb +66 -0
- data/lib/unimatrix_cli/config/production.yml +15 -0
- data/lib/unimatrix_cli/config/staging.yml +15 -0
- data/lib/unimatrix_cli/config/unimatrix.rb +4 -0
- data/lib/unimatrix_cli/iris/stream/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream/describe_command.rb +35 -0
- data/lib/unimatrix_cli/iris/stream_encoder/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream_encoder/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_input/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream_input/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_output/create_command.rb +55 -0
- data/lib/unimatrix_cli/iris/stream_output/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_recorder/create_command.rb +49 -0
- data/lib/unimatrix_cli/iris/stream_recorder/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_transcriber/create_command.rb +54 -0
- data/lib/unimatrix_cli/iris/stream_transcriber/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_transmutator/create_command.rb +65 -0
- data/lib/unimatrix_cli/iris/stream_transmutator/describe_command.rb +38 -0
- data/lib/unimatrix_cli/login_command.rb +91 -0
- data/lib/unimatrix_cli/logout_command.rb +21 -0
- data/lib/unimatrix_cli/version.rb +3 -0
- data/lib/unimatrix_cli/zephyrus/input/create_command.rb +40 -0
- data/lib/unimatrix_cli/zephyrus/input/describe_command.rb +35 -0
- data/lib/unimatrix_cli/zephyrus/input/list_command.rb +38 -0
- data/lib/unimatrix_cli/zephyrus/output/list_command.rb +44 -0
- data/lib/unimatrix_cli/zephyrus/recording/configuration_command.rb +43 -0
- data/lib/unimatrix_cli/zephyrus/rendition/list_command.rb +40 -0
- data/lib/unimatrix_cli/zephyrus/routing/configuration_command.rb +43 -0
- data/lib/unimatrix_cli/zephyrus/transcoding/configuration_command.rb +55 -0
- data/lib/unimatrix_cli/zephyrus/transcribing/configuration_command.rb +55 -0
- data/lib/unimatrix_cli/zephyrus/transmutation/configuration_command.rb +55 -0
- data/lib/unimatrix_parser.rb +796 -0
- data/unimatrix-cli.gemspec +30 -0
- metadata +247 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
class CLI < Command
|
3
|
+
|
4
|
+
synopsis Unimatrix::VERSION +
|
5
|
+
"\n\nA CLI for interacting with Unimatrix apps\n\nAvailable commands:\n\n" +
|
6
|
+
available_commands +
|
7
|
+
"\n\nType 'unimatrix <command> --help' to see options\n"
|
8
|
+
|
9
|
+
educate_if_empty
|
10
|
+
stop_on_unknown
|
11
|
+
|
12
|
+
def parse_command
|
13
|
+
command = ARGV.shift
|
14
|
+
|
15
|
+
if command == "login" || command == "logout"
|
16
|
+
sub_command = "#{ command.capitalize }Command"
|
17
|
+
"#{ sub_command }"
|
18
|
+
else
|
19
|
+
unimatrix_app, resource, sub_command = command.split( '::' ).
|
20
|
+
map { | word | camelize( word ) }
|
21
|
+
sub_command = "#{ sub_command }Command"
|
22
|
+
"#{ unimatrix_app }::#{ resource }::#{ sub_command }"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private; def camelize( word )
|
27
|
+
word.split( '_' ).map( &:capitalize ).join
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module UnimatrixCLI
|
5
|
+
class Command
|
6
|
+
include UnimatrixParser
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@options = self.class.options
|
10
|
+
end
|
11
|
+
|
12
|
+
#----------------------------------------------------------------------------
|
13
|
+
# Helpers
|
14
|
+
|
15
|
+
# TODO add color options
|
16
|
+
def write( options )
|
17
|
+
if options[ :error ]
|
18
|
+
$stderr.puts options[ :message ]
|
19
|
+
else
|
20
|
+
$stdout.puts options[ :message ]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate( object, type )
|
25
|
+
object.present? &&
|
26
|
+
object.is_a?( "Unimatrix::#{ type }".constantize ) &&
|
27
|
+
object.errors.blank? rescue nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_collection( objects, type )
|
31
|
+
if objects.present?
|
32
|
+
objects.all? do | object |
|
33
|
+
validate( object, type )
|
34
|
+
end
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#----------------------------------------------------------------------------
|
41
|
+
# File Reading
|
42
|
+
|
43
|
+
def read_file( path )
|
44
|
+
begin
|
45
|
+
file = File.open( path, 'r' )
|
46
|
+
extension = path.split( '.' ).last.downcase
|
47
|
+
contents = file_to_hash( file, extension )
|
48
|
+
rescue
|
49
|
+
write(
|
50
|
+
message: "There was a problem accessing and reading #{ path }",
|
51
|
+
error: true
|
52
|
+
)
|
53
|
+
ensure
|
54
|
+
file.close
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private; def file_to_hash( file, extension )
|
59
|
+
contents = file.read
|
60
|
+
if extension == 'yaml'
|
61
|
+
contents = YAML.load( contents )
|
62
|
+
elsif extension == 'json'
|
63
|
+
contents = JSON.parse( contents )
|
64
|
+
else
|
65
|
+
write(
|
66
|
+
message: "The file extension #{ extension } is not supported",
|
67
|
+
error: true
|
68
|
+
)
|
69
|
+
end
|
70
|
+
contents
|
71
|
+
end
|
72
|
+
|
73
|
+
#----------------------------------------------------------------------------
|
74
|
+
# Requests
|
75
|
+
|
76
|
+
def operation( endpoint, options = {} )
|
77
|
+
unless self.class.to_s.include?( 'Zephyrus' )
|
78
|
+
options.merge!( access_token: Configuration.access_token )
|
79
|
+
end
|
80
|
+
Unimatrix::Operation.new( endpoint, options )
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_object( object )
|
84
|
+
object.fields.map do | field |
|
85
|
+
key = field.first
|
86
|
+
value = object.send( key )
|
87
|
+
"#{ key }: #{ value }\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def make_request( endpoint, http_method, body = nil )
|
92
|
+
uri = URI.parse( endpoint )
|
93
|
+
http = Net::HTTP.new( uri.host, uri.port )
|
94
|
+
http.use_ssl = ( uri.scheme == 'https' )
|
95
|
+
|
96
|
+
http_request = Module.const_get( "Net::HTTP::#{ http_method }" )
|
97
|
+
|
98
|
+
response = nil
|
99
|
+
begin
|
100
|
+
request = http_request.new( endpoint, 'Content-Type' => 'application/json' )
|
101
|
+
request.body = body.to_json if body
|
102
|
+
response = http.request( request )
|
103
|
+
rescue Timeout::Error
|
104
|
+
response = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
parse_response( response.body ) if response
|
108
|
+
end
|
109
|
+
|
110
|
+
private; def parse_response( body )
|
111
|
+
JSON.parse( body ) rescue nil
|
112
|
+
end
|
113
|
+
|
114
|
+
#----------------------------------------------------------------------------
|
115
|
+
# Class Methods
|
116
|
+
|
117
|
+
class << self
|
118
|
+
def descendants
|
119
|
+
ObjectSpace.each_object( Class ).select { | klass | klass < self }
|
120
|
+
end
|
121
|
+
|
122
|
+
def available_commands
|
123
|
+
commands = Command.descendants
|
124
|
+
commands.delete( UnimatrixCLI::CLI )
|
125
|
+
commands.delete( UnimatrixCLI::Citadel::CitadelCommand )
|
126
|
+
|
127
|
+
parsed_commands = commands.map do | command |
|
128
|
+
command_words = command.to_s.split( '::' )
|
129
|
+
command_words.shift
|
130
|
+
command_words.last.slice!( 'Command' )
|
131
|
+
command_words.map( &:underscore ).join( '::' )
|
132
|
+
end
|
133
|
+
|
134
|
+
parsed_commands.sort.join( "\n" )
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
us-east-1:
|
2
|
+
keymaker_url: "http://keymaker.acceptance.boxxspring.com"
|
3
|
+
archivist_url: ""
|
4
|
+
rendition_profile_uuids:
|
5
|
+
- "011e6f444a1e8948f246b45c798e5a16"
|
6
|
+
- "c22924af95ba00825b9863c6b184e7b5"
|
7
|
+
- "2dd9cbab22ed92bb9d2ccc9837df9a3c"
|
8
|
+
- "fef49a4734f873246388384ce1c7e200"
|
9
|
+
- "17ed2ae6c2f091c4c7b1753a17a698b7"
|
10
|
+
zephyrus_url: "http://zephyrus.acceptance.boxxspring.com"
|
11
|
+
unimatrix_api_url: "http://api.acceptance.unimatrix.io"
|
12
|
+
|
13
|
+
us-west-2:
|
14
|
+
keymaker_url: "http://us-west-2.keymaker.acceptance.boxxspring.net"
|
15
|
+
archivist_url: "http://us-west-2.archivist.acceptance.boxxspring.net"
|
16
|
+
rendition_profile_uuids:
|
17
|
+
- "011e6f444a1e8948f246b45c798e5a16"
|
18
|
+
- "c22924af95ba00825b9863c6b184e7b5"
|
19
|
+
- "2dd9cbab22ed92bb9d2ccc9837df9a3c"
|
20
|
+
- "fef49a4734f873246388384ce1c7e200"
|
21
|
+
- "17ed2ae6c2f091c4c7b1753a17a698b7"
|
22
|
+
zephyrus_url: "http://zephyrus.acceptance.boxxspring.com"
|
23
|
+
unimatrix_api_url: "http://us-west-2.api.acceptance.unimatrix.io"
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module UnimatrixCLI
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def default_config
|
9
|
+
@default_config ||= begin
|
10
|
+
default_config_path = File.join(
|
11
|
+
File.dirname( __FILE__ ), "./#{ environment }.yml"
|
12
|
+
)
|
13
|
+
if File.file?( default_config_path )
|
14
|
+
default_config_file = YAML.load_file( default_config_path )
|
15
|
+
default_config_file[ region ]
|
16
|
+
else
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def environment
|
23
|
+
@environment ||= begin
|
24
|
+
File.read( environment_file_path ).chomp if File.file?( environment_file_path )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def access_token
|
29
|
+
@access_token ||= begin
|
30
|
+
File.read( access_token_file_path ).chomp if File.file?( access_token_file_path )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def region
|
35
|
+
@region ||= begin
|
36
|
+
File.read( region_file_path ).chomp if File.file?( region_file_path ) ||
|
37
|
+
ENV[ 'AWS_REGION' ]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def unimatrix_directory_path
|
42
|
+
@unimatrix_directory_path ||= File.join( Dir.home, ".unimatrix" )
|
43
|
+
end
|
44
|
+
|
45
|
+
def environment_file_path
|
46
|
+
@environment_file_path ||= begin
|
47
|
+
File.join( unimatrix_directory_path, "/.environment" )
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def access_token_file_path
|
52
|
+
@access_token_file_path ||= begin
|
53
|
+
File.join( unimatrix_directory_path, "/.access_token" )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def region_file_path
|
58
|
+
@region_file_path ||= begin
|
59
|
+
File.join( unimatrix_directory_path, "/.region" )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
us-east-1:
|
2
|
+
keymaker_url: "http://keymaker.boxxspring.com"
|
3
|
+
archivist_url: ""
|
4
|
+
rendition_profile_uuids:
|
5
|
+
- ""
|
6
|
+
zephyrus_url: ""
|
7
|
+
unimatrix_api_url: ""
|
8
|
+
|
9
|
+
us-west-2:
|
10
|
+
keymaker_url: ""
|
11
|
+
archivist_url: ""
|
12
|
+
rendition_profile_uuids:
|
13
|
+
- ""
|
14
|
+
zephyrus_url: ""
|
15
|
+
unimatrix_api_url: ""
|
@@ -0,0 +1,15 @@
|
|
1
|
+
us-east-1:
|
2
|
+
keymaker_url: "http://keymaker.staging.boxxspring.com"
|
3
|
+
archivist_url: ""
|
4
|
+
rendition_profile_uuids:
|
5
|
+
- ""
|
6
|
+
zephyrus_url: "http://zephyrus.staging.boxxspring.com"
|
7
|
+
unimatrix_api_url: "http://api.staging.unimatrix.io"
|
8
|
+
|
9
|
+
us-west-2:
|
10
|
+
keymaker_url: "http://us-west-2.keymaker.staging.boxxspring.net"
|
11
|
+
archivist_url: "http://us-west-2.archivist.staging.boxxspring.net"
|
12
|
+
rendition_profile_uuids:
|
13
|
+
- ""
|
14
|
+
zephyrus_url: "http://zephyrus.staging.boxxspring.com"
|
15
|
+
unimatrix_api_url: "http://us-west-2.api.staging.unimatrix.io"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Iris
|
3
|
+
module Stream
|
4
|
+
class CreateCommand < Command
|
5
|
+
|
6
|
+
option :realm, "The realm uuid", type: :string, required: true
|
7
|
+
option :name, "The name of the stream", type: :string, required: true
|
8
|
+
option :type, "The type (event or continuous)", type: :string, default: "event"
|
9
|
+
|
10
|
+
synopsis "Create a new stream"
|
11
|
+
|
12
|
+
def execute
|
13
|
+
stream = create_stream
|
14
|
+
|
15
|
+
if validate( stream, 'Iris::Stream' )
|
16
|
+
write(
|
17
|
+
message: "Stream uuid: #{ stream.uuid }"
|
18
|
+
)
|
19
|
+
else
|
20
|
+
write(
|
21
|
+
message: "Error creating stream: #{ stream.inspect }",
|
22
|
+
error: true
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#----------------------------------------------------------------------------
|
28
|
+
|
29
|
+
private; def create_stream
|
30
|
+
stream = Unimatrix::Iris::Stream.new(
|
31
|
+
name: @options[ :name ],
|
32
|
+
stream_type: @options[ :type ]
|
33
|
+
)
|
34
|
+
|
35
|
+
operation(
|
36
|
+
'/streams',
|
37
|
+
realm_uuid: @options[ :realm ]
|
38
|
+
).write( 'streams', stream ).first rescue nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Iris
|
3
|
+
module Stream
|
4
|
+
class DescribeCommand < Command
|
5
|
+
|
6
|
+
option :stream, "The stream uuid", type: :string, required: true
|
7
|
+
|
8
|
+
synopsis "Describe a stream"
|
9
|
+
|
10
|
+
def execute
|
11
|
+
stream = find_stream
|
12
|
+
|
13
|
+
if validate( stream, 'Iris::Stream' )
|
14
|
+
parse_object( stream ).each do | response |
|
15
|
+
write( message: response )
|
16
|
+
end
|
17
|
+
else
|
18
|
+
write(
|
19
|
+
message: "Stream could not be found: #{ stream.inspect }",
|
20
|
+
error: true
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#----------------------------------------------------------------------------
|
26
|
+
|
27
|
+
private; def find_stream
|
28
|
+
endpoint = "/streams/#{ @options[ :stream ] }"
|
29
|
+
|
30
|
+
operation( endpoint ).read.first
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Iris
|
3
|
+
module StreamEncoder
|
4
|
+
class CreateCommand < Command
|
5
|
+
|
6
|
+
option :realm, "The realm uuid", type: :string, required: true
|
7
|
+
option :stream, "The stream_encoder's stream", type: :string, required: true
|
8
|
+
option :region, "The region of the stream", type: :string, default: "us-east-1"
|
9
|
+
|
10
|
+
synopsis "Create a new stream_encoder"
|
11
|
+
|
12
|
+
def execute
|
13
|
+
stream_encoder = create_stream_encoder
|
14
|
+
|
15
|
+
if validate( stream_encoder, 'Iris::StreamEncoder' )
|
16
|
+
write( message: "StreamEncoder uuid: #{ stream_encoder.uuid }" )
|
17
|
+
else
|
18
|
+
write(
|
19
|
+
message: "Error creating stream_encoder: #{ stream_encoder.inspect }",
|
20
|
+
error: true
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#----------------------------------------------------------------------------
|
26
|
+
|
27
|
+
private; def create_stream_encoder
|
28
|
+
endpoint = "/streams/#{ @options[ :stream ] }/stream_encoders"
|
29
|
+
|
30
|
+
stream_encoder = Unimatrix::Iris::StreamEncoder.new(
|
31
|
+
region: @options[ :region ],
|
32
|
+
state: 'default'
|
33
|
+
)
|
34
|
+
|
35
|
+
operation(
|
36
|
+
endpoint,
|
37
|
+
realm_uuid: @options[ :realm ]
|
38
|
+
).write( 'stream_encoders', stream_encoder ).first rescue nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Iris
|
3
|
+
module StreamEncoder
|
4
|
+
class DescribeCommand < Command
|
5
|
+
|
6
|
+
option :stream_encoder, "The stream_encoder uuid", type: :string, required: true, short: :e
|
7
|
+
option :stream, "The uuid of its stream", type: :string, required: true
|
8
|
+
|
9
|
+
synopsis "Describe a stream_encoder"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
stream_encoder = find_stream_encoder
|
13
|
+
|
14
|
+
if validate( stream_encoder, 'Iris::StreamEncoder' )
|
15
|
+
parse_object( stream_encoder ).each do | response |
|
16
|
+
write( message: response )
|
17
|
+
end
|
18
|
+
else
|
19
|
+
write(
|
20
|
+
message: "StreamEncoder could not be found: #{ stream_encoder.inspect }",
|
21
|
+
error: true
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#----------------------------------------------------------------------------
|
27
|
+
|
28
|
+
private; def find_stream_encoder
|
29
|
+
endpoint = "/streams/#{ @options[ :stream ] }/stream_encoders/" +
|
30
|
+
"#{ @options[ :stream_encoder ] }"
|
31
|
+
|
32
|
+
operation( endpoint ).read.first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|