stockboy 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +7 -2
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +6 -3
  6. data/lib/stockboy/configurator.rb +3 -1
  7. data/lib/stockboy/filters.rb +0 -9
  8. data/lib/stockboy/job.rb +3 -3
  9. data/lib/stockboy/provider.rb +6 -8
  10. data/lib/stockboy/provider_repeater.rb +2 -3
  11. data/lib/stockboy/providers/file.rb +32 -21
  12. data/lib/stockboy/providers/ftp.rb +6 -2
  13. data/lib/stockboy/providers/http.rb +13 -10
  14. data/lib/stockboy/providers/imap.rb +47 -24
  15. data/lib/stockboy/providers/imap/search_options.rb +1 -1
  16. data/lib/stockboy/providers/soap.rb +3 -2
  17. data/lib/stockboy/railtie.rb +1 -0
  18. data/lib/stockboy/reader.rb +2 -0
  19. data/lib/stockboy/readers/spreadsheet.rb +1 -0
  20. data/lib/stockboy/registry.rb +4 -8
  21. data/lib/stockboy/version.rb +1 -1
  22. data/spec/fixtures/email/csv_attachment.eml +20 -0
  23. data/spec/spec_helper.rb +24 -2
  24. data/spec/stockboy/configurator_spec.rb +54 -3
  25. data/spec/stockboy/filter_spec.rb +7 -0
  26. data/spec/stockboy/job_spec.rb +65 -2
  27. data/spec/stockboy/provider_repeater_spec.rb +1 -1
  28. data/spec/stockboy/provider_spec.rb +21 -0
  29. data/spec/stockboy/providers/file_spec.rb +38 -18
  30. data/spec/stockboy/providers/http_spec.rb +1 -10
  31. data/spec/stockboy/providers/imap/search_options_spec.rb +13 -2
  32. data/spec/stockboy/providers/imap_spec.rb +83 -5
  33. data/spec/stockboy/providers/soap_spec.rb +1 -1
  34. data/spec/stockboy/reader_spec.rb +26 -0
  35. data/spec/stockboy/readers/spreadsheet_spec.rb +1 -1
  36. data/spec/stockboy/readers/xml_spec.rb +1 -1
  37. data/spec/stockboy/translations_spec.rb +25 -0
  38. metadata +27 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 64fb63c74d214447769a32a463ccab05bc92516e
4
- data.tar.gz: c79abf6d768765673e66503d5ed5cab1d89dade7
3
+ metadata.gz: 65b059955605b61f5eca5555bf69e013660fbc41
4
+ data.tar.gz: b23aa61607e930fe36b5920afe48b397aee52585
5
5
  SHA512:
6
- metadata.gz: 55c4b8106f01b7f822915b4ce2e655c70afa4770a8e42794dea09345326d88a2a16edd00633fad2822a7054b7f58fee81064c00583ef93ccf0eaf849a63814de
7
- data.tar.gz: f28bc29494c073b3b5570dc9294e089c25b15e549f8f4db5349dd0df0df310429a06d39e57dc5acf858ca90035f3bb85bb07c02a27adb4b08f6f92202f203799
6
+ metadata.gz: 303f8e62f4025c494049ec3a5bf5b9eb004809f433f503cdea7edde82012e48a1ff0871a5992a32bfd31990f9663fdbc8b7a55182f22474e96019f0b0732a9d3
7
+ data.tar.gz: 5c8dee0518d7c27137da5e40c01409f56c9c793a1a24150631013672176cff95750cf8c677e56ad5d9c46be44e90314591a10e3c4bf23693408f6d43f87492e8
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  /.bundle
4
4
  /.yardoc
5
5
  /Gemfile.lock
