stockboy 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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