drudgery 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -55,7 +55,7 @@ m = Drudgery::Manager.new
55
55
  m.prepare do |job|
56
56
  job.extract :csv, 'src/addresses.csv'
57
57
 
58
- job.transform do |data|
58
+ job.transform do |data, cache|
59
59
  first_name, last_name = data.delete(:name).split(' ')
60
60
 
61
61
  data[:first_name] = first_name
@@ -93,7 +93,7 @@ m.prepare do |job|
93
93
  extractor.order('name')
94
94
  end
95
95
 
96
- job.transform do |data|
96
+ job.transform do |data, cache|
97
97
  first_name, last_name = data.delete(:name).split(' ')
98
98
 
99
99
  data[:first_name] = first_name
@@ -132,9 +132,9 @@ end
132
132
  source = []
133
133
 
134
134
  m = Drudgery::Manager.new
135
- job = Drudgery::Job.new(:extractor => ArrayExtractor.new(source))
136
135
 
137
- m.prepare(job) do |job|
136
+ m.prepare do |job|
137
+ m.extract ArrayExtractor.new(source)
138
138
  m.load :csv, 'destination.csv'
139
139
  end
140
140
  ```
@@ -173,12 +173,12 @@ Transformers
173
173
  ------------
174
174
 
175
175
  Drudgery comes with a basic Transformer class. It symbolizes the keys of
176
- each record and allows you to register processors to process data. Registered
177
- processors should implement a `#call` method and return a `Hash` or `nil`.
176
+ each record and allows you to register a processor to process data. The
177
+ processor should implement a `#call` method and return a `Hash` or `nil`.
178
178
 
179
179
  ```ruby
180
180
  custom_processor = Proc.new do |data, cache|
181
- data[:initials] = data[:name].split(' ').map(&:capitalize)
181
+ data[:initials] = data[:name].split(' ').map(&:capitalize).join()
182
182
  data
183
183
  end
184
184
 
@@ -190,7 +190,7 @@ transformer.transform({ :name => 'John Doe' }) # == { :name => 'John Doe', :init
190
190
 
191
191
  You could also implement your own transformer if you need more custom
192
192
  processing power. If you inherit from `Drudgery::Transfomer`, you need
193
- only implement the `#transform` method that accepts a hash as an
193
+ only implement the `#transform` method that accepts a hash argument as an
194
194
  argument and returns a `Hash` or `nil`.
195
195
 
196
196
  ```ruby
@@ -201,10 +201,10 @@ class CustomTransformer < Drudgery::Transformer
201
201
  end
202
202
 
203
203
  m = Drudgery::Manager.new
204
- job = Drudgery::Job.new(:transformer => CustomTransformer.new)
205
204
 
206
- m.prepare(job) do |job|
205
+ m.prepare do |job|
207
206
  m.extract :csv, 'source.csv'
207
+ m.transform( CustomTransformer.new)
208
208
  m.load :csv, 'destination.csv'
209
209
  end
210
210
  ```
@@ -237,10 +237,10 @@ end
237
237
  destination = []
238
238
 
239
239
  m = Drudgery::Manager.new
240
- job = Drudgery::Job.new(:loader => ArrayLoader.new(destination))
241
240
 
242
- m.prepare(job) do |job|
241
+ m.prepare do |job|
243
242
  m.extract :csv, 'source.csv'
243
+ m.load ArrayLoader.new(destination)
244
244
  end
