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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +7 -2
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -3
- data/lib/stockboy/configurator.rb +3 -1
- data/lib/stockboy/filters.rb +0 -9
- data/lib/stockboy/job.rb +3 -3
- data/lib/stockboy/provider.rb +6 -8
- data/lib/stockboy/provider_repeater.rb +2 -3
- data/lib/stockboy/providers/file.rb +32 -21
- data/lib/stockboy/providers/ftp.rb +6 -2
- data/lib/stockboy/providers/http.rb +13 -10
- data/lib/stockboy/providers/imap.rb +47 -24
- data/lib/stockboy/providers/imap/search_options.rb +1 -1
- data/lib/stockboy/providers/soap.rb +3 -2
- data/lib/stockboy/railtie.rb +1 -0
- data/lib/stockboy/reader.rb +2 -0
- data/lib/stockboy/readers/spreadsheet.rb +1 -0
- data/lib/stockboy/registry.rb +4 -8
- data/lib/stockboy/version.rb +1 -1
- data/spec/fixtures/email/csv_attachment.eml +20 -0
- data/spec/spec_helper.rb +24 -2
- data/spec/stockboy/configurator_spec.rb +54 -3
- data/spec/stockboy/filter_spec.rb +7 -0
- data/spec/stockboy/job_spec.rb +65 -2
- data/spec/stockboy/provider_repeater_spec.rb +1 -1
- data/spec/stockboy/provider_spec.rb +21 -0
- data/spec/stockboy/providers/file_spec.rb +38 -18
- data/spec/stockboy/providers/http_spec.rb +1 -10
- data/spec/stockboy/providers/imap/search_options_spec.rb +13 -2
- data/spec/stockboy/providers/imap_spec.rb +83 -5
- data/spec/stockboy/providers/soap_spec.rb +1 -1
- data/spec/stockboy/reader_spec.rb +26 -0
- data/spec/stockboy/readers/spreadsheet_spec.rb +1 -1
- data/spec/stockboy/readers/xml_spec.rb +1 -1
- data/spec/stockboy/translations_spec.rb +25 -0
- metadata +27 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65b059955605b61f5eca5555bf69e013660fbc41
|
4
|
+
data.tar.gz: b23aa61607e930fe36b5920afe48b397aee52585
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 303f8e62f4025c494049ec3a5bf5b9eb004809f433f503cdea7edde82012e48a1ff0871a5992a32bfd31990f9663fdbc8b7a55182f22474e96019f0b0732a9d3
|
7
|
+
data.tar.gz: 5c8dee0518d7c27137da5e40c01409f56c9c793a1a24150631013672176cff95750cf8c677e56ad5d9c46be44e90314591a10e3c4bf23693408f6d43f87492e8
|
data/.gitignore
CHANGED
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:
|
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
|
-
|
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
|
data/lib/stockboy/filters.rb
CHANGED
@@ -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
|
192
|
-
read = "reader=#{(Stockboy::Readers.all.key(reader.class) || reader.class
|
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}"
|
data/lib/stockboy/provider.rb
CHANGED
@@ -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
|
39
|
+
return provider
|
40
40
|
end
|
41
|
-
|
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
|
-
|
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
|
-
|
110
|
-
if 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
|
-
|
117
|
-
if 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
|
-
|
124
|
-
if 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.
|
151
|
+
logger.debug "FTP getting file #{inspect_matching_file}"
|
152
152
|
@data = ftp.get(matching_file, nil)
|
153
|
-
logger.
|
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
|
-
|
129
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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 [#
|
173
|
+
# Purge the email from the mailbox corresponding to the [#message_key]
|
174
174
|
#
|
175
|
-
# This can only be called after selecting 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,
|
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
|
-
|
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
|
191
|
-
return @
|
191
|
+
def message_key
|
192
|
+
return @message_key if @message_key
|
192
193
|
message_ids = search(default_search_options)
|
193
|
-
@
|
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
|
-
@
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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.
|
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
|
263
|
-
!!@
|
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
|
-
|
293
|
-
if 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
|
-
|
300
|
-
if 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
|