worker_tools 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +42 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +211 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/worker_tools/basics.rb +96 -0
- data/lib/worker_tools/csv_input.rb +191 -0
- data/lib/worker_tools/csv_output.rb +82 -0
- data/lib/worker_tools/recorder.rb +81 -0
- data/lib/worker_tools/rocketchat_error_notifier.rb +68 -0
- data/lib/worker_tools/version.rb +3 -0
- data/lib/worker_tools/xlsx_input.rb +169 -0
- data/lib/worker_tools.rb +4 -0
- data/worker_tools.gemspec +43 -0
- metadata +259 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7b0794f3ef2c1157bf4254ec78087218ff50dcc4
|
4
|
+
data.tar.gz: 28cb0e939304f7d81becb39e6d223493b059c60e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78e98bd9a94c25f990f809b72d60257121467875154c2bda55a1b7ef65e25b7ecaba0faffebd932f22c775a213eb554886d87aab8080aab3458f3aaf8bc71ae8
|
7
|
+
data.tar.gz: 2d910f18319f729d0b05ced99d6fc24886308439e12c01ea2a58a09ee1320e4ee83bd3d2e0f36aa1915fff11fab7d578d5134805de0da7cd3c7b7fefe2ebcd51
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# See all options in these files:
|
2
|
+
# https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
3
|
+
# https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml
|
4
|
+
# https://github.com/bbatsov/rubocop/blob/master/config/disabled.yml
|
5
|
+
|
6
|
+
# run bundle exec rubocop -D to see what cop is reporting
|
7
|
+
|
8
|
+
# If you want to skip the check for line length in a specific case do:
|
9
|
+
# rubocop:disable LineLength
|
10
|
+
# superlong-line-with-unavoidable-excesive-length
|
11
|
+
# rubocop:enable LineLength
|
12
|
+
|
13
|
+
AllCops:
|
14
|
+
TargetRubyVersion: 2.3
|
15
|
+
Exclude:
|
16
|
+
- bin/*
|
17
|
+
- Rakefile
|
18
|
+
|
19
|
+
Documentation:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Metrics/BlockLength:
|
23
|
+
Exclude:
|
24
|
+
- test/**/*
|
25
|
+
- ./*.gemspec
|
26
|
+
|
27
|
+
Metrics/LineLength:
|
28
|
+
Max: 120
|
29
|
+
Exclude:
|
30
|
+
- test/**/*
|
31
|
+
|
32
|
+
Metrics/MethodLength:
|
33
|
+
Exclude:
|
34
|
+
- test/**/*
|
35
|
+
|
36
|
+
Style/FrozenStringLiteralComment:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
# Although casecmp is more performant, downcase is more readable
|
40
|
+
# 'Foo'.downcase.eql?('foo') vs 'Foo'.casecmp('foo').zero?
|
41
|
+
Performance/Casecmp:
|
42
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 i22 Digitalagentur GmbH
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
# WorkerTools
|
2
|
+
|
3
|
+
[](https://travis-ci.org/i22-digitalagentur/worker-tools)
|
4
|
+
|
5
|
+
WorkerTools is a collection of modules meant to speed up how we write background tasks following a few basic patterns. The structure of plain independent modules with limited abstraction allows to define and override a few methods according to your needs without requiring a deep investment in the library.
|
6
|
+
|
7
|
+
These modules provide some features and conventions to address the following points with little configuration on your part:
|
8
|
+
|
9
|
+
- How to save the state the task.
|
10
|
+
- How to save information relevant to the admins / customers.
|
11
|
+
- How to log the details
|
12
|
+
- How to handle exceptions and send notifications
|
13
|
+
- How to process CSV files (as input and output)
|
14
|
+
- How to process XLXS files (as input, output coming next)
|
15
|
+
- How to set options
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'worker_tools'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install worker_tools
|
32
|
+
|
33
|
+
## Conventions
|
34
|
+
|
35
|
+
Most of the modules require an ActiveRecord model to keep track of the state, information, and files related to the job. The class of this model is typically an Import, Export, Report.. or something more generic like a JobEntry.
|
36
|
+
|
37
|
+
An example of this model for an Import using Paperclip would be something like this:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Import < ApplicationRecord
|
41
|
+
enum state: { waiting: 0, complete: 1, failed: 2 }
|
42
|
+
enum kind: { foo: 0, bar: 1 }
|
43
|
+
|
44
|
+
has_attached_file :attachment
|
45
|
+
|
46
|
+
validates :kind, presence: true
|
47
|
+
validates :state, presence: true
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
The state `complete` and `failed` are used by the modules. Both `state` and `kind` could be an enum or just a string field. Whether you have one, none or many attachments, and which library you use to handle it's up to you.
|
52
|
+
|
53
|
+
In this case the migration would be something like this:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def change
|
57
|
+
create_table :imports do |t|
|
58
|
+
t.integer :kind, null: false
|
59
|
+
t.integer :state, default: 0, null: false
|
60
|
+
t.text :information
|
61
|
+
t.json :options, default: {}
|
62
|
+
|
63
|
+
t.string :attachment_file_name
|
64
|
+
t.integer :attachment_file_size
|
65
|
+
t.string :attachment_content_type
|
66
|
+
|
67
|
+
t.timestamps
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
## Module 'Basics'
|
72
|
+
|
73
|
+
the [basics module](/lib/worker_tools/basics.rb) takes care of finding or creating the model, marking it as completed or failed, and calling any flow control wrappers around `run` that had been specified. (See wrappers)
|
74
|
+
|
75
|
+
A simple example would be as follows:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class MyImporter
|
79
|
+
include WorkerTools::Basic
|
80
|
+
wrappers :basics
|
81
|
+
|
82
|
+
def model_class
|
83
|
+
Import
|
84
|
+
end
|
85
|
+
|
86
|
+
def model_kind
|
87
|
+
'foo'
|
88
|
+
end
|
89
|
+
|
90
|
+
def run
|
91
|
+
# do stuff
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
The basics module contains a `perform` method, which is the usual entry point for ApplicationJob and Sidekiq. It can receive the id of the model, the model instance, or nothing, in which case it will attempt to create this model on its own.
|
97
|
+
|
98
|
+
|
99
|
+
## Module 'Recorder'
|
100
|
+
|
101
|
+
Provides some methods to manage a log and the `information` field of the model. The main methods are `add_info`, `add_log`, and `record` (which both logs and appends the message to the information field). See all methods in [recorder](/lib/worker_tools/recorder.rb)
|
102
|
+
|
103
|
+
This module has a _recoder_ wrapper that will register the exception details into the log and information field in case of error:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class MyImporter
|
107
|
+
include WorkerTools::Basic
|
108
|
+
wrappers :basics, :recorder
|
109
|
+
# ...
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
If you only want the logger functions, without worrying about persisting a model, you can use the `logger` wrapper and include the module as a stand alone component (without the basics module), like this:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class StandAloneWithLogging
|
117
|
+
include WorkerTools::Recorder
|
118
|
+
|
119
|
+
def perform
|
120
|
+
with_wrapper_logger do
|
121
|
+
# do stuff
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
## Module RocketchatErrorNotifier
|
128
|
+
|
129
|
+
[recorder](/lib/worker_tools/rocketchat_error_notifier.rb)
|
130
|
+
|
131
|
+
## Module CSV Input
|
132
|
+
|
133
|
+
[csv_input](/lib/worker_tools/csv_input.rb)
|
134
|
+
|
135
|
+
## Module CSV Output
|
136
|
+
|
137
|
+
[csv_output](/lib/worker_tools/csv_output.rb)
|
138
|
+
|
139
|
+
## Module XLSX Input
|
140
|
+
|
141
|
+
[xlsx_input](/lib/worker_tools/xlsx_input.rb)
|
142
|
+
|
143
|
+
|
144
|
+
## Wrappers
|
145
|
+
|
146
|
+
In the [basics module](/lib/worker_tools/basics.rb), `perform` calls your custom method `run` to do the actual work of the task, and wraps it around any methods expecting a block that you might have had defined using `wrappers`. That gives us a systematic way to add logic depending on the output of `run` and any exceptions that might arise, such as logging the error and context, sending a chat notification, retrying under some circumstances, etc.
|
147
|
+
|
148
|
+
The following code
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class MyImporter
|
152
|
+
include WorkerTools::Basic
|
153
|
+
wrappers :basics
|
154
|
+
|
155
|
+
def run
|
156
|
+
# do stuff
|
157
|
+
end
|
158
|
+
|
159
|
+
# ..
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
is internally handled as
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
def perform(model_id)
|
167
|
+
# set model
|
168
|
+
with_wrapper_basics do
|
169
|
+
run
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
where this wrapper method looks like
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
def with_wrapper_basics(&block)
|
178
|
+
block.yield # calls run
|
179
|
+
# marks the import as complete
|
180
|
+
rescue Exception
|
181
|
+
# marks the import as failed
|
182
|
+
raise
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
if we also add a wrapper to send notifications, such as
|
187
|
+
`wrappers :basics, :rocketchat_error_notifier`
|
188
|
+
|
189
|
+
the resulting nested calls would look like
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
def perform(model_id)
|
193
|
+
# set model
|
194
|
+
with_wrapper_basics do
|
195
|
+
with_wrapper_rocketchat_error_notifier do
|
196
|
+
run
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
|
203
|
+
## Contributing
|
204
|
+
|
205
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/i22-digitalagentur/worker_tools.
|
206
|
+
|
207
|
+
|
208
|
+
## License
|
209
|
+
|
210
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
211
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "worker_tools"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module WorkerTools
|
4
|
+
module Basics
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_writer :model
|
9
|
+
attr_accessor :information
|
10
|
+
|
11
|
+
def self.wrappers(*args)
|
12
|
+
@wrappers ||= args.flatten
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.read_wrappers
|
16
|
+
@wrappers || []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def model_class
|
21
|
+
# Ex: Import
|
22
|
+
raise "model_class has to be defined in #{self}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def model_kind
|
26
|
+
# Ex: 'sdom'
|
27
|
+
raise "model_kind has to be defined in #{self}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
raise "run has to be defined in #{self}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform(model_id = nil)
|
35
|
+
@model_id = model_id
|
36
|
+
with_wrappers(wrapper_methods) do
|
37
|
+
run
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def wrapper_methods
|
42
|
+
self.class.read_wrappers.map do |wrapper|
|
43
|
+
symbolized_method = "with_wrapper_#{wrapper}".to_sym
|
44
|
+
raise "Missing wrapper #{wrapper}" unless respond_to?(symbolized_method)
|
45
|
+
symbolized_method
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_wrapper_basics(&block)
|
50
|
+
block.yield
|
51
|
+
finalize
|
52
|
+
# this time we do want to catch Exception to attempt to handle some of the
|
53
|
+
# critical errors.
|
54
|
+
# rubocop:disable Lint/RescueException
|
55
|
+
rescue Exception
|
56
|
+
# rubocop:enable Lint/RescueException
|
57
|
+
model.state = 'failed'
|
58
|
+
model.save!(validate: false)
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
|
62
|
+
def finalize
|
63
|
+
model.update_attributes!(
|
64
|
+
state: 'complete',
|
65
|
+
information: information
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_model_if_not_available
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
def model
|
74
|
+
@model ||= find_model
|
75
|
+
end
|
76
|
+
|
77
|
+
def with_wrappers(wrapper_symbols, &block)
|
78
|
+
return yield if wrapper_symbols.blank?
|
79
|
+
current_wrapper_symbol = wrapper_symbols.shift
|
80
|
+
send(current_wrapper_symbol) { with_wrappers(wrapper_symbols, &block) }
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def find_model
|
86
|
+
@model_id ||= nil
|
87
|
+
return @model_id if @model_id.is_a?(model_class)
|
88
|
+
return model_class.find(@model_id) if @model_id
|
89
|
+
raise 'Model not available' unless create_model_if_not_available
|
90
|
+
t = model_class.new
|
91
|
+
t.kind = model_kind if t.respond_to?(:kind=)
|
92
|
+
t.save!(validate: false)
|
93
|
+
t
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module WorkerTools
|
4
|
+
module CsvInput
|
5
|
+
# If an array is provided, the names will be used as the row keys, the row
|
6
|
+
# values will be assign according to the columns order.
|
7
|
+
#
|
8
|
+
# Ex: %w(tenant segment area)
|
9
|
+
# row => {
|
10
|
+
# tenant: _value_at_first_column_,
|
11
|
+
# segment: _value_at_second_column_,
|
12
|
+
# area: _value_at_third_column_
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# If a hash if provided, the keys will turn into the row keys, the values
|
16
|
+
# will be used to find the corresponding columns (the order in the csv won't
|
17
|
+
# affect the import)
|
18
|
+
#
|
19
|
+
# Ex: { tenant: 'Mandant', segment: 'Segment', area: 'Bereich')
|
20
|
+
# row => {
|
21
|
+
# tenant: _value_at_column_Mandant,
|
22
|
+
# segment: _value_at_column_Segment,
|
23
|
+
# area: _value_at_column_Bereich
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# The name of the column is filtered using the csv_input_header_normalized
|
27
|
+
# method, which takes care of extra spaces and looks for a case insentive
|
28
|
+
# match (so 'Bereich' matches ' Bereich', 'bereich', etc.). You can override
|
29
|
+
# that method as well.
|
30
|
+
#
|
31
|
+
# Besides matching the columns using strings, it is possible to use a regular
|
32
|
+
# expression or a proc:
|
33
|
+
# {
|
34
|
+
# tenant: 'Mandant',
|
35
|
+
# segment: /Segment/i,
|
36
|
+
# area: ->(name) { name.downcase == 'area' }
|
37
|
+
# }
|
38
|
+
def csv_input_columns
|
39
|
+
raise "csv_input_columns has to be defined in #{self}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def csv_input_header_normalized(name)
|
43
|
+
name = name.to_s.strip
|
44
|
+
name = name.downcase if csv_input_header_normalize?
|
45
|
+
name
|
46
|
+
end
|
47
|
+
|
48
|
+
# Allows for some basic cleanup of the values, such as applying strip to
|
49
|
+
# the strings.
|
50
|
+
def cvs_input_value_cleanup(value)
|
51
|
+
value.is_a?(String) ? value.strip : value
|
52
|
+
end
|
53
|
+
|
54
|
+
def csv_input_columns_check(csv_rows_enum)
|
55
|
+
# override and return true if you do not want this check to be performed
|
56
|
+
return csv_input_columns_array_check(csv_rows_enum) if csv_input_columns.is_a?(Array)
|
57
|
+
csv_input_columns_hash_check(csv_rows_enum)
|
58
|
+
end
|
59
|
+
|
60
|
+
def csv_input_columns_array_check(csv_rows_enum)
|
61
|
+
expected_columns_length = csv_input_columns.length
|
62
|
+
actual_columns_length = csv_rows_enum.first.length
|
63
|
+
return if expected_columns_length == actual_columns_length
|
64
|
+
raise "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
|
65
|
+
end
|
66
|
+
|
67
|
+
def csv_input_columns_hash_check(csv_rows_enum)
|
68
|
+
expected_names = csv_input_columns.values
|
69
|
+
filtered_actual_names = csv_rows_enum.first.map { |n| csv_input_header_normalized(n) }
|
70
|
+
csv_input_columns_hash_check_duplicates(filtered_actual_names)
|
71
|
+
csv_input_columns_hash_check_missing(filtered_actual_names, expected_names)
|
72
|
+
end
|
73
|
+
|
74
|
+
def csv_input_columns_hash_check_duplicates(names)
|
75
|
+
dups = names.group_by(&:itself).select { |_, v| v.count > 1 }.keys
|
76
|
+
raise "The file contains duplicated columns: #{dups}" if dups.present?
|
77
|
+
end
|
78
|
+
|
79
|
+
def csv_input_columns_hash_check_missing(actual_names, expected_names)
|
80
|
+
missing = expected_names.reject do |name|
|
81
|
+
matchable = name.is_a?(String) ? csv_input_header_normalized(name) : name
|
82
|
+
actual_names.any? { |n| case n when matchable then true end } # rubocop does not like ===
|
83
|
+
end
|
84
|
+
raise "Some columns are missing: #{missing}" unless missing.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def csv_input_csv_options
|
88
|
+
# Ex: { col_sep: ';', encoding: Encoding::ISO_8859_1 }
|
89
|
+
{ col_sep: ';' }
|
90
|
+
end
|
91
|
+
|
92
|
+
def csv_input_include_other_columns
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
def csv_input_header_normalize?
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Compares the first row (header names) with the csv_input_columns hash to find
|
101
|
+
# the corresponding positions.
|
102
|
+
#
|
103
|
+
# Ex: csv_input_columns: {tenant: 'Mandant', area: 'Bereich'}
|
104
|
+
# headers: ['Bereich', 'Mandant']
|
105
|
+
# => { tenant: 1, area: 0}
|
106
|
+
def csv_input_mapping_order(header_names)
|
107
|
+
return csv_input_columns.map.with_index { |n, i| [n, i] }.to_h if csv_input_columns.is_a?(Array)
|
108
|
+
csv_input_mapping_order_for_hash(header_names)
|
109
|
+
end
|
110
|
+
|
111
|
+
def csv_input_mapping_order_for_hash(header_names)
|
112
|
+
filtered_column_names = header_names.map { |n| csv_input_header_normalized(n) }
|
113
|
+
mapping = csv_input_columns.each_with_object({}) do |(k, v), h|
|
114
|
+
matchable = v.is_a?(String) ? csv_input_header_normalized(v) : v
|
115
|
+
h[k] = filtered_column_names.index { |n| case n when matchable then true end }
|
116
|
+
end
|
117
|
+
return mapping unless csv_input_include_other_columns
|
118
|
+
csv_input_mapping_order_with_other_columns(mapping, filtered_column_names)
|
119
|
+
end
|
120
|
+
|
121
|
+
def csv_input_mapping_order_with_other_columns(mapping, filtered_column_names)
|
122
|
+
positions_taken = mapping.values
|
123
|
+
filtered_column_names.each_with_index do |header, index|
|
124
|
+
mapping[header.to_sym] = index unless positions_taken.include?(index)
|
125
|
+
end
|
126
|
+
mapping
|
127
|
+
end
|
128
|
+
|
129
|
+
def csv_input_file_path
|
130
|
+
model.attachment.path.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
def csv_rows_enum
|
134
|
+
@csv_rows_enum ||= CSV.foreach(csv_input_file_path, csv_input_csv_options)
|
135
|
+
end
|
136
|
+
|
137
|
+
def csv_input_headers_present
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def csv_input_foreach
|
142
|
+
@csv_input_foreach ||= begin
|
143
|
+
csv_input_columns_check(csv_rows_enum)
|
144
|
+
|
145
|
+
CsvInputForeach.new(
|
146
|
+
rows_enum: csv_rows_enum,
|
147
|
+
input_columns: csv_input_columns,
|
148
|
+
mapping_order: csv_input_mapping_order(csv_rows_enum.first),
|
149
|
+
cleanup_method: method(:cvs_input_value_cleanup),
|
150
|
+
headers_present: csv_input_headers_present
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class CsvInputForeach
|
156
|
+
include Enumerable
|
157
|
+
|
158
|
+
def initialize(rows_enum:, input_columns:, mapping_order:, cleanup_method:, headers_present:)
|
159
|
+
@rows_enum = rows_enum
|
160
|
+
@input_columns = input_columns
|
161
|
+
@mapping_order = mapping_order
|
162
|
+
@cleanup_method = cleanup_method
|
163
|
+
@headers_present = headers_present
|
164
|
+
end
|
165
|
+
|
166
|
+
def each
|
167
|
+
return enum_for(:each) unless block_given?
|
168
|
+
|
169
|
+
@rows_enum.with_index.each do |values, index|
|
170
|
+
next if index.zero? && @headers_present
|
171
|
+
yield values_to_row(values)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def values_to_row(values)
|
176
|
+
return values_to_row_according_to_mapping(values) if @mapping_order
|
177
|
+
values_to_row_according_to_position(values)
|
178
|
+
end
|
179
|
+
|
180
|
+
def values_to_row_according_to_mapping(values)
|
181
|
+
@mapping_order.each_with_object(HashWithIndifferentAccess.new) do |(k, v), h|
|
182
|
+
h[k] = @cleanup_method.call(values[v])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def values_to_row_according_to_position(values)
|
187
|
+
@input_columns.map.with_index { |c, i| [c, @cleanup_method.call(values[i])] }.to_h.with_indifferent_access
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module WorkerTools
|
4
|
+
module CsvOutput
|
5
|
+
# if defined, this file will be written to this destination (regardless
|
6
|
+
# of whether the model saves the file as well)
|
7
|
+
def csv_output_target
|
8
|
+
# Ex: Rails.root.join('shared', 'foo', 'bar.csv')
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def csv_output_entries
|
13
|
+
raise "csv_output_entries has to be defined in #{self}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def csv_output_column_headers
|
17
|
+
# These columns are used to set the headers, also
|
18
|
+
# to set the row values depending on your implementation.
|
19
|
+
#
|
20
|
+
# To ignore them set it to _false_
|
21
|
+
#
|
22
|
+
# Ex:
|
23
|
+
# @csv_output_column_headers ||= {
|
24
|
+
# foo: 'Foo Header',
|
25
|
+
# bar: 'Bar Header'
|
26
|
+
# }
|
27
|
+
raise "csv_output_column_headers has to be defined in #{self}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
31
|
+
def csv_output_row_values(entry)
|
32
|
+
# Ex:
|
33
|
+
# {
|
34
|
+
# foo: entry.foo,
|
35
|
+
# bar: entry.bar
|
36
|
+
# }.values_at(*csv_output_column_headers.keys)
|
37
|
+
raise "csv_output_row_values has to be defined in #{self}"
|
38
|
+
end
|
39
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
40
|
+
|
41
|
+
def cvs_output_target_folder
|
42
|
+
File.dirname(csv_output_target)
|
43
|
+
end
|
44
|
+
|
45
|
+
def csv_output_target_file_name
|
46
|
+
File.basename(csv_output_target)
|
47
|
+
end
|
48
|
+
|
49
|
+
def csv_ouput_ensure_target_folder
|
50
|
+
FileUtils.mkdir_p(cvs_output_target_folder) unless File.directory?(cvs_output_target_folder)
|
51
|
+
end
|
52
|
+
|
53
|
+
def csv_output_tmp_file
|
54
|
+
@csv_output_tmp_file ||= Tempfile.new(['output', '.csv'])
|
55
|
+
end
|
56
|
+
|
57
|
+
def csv_output_col_sep
|
58
|
+
';'
|
59
|
+
end
|
60
|
+
|
61
|
+
def csv_output_encoding
|
62
|
+
Encoding::UTF_8
|
63
|
+
end
|
64
|
+
|
65
|
+
def csv_output_insert_headers(csv)
|
66
|
+
csv << csv_output_column_headers.values if csv_output_column_headers
|
67
|
+
end
|
68
|
+
|
69
|
+
def csv_output_write_file
|
70
|
+
CSV.open(csv_output_tmp_file, 'wb', col_sep: csv_output_col_sep, encoding: csv_output_encoding) do |csv|
|
71
|
+
csv_output_insert_headers(csv)
|
72
|
+
csv_output_entries.each { |entry| csv << csv_output_row_values(entry) }
|
73
|
+
end
|
74
|
+
csv_output_write_target if csv_output_target
|
75
|
+
end
|
76
|
+
|
77
|
+
def csv_output_write_target
|
78
|
+
csv_ouput_ensure_target_folder
|
79
|
+
FileUtils.cp(csv_output_tmp_file.path, csv_output_target)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module WorkerTools
|
2
|
+
module Recorder
|
3
|
+
def with_wrapper_recorder(&block)
|
4
|
+
block.yield
|
5
|
+
# this time we do want to catch Exception to attempt to handle some of the
|
6
|
+
# critical errors.
|
7
|
+
# rubocop:disable Lint/RescueException
|
8
|
+
rescue Exception => e
|
9
|
+
# rubocop:enable Lint/RescueException
|
10
|
+
record_fail(e)
|
11
|
+
raise
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_wrapper_logger(&block)
|
15
|
+
block.yield
|
16
|
+
# this time we do want to catch Exception to attempt to handle some of the
|
17
|
+
# critical errors.
|
18
|
+
# rubocop:disable Lint/RescueException
|
19
|
+
rescue Exception => e
|
20
|
+
# rubocop:enable Lint/RescueException
|
21
|
+
add_log(e, :error)
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
|
25
|
+
def record_fail(error)
|
26
|
+
record "ID #{model.id} - Error"
|
27
|
+
record(error, :error)
|
28
|
+
model.information = information
|
29
|
+
model.save!(validate: false)
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_log(message, level = :info)
|
33
|
+
logger.public_send(level, format_log_message(message))
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_info(message)
|
37
|
+
@information ||= ''
|
38
|
+
information << "#{format_info_message(message)}\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def record(message, level = :info)
|
42
|
+
add_log(message, level)
|
43
|
+
add_info(message)
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_log_message(message)
|
47
|
+
return error_to_text(message, log_error_trace_lines) if message.is_a?(Exception)
|
48
|
+
message
|
49
|
+
end
|
50
|
+
|
51
|
+
def format_info_message(message)
|
52
|
+
return error_to_text(message, info_error_trace_lines) if message.is_a?(Exception)
|
53
|
+
message
|
54
|
+
end
|
55
|
+
|
56
|
+
def logger
|
57
|
+
@logger ||= Logger.new(File.join(log_directory, log_file_name))
|
58
|
+
end
|
59
|
+
|
60
|
+
def log_directory
|
61
|
+
Rails.root.join('log')
|
62
|
+
end
|
63
|
+
|
64
|
+
def log_file_name
|
65
|
+
"#{self.class.name.underscore.tr('/', '_')}.log"
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_error_trace_lines
|
69
|
+
20
|
70
|
+
end
|
71
|
+
|
72
|
+
def info_error_trace_lines
|
73
|
+
20
|
74
|
+
end
|
75
|
+
|
76
|
+
def error_to_text(error, trace_lines = 20)
|
77
|
+
txt = "Error: #{error.message} (#{error.class})"
|
78
|
+
txt << "Backtrace:\n#{error.backtrace[0, trace_lines].join("\n\t")}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module WorkerTools
|
2
|
+
module RocketchatErrorNotifier
|
3
|
+
def with_wrapper_rocketchat_error_notifier(&block)
|
4
|
+
block.yield
|
5
|
+
rescue StandardError => e
|
6
|
+
rocketchat_error_notify(e) if rocketchat_error_notifier_enabled
|
7
|
+
raise
|
8
|
+
end
|
9
|
+
|
10
|
+
def rocketchat_error_notifier_enabled
|
11
|
+
Rails.env.production?
|
12
|
+
end
|
13
|
+
|
14
|
+
def rocketchat_error_notifier_emoji
|
15
|
+
':red_circle:'
|
16
|
+
end
|
17
|
+
|
18
|
+
def rocketchat_error_notifier_receivers
|
19
|
+
# Ex: '@all'
|
20
|
+
end
|
21
|
+
|
22
|
+
def rocketchat_error_notifier_event
|
23
|
+
'Worker Error Notifier'
|
24
|
+
end
|
25
|
+
|
26
|
+
def rocketchat_error_notifier_title
|
27
|
+
# Example with a link:
|
28
|
+
#
|
29
|
+
# For urls a default_url_options[:host] might be necessary.
|
30
|
+
# In this example I just copy it from existing action_mailer defaults.
|
31
|
+
#
|
32
|
+
# import = rocketchat_error_notifier_model
|
33
|
+
# host = Rails.application.config.action_mailer.default_url_options[:host]
|
34
|
+
# url = Rails.application.routes.url_helpers.import_url(import, host: host, protocol: :https)
|
35
|
+
# kind = I18n.t(import.kind, scope: 'import.kinds')
|
36
|
+
# text = "##{import.id} *#{kind}*"
|
37
|
+
# "[#{text}](#{url})"
|
38
|
+
klass = model.class.model_name.human
|
39
|
+
kind = I18n.t("activerecord.attributes.#{model.class.name.underscore}.kinds.#{model.kind}")
|
40
|
+
"#{klass} #{kind} ##{model.id}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def rocketchat_error_notifier_error_details(error)
|
44
|
+
details = "#{error.class}: #{error.message}\n"
|
45
|
+
details << error.backtrace[0..10].join("\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
def rocketchat_error_notifier_message
|
49
|
+
message = []
|
50
|
+
message << rocketchat_error_notifier_receivers
|
51
|
+
message << rocketchat_error_notifier_title
|
52
|
+
message.compact.join(' - ')
|
53
|
+
end
|
54
|
+
|
55
|
+
def rocketchat_error_notifier_attachment(error)
|
56
|
+
{ collapsed: true, title: 'Error', text: rocketchat_error_notifier_error_details(error) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def rocketchat_error_notify(error)
|
60
|
+
RocketChatNotifier.notify(
|
61
|
+
rocketchat_error_notifier_message,
|
62
|
+
emoji: rocketchat_error_notifier_emoji,
|
63
|
+
event: "#{rocketchat_error_notifier_event} (#{Rails.env})",
|
64
|
+
attachment: rocketchat_error_notifier_attachment(error)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'roo'
|
2
|
+
|
3
|
+
module WorkerTools
|
4
|
+
module XlsxInput
|
5
|
+
# If an array is provided, the names will be used as the row keys, the row
|
6
|
+
# values will be assign according to the columns order.
|
7
|
+
#
|
8
|
+
# Ex: %w(tenant segment area)
|
9
|
+
# row => {
|
10
|
+
# tenant: _value_at_first_column_,
|
11
|
+
# segment: _value_at_second_column_,
|
12
|
+
# area: _value_at_third_column_
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# If a hash if provided, the keys will turn into the row keys, the values
|
16
|
+
# will be used to find the corresponding columns (the order in the spreadsheet
|
17
|
+
# won't affect the import)
|
18
|
+
#
|
19
|
+
# Ex: { tenant: 'Mandant', segment: 'Segment', area: 'Bereich')
|
20
|
+
# row => {
|
21
|
+
# tenant: _value_at_column_Mandant,
|
22
|
+
# segment: _value_at_column_Segment,
|
23
|
+
# area: _value_at_column_Bereich
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# The name of the column is filtered using the xlsx_input_header_normalized
|
27
|
+
# method, which takes care of extra spaces and looks for a case insentive
|
28
|
+
# match (so 'Bereich' matches ' Bereich', 'bereich', etc.). You can override
|
29
|
+
# that method as well.
|
30
|
+
#
|
31
|
+
# Besides matching the columns using strings, it is possible to use a regular
|
32
|
+
# expression or a proc:
|
33
|
+
# {
|
34
|
+
# tenant: 'Mandant',
|
35
|
+
# segment: /Segment/i,
|
36
|
+
# area: ->(name) { name.downcase == 'area' }
|
37
|
+
# }
|
38
|
+
def xlsx_input_columns
|
39
|
+
raise "xlsx_input_columns has to be defined in #{self}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# If true, the rows will append those columns that don't belong to the
|
43
|
+
# xlsx_input_columns list. Useful when the spreadsheet contains some fixed
|
44
|
+
# columns and a number of variable ones.
|
45
|
+
def xlsx_input_include_other_columns
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def xlsx_input_header_normalized(name)
|
50
|
+
name.to_s.strip.downcase
|
51
|
+
end
|
52
|
+
|
53
|
+
# Allows for some basic cleanup of the values, such as applying strip to
|
54
|
+
# the strings.
|
55
|
+
def xlsx_input_value_cleanup(value)
|
56
|
+
value.is_a?(String) ? value.strip : value
|
57
|
+
end
|
58
|
+
|
59
|
+
def xlsx_input_columns_check(xlsx_rows_enum)
|
60
|
+
# override and return true if you do not want this check to be performed
|
61
|
+
return xlsx_input_columns_array_check(xlsx_rows_enum) if xlsx_input_columns.is_a?(Array)
|
62
|
+
xlsx_input_columns_hash_check(xlsx_rows_enum)
|
63
|
+
end
|
64
|
+
|
65
|
+
def xlsx_input_columns_array_check(xlsx_rows_enum)
|
66
|
+
expected_columns_length = xlsx_input_columns.length
|
67
|
+
actual_columns_length = xlsx_rows_enum.first.length
|
68
|
+
return if expected_columns_length == actual_columns_length
|
69
|
+
raise "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
|
70
|
+
end
|
71
|
+
|
72
|
+
def xlsx_input_columns_hash_check(xlsx_rows_enum)
|
73
|
+
expected_names = xlsx_input_columns.values
|
74
|
+
filtered_actual_names = xlsx_rows_enum.first.map { |n| xlsx_input_header_normalized(n) }
|
75
|
+
xlsx_input_columns_hash_check_duplicates(filtered_actual_names)
|
76
|
+
xlsx_input_columns_hash_check_missing(filtered_actual_names, expected_names)
|
77
|
+
end
|
78
|
+
|
79
|
+
def xlsx_input_columns_hash_check_duplicates(names)
|
80
|
+
dups = names.group_by(&:itself).select { |_, v| v.count > 1 }.keys
|
81
|
+
raise "The file contains duplicated columns: #{dups}" if dups.present?
|
82
|
+
end
|
83
|
+
|
84
|
+
def xlsx_input_columns_hash_check_missing(actual_names, expected_names)
|
85
|
+
missing = expected_names.reject do |name|
|
86
|
+
matchable = name.is_a?(String) ? xlsx_input_header_normalized(name) : name
|
87
|
+
actual_names.any? { |n| case n when matchable then true end } # rubocop does not like ===
|
88
|
+
end
|
89
|
+
raise "Some columns are missing: #{missing}" unless missing.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Compares the first row (header names) with the xlsx_input_columns hash to find
|
93
|
+
# the corresponding positions.
|
94
|
+
#
|
95
|
+
# Ex: xlsx_input_columns: {tenant: 'Mandant', area: 'Bereich'}
|
96
|
+
# headers: ['Bereich', 'Mandant']
|
97
|
+
# => { tenant: 1, area: 0}
|
98
|
+
def xlsx_input_mapping_order(header_names)
|
99
|
+
return xlsx_input_columns.map.with_index { |n, i| [n, i] }.to_h if xlsx_input_columns.is_a?(Array)
|
100
|
+
xlsx_input_mapping_order_for_hash(header_names)
|
101
|
+
end
|
102
|
+
|
103
|
+
def xlsx_input_mapping_order_for_hash(header_names)
|
104
|
+
filtered_column_names = header_names.map { |n| xlsx_input_header_normalized(n) }
|
105
|
+
mapping = xlsx_input_columns.each_with_object({}) do |(k, v), h|
|
106
|
+
matchable = v.is_a?(String) ? xlsx_input_header_normalized(v) : v
|
107
|
+
h[k] = filtered_column_names.index { |n| case n when matchable then true end }
|
108
|
+
end
|
109
|
+
return mapping unless xlsx_input_include_other_columns
|
110
|
+
xlsx_input_mapping_order_with_other_columns(mapping, filtered_column_names)
|
111
|
+
end
|
112
|
+
|
113
|
+
def xlsx_input_mapping_order_with_other_columns(mapping, filtered_column_names)
|
114
|
+
positions_taken = mapping.values
|
115
|
+
filtered_column_names.each_with_index do |header, index|
|
116
|
+
mapping[header] = index unless positions_taken.include?(index)
|
117
|
+
end
|
118
|
+
mapping
|
119
|
+
end
|
120
|
+
|
121
|
+
def xlsx_input_file_path
|
122
|
+
model.attachment.path.to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
def xlsx_rows_enum
|
126
|
+
@xlsx_rows_enum ||= begin
|
127
|
+
spreadsheet = Roo::Excelx.new(xlsx_input_file_path)
|
128
|
+
spreadsheet.each_row_streaming(sheet: spreadsheet.sheets.first, pad_cells: true)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def xlsx_input_foreach
|
133
|
+
@xlsx_input_foreach ||= begin
|
134
|
+
xlsx_input_columns_check(xlsx_rows_enum)
|
135
|
+
|
136
|
+
XlsxInputForeach.new(
|
137
|
+
rows_enum: xlsx_rows_enum,
|
138
|
+
mapping_order: xlsx_input_mapping_order(xlsx_rows_enum.first),
|
139
|
+
cleanup_method: method(:xlsx_input_value_cleanup)
|
140
|
+
)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class XlsxInputForeach
|
145
|
+
include Enumerable
|
146
|
+
|
147
|
+
def initialize(rows_enum:, mapping_order:, cleanup_method:)
|
148
|
+
@rows_enum = rows_enum
|
149
|
+
@mapping_order = mapping_order
|
150
|
+
@cleanup_method = cleanup_method
|
151
|
+
end
|
152
|
+
|
153
|
+
def each
|
154
|
+
return enum_for(:each) unless block_given?
|
155
|
+
|
156
|
+
@rows_enum.with_index.each do |values, index|
|
157
|
+
next if index.zero? # headers
|
158
|
+
yield values_to_row(values)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def values_to_row(values)
|
163
|
+
@mapping_order.each_with_object(HashWithIndifferentAccess.new) do |(k, v), h|
|
164
|
+
h[k] = @cleanup_method.call(values[v].try(:value))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/worker_tools.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'worker_tools/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'worker_tools'
|
7
|
+
spec.version = WorkerTools::VERSION
|
8
|
+
spec.authors = ['fsainz']
|
9
|
+
spec.email = ['fernando.sainz@i22.de']
|
10
|
+
|
11
|
+
spec.summary = 'A collection of modules to help writing common worker tasks)'
|
12
|
+
# spec.description = %q{TODO: Write a longer description or delete this line.}
|
13
|
+
spec.homepage = 'https://github.com/i22-digitalagentur/worker-tools'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
23
|
+
end
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
|
28
|
+
spec.add_dependency 'activesupport'
|
29
|
+
spec.add_dependency 'rocketchat-notifier', '>= 0.1.2'
|
30
|
+
spec.add_dependency 'roo'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'activerecord'
|
33
|
+
spec.add_development_dependency 'bundler'
|
34
|
+
spec.add_development_dependency 'database_cleaner'
|
35
|
+
spec.add_development_dependency 'm'
|
36
|
+
spec.add_development_dependency 'minitest'
|
37
|
+
spec.add_development_dependency 'mocha'
|
38
|
+
spec.add_development_dependency 'pry'
|
39
|
+
spec.add_development_dependency 'rake'
|
40
|
+
spec.add_development_dependency 'rubocop', '0.52.1'
|
41
|
+
spec.add_development_dependency 'simplecov'
|
42
|
+
spec.add_development_dependency 'sqlite3'
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: worker_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- fsainz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rocketchat-notifier
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: roo
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activerecord
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: database_cleaner
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: m
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: mocha
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - '='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 0.52.1
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - '='
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 0.52.1
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: simplecov
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: sqlite3
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
description:
|
210
|
+
email:
|
211
|
+
- fernando.sainz@i22.de
|
212
|
+
executables: []
|
213
|
+
extensions: []
|
214
|
+
extra_rdoc_files: []
|
215
|
+
files:
|
216
|
+
- ".gitignore"
|
217
|
+
- ".rubocop.yml"
|
218
|
+
- ".travis.yml"
|
219
|
+
- Gemfile
|
220
|
+
- LICENSE
|
221
|
+
- README.md
|
222
|
+
- Rakefile
|
223
|
+
- bin/console
|
224
|
+
- bin/setup
|
225
|
+
- lib/worker_tools.rb
|
226
|
+
- lib/worker_tools/basics.rb
|
227
|
+
- lib/worker_tools/csv_input.rb
|
228
|
+
- lib/worker_tools/csv_output.rb
|
229
|
+
- lib/worker_tools/recorder.rb
|
230
|
+
- lib/worker_tools/rocketchat_error_notifier.rb
|
231
|
+
- lib/worker_tools/version.rb
|
232
|
+
- lib/worker_tools/xlsx_input.rb
|
233
|
+
- worker_tools.gemspec
|
234
|
+
homepage: https://github.com/i22-digitalagentur/worker-tools
|
235
|
+
licenses:
|
236
|
+
- MIT
|
237
|
+
metadata:
|
238
|
+
allowed_push_host: https://rubygems.org
|
239
|
+
post_install_message:
|
240
|
+
rdoc_options: []
|
241
|
+
require_paths:
|
242
|
+
- lib
|
243
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - ">="
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: '0'
|
248
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
249
|
+
requirements:
|
250
|
+
- - ">="
|
251
|
+
- !ruby/object:Gem::Version
|
252
|
+
version: '0'
|
253
|
+
requirements: []
|
254
|
+
rubyforge_project:
|
255
|
+
rubygems_version: 2.5.1
|
256
|
+
signing_key:
|
257
|
+
specification_version: 4
|
258
|
+
summary: A collection of modules to help writing common worker tasks)
|
259
|
+
test_files: []
|