245
245
  ```
246
246
 
data/lib/drudgery/job.rb CHANGED
@@ -3,29 +3,40 @@ module Drudgery
3
3
  def initialize(options={})
4
4
  @extractor = options[:extractor]
5
5
  @loader = options[:loader]
6
- @transformer = options[:transformer] || Drudgery::Transformer.new
6
+ @transformer = options[:transformer]
7
+ @batch_size = options[:batch_size] || 1000
7
8
 
8
- @batch_size, @records = 1000, []
9
+ @records = []
9
10
  end
10
11
 
11
12
  def batch_size(size)
12
13
  @batch_size = size
13
14
  end
14
15
 
15
- def extract(type, *args)
16
- extractor = Drudgery::Extractors.instantiate(type, *args)
16
+ def extract(*args)
17
+ if args.first.kind_of?(Symbol)
18
+ extractor = Drudgery::Extractors.instantiate(*args)
19
+ else
20
+ extractor = args.first
21
+ end
17
22
 
18
23
  yield extractor if block_given?
19
24
 
20
25
  @extractor = extractor
21
26
  end
22
27
 
23
- def transform(&processor)
24
- @transformer.register(processor)
28
+ def transform(transformer=Drudgery::Transformer.new, &processor)
29
+ transformer.register(processor)
30
+
31
+ @transformer = transformer
25
32
  end
26
33
 
27
- def load(type, *args)
28
- loader = Drudgery::Loaders.instantiate(type, *args)
34
+ def load(*args)
35
+ if args.first.kind_of?(Symbol)
36
+ loader = Drudgery::Loaders.instantiate(*args)
37
+ else
38
+ loader = args.first
39
+ end
29
40
 
30
41
  yield loader if block_given?
31
42
 
@@ -47,7 +58,7 @@ module Drudgery
47
58
  private
48
59
  def extract_records
49
60
  @extractor.extract do |data|
50
- record = @transformer.transform(data)
61
+ record = transform_data(data)
51
62
  next if record.nil?
52
63
 
53
64
  yield record
@@ -58,5 +69,13 @@ module Drudgery
58
69
  @loader.load(@records)
59
70
  @records.clear
60
71
  end
72
+
73
+ def transform_data(data)
74
+ if @transformer
75
+ @transformer.transform(data)
76
+ else
77
+ data
78
+ end
79
+ end
61
80
  end
62
81
  end
@@ -1,23 +1,17 @@
1
1
  module Drudgery
2
2
  class Transformer
3
3
  def initialize
4
- @processors = []
5
4
  @cache = {}
6
5
  end
7
6
 
8
7
  def register(processor)
9
- @processors << processor
8
+ @processor = processor
10
9
  end
11
10
 
12
11
  def transform(data)
13
12
  symbolize_keys!(data)
14
13
 
15
- @processors.each do |processor|
16
- data = processor.call(data, @cache)
17
- break if data.nil?
18
- end
19
-
20
- data
14
+ @processor ? @processor.call(data, @cache) : data
21
15
  end
22
16
 
23
17
  private
@@ -1,3 +1,3 @@
1
1
  module Drudgery
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -16,10 +16,15 @@ describe Drudgery::Job do
16
16
  @job.instance_variable_get('@loader').must_equal @loader
17
17
  end
18
18
 
19
+ it 'sets batch_size with provided argument' do
20
+ job = Drudgery::Job.new(:batch_size => 100)
21
+ job.instance_variable_get('@batch_size').must_equal(100)
22
+ end
23
+
19
24
  it 'initializes extractor, transformer, and loader if none provided' do
20
25
  job = Drudgery::Job.new
21
26
  job.instance_variable_get('@extractor').must_be_nil
22
- job.instance_variable_get('@transformer').must_be_instance_of(Drudgery::Transformer)
27
+ job.instance_variable_get('@transformer').must_be_nil
23
28
  job.instance_variable_get('@loader').must_be_nil
24
29
  end
25
30
 
@@ -27,7 +32,8 @@ describe Drudgery::Job do
27
32
  @job.instance_variable_get('@records').must_equal []
28
33
  end
29
34
 
30
- it 'initializes batch_size as 1000' do
35
+
36
+ it 'initializes batch_size as 1000 if none provided' do
31
37
  @job.instance_variable_get('@batch_size').must_equal 1000
32
38
  end
33
39
  end
@@ -41,85 +47,196 @@ describe Drudgery::Job do
41
47
  end
42
48
 
43
49
  describe '#extract' do
44
- it 'instantiates extractor with type and args' do
45
- Drudgery::Extractors.expects(:instantiate).with(:csv, 'filename.csv', :col_sep => '|')
50
+ describe 'when type and args provided' do
51
+ it 'instantiates extractor with type and args' do
52
+ Drudgery::Extractors.expects(:instantiate).with(:csv, 'filename.csv', :col_sep => '|')
46
53
 
47
- job = Drudgery::Job.new
48
- job.extract(:csv, 'filename.csv', :col_sep => '|')
49
- end
54
+ job = Drudgery::Job.new
55
+ job.extract(:csv, 'filename.csv', :col_sep => '|')
56
+ end
50
57
 
51
- it 'yields extractor if block_given' do
52
- extractor = mock
53
- extractor.expects(:col_sep).with('|')
58
+ it 'yields extractor if block_given' do
59
+ extractor = mock
60
+ extractor.expects(:col_sep).with('|')
54
61
 
55
- Drudgery::Extractors.stubs(:instantiate).returns(extractor)
62
+ Drudgery::Extractors.stubs(:instantiate).returns(extractor)
56
63
 
57
- job = Drudgery::Job.new
58
- job.extract(:csv, 'filename.csv') do |extractor|
59
- extractor.col_sep '|'
64
+ job = Drudgery::Job.new
65
+ job.extract(:csv, 'filename.csv') do |extractor|
66
+ extractor.col_sep '|'
67
+ end
68
+ end
69
+
70
+ it 'sets extractor' do
71
+ extractor = mock
72
+
73
+ Drudgery::Extractors.stubs(:instantiate).returns(extractor)
74
+
75
+ job = Drudgery::Job.new
76
+ job.extract(:csv, 'filename.csv', :col_sep => '|')
77
+
78
+ job.instance_variable_get('@extractor').must_equal extractor
60
79
  end
61
80
  end
62
81
 
63
- it 'sets extractor' do
64
- extractor = mock
82
+ describe 'when extractor provided' do
83
+ it 'does not instantiat extractor with type and args' do
84
+ extractor = mock
65
85
 
66
- Drudgery::Extractors.stubs(:instantiate).returns(extractor)
86
+ Drudgery::Extractors.expects(:instantiate).never
67
87
 
68
- job = Drudgery::Job.new
69
- job.extract(:csv, 'filename.csv', :col_sep => '|')
88
+ job = Drudgery::Job.new
89
+ job.extract(extractor)
90
+ end
91
+
92
+ it 'yields extractor if block_given' do
93
+ extractor = mock
94
+ extractor.expects(:col_sep).with('|')
95
+
96
+ job = Drudgery::Job.new
97
+ job.extract(extractor) do |ext|
98
+ ext.col_sep '|'
99
+ end
100
+ end
70
101
 
71
- job.instance_variable_get('@extractor').must_equal extractor
102
+ it 'sets extractor' do
103
+ extractor = mock
104
+
105
+ job = Drudgery::Job.new
106
+ job.extract(extractor)
107
+
108
+ job.instance_variable_get('@extractor').must_equal extractor
109
+ end
72
110
  end
73
111
  end
74
112
 
75
113
  describe '#transform' do
76
- it 'registers provided proc with transformer' do
77
- block = Proc.new { |data, cache| data }
114
+ describe 'when transformer provided' do
115
+ it 'sets transformer to provided transformer' do
116
+ transformer = mock
117
+ transformer.stubs(:register)
78
118
 
79
- transformer = mock
80
- transformer.expects(:register).with(block)
119
+ job = Drudgery::Job.new
120
+ job.transform(transformer)
121
+
122
+ job.instance_variable_get('@transformer').must_equal transformer
123
+ end
124
+
125
+ it 'registers provided proc with provided transformer' do
126
+ block = Proc.new { |data, cache| data }
127
+
128
+ transformer = mock
129
+ transformer.expects(:register).with(block)
130
+
131
+ job = Drudgery::Job.new
132
+ job.transform(transformer, &block)
133
+ end
81
134
 
82
- job = Drudgery::Job.new(:transformer => transformer)
83
- job.transform(&block)
135
+ it 'registers provided block with provided transformer' do
136
+ transformer = mock
137
+ transformer.expects(:register).with { |data, cache| data }
138
+
139
+ job = Drudgery::Job.new
140
+ job.transform(transformer) { |data, cache| data }
141
+ end
84
142
  end
85
143
 
86
- it 'registers provided block with transformer' do
87
- transformer = mock
88
- transformer.expects(:register).with { |data, cache| data }
144
+ describe 'when no transformer provided' do
145
+ it 'sets transformer to default transformer' do
146
+ transformer = mock
147
+ transformer.stubs(:register)
148
+
149
+ Drudgery::Transformer.expects(:new).returns(transformer)
150
+
151
+ job = Drudgery::Job.new
152
+ job.transform
153
+
154
+ job.instance_variable_get('@transformer').must_equal transformer
155
+ end
156
+
157
+ it 'registers provided proc with default transformer' do
158
+ block = Proc.new { |data, cache| data }
159
+
160
+ transformer = mock
161
+ transformer.expects(:register).with(block)
162
+
163
+ Drudgery::Transformer.stubs(:new).returns(transformer)
164
+
165
+ job = Drudgery::Job.new
166
+ job.transform(&block)
167
+ end
89
168
 
90
- job = Drudgery::Job.new(:transformer => transformer)
91
- job.transform { |data, cache| data }
169
+ it 'registers provided block with default transformer' do
170
+ transformer = Drudgery::Transformer.new
171
+ transformer.expects(:register).with { |data, cache| data }
172
+
173
+ Drudgery::Transformer.stubs(:new).returns(transformer)
174
+
175
+ job = Drudgery::Job.new
176
+ job.transform { |data, cache| data }
177
+ end
92
178
  end
93
179
  end
94
180
 
95
181
  describe '#load' do
96
- it 'instantiates loader with type with args' do
97
- Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename')
182
+ describe 'when type and args provided' do
183
+ it 'instantiates loader with type with args' do
184
+ Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename')
98
185
 
99
- job = Drudgery::Job.new
100
- job.load(:sqlite3, 'db.sqlite3', 'tablename')
101
- end
186
+ job = Drudgery::Job.new
187
+ job.load(:sqlite3, 'db.sqlite3', 'tablename')
188
+ end
102
189
 
103
- it 'yields loader if block_given' do
104
- loader = mock
105
- loader.expects(:select).with('a', 'b', 'c')
190
+ it 'yields loader if block_given' do
191
+ loader = mock
192
+ loader.expects(:select).with('a', 'b', 'c')
106
193
 
107
- Drudgery::Loaders.stubs(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
194
+ Drudgery::Loaders.stubs(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
108
195
 
109
- job = Drudgery::Job.new
110
- job.load(:sqlite3, 'db.sqlite3', 'tablename') do |loader|
111
- loader.select('a', 'b', 'c')
196
+ job = Drudgery::Job.new
197
+ job.load(:sqlite3, 'db.sqlite3', 'tablename') do |loader|
198
+ loader.select('a', 'b', 'c')
199
+ end
200
+ end
201
+
202
+ it 'sets loader' do
203
+ loader = mock
204
+
205
+ Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
206
+
207
+ job = Drudgery::Job.new
208
+ job.load(:sqlite3, 'db.sqlite3', 'tablename')
209
+ job.instance_variable_get('@loader').must_equal loader
112
210
  end
113
211
  end
114
212
 
115
- it 'sets extractor' do
116
- loader = mock
213
+ describe 'when loader provided' do
214
+ it 'does not instantiate loader with type with args' do
215
+ loader = mock
117
216
 
118
- Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
217
+ Drudgery::Loaders.expects(:instantiate).never
119
218
 
120
- job = Drudgery::Job.new
121
- job.load(:sqlite3, 'db.sqlite3', 'tablename')
122
- job.instance_variable_get('@loader').must_equal loader
219
+ job = Drudgery::Job.new
220
+ job.load(loader)
221
+ end
222
+
223
+ it 'yields loader if block_given' do
224
+ loader = mock
225
+ loader.expects(:select).with('a', 'b', 'c')
226
+
227
+ job = Drudgery::Job.new
228
+ job.load(loader) do |loader|
229
+ loader.select('a', 'b', 'c')
230
+ end
231
+ end
232
+
233
+ it 'sets loader' do
234
+ loader = mock
235
+
236
+ job = Drudgery::Job.new
237
+ job.load(loader)
238
+ job.instance_variable_get('@loader').must_equal loader
239
+ end
123
240
  end
124
241
  end
125
242
 
@@ -156,8 +273,8 @@ describe Drudgery::Job do
156
273
  extractor.stubs(:extract).multiple_yields([{ 'a' => 1 }], [{ 'b' => 2 }], [{ 'c' => 3 }])
157
274
 
158
275
  loader = mock
159
- loader.expects(:load).with([{ :a => 1 }, { :b => 2 }])
160
- loader.expects(:load).with([{ :c => 3 }])
276
+ loader.expects(:load).with([{ 'a' => 1 }, { 'b' => 2 }])
277
+ loader.expects(:load).with([{ 'c' => 3 }])
161
278
 
162
279
  job = Drudgery::Job.new(:extractor => extractor, :loader => loader)
163
280
  job.batch_size 2
@@ -6,21 +6,19 @@ describe Drudgery::Transformer do
6
6
  end
7
7
 
8
8
  describe '#initialize' do
9
- it 'initializes processors array' do
10
- @transformer.instance_variable_get('@processors').must_equal []
11
- end
12
-
13
9
  it 'initializes cache hash' do
14
10
  @transformer.instance_variable_get('@cache').must_equal({})
15
11
  end
16
12
  end
17
13
 
18
14
  describe '#register' do
19
- it 'adds processor to processors array' do
15
+ it 'sets processor with provided proc' do
20
16
  processor = Proc.new { |data, cache| data }
21
17
 
22
18
  @transformer.register(processor)
23
- @transformer.instance_variable_get('@processors').must_include processor
19
+
20
+ # must_equal bug with comparing procs, so use assert_equal instead
21
+ assert_equal @transformer.instance_variable_get('@processor'), processor
24
22
  end
25
23
  end
26
24
 
@@ -29,14 +27,14 @@ describe Drudgery::Transformer do
29
27
  @transformer.transform({ 'a' => 1 }).must_equal({ :a => 1 })
30
28
  end
31
29
 
32
- it 'processes data in each processor' do
30
+ it 'processes data with processor' do
33
31
  processor = Proc.new { |data, cache| data[:b] = 2; data }
34
32
 
35
33
  @transformer.register(processor)
36
34
  @transformer.transform({ 'a' => 1 }).must_equal({ :a => 1, :b => 2 })
37
35
  end
38
36
 
39
- it 'allows processors to use cache' do
37
+ it 'allows processor to use cache' do
40
38
  processor = Proc.new do |data, cache|
41
39
  cache[:a] ||= 0
42
40
  cache[:a] += data[:a]
@@ -49,12 +47,10 @@ describe Drudgery::Transformer do
49
47
  @transformer.instance_variable_get('@cache').must_equal({ :a => 6 })
50
48
  end
51
49
 
52
- it 'skips remaining processors and returns nil if any processor returns nil' do
53
- processor1 = Proc.new { |data, cache| nil }
54
- processor2 = Proc.new { |data, cache| raise 'should not get here' }
50
+ it 'returns nil if any processor returns nil' do
51
+ processor = Proc.new { |data, cache| nil }
55
52
 
56
- @transformer.register(processor1)
57
- @transformer.register(processor2)
53
+ @transformer.register(processor)
58
54
  @transformer.transform({ 'a' => 1 }).must_be_nil
59
55
  end
60
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: drudgery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-03 00:00:00.000000000 Z
12
+ date: 2012-04-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &70334397325460 !ruby/object:Gem::Requirement
16
+ requirement: &70230466813560 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70334397325460
24
+ version_requirements: *70230466813560
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &70334397324780 !ruby/object:Gem::Requirement
27
+ requirement: &70230466812880 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.1'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70334397324780
35
+ version_requirements: *70230466812880
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mocha
38
- requirement: &70334397324280 !ruby/object:Gem::Requirement
38
+ requirement: &70230466812240 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0.10'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70334397324280
46
+ version_requirements: *70230466812240
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: simplecov
49
- requirement: &70334397323700 !ruby/object:Gem::Requirement
49
+ requirement: &70230466811700 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0.6'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70334397323700
57
+ version_requirements: *70230466811700
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: guard-minitest
60
- requirement: &70334397323080 !ruby/object:Gem::Requirement
60
+ requirement: &70230466811240 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0.5'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70334397323080
68
+ version_requirements: *70230466811240
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: activerecord
71
- requirement: &70334397322440 !ruby/object:Gem::Requirement
71
+ requirement: &70230466810720 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '3.0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70334397322440
79
+ version_requirements: *70230466810720
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: activerecord-import
82
- requirement: &70334397321700 !ruby/object:Gem::Requirement
82
+ requirement: &70230466810220 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: 0.2.9
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70334397321700
90
+ version_requirements: *70230466810220
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: sqlite3
93
- requirement: &70334397321080 !ruby/object:Gem::Requirement
93
+ requirement: &70230466809620 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ~>
@@ -98,7 +98,7 @@ dependencies:
98
98
  version: '1.3'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70334397321080
101
+ version_requirements: *70230466809620
102
102
  description: A simple ETL library that supports CSV, SQLite3, and ActiveRecord sources
103
103
  and destinations.
104
104
  email: