detroit 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.index +59 -0
- data/EXAMPLE.md +66 -64
- data/{HISTORY.rdoc → HISTORY.md} +32 -5
- data/{COPYING.rdoc → LICENSE.txt} +0 -0
- data/README.md +142 -0
- data/bin/detroit +1 -1
- data/lib/detroit.rb +112 -40
- data/lib/detroit.yml +44 -29
- data/lib/detroit/assembly.rb +49 -193
- data/lib/detroit/basic_tool.rb +200 -0
- data/lib/detroit/basic_utils.rb +66 -0
- data/lib/detroit/core_ext.rb +2 -136
- data/lib/detroit/{tool/core_ext → core_ext}/facets.rb +3 -0
- data/lib/detroit/{tool/core_ext → core_ext}/filetest.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/shell_extensions.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_actual_filename.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_console.rb +1 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_list.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_yamlfrag.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/unfold_paragraphs.rb +0 -0
- data/lib/detroit/{tool/email_utils.rb → email_utils.rb} +3 -1
- data/lib/detroit/exec.rb +55 -0
- data/lib/detroit/project.rb +134 -0
- data/lib/detroit/ruby_utils.rb +29 -0
- data/lib/detroit/{tool/shell_utils.rb → shell_utils.rb} +10 -5
- data/lib/detroit/toolchain.rb +6 -0
- data/lib/detroit/toolchain/cli.rb +320 -0
- data/lib/detroit/toolchain/config.rb +223 -0
- data/lib/detroit/toolchain/runner.rb +678 -0
- data/lib/detroit/toolchain/script.rb +248 -0
- data/lib/detroit/toolchain/worker.rb +84 -0
- data/man/detroit.1 +116 -0
- data/man/detroit.1.html +171 -0
- data/man/detroit.1.ronn +99 -0
- metadata +90 -51
- data/.ruby +0 -44
- data/README.rdoc +0 -132
- data/lib/detroit/application.rb +0 -463
- data/lib/detroit/assembly_system.rb +0 -80
- data/lib/detroit/config.rb +0 -203
- data/lib/detroit/control.rb +0 -129
- data/lib/detroit/custom.rb +0 -102
- data/lib/detroit/dsl.rb +0 -55
- data/lib/detroit/service.rb +0 -78
- data/lib/detroit/standard_assembly.rb +0 -51
- data/lib/detroit/tool.rb +0 -295
- data/lib/detroit/tool/core_ext.rb +0 -3
- data/lib/detroit/tool/project_utils.rb +0 -41
@@ -1,80 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# All assemblies and tracks have a maintenance sub-track.
|
4
|
-
# For this reason stop names `reset`, `clean` and `purge`
|
5
|
-
# are reserved names and MUST not be used as stop names
|
6
|
-
# in defining custom lines.
|
7
|
-
MAINTENANCE_TRACK = [:reset, :clean, :purge]
|
8
|
-
|
9
|
-
# Returns Hash of name and Circuit instance pairs.
|
10
|
-
def self.assembly_systems
|
11
|
-
@assembly_system ||= {}
|
12
|
-
end
|
13
|
-
|
14
|
-
# Define a new assembly system.
|
15
|
-
def self.assembly_system(name, &block)
|
16
|
-
assembly_systems[name.to_sym] = AssemblySystem.new(name, &block)
|
17
|
-
end
|
18
|
-
|
19
|
-
# The AssemblySystem class encapsulates a set of interrelated
|
20
|
-
# assembly lines, or _tracks_.
|
21
|
-
class AssemblySystem
|
22
|
-
|
23
|
-
# Name of the assembly system.
|
24
|
-
attr :name
|
25
|
-
|
26
|
-
# Returns a Hash of track names mapped to list of stops.
|
27
|
-
attr :lines
|
28
|
-
|
29
|
-
# Lines are also called `tracks`.
|
30
|
-
alias_method :tracks, :lines
|
31
|
-
|
32
|
-
# Create a new instance.
|
33
|
-
def initialize(name, &block)
|
34
|
-
@name = name.to_sym
|
35
|
-
@lines = {:maintenance => MAINTENANCE_TRACK}
|
36
|
-
instance_eval(&block) if block
|
37
|
-
end
|
38
|
-
|
39
|
-
# Define an assembly line.
|
40
|
-
def line(name, *stops)
|
41
|
-
if stops.empty?
|
42
|
-
@lines[name.to_sym]
|
43
|
-
else
|
44
|
-
@lines[name.to_sym] = stops.map{ |s| s.to_sym }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Lines are also called tracks.
|
49
|
-
alias_method :track, :line
|
50
|
-
|
51
|
-
# Lookup track by name and (optional) stop. If the stop belongs
|
52
|
-
# to the maintenance sub-track then the maintenance sub-track will
|
53
|
-
# be returned instead of the track itself.
|
54
|
-
#
|
55
|
-
# The Application class uses this to simplify track lookup.
|
56
|
-
def get_track(name, stop=nil)
|
57
|
-
name = name.to_sym
|
58
|
-
if stop
|
59
|
-
stop = stop.to_sym
|
60
|
-
if MAINTENANCE_TRACK.include?(stop.to_sym)
|
61
|
-
track = MAINTENANCE_TRACK
|
62
|
-
else
|
63
|
-
track = tracks[name]
|
64
|
-
raise "Unknown track `#{name}'." unless track
|
65
|
-
unless track.include?(stop)
|
66
|
-
raise "Unknown stop `#{stop}` for track `#{name}'."
|
67
|
-
end
|
68
|
-
end
|
69
|
-
else
|
70
|
-
track = tracks[name]
|
71
|
-
end
|
72
|
-
track
|
73
|
-
end
|
74
|
-
|
75
|
-
# Did I mention that `line` and `track` are synonyms?
|
76
|
-
alias_method :get_line, :get_track
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
end
|
data/lib/detroit/config.rb
DELETED
@@ -1,203 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# Detroit configuration. Configuration comes from a main +Routine+
|
4
|
-
# and/or +.routine+ files.
|
5
|
-
class Config
|
6
|
-
#instance_methods.each{ |m| private m unless /^__/ =~ m.to_s }
|
7
|
-
|
8
|
-
# Configuration directory name (most likely a hidden "dot" directory).
|
9
|
-
DIRECTORY = "detroit"
|
10
|
-
|
11
|
-
# File identifier used to find a project's Assembly(s).
|
12
|
-
FILE_EXTENSION = "assembly"
|
13
|
-
|
14
|
-
# Current POM::Project object.
|
15
|
-
#attr :project
|
16
|
-
|
17
|
-
# The list of a project's assembly files.
|
18
|
-
#
|
19
|
-
# @return [Array<String>] routine files
|
20
|
-
attr :assemblies
|
21
|
-
|
22
|
-
# Service configurations from Assembly or *.assembly files.
|
23
|
-
#
|
24
|
-
# @return [Hash] service settings
|
25
|
-
attr :services
|
26
|
-
|
27
|
-
# Service defaults. This is a mapping of service names to
|
28
|
-
# default settings. Very useful for when using the same
|
29
|
-
# service more than once.
|
30
|
-
#
|
31
|
-
# @return [Hash] default settings
|
32
|
-
attr :defaults
|
33
|
-
|
34
|
-
#
|
35
|
-
def initialize(assembly_files=nil)
|
36
|
-
if assembly_files && !assembly_files.empty?
|
37
|
-
@assembly_filenames = assembly_files
|
38
|
-
else
|
39
|
-
@assembly_filenames = nil
|
40
|
-
end
|
41
|
-
|
42
|
-
@assemblies = {}
|
43
|
-
@services = {}
|
44
|
-
@defaults = {}
|
45
|
-
|
46
|
-
@loaded_plugins = {}
|
47
|
-
|
48
|
-
load_plugins
|
49
|
-
load_defaults
|
50
|
-
load_assemblies
|
51
|
-
end
|
52
|
-
|
53
|
-
#--
|
54
|
-
# TODO: Use this, or pass in via initialize?
|
55
|
-
#++
|
56
|
-
def project
|
57
|
-
Detroit.project
|
58
|
-
end
|
59
|
-
|
60
|
-
# Load a plugin.
|
61
|
-
def load_plugin(name)
|
62
|
-
@loaded_plugins[name] ||= (
|
63
|
-
begin
|
64
|
-
require "detroit-#{name}"
|
65
|
-
rescue LoadError => e
|
66
|
-
$stderr.puts "ERROR: #{e.message.capitalize}"
|
67
|
-
$stderr.puts " Perhaps `gem install detroit-#{name}`?"
|
68
|
-
exit -1
|
69
|
-
end
|
70
|
-
name # true ?
|
71
|
-
)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Pre-load plugins using `.detroit/plugins.rb`.
|
75
|
-
def load_plugins
|
76
|
-
if file = project.root.glob('{.,}#{DIRECTORY}/plugins{,.rb}').first
|
77
|
-
require file
|
78
|
-
else
|
79
|
-
self.defaults = {}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Load defaults from `.detroit/defaults.yml`.
|
84
|
-
def load_defaults
|
85
|
-
if file = project.root.glob('{.,}#{DIRECTORY}/defaults{,.yml,.yaml}').first
|
86
|
-
self.defaults = YAML.load(File.new(file))
|
87
|
-
else
|
88
|
-
self.defaults = {}
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
#
|
93
|
-
def load_assemblies
|
94
|
-
assembly_filenames.each do |file|
|
95
|
-
load_assembly_file(file)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
#
|
100
|
-
def load_assembly_file(file)
|
101
|
-
@assemblies[file] = Assembly.load(File.new(file))
|
102
|
-
@services.merge!(assemblies[file].services)
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set defaults.
|
106
|
-
def defaults=(hash)
|
107
|
-
@defaults = hash.to_h
|
108
|
-
end
|
109
|
-
|
110
|
-
# If a `Assembly` or `.assembly` file exists, then it is returned. Otherwise
|
111
|
-
# all `*.assembly` files are loaded. To load `*.assembly` files from another
|
112
|
-
# directory add the directory to config options file.
|
113
|
-
def assembly_filenames
|
114
|
-
@assembly_filenames ||= (
|
115
|
-
files = []
|
116
|
-
## match 'Assembly' or '.assembly' file
|
117
|
-
files = project.root.glob("{,.,*.}#{FILE_EXTENSION}{,.rb,.yml,.yaml}", :casefold)
|
118
|
-
## only files
|
119
|
-
files = files.select{ |f| File.file?(f) }
|
120
|
-
##
|
121
|
-
if files.empty?
|
122
|
-
## match '.detroit/*.assembly' or 'detroit/*.assembly'
|
123
|
-
files += project.root.glob("{,.}#{DIRECTORY}/*.#{FILE_EXTENSION}", :casefold)
|
124
|
-
## match 'task/*.assembly' (OLD SCHOOL)
|
125
|
-
files += project.root.glob("{task,tasks}/*.#{FILE_EXTENSION}", :casefold)
|
126
|
-
## only files
|
127
|
-
files = files.select{ |f| File.file?(f) }
|
128
|
-
end
|
129
|
-
files
|
130
|
-
)
|
131
|
-
end
|
132
|
-
|
133
|
-
#
|
134
|
-
def each(&block)
|
135
|
-
services.each(&block)
|
136
|
-
end
|
137
|
-
|
138
|
-
#
|
139
|
-
def size
|
140
|
-
services.size
|
141
|
-
end
|
142
|
-
|
143
|
-
=begin
|
144
|
-
# If using a `Routine` file and want to import antoher file then use
|
145
|
-
# `import:` entry.
|
146
|
-
def load_detroit_file(file)
|
147
|
-
#@dir = File.dirname(file)
|
148
|
-
|
149
|
-
assemblies[file] =
|
150
|
-
|
151
|
-
# TODO: can we just read the first line of the file and go from there?
|
152
|
-
#text = File.read(file).strip
|
153
|
-
|
154
|
-
## if yaml vs. ruby file
|
155
|
-
#if (/\A---/ =~ text || /\.(yml|yaml)$/ =~ File.extname(file))
|
156
|
-
# #data = parse_detroit_file_yaml(text, file)
|
157
|
-
# YAML.load(text)
|
158
|
-
#else
|
159
|
-
# data = parse_detroit_file_ruby(text, file)
|
160
|
-
#end
|
161
|
-
|
162
|
-
## extract defaults
|
163
|
-
#if defaults = data.delete('defaults')
|
164
|
-
# @defaults.merge!(defaults)
|
165
|
-
#end
|
166
|
-
|
167
|
-
## import other files
|
168
|
-
#if import = data.delete('import')
|
169
|
-
# [import].flatten.each do |glob|
|
170
|
-
# routine(glob)
|
171
|
-
# end
|
172
|
-
#end
|
173
|
-
|
174
|
-
## require plugins
|
175
|
-
#if plugins = data.delete('plugins')
|
176
|
-
# [plugins].flatten.each do |file|
|
177
|
-
# require file
|
178
|
-
# end
|
179
|
-
#end
|
180
|
-
|
181
|
-
#@services.update(data)
|
182
|
-
end
|
183
|
-
=end
|
184
|
-
|
185
|
-
## Parse a YAML-based routine.
|
186
|
-
#def parse_detroit_file_yaml(text, file)
|
187
|
-
# YAMLParser.parse(self, text, file)
|
188
|
-
#end
|
189
|
-
|
190
|
-
## Parse a Ruby-based routine.
|
191
|
-
#def parse_detroit_file_ruby(text, file)
|
192
|
-
# RubyParser.parse(self, text, file)
|
193
|
-
#end
|
194
|
-
|
195
|
-
## TODO: Should the +dir+ be relative to the file or project.root?
|
196
|
-
#def routine(glob)
|
197
|
-
# pattern = File.join(@dir, glob)
|
198
|
-
# Dir[pattern].each{ |f| load_detroit_file(f) }
|
199
|
-
#end
|
200
|
-
|
201
|
-
end
|
202
|
-
|
203
|
-
end
|
data/lib/detroit/control.rb
DELETED
@@ -1,129 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# The control module is a function module that extends
|
4
|
-
# the toplevel Detroit namespace module.
|
5
|
-
module Control
|
6
|
-
|
7
|
-
# Location of standard plugins.
|
8
|
-
#PLUGIN_DIRECTORY = File.dirname(__FILE__) + '/plugins'
|
9
|
-
|
10
|
-
# Returns Array of standard plugin file names.
|
11
|
-
#def standard_plugins
|
12
|
-
# Dir[PLUGIN_DIRECTORY + '/*.rb']
|
13
|
-
#end
|
14
|
-
|
15
|
-
# Universal acccess to the current project.
|
16
|
-
#
|
17
|
-
# TODO: Is Control#project being used?
|
18
|
-
def project
|
19
|
-
@project ||= POM::Project.find
|
20
|
-
end
|
21
|
-
|
22
|
-
# Returns Application given options.
|
23
|
-
def application(options={})
|
24
|
-
Application.new(options)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Run the command line interface.
|
28
|
-
def cli(*argv)
|
29
|
-
cli_options = {
|
30
|
-
:system => nil, :assemblies => [],
|
31
|
-
:trace=>nil, :trial=>nil, :debug=>nil, :quiet=>nil, :verbose=>nil,
|
32
|
-
:force=>nil, :multitask=>nil, :skip=>[]
|
33
|
-
}
|
34
|
-
|
35
|
-
usage = cli_usage(cli_options)
|
36
|
-
usage.parse!(argv)
|
37
|
-
|
38
|
-
#if /\.assembly$/ =~ argv[0]
|
39
|
-
# job = argv[1]
|
40
|
-
# begin
|
41
|
-
# application(cli_options).runscript(argv[0], job)
|
42
|
-
# rescue => error
|
43
|
-
# $stderr.puts error.message
|
44
|
-
# exit -1
|
45
|
-
# end
|
46
|
-
#else
|
47
|
-
begin
|
48
|
-
application(cli_options).start(*argv)
|
49
|
-
rescue => error
|
50
|
-
if $DEBUG
|
51
|
-
raise error
|
52
|
-
else
|
53
|
-
$stderr.puts error.message
|
54
|
-
exit -1
|
55
|
-
end
|
56
|
-
end
|
57
|
-
#end
|
58
|
-
end
|
59
|
-
|
60
|
-
# Returns an instance of OptionParser.
|
61
|
-
def cli_usage(options)
|
62
|
-
@usage ||= (
|
63
|
-
OptionParser.new do |usage|
|
64
|
-
usage.banner = "Usage: detroit [<track>:]<stop> [options]"
|
65
|
-
usage.on('-m', '--multitask', "Run work elements in parallel.") do
|
66
|
-
options[:multitask] = true
|
67
|
-
end
|
68
|
-
usage.on('-S', '--skip [SERVICE]', 'Skip a service.') do |skip|
|
69
|
-
options[:skip] << skip
|
70
|
-
end
|
71
|
-
|
72
|
-
usage.on('-s', '--system=NAME', "Select assembly system. Default is `standard'.") do |system|
|
73
|
-
options[:system] = system
|
74
|
-
end
|
75
|
-
usage.on('-a', '--assembly [FILE]', 'Use specific assembly file(s).') do |file|
|
76
|
-
options[:assemblies] << file
|
77
|
-
end
|
78
|
-
|
79
|
-
usage.on('-F', '--force', "Force operations.") do
|
80
|
-
options[:force] = true
|
81
|
-
end
|
82
|
-
usage.on('--trace', "Run in TRACE mode.") do
|
83
|
-
#$TRACE = true
|
84
|
-
options[:trace] = true
|
85
|
-
end
|
86
|
-
usage.on('--trial', "Run in TRIAL mode (no disk writes).") do
|
87
|
-
#$TRIAL = true
|
88
|
-
options[:trial] = true
|
89
|
-
end
|
90
|
-
# TODO: do we really need verbose?
|
91
|
-
usage.on('--verbose', "Provide extra output.") do
|
92
|
-
options[:verbose] = true
|
93
|
-
end
|
94
|
-
usage.on('-q', '--quiet', "Run silently.") do
|
95
|
-
options[:quiet] = true
|
96
|
-
end
|
97
|
-
|
98
|
-
usage.on('-I=PATH', "Add directory to $LOAD_PATH") do |dirs|
|
99
|
-
dirs.to_list.each do |dir|
|
100
|
-
$LOAD_PATH.unshift(dir)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
usage.on('--debug', "Run with $DEBUG set to true.") do
|
104
|
-
$DEBUG = true
|
105
|
-
options[:debug] = true # DEPRECATE?
|
106
|
-
end
|
107
|
-
usage.on('--warn', "Run with $VERBOSE set to true.") do
|
108
|
-
$VERBOSE = true # wish this were called $WARN
|
109
|
-
end
|
110
|
-
usage.on_tail('--help [TOOL]', "Display this help message.") do |tool|
|
111
|
-
if tool
|
112
|
-
application.display_help(tool)
|
113
|
-
else
|
114
|
-
puts usage
|
115
|
-
end
|
116
|
-
exit
|
117
|
-
end
|
118
|
-
usage.on_tail('-c', '--config TOOL', "Produce a configuration template.") do |tool|
|
119
|
-
puts application.config_template(tool).to_yaml
|
120
|
-
exit
|
121
|
-
end
|
122
|
-
end
|
123
|
-
)
|
124
|
-
end
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
extend Control
|
129
|
-
end
|
data/lib/detroit/custom.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# Custom tool is used to create "quicky" services.
|
4
|
-
#
|
5
|
-
# This is a useful alternative to writing a full-blown plugin
|
6
|
-
# when the need is simple.
|
7
|
-
#
|
8
|
-
class Custom < Tool
|
9
|
-
|
10
|
-
# Default track(s) in which this plugin operates.
|
11
|
-
DEFAULT_TRACK = "main"
|
12
|
-
|
13
|
-
# Which track(s) to run this custom plugin.
|
14
|
-
attr_accessor :track
|
15
|
-
|
16
|
-
# Special writer to allow single track or a list of tracks.
|
17
|
-
def track=(val)
|
18
|
-
@track = val.to_list #[val].flatten
|
19
|
-
end
|
20
|
-
|
21
|
-
# Plural alias for #track.
|
22
|
-
alias_accessor :tracks, :track
|
23
|
-
|
24
|
-
alias_accessor :on, :track
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
SPECIAL_OPTIONS = %w{
|
29
|
-
service track tracks on active priority project
|
30
|
-
trial trace verbose force quiet
|
31
|
-
}
|
32
|
-
|
33
|
-
# Instantiate new custom plugin.
|
34
|
-
#
|
35
|
-
# FIXME: Custom#initialize seems to be running twice at startup. Why?
|
36
|
-
#
|
37
|
-
# This works by interpreting the service configuration as a hash of
|
38
|
-
# stop names to ruby code.
|
39
|
-
#
|
40
|
-
def initialize(options)
|
41
|
-
super(options)
|
42
|
-
options.each do |stop, script|
|
43
|
-
# skip specific names used for configuration
|
44
|
-
next if SPECIAL_OPTIONS.include? stop
|
45
|
-
# remaining options are names of track stops
|
46
|
-
#tracks.each do |t|
|
47
|
-
src = %{
|
48
|
-
def station_#{stop}
|
49
|
-
#{script}
|
50
|
-
end
|
51
|
-
}
|
52
|
-
(class << self; self; end).module_eval(src)
|
53
|
-
#end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# Set initial attribute defaults.
|
58
|
-
def initialize_defaults
|
59
|
-
@track = [DEFAULT_TRACK]
|
60
|
-
end
|
61
|
-
|
62
|
-
#
|
63
|
-
def method_missing(s, *a, &b)
|
64
|
-
if s.to_s.end_with?('=')
|
65
|
-
# stop = s.to_s.chomp('=')
|
66
|
-
# if !SPECIAL_OPTIONS.include?(stop)
|
67
|
-
# (class << self; self; end).module_eval %{
|
68
|
-
# def station_#{stop}
|
69
|
-
# #{a.first}
|
70
|
-
# end
|
71
|
-
# }
|
72
|
-
# end
|
73
|
-
else
|
74
|
-
if @context.respond_to?(s)
|
75
|
-
@context.__send__(s,*a,&b)
|
76
|
-
else
|
77
|
-
super(s, *a, &b)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# @todo should only respond to stop names and special options.
|
83
|
-
#def respond_to?(s)
|
84
|
-
# return true if SPECIAL_OPTIONS.include?(s.to_s)
|
85
|
-
# return true
|
86
|
-
#end
|
87
|
-
|
88
|
-
# RUBY 1.9
|
89
|
-
def respond_to_missing?(name, privy)
|
90
|
-
#return true if name.to_s.start_with?('station_')
|
91
|
-
return true if name.to_s.end_with?('=')
|
92
|
-
return true if @context.respond_to?(name)
|
93
|
-
false
|
94
|
-
end
|
95
|
-
|
96
|
-
def inspect
|
97
|
-
"#<Custom @on=#{track.join(',')}>"
|
98
|
-
end
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
end
|