6
+ /coverage
6
7
  /pkg/*
7
8
  /doc
8
9
  /tags
data/.travis.yml CHANGED
@@ -4,11 +4,16 @@ env:
4
4
  - CI=true
5
5
  rvm:
6
6
  - "1.9.3"
7
- - "2.0.0"
8
7
  - "2.1.1"
9
8
  - "jruby-19mode"
10
9
  - "rbx"
10
+ before_install:
11
+ - gem install bundler
11
12
  script: bundle exec rspec spec
12
13
  matrix:
13
14
  allow_failures:
14
- - rvm: "rbx"
15
+ - rvm:
16
+ - "jruby-19mode"
17
+ addons:
18
+ code_climate:
19
+ repo_token: 7591072ad04d7a8ee788ce9259baa284724740cab1ae9ca94c162a583acd8f10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1 / 2014-03-25
4
+
5
+ * [ENHANCEMENT] Job initializes with an empty attribute map to allow adding
6
+ * [ENHANCEMENT] Default to rails logger when loaded
7
+ * [ENHANCEMENT] Use same configured logger for all provider clients
8
+ * [BUGFIX] Compatibility with Rubinius
9
+
3
10
  ## 0.7.0 / 2014-03-21
4
11
 
5
12
  * [FEATURE] Add individual attribute mappings
data/Gemfile CHANGED
@@ -4,14 +4,17 @@ gemspec
4
4
  unless ENV["CI"]
5
5
  group :debug do
6
6
  gem "pry"
7
- gem "pry-debugger" if RUBY_VERSION.start_with? "1.9"
8
- gem "pry-byebug" if RUBY_VERSION.start_with? "2."
9
7
  end
10
8
  end
11
9
 
12
10
  group :doc do
13
- gem "redcarpet", "~> 1.0"
14
11
  gem "yard"
12
+ gem "kramdown"
13
+ end
14
+
15
+ group :test do
16
+ gem "simplecov", require: nil
17
+ gem "codeclimate-test-reporter", require: nil
15
18
  end
16
19
 
17
20
  platforms :rbx do
@@ -162,7 +162,9 @@ module Stockboy
162
162
  # filter :update, proc{ true } # capture all remaining items
163
163
  #
164
164
  def filter(key, callable=nil, *args, &block)
165
- @config[:filters][key] = block || Filters.build(callable, args)
165
+ filter = Filters.build(callable, args, block) || block
166
+ filter or raise ArgumentError, "Missing filter arguments for #{key}"
167
+ @config[:filters][key] = filter
166
168
  end
167
169
 
168
170
  # Register a trigger to notify the job of external events
@@ -6,15 +6,6 @@ module Stockboy
6
6
  #
7
7
  module Filters
8
8
  extend Stockboy::Registry
9
-
10
- def self.build(callable, args)
11
- if callable.is_a?(Symbol)
12
- callable = find(callable)
13
- callable = callable.new(*args) if callable.is_a? Class
14
- end
15
- callable
16
- end
17
-
18
9
  end
19
10
 
20
11
  end
data/lib/stockboy/job.rb CHANGED
@@ -83,7 +83,7 @@ module Stockboy
83
83
  def initialize(params={}, &block)
84
84
  @provider = params[:provider]
85
85
  @reader = params[:reader]
86
- @attributes = params[:attributes]
86
+ @attributes = params[:attributes] || AttributeMap.new
87
87
  @filters = FilterChain.new params[:filters]
88
88
  @triggers = Hash.new { |h,k| h[k] = [] }
89
89
  @triggers.replace params[:triggers] if params[:triggers]
@@ -188,8 +188,8 @@ module Stockboy
188
188
  # @return [String]
189
189
  #
190
190
  def inspect
191
- prov = "provider=#{(Stockboy::Providers.all.key(provider.class) || provider.class.to_s).inspect}"
192
- read = "reader=#{(Stockboy::Readers.all.key(reader.class) || reader.class.to_s).inspect}"
191
+ prov = "provider=#{(Stockboy::Providers.all.key(provider.class) || provider.class)}"
192
+ read = "reader=#{(Stockboy::Readers.all.key(reader.class) || reader.class)}"
193
193
  attr = "attributes=#{attributes.map(&:to)}"
194
194
  filt = "filters=#{filters.keys}"
195
195
  cnts = "record_counts=#{record_counts}"
@@ -33,14 +33,6 @@ module Stockboy
33
33
  class Provider
34
34
  extend Stockboy::DSL
35
35
 
36
- # Default logger if none is provided to the instance
37
- #
38
- # @return [Logger]
39
- #
40
- def self.logger
41
- Logger.new(STDERR)
42
- end
43
-
44
36
  # @return [Logger]
45
37
  #
46
38
  attr_accessor :logger
@@ -49,6 +41,12 @@ module Stockboy
49
41
  #
50
42
  attr_reader :errors
51
43
 
44
+ # Size of the received data
45
+ #
46
+ # @return [Time]
47
+ #
48
+ attr_reader :data_size
49
+
52
50
  # Timestamp of the received data
53
51
  #
54
52
  # @return [Time]
@@ -36,11 +36,10 @@ module Stockboy
36
36
  raise ArgumentError, "expected Provider, got #{provider.class}"
37
37
  end
38
38
  rescue StopIteration
39
- return $!.result
39
+ return provider
40
40
  end
41
- y = yield provider
41
+ yield provider
42
42
  provider.clear
43
- enum.feed y
44
43
  end
45
44
  end
46
45
 
@@ -56,16 +56,7 @@ module Stockboy::Providers
56
56
  end
57
57
 
58
58
  def matching_file
59
- return @matching_file if @matching_file
60
- files = case file_name
61
- when Regexp
62
- Dir.entries(file_dir)
63
- .select { |i| i =~ file_name }
64
- .map { |i| ::File.join(file_dir, i) }
65
- when String
66
- Dir[::File.join(file_dir, file_name)]
67
- end
68
- @matching_file = pick_from(files.sort) if files.any?
59
+ @matching_file ||= pick_from(file_list.sort)
69
60
  end
70
61
 
71
62
  def clear
@@ -81,11 +72,7 @@ module Stockboy::Providers
81
72
  errors << "file #{file_name} not found" unless matching_file
82
73
  data_file = ::File.new(matching_file, 'r') if matching_file
83
74
  validate_file(data_file)
84
- if valid?
85
- logger.info "Getting file #{file_dir}/#{matching_file}"
86
- @data = data_file.read
87
- logger.info "Got file #{file_dir}/#{matching_file} (#{@data_size} bytes)"
88
- end
75
+ @data = data_file.read if valid?
89
76
  end
90
77
 
91
78
  def validate
@@ -98,6 +85,21 @@ module Stockboy::Providers
98
85
  !!@matching_file
99
86
  end
100
87
 
88
+ def file_list
89
+ case file_name
90
+ when Regexp
91
+ Dir.entries(file_dir)
92
+ .select { |i| i =~ file_name }
93
+ .map { |i| full_path(i) }
94
+ when String
95
+ Dir[full_path(file_name)]
96
+ end
97
+ end
98
+
99
+ def full_path(file_name)
100
+ ::File.join(file_dir, file_name)
101
+ end
102
+
101
103
  def validate_file(data_file)
102
104
  return errors << "no matching files" unless data_file
103
105
  validate_file_newer(data_file)
@@ -106,24 +108,33 @@ module Stockboy::Providers
106
108
  end
107
109
 
108
110
  def validate_file_newer(data_file)
109
- @data_time ||= data_file.mtime
110
- if file_newer && @data_time < file_newer
111
+ read_data_time(data_file)
112
+ if file_newer && data_time < file_newer
111
113
  errors << "no new files since #{file_newer}"
112
114
  end
113
115
  end
114
116
 
115
117
  def validate_file_smaller(data_file)
116
- @data_size ||= data_file.size
117
- if file_smaller && @data_size > file_smaller
118
+ read_data_size(data_file)
119
+ if file_smaller && data_size > file_smaller
118
120
  errors << "file size larger than #{file_smaller}"
119
121
  end
120
122
  end
121
123
 
122
124
  def validate_file_larger(data_file)
123
- @data_size ||= data_file.size
124
- if file_larger && @data_size < file_larger
125
+ read_data_size(data_file)
126
+ if file_larger && data_size < file_larger
125
127
  errors << "file size smaller than #{file_larger}"
126
128
  end
127
129
  end
130
+
131
+ def read_data_size(file)
132
+ @data_size ||= file.size
133
+ end
134
+
135
+ def read_data_time(file)
136
+ @data_time ||= file.mtime
137
+ end
138
+
128
139
  end
129
140
  end
@@ -148,9 +148,9 @@ module Stockboy::Providers
148
148
  client do |ftp|
149
149
  validate_file(matching_file)
150
150
  if valid?
151
- logger.info "FTP getting file #{host} #{file_dir}/#{matching_file}"
151
+ logger.debug "FTP getting file #{inspect_matching_file}"
152
152
  @data = ftp.get(matching_file, nil)
153
- logger.info "FTP got file #{host} #{file_dir}/#{matching_file} (#{@data_size} bytes)"
153
+ logger.debug "FTP got file #{inspect_matching_file} (#{data_size} bytes)"
154
154
  end
155
155
  end
156
156
  !@data.nil?
@@ -202,5 +202,9 @@ module Stockboy::Providers
202
202
  errors << "File size smaller than #{file_larger}"
203
203
  end
204
204
  end
205
+
206
+ def inspect_matching_file
207
+ "#{host} #{file_dir}/#{matching_file}"
208
+ end
205
209
  end
206
210
  end
@@ -125,8 +125,12 @@ module Stockboy::Providers
125
125
  end
126
126
 
127
127
  def client
128
- return HTTPI unless block_given?
129
- yield HTTPI
128
+ orig_logger, HTTPI.logger = HTTPI.logger, logger
129
+ req = HTTPI::Request.new(uri)
130
+ req.auth.basic(username, password) if username && password
131
+ yield req if block_given?
132
+ HTTPI.logger = orig_logger
133
+ req
130
134
  end
131
135
 
132
136
  private
@@ -138,14 +142,13 @@ module Stockboy::Providers
138
142
  end
139
143
 
140
144
  def fetch_data
141
- request = HTTPI::Request.new
142
- request.url = uri
143
- request.auth.basic(username, password) if username && password
144
- response = HTTPI.send(method, request)
145
- if response.error?
146
- errors << "HTTP response error: #{response.code}"
147
- else
148
- @data = response.body
145
+ client do |request|
146
+ response = HTTPI.request(method, request)
147
+ if response.error?
148
+ errors << "HTTP response error: #{response.code}"
149
+ else
150
+ @data = response.body
151
+ end
149
152
  end
150
153
  end
151
154
 
@@ -170,34 +170,35 @@ module Stockboy::Providers
170
170
  end
171
171
  end
172
172
 
173
- # Purge the email from the mailbox corresponding to the [#matching_file]
173
+ # Purge the email from the mailbox corresponding to the [#message_key]
174
174
  #
175
- # This can only be called after selecting the matching file to confirm the
176
- # selected item, or after fetching the data
175
+ # This can only be called after selecting the message_key to confirm the
176
+ # selected item, or after fetching the data.
177
177
  #
178
178
  def delete_data
179
- raise Stockboy::OutOfSequence, "must confirm #matching_message or calling #data" unless picked_matching_message?
179
+ picked_message_key? or raise Stockboy::OutOfSequence,
180
+ "must confirm #message_key or calling #data"
180
181
 
181
- logger.info "Deleting message #{username}:#{host} message_uid: #{matching_message}"
182
182
  client do |imap|
183
- imap.uid_store(matching_message, "+FLAGS", [:Deleted])
183
+ logger.info "Deleting message #{inspect_message_key}"
184
+ imap.uid_store(message_key, "+FLAGS", [:Deleted])
184
185
  imap.expunge
185
186
  end
186
187
  end
187
188
 
188
189
  # IMAP message id for the email that contains the selected data to process
189
190
  #
190
- def matching_message
191
- return @matching_message if @matching_message
191
+ def message_key
192
+ return @message_key if @message_key
192
193
  message_ids = search(default_search_options)
193
- @matching_message = pick_from(message_ids) unless message_ids.empty?
194
+ @message_key = pick_from(message_ids) unless message_ids.empty?
194
195
  end
195
196
 
196
197
  # Clear received data and allow for selecting a new item from the server
197
198
  #
198
199
  def clear
199
200
  super
200
- @matching_message = nil
201
+ @message_key = nil
201
202
  @data_time = nil
202
203
  @data_size = nil
203
204
  end
@@ -237,21 +238,35 @@ module Stockboy::Providers
237
238
 
238
239
  def fetch_data
239
240
  client do |imap|
240
- return false unless matching_message
241
- mail = ::Mail.new(imap.fetch(matching_message, 'RFC822')[0].attr['RFC822'])
242
- if part = mail.attachments.detect { |part| validate_attachment(part) }
243
- validate_file(part.decoded)
244
- if valid?
245
- logger.info "Getting file from #{username}:#{host} message_uid #{matching_message}"
246
- @data = part.decoded
241
+ open_message(message_key) do |mail|
242
+ open_attachment(mail) do |part|
243
+ logger.debug "Getting file from #{inspect_message_key}"
244
+ @data = part
247
245
  @data_time = normalize_imap_datetime(mail.date)
248
- logger.info "Got file from #{username}:#{host} message_uid #{matching_message}"
246
+ logger.debug "Got file from #{inspect_message_key}"
249
247
  end
250
248
  end
251
249
  end
252
250
  !@data.nil?
253
251
  end
254
252
 
253
+ def open_message(id)
254
+ return unless id
255
+ client do |imap|
256
+ imap_message = imap.fetch(id, 'RFC822').first or return
257
+ mail = ::Mail.new(imap_message.attr['RFC822'])
258
+ yield mail if block_given?
259
+ mail
260
+ end
261
+ end
262
+
263
+ def open_attachment(mail)
264
+ part = mail.attachments.detect { |part| validate_attachment(part) }
265
+ validate_file(part) if part or return
266
+ yield part.decoded if valid?
267
+ part
268
+ end
269
+
255
270
  def validate
256
271
  errors << "host must be specified" if host.blank?
257
272
  errors << "username must be specified" if username.blank?
@@ -259,8 +274,8 @@ module Stockboy::Providers
259
274
  errors.empty?
260
275
  end
261
276
 
262
- def picked_matching_message?
263
- !!@matching_message
277
+ def picked_message_key?
278
+ !!@message_key
264
279
  end
265
280
 
266
281
  def validate_attachment(part)
@@ -289,18 +304,26 @@ module Stockboy::Providers
289
304
  end
290
305
 
291
306
  def validate_file_smaller(data_file)
292
- @data_size ||= data_file.bytesize
293
- if file_smaller && @data_size > file_smaller
307
+ read_data_size(data_file)
308
+ if file_smaller && data_size > file_smaller
294
309
  errors << "File size larger than #{file_smaller}"
295
310
  end
296
311
  end
297
312
 
298
313
  def validate_file_larger(data_file)
299
- @data_size ||= data_file.bytesize
300
- if file_larger && @data_size < file_larger
314
+ read_data_size(data_file)
315
+ if file_larger && data_size < file_larger
301
316
  errors << "File size smaller than #{file_larger}"
302
317
  end
303
318
  end
319
+
320
+ def read_data_size(data_file)
321
+ @data_size ||= data_file.body.raw_source.bytesize
322
+ end
323
+
324
+ def inspect_message_key
325
+ "#{username}:#{host} message_uid #{message_key}"
326
+ end
304
327
  end
305
328
 
306
329
  end