worker_tools 0.2.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a53ae86156768dd1b1f80601a50f8c6926918d3f625be933173f7d4513c0f2d8
4
- data.tar.gz: 48dfc8c5430311e2b6dae3d5bfbb55d6d2ce0444d3c5e5dd5a9cf044e42afdf7
3
+ metadata.gz: 2d73219c29b55493f0be76ce047dbe7f72259f4efaab15d1528230bdfc1920b5
4
+ data.tar.gz: a8ef47af79183aa6e6b465a0cbc6c7396ccccf3cb92574c386b956ebd3415056
5
5
  SHA512:
6
- metadata.gz: 95ac0fe0d5255b995a5c7a0193edf6a56eb5d376234e1f6088f6c97ad03ea6e760d499c62c2c711f5ad2b453f5c9f7b5309f9bea2acfb50b9c60621b83ed3800
7
- data.tar.gz: e2a09d6b64d813a0296f81d5ef995ed49178957bfdbf67349e5b2d811ec84e98b08850e9565f875f02a6b105f9ae9769cfd177d660b2a4d98bf0b2ae03ecf77c
6
+ metadata.gz: 30287518cdce7bb9536d802fffdaa54b893bfcc811b03f1c226ea59b45787c0f5b4090b82796984e5c77741679bd035620cd3e38aecb4ebb0f1dcac9de50cb16
7
+ data.tar.gz: 18c1c3ff22097f710cea838c6cf4a8058c35343d7f76312e063771ef2dc02c6ad0fbdace866c1b3dc9e21ab3545ea91f041ff16f4d540402be601c44c834d0dc
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2022-05-20
4
+
5
+ Compared to 0.2.1
6
+
7
+ ### New
8
+
9
+ - Namespaced errors and non failure logic
10
+ - Support for status running and complete_with_warnings
11
+ - Benchmark wrapper
12
+ - Counters wrapper
13
+ - Notes instead of information field
14
+ - Filter for slack errors (`slack_error_notifiable`)
15
+ - Model attachments convention
16
+ - Complete specification of csv open arguments
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ Instead of writing the final csv or xlsx to a folder, the gem assumes that the model provides an add_attachment method.
21
+
22
+ Both csv and xlsx output modules use entry hashes for content (`csv_output_entries`, `xlsx_output_entries`). The mapper methods `csv_output_row_values` and `xlsx_output_row_values` do not need (in most cases) to be defined, there is a default now. See the complete examples in the README.
23
+
24
+ These methods were renamed
25
+
26
+ - `xlsx_output_values` => `xlsx_output_row_values`
27
+ - `xlsx_insert_headers` => `xlsx_output_insert_headers`
28
+ - `xlsx_insert_rows` => ` xlsx_output_insert_rows`
29
+ - `xlsx_iterators` => `xlsx_output_iterators`
30
+ - `xlsx_style_columns` => `xlsx_output_style_columns`
31
+ - `xlsx_write_sheet` => `xlsx_output_write_sheet`
32
+
33
+ These methods were removed
34
+
35
+ - `add_info` in favor of `add_note`
36
+ - `create_model_if_not_available`, a model is always created.
37
+ - `format_log_message`, `format_info_message` in favor of `format_message`
38
+ - `csv_output_target`
39
+ - `cvs_output_target_folder`
40
+ - `csv_output_target_file_name`
41
+ - `csv_ouput_ensure_target_folder`
42
+ - `csv_output_write_target`
43
+ - `xlsx_output_target`
44
+ - `xlsx_output_target_folder`
45
+ - `xlsx_ensure_output_target_folder`
46
+ - `xlsx_write_output_target`
47
+
48
+ [1.0.0]: https://github.com/i22-digitalagentur/worker-tools/compare/0.2.1...1.0.0
data/README.md CHANGED
@@ -1,13 +1,53 @@
1
1
  # WorkerTools
2
2
 
3
- [![Build Status](https://travis-ci.org/i22-digitalagentur/worker-tools.svg?branch=master)](https://travis-ci.org/i22-digitalagentur/worker-tools)
3
+ [![Build Status][build-badge]][build-url]
4
+ [![MIT License][license-shield]][license-url]
5
+ [![Release][release-shield]][release-url]
6
+ ![Maintenance][maintained-shield]
7
+
8
+ <br>
9
+
10
+ <details open="open">
11
+ <summary>Table of Contents</summary>
12
+ <ol>
13
+ <li>
14
+ <a href="#about-the-project">About The Project</a>
15
+ </li>
16
+ <li>
17
+ <a href="#installation">Installation</a>
18
+ </li>
19
+ <li><a href="#conventions">Conventions</a></li>
20
+ <li><a href="#module-basics">Module 'Basics'</a></li>
21
+ <li><a href="#module-recorder">Module 'Recorder'</a></li>
22
+ <li><a href="#module-slackerrornotifier">Module 'SlackErrorNotifier'</a></li>
23
+ <li><a href="#wrappers">Wrappers</a></li>
24
+ <li><a href="#module-notes">Module 'Notes'</a></li>
25
+ <li><a href="#attachments">Attachments</a></li>
26
+ <li>
27
+ <a href="#complete-examples">Complete Examples</a>
28
+ <ul>
29
+ <li><a href="#xlsx-input-example">XLSX Input Example</a></li>
30
+ <li><a href="#csv-input-example">CSV Input Example</a></li>
31
+ <li><a href="#csv-output-example">CSV Output Example</a></li>
32
+ <li><a href="#xlsx-output-example">XLSX Output Example</a></li>
33
+ </ul>
34
+ </li>
35
+ <li><a href="#changelog">Changelog</a></li>
36
+ <li><a href="#requirements">Requirements</a></li>
37
+ <li><a href="#contributing">Contributing</a></li>
38
+ <li><a href="#license">License</a></li>
39
+ <li><a href="#acknowledgement">Acknowledgement</a></li>
40
+ </ol>
41
+ </details>
42
+
43
+ ## About The Project
4
44
 
5
45
  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
46
 
7
47
  These modules provide some features and conventions to address the following points with little configuration on your part:
8
48
 
9
49
  - How to save the state the task.
10
- - How to save information relevant to the admins / customers.
50
+ - How to save notes relevant to the admins / customers.
11
51
  - How to log the details
12
52
  - How to handle exceptions and send notifications
13
53
  - How to process CSV files (as input and output)
@@ -32,13 +72,20 @@ Or install it yourself as:
32
72
 
33
73
  ## Conventions
34
74
 
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.
75
+ Most of the modules require an ActiveRecord model to keep track of the state, notes, 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
76
 
37
77
  An example of this model for an Import using Paperclip would be something like this:
38
78
 
39
79
  ```ruby
40
80
  class Import < ApplicationRecord
41
- enum state: { waiting: 0, complete: 1, failed: 2 }
81
+ enum state: %w[
82
+ waiting
83
+ complete
84
+ complete_with_warnings
85
+ failed
86
+ running
87
+ ].map { |e| [e, e] }.to_h
88
+
42
89
  enum kind: { foo: 0, bar: 1 }
43
90
 
44
91
  has_attached_file :attachment
@@ -48,7 +95,9 @@ class Import < ApplicationRecord
48
95
  end
49
96
  ```
50
97
 
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.
98
+ 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.
99
+
100
+ The state `complete_with_warnings` indicates that the model contains notes that did not lead to a failure but should get some attention. By default those levels are `warning` and `errors` and can be customized.
52
101
 
53
102
  In this case the migration would be something like this:
54
103
 
@@ -56,8 +105,8 @@ In this case the migration would be something like this:
56
105
  def change
57
106
  create_table :imports do |t|
58
107
  t.integer :kind, null: false
59
- t.integer :state, default: 0, null: false
60
- t.text :information
108
+ t.string :state, default: 'waiting', null: false
109
+ t.json :notes, default: []
61
110
  t.json :options, default: {}
62
111
  t.json :meta, default: {}
63
112
 
@@ -67,7 +116,7 @@ In this case the migration would be something like this:
67
116
 
68
117
  t.timestamps
69
118
  end
70
- ```
119
+ ```
71
120
 
72
121
  ## Module 'Basics'
73
122
 
@@ -96,12 +145,13 @@ end
96
145
 
97
146
  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.
98
147
 
148
+ By default errors subclassed from WorkerTools::Errors::Invalid (such as those related to wrong headers in the input modules) will not raise and mark the model as failed. The method `non_failure_error?` lets you modifiy this behaviour.
99
149
 
100
150
  ## Module 'Recorder'
101
151
 
102
- 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)
152
+ Provides some methods to manage a log and the `notes` field of the model. The main methods are `add_info`, `add_log`, and `record` (which both logs and appends the message to the notes field). See all methods in [recorder](/lib/worker_tools/recorder.rb)
103
153
 
104
- This module has a _recoder_ wrapper that will register the exception details into the log and information field in case of error:
154
+ This module has a _recoder_ wrapper that will register the exception details into the log and notes field in case of error:
105
155
 
106
156
  ```ruby
107
157
  class MyImporter
@@ -127,24 +177,43 @@ If you only want the logger functions, without worrying about persisting a model
127
177
 
128
178
  ## Module SlackErrorNotifier
129
179
 
130
- [slack_error_notifier](/lib/worker_tools/slack_error_notifier.rb)
180
+ Provides a Slack error notifier wrapper. To do this, you need to define SLACK_NOTIFIER_WEBHOOK as well as SLACK_NOTIFIER_CHANNEL. Then you need to include the SlackErrorNotifier module in your class and append slack_error_notifier to your wrappers. Below you can see an example.
181
+
182
+ ```ruby
183
+ class MyImporter
184
+ include WorkerTools::SlackErrorNotifier
185
+
186
+ wrappers :slack_error_notifier
187
+
188
+ def perform
189
+ with_wrapper_logger do
190
+ # do stuff
191
+ end
192
+ end
193
+ end
194
+ ```
195
+
196
+ See all methods in [slack_error_notifier](/lib/worker_tools/slack_error_notifier.rb)
131
197
 
132
198
  ## Module CSV Input
133
199
 
134
- [csv_input](/lib/worker_tools/csv_input.rb)
200
+ See all methods in [csv_input](/lib/worker_tools/csv_input.rb)
135
201
 
136
202
  ## Module CSV Output
137
203
 
138
- [csv_output](/lib/worker_tools/csv_output.rb)
204
+ See all methods in [csv_output](/lib/worker_tools/csv_output.rb)
139
205
 
140
206
  ## Module XLSX Input
141
207
 
142
- [xlsx_input](/lib/worker_tools/xlsx_input.rb)
208
+ See all methods in [xlsx_input](/lib/worker_tools/xlsx_input.rb)
143
209
 
210
+ ## Module XLSX Output
211
+
212
+ See all methods in [xlsx_output](/lib/worker_tools/xlsx_output.rb)
144
213
 
145
214
  ## Wrappers
146
215
 
147
- 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.
216
+ 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.
148
217
 
149
218
  The following code
150
219
 
@@ -200,13 +269,14 @@ def perform(model_id)
200
269
  end
201
270
  ```
202
271
 
203
- ## Counters
272
+ ## Counter
204
273
 
205
274
  There is a counter wrapper that you can use to add custom counters to the meta attribute. To do this, you need to complete the following tasks:
275
+
206
276
  - include WorkerTools::Counters to your class
207
277
  - add :counters to the wrappers method props
208
278
  - call counters method with your custom counters
209
- You can see an example below. After that, you can access your custom counters via the meta attribute.
279
+ You can see an example below. After that, you can access your custom counters via the meta attribute.
210
280
 
211
281
  ```ruby
212
282
  class MyImporter
@@ -219,13 +289,15 @@ class MyImporter
219
289
  end
220
290
 
221
291
  def example_foo_counter_methods
222
- self.foo = 0
223
-
292
+ # you can use the increment helper
224
293
  10.times { increment_foo }
225
294
 
226
- puts foo # foo == 10
295
+ # the counter works like a regular accessor, you can read it and modify it
296
+ # directly
297
+ self.bar = 100
298
+ puts bar # => 100
227
299
  end
228
-
300
+
229
301
  # ..
230
302
  end
231
303
  ```
@@ -236,7 +308,7 @@ There is a benchmark wrapper that you can use to record the benchmark. The only
236
308
 
237
309
  ```ruby
238
310
  class MyImporter
239
- include WorkerTools::CustomBenchmark
311
+ include WorkerTools::Benchmark
240
312
  wrappers :benchmark
241
313
 
242
314
  def run
@@ -247,11 +319,235 @@ class MyImporter
247
319
  end
248
320
  ```
249
321
 
322
+ ## Module 'Notes'
323
+
324
+ If you use ActiveRecord you may need to modify the serializer as well as deserializer from the note attribute. After that you can easily serialize hashes and array of hashes with indifferent access. For that purpose the gem provides two utility methods. (HashWithIndifferentAccessType, SerializedArrayType). There is an example of how you can use it.
325
+
326
+ ```ruby
327
+ class ServiceTask < ApplicationRecord
328
+
329
+ attribute :notes, SerializedArrayType.new(type: HashWithIndifferentAccessType.new)
330
+ end
331
+ ```
332
+
333
+ See all methods in [utils](/lib/worker_tools/utils)
334
+
335
+ ## Attachments
336
+
337
+ The modules that generate a file expect the model to provide an `add_attachment` method with following signature:
338
+
339
+ ```ruby
340
+ def add_attachment(file, file_name: nil, content_type: nil)
341
+ # your logic
342
+ end
343
+ ```
344
+
345
+ You can skip this convention by overwriting the module related method, for example after including `CsvOutput`
346
+
347
+ ```ruby
348
+ def csv_output_add_attachment
349
+ # default implementation
350
+ # model.add_attachment(csv_output_tmp_file, file_name: csv_output_file_name, content_type: 'text/csv')
351
+
352
+ # your method
353
+ ftp_upload(csv_output_tmp_file)
354
+ end
355
+ ```
356
+
357
+ ## Complete Examples
358
+
359
+ ### XLSX Input Example
360
+
361
+ ```ruby
362
+ class XlsxInputExample
363
+ include Sidekiq::Worker
364
+ include WorkerTools::Basics
365
+ include WorkerTools::Recorder
366
+ include WorkerTools::XlsxInput
367
+
368
+ wrappers %i[basics recorder]
369
+
370
+ def model_class
371
+ Import
372
+ end
373
+
374
+ def model_kind
375
+ 'xlsx_input_example'
376
+ end
377
+
378
+ def run
379
+ xlsx_input_foreach.each { |row| SomeModel.create!(row) }
380
+ end
381
+
382
+ def xlsx_input_columns
383
+ {
384
+ foo: 'Your Foo',
385
+ bar: 'Your Bar'
386
+ }
387
+ end
388
+ end
389
+ ```
390
+
391
+ ### CSV Input Example
392
+
393
+ ```ruby
394
+ class CsvInputExample
395
+ include Sidekiq::Worker
396
+ include WorkerTools::Basics
397
+ include WorkerTools::Recorder
398
+ include WorkerTools::CsvInput
399
+
400
+ wrappers %i[basics recorder]
401
+
402
+ def model_class
403
+ Import
404
+ end
405
+
406
+ def model_kind
407
+ 'csv_input_example'
408
+ end
409
+
410
+ def csv_input_columns
411
+ {
412
+ flavour: 'Flavour',
413
+ number: 'Number'
414
+ }
415
+ end
416
+
417
+ def run
418
+ csv_input_foreach.map { |row| do_something row_to_attributes(row) }
419
+ end
420
+
421
+ def row_to_attributes(row)
422
+ {
423
+ flavour: row['flavour'].downcase,
424
+ number: row['number'].to_i * 10
425
+ }
426
+ end
427
+ end
428
+ ```
429
+
430
+ ### CSV Output Example
431
+
432
+ ```ruby
433
+ # More complex example with CsvOutput
434
+ class CsvOutputExample
435
+ include Sidekiq::Worker
436
+ include WorkerTools::Basics
437
+ include WorkerTools::CsvOutput
438
+ include WorkerTools::Recorder
439
+
440
+ wrappers %i[basics recorder]
441
+
442
+ def model_class
443
+ Report
444
+ end
445
+
446
+ def model_kind
447
+ 'csv_out_example'
448
+ end
449
+
450
+ def model_file_name
451
+ "#{model_kind}-#{Date.current}.csv"
452
+ end
453
+
454
+ def run
455
+ csv_output_write_file
456
+ end
457
+
458
+ def csv_output_column_headers
459
+ @csv_output_column_headers ||= {
460
+ foo: 'Foo',
461
+ bar: 'Bar'
462
+ }
463
+ end
464
+
465
+ def csv_output_entries
466
+ @csv_output_entries ||= User.includes(...).lazy.map do |user|
467
+ {
468
+ foo: user.foo,
469
+ bar: user.bar
470
+ }
471
+ end
472
+ end
473
+
474
+ end
475
+ ```
476
+
477
+ ### XLSX Output Example
478
+
479
+ ```ruby
480
+ # ExampleXlsxOutput
481
+ class XlsxOutputExample
482
+ include Sidekiq::Worker
483
+ include WorkerTools::Basics
484
+ include WorkerTools::Recorder
485
+ include WorkerTools::XlsxOutput
486
+
487
+ wrappers %i[basics recorder]
488
+
489
+ def model_class
490
+ Export
491
+ end
492
+
493
+ def model_kind
494
+ 'xlsx_output_example'
495
+ end
496
+
497
+ def run
498
+ xlsx_output_write_file
499
+ end
500
+
501
+ def xlsx_output_column_headers
502
+ @xlsx_output_column_headers ||= {
503
+ foo: 'Foo',
504
+ bar: 'Bar'
505
+ }
506
+ end
507
+
508
+ def xlsx_output_entries
509
+ @xlsx_output_entries ||= SomeArray.map do |entry|
510
+ {
511
+ foo: user.foo,
512
+ bar: user.bar
513
+ }
514
+ end
515
+ end
516
+ end
517
+ ```
518
+
519
+ ## Changelog
520
+
521
+ See [CHANGELOG](CHANGELOG.md)
522
+
523
+ ## Requirements
524
+
525
+ - ruby > 2.3.1
526
+
250
527
  ## Contributing
251
528
 
252
- Bug reports and pull requests are welcome on GitHub at https://github.com/i22-digitalagentur/worker_tools.
529
+ Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
253
530
 
531
+ 1. Fork the Project
532
+ 2. Create your Feature Branch (`git checkout -b feature/new_feature`)
533
+ 3. Commit your Changes (`git commit -m 'feat: Add new feature'`)
534
+ 4. Push to the Branch (`git push origin feature/new_feature`)
535
+ 5. Open a Pull Request
254
536
 
255
537
  ## License
256
538
 
257
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
539
+ The gem is available under the MIT License. See `LICENSE` for more information.
540
+
541
+ ## Acknowledgement
542
+
543
+ - [Img Shields](https://shields.io)
544
+
545
+ <!--shield-styles-->
546
+
547
+ [build-badge]: https://travis-ci.org/i22-digitalagentur/worker-tools.svg?branch=master
548
+ [build-url]: https://travis-ci.org/i22-digitalagentur/worker-tools
549
+ [maintained-shield]: https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=flat
550
+ [release-shield]: https://img.shields.io/github/release/i22-digitalagentur/coverage-badge-creator.svg?style=flat
551
+ [release-url]: https://github.com/i22-digitalagentur/worker-tools/releases/
552
+ [license-shield]: https://img.shields.io/badge/License-MIT-yellow.svg?style=flat
553
+ [license-url]: https://github.com/i22-digitalagentur/worker-tools/blob/master/LICENSE
@@ -6,7 +6,6 @@ module WorkerTools
6
6
 
7
7
  included do
8
8
  attr_writer :model
9
- attr_accessor :information
10
9
 
11
10
  def self.wrappers(*args)
12
11
  @wrappers ||= args.flatten
@@ -49,27 +48,30 @@ module WorkerTools
49
48
  end
50
49
 
51
50
  def with_wrapper_basics(&block)
51
+ save_state_without_validate('running')
52
52
  block.yield
53
53
  finalize
54
54
  # this time we do want to catch Exception to attempt to handle some of the
55
55
  # critical errors.
56
56
  # rubocop:disable Lint/RescueException
57
- rescue Exception
57
+ rescue Exception => e
58
+ return finalize if non_failure_error?(e)
59
+
58
60
  # rubocop:enable Lint/RescueException
59
- model.state = 'failed'
60
- model.save!(validate: false)
61
+ save_state_without_validate('failed')
61
62
  raise
62
63
  end
63
64
 
64
65
  def finalize
65
- model.update!(
66
- state: 'complete',
67
- information: information
68
- )
66
+ mark_with_warnings = model.notes.any? do |note|
67
+ complete_with_warnings_note_levels.include?(note.with_indifferent_access[:level].to_s)
68
+ end
69
+
70
+ model.update!(state: mark_with_warnings ? :complete_with_warnings : :complete)
69
71
  end
70
72
 
71
- def create_model_if_not_available
72
- false
73
+ def complete_with_warnings_note_levels
74
+ %w[error warning]
73
75
  end
74
76
 
75
77
  def model
@@ -83,13 +85,23 @@ module WorkerTools
83
85
  send(current_wrapper_symbol) { with_wrappers(wrapper_symbols, &block) }
84
86
  end
85
87
 
88
+ def non_failure_error?(error)
89
+ error.is_a?(WorkerTools::Errors::Invalid)
90
+ # or add your list
91
+ # [WorkerTools::Errors::Invalid, SomeOtherError].any? { |k| e.is_a?(k) }
92
+ end
93
+
86
94
  private
87
95
 
96
+ def save_state_without_validate(state)
97
+ model.state = state
98
+ model.save!(validate: false)
99
+ end
100
+
88
101
  def find_model
89
102
  @model_id ||= nil
90
103
  return @model_id if @model_id.is_a?(model_class)
91
104
  return model_class.find(@model_id) if @model_id
92
- raise 'Model not available' unless create_model_if_not_available
93
105
 
94
106
  t = model_class.new
95
107
  t.kind = model_kind if t.respond_to?(:kind=)
@@ -1,12 +1,12 @@
1
1
  module WorkerTools
2
- module CustomBenchmark
2
+ module Benchmark
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
6
  attr_accessor :benchmark
7
7
 
8
8
  def with_wrapper_benchmark(&block)
9
- @benchmark = Benchmark.measure(&block)
9
+ @benchmark = ::Benchmark.measure(&block)
10
10
 
11
11
  model.meta['duration'] = @benchmark.real.round if model.respond_to?(:meta)
12
12
  end
@@ -14,15 +14,14 @@ module WorkerTools
14
14
 
15
15
  def self.add_counter_methods
16
16
  @counters.each do |name|
17
- define_method name do
18
- model.meta[name]
19
- end
20
- define_method "#{name}=" do |value|
21
- model.meta[name] = value
22
- end
23
- define_method "increment_#{name}" do
24
- model.meta[name] += 1
25
- end
17
+ # ex `inserts`
18
+ define_method(name) { model.meta[name] }
19
+
20
+ # ex `inserts=`
21
+ define_method("#{name}=") { |value| model.meta[name] = value }
22
+
23
+ # ex `increment_inserts`
24
+ define_method("increment_#{name}") { model.meta[name] += 1 }
26
25
  end
27
26
  end
28
27
 
@@ -63,7 +63,8 @@ module WorkerTools
63
63
  actual_columns_length = csv_rows_enum.first.length
64
64
  return if expected_columns_length == actual_columns_length
65
65
 
66
- raise "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
66
+ msg = "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
67
+ raise Errors::WrongNumberOfColumns, msg
67
68
  end
68
69
 
69
70
  def csv_input_columns_hash_check(csv_rows_enum)
@@ -75,7 +76,9 @@ module WorkerTools
75
76
 
76
77
  def csv_input_columns_hash_check_duplicates(names)
77
78
  dups = names.group_by(&:itself).select { |_, v| v.count > 1 }.keys
78
- raise "The file contains duplicated columns: #{dups}" if dups.present?
79
+ return unless dups.present?
80
+
81
+ raise Errors::DuplicatedColumns, "The file contains duplicated columns: #{dups}"
79
82
  end
80
83
 
81
84
  def csv_input_columns_hash_check_missing(actual_names, expected_names)
@@ -83,7 +86,7 @@ module WorkerTools
83
86
  matchable = name.is_a?(String) ? csv_input_header_normalized(name) : name
84
87
  actual_names.any? { |n| case n when matchable then true end } # rubocop does not like ===
85
88
  end
86
- raise "Some columns are missing: #{missing}" unless missing.empty?
89
+ raise Errors::MissingColumns, "Some columns are missing: #{missing}" unless missing.empty?
87
90
  end
88
91
 
89
92
  def csv_input_csv_options
@@ -2,13 +2,6 @@ require 'csv'
2
2
 
3
3
  module WorkerTools
4
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
5
  def csv_output_entries
13
6
  raise "csv_output_entries has to be defined in #{self}"
14
7
  end
@@ -27,33 +20,18 @@ module WorkerTools
27
20
  raise "csv_output_column_headers has to be defined in #{self}"
28
21
  end
29
22
 
30
- # rubocop:disable Lint/UnusedMethodArgument
31
23
  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)
24
+ entry.values_at(*csv_output_column_headers.keys)
51
25
  end
52
26
 
53
27
  def csv_output_tmp_file
54
28
  @csv_output_tmp_file ||= Tempfile.new(['output', '.csv'])
55
29
  end
56
30
 
31
+ def csv_output_file_name
32
+ "#{model_kind}.csv"
33
+ end
34
+
57
35
  def csv_output_col_sep
58
36
  ';'
59
37
  end
@@ -62,21 +40,29 @@ module WorkerTools
62
40
  Encoding::UTF_8
63
41
  end
64
42
 
43
+ def csv_output_write_mode
44
+ 'wb'
45
+ end
46
+
47
+ def csv_output_csv_options
48
+ { col_sep: csv_output_col_sep, encoding: csv_output_encoding }
49
+ end
50
+
65
51
  def csv_output_insert_headers(csv)
66
52
  csv << csv_output_column_headers.values if csv_output_column_headers
67
53
  end
68
54
 
55
+ def csv_output_add_attachment
56
+ model.add_attachment(csv_output_tmp_file, file_name: csv_output_file_name, content_type: 'text/csv')
57
+ end
58
+
69
59
  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|
60
+ CSV.open(csv_output_tmp_file, csv_output_write_mode, **csv_output_csv_options) do |csv|
71
61
  csv_output_insert_headers(csv)
72
62
  csv_output_entries.each { |entry| csv << csv_output_row_values(entry) }
73
63
  end
74
- csv_output_write_target if csv_output_target
75
- end
76
64
 
77
- def csv_output_write_target
78
- csv_ouput_ensure_target_folder
79
- FileUtils.cp(csv_output_tmp_file.path, csv_output_target)
65
+ csv_output_add_attachment
80
66
  end
81
67
  end
82
68
  end
@@ -0,0 +1,10 @@
1
+ module WorkerTools
2
+ Error = Class.new(StandardError)
3
+
4
+ module Errors
5
+ Invalid = Class.new(Error)
6
+ WrongNumberOfColumns = Class.new(Invalid)
7
+ DuplicatedColumns = Class.new(Invalid)
8
+ MissingColumns = Class.new(Invalid)
9
+ end
10
+ end
@@ -1,6 +1,5 @@
1
1
  module WorkerTools
2
2
  module Recorder
3
-
4
3
  def with_wrapper_recorder(&block)
5
4
  block.yield
6
5
  # this time we do want to catch Exception to attempt to handle some of the
@@ -24,38 +23,44 @@ module WorkerTools
24
23
  end
25
24
 
26
25
  def record_fail(error)
27
- record "ID #{model.id} - Error"
28
26
  record(error, :error)
29
- model.information = information
30
27
  model.save!(validate: false)
31
28
  end
32
29
 
33
- def add_log(message, level = :info)
34
- logger.public_send(level, format_log_message(message))
30
+ def add_log(message, level = nil)
31
+ attrs = default_message_attrs(message, level)
32
+ logger.public_send(attrs[:level], format_message(attrs[:message]))
35
33
  end
36
34
 
37
- def add_info(message)
38
- @information ||= ''
39
- information << "#{format_info_message(message)}\n"
35
+ def add_note(message, level = nil)
36
+ attrs = default_message_attrs(message, level)
37
+ model.notes.push(level: attrs[:level], message: attrs[:message])
40
38
  end
41
39
 
42
40
  def record(message, level = :info)
43
41
  add_log(message, level)
44
- add_info(message)
42
+ add_note(message, level)
45
43
  end
46
44
 
47
- def format_log_message(message)
48
- return error_to_text(message, log_error_trace_lines) if message.is_a?(Exception)
45
+ def level_from_message_type(message)
46
+ return :error if message.is_a?(Exception)
49
47
 
50
- message
48
+ :info
51
49
  end
52
50
 
53
- def format_info_message(message)
54
- return error_to_text(message, info_error_trace_lines) if message.is_a?(Exception)
51
+ def format_message(message)
52
+ return error_to_text(message, log_error_trace_lines) if message.is_a?(Exception)
55
53
 
56
54
  message
57
55
  end
58
56
 
57
+ def default_message_attrs(message, level)
58
+ {
59
+ message: format_message(message),
60
+ level: level || level_from_message_type(message)
61
+ }
62
+ end
63
+
59
64
  def logger
60
65
  @logger ||= Logger.new(File.join(log_directory, log_file_name))
61
66
  end
@@ -5,7 +5,7 @@ module WorkerTools
5
5
  def with_wrapper_slack_error_notifier(&block)
6
6
  block.yield
7
7
  rescue StandardError => e
8
- slack_error_notify(e) if slack_error_notifier_enabled
8
+ slack_error_notify(e) if slack_error_notifier_enabled && slack_error_notifiable?(e)
9
9
  raise
10
10
  end
11
11
 
@@ -13,6 +13,10 @@ module WorkerTools
13
13
  Rails.env.production?
14
14
  end
15
15
 
16
+ def slack_error_notifiable?(error)
17
+ error.is_a?(StandardError)
18
+ end
19
+
16
20
  def slack_error_notifier_emoji
17
21
  ':red_circle:'
18
22
  end
@@ -0,0 +1,9 @@
1
+ module WorkerTools
2
+ module Utils
3
+ class HashWithIndifferentAccessType < ActiveRecord::Type::Json
4
+ def deserialize(value)
5
+ HashWithIndifferentAccess.new(super)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module WorkerTools
2
+ module Utils
3
+ class SerializedArrayType < ActiveRecord::Type::Json
4
+ def initialize(type: nil)
5
+ @type = type
6
+ end
7
+
8
+ def deserialize(value)
9
+ super(value)&.map { |d| @type.deserialize(d) }
10
+ end
11
+
12
+ def serialize(value)
13
+ raise 'not an array' unless value.is_a?(Array)
14
+
15
+ super value
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module WorkerTools
2
- VERSION = '0.2.2'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -47,7 +47,9 @@ module WorkerTools
47
47
  end
48
48
 
49
49
  def xlsx_input_header_normalized(name)
50
- name.to_s.strip.downcase
50
+ # some elements return obj.to_s => nil
51
+ # for example [#<Roo::Excelx::Cell::Empty:0x0000000af8d4c8 ... @value=nil>]
52
+ name&.to_s&.strip&.downcase || ''
51
53
  end
52
54
 
53
55
  # Allows for some basic cleanup of the values, such as applying strip to
@@ -68,7 +70,8 @@ module WorkerTools
68
70
  actual_columns_length = xlsx_rows_enum.first.length
69
71
  return if expected_columns_length == actual_columns_length
70
72
 
71
- raise "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
73
+ msg = "The number of columns (#{actual_columns_length}) is not the expected (#{expected_columns_length})"
74
+ raise Errors::WrongNumberOfColumns, msg
72
75
  end
73
76
 
74
77
  def xlsx_input_columns_hash_check(xlsx_rows_enum)
@@ -80,7 +83,9 @@ module WorkerTools
80
83
 
81
84
  def xlsx_input_columns_hash_check_duplicates(names)
82
85
  dups = names.group_by(&:itself).select { |_, v| v.count > 1 }.keys
83
- raise "The file contains duplicated columns: #{dups}" if dups.present?
86
+ return unless dups.present?
87
+
88
+ raise Errors::DuplicatedColumns, "The file contains duplicated columns: #{dups}"
84
89
  end
85
90
 
86
91
  def xlsx_input_columns_hash_check_missing(actual_names, expected_names)
@@ -88,7 +93,7 @@ module WorkerTools
88
93
  matchable = name.is_a?(String) ? xlsx_input_header_normalized(name) : name
89
94
  actual_names.any? { |n| case n when matchable then true end } # rubocop does not like ===
90
95
  end
91
- raise "Some columns are missing: #{missing}" unless missing.empty?
96
+ raise Errors::MissingColumns, "Some columns are missing: #{missing}" unless missing.empty?
92
97
  end
93
98
 
94
99
  # Compares the first row (header names) with the xlsx_input_columns hash to find
@@ -1,26 +1,8 @@
1
1
  require 'rubyXL'
2
2
  module WorkerTools
3
3
  module XlsxOutput
4
- # if defined, this file will be written to this destination (regardless
5
- # of whether the model saves the file as well)
6
- def xlsx_output_target
7
- # Ex: Rails.root.join('shared', 'foo', 'bar.xlsx')
8
- raise "xlsx_output_target has to be defined in #{self}"
9
- end
10
-
11
- def xlsx_output_content
12
- {
13
- sheet1: {
14
- label: 'Sheet 1',
15
- headers: xlsx_output_column_headers,
16
- rows: xlsx_output_values,
17
- column_style: xlsx_output_column_format
18
- }
19
- }
20
- end
21
-
22
- def xlsx_output_values
23
- raise "xlsx_output_values has to be defined in #{self}"
4
+ def xlsx_output_entries
5
+ raise "xlsx_output_entries has to be defined in #{self}"
24
6
  end
25
7
 
26
8
  def xlsx_output_column_headers
@@ -37,6 +19,21 @@ module WorkerTools
37
19
  raise "xlsx_output_column_headers has to be defined in #{self}"
38
20
  end
39
21
 
22
+ def xlsx_output_content
23
+ {
24
+ sheet1: {
25
+ label: 'Sheet 1',
26
+ headers: xlsx_output_column_headers,
27
+ rows: xlsx_output_entries.lazy.map { |entry| xlsx_output_row_values(entry) },
28
+ column_style: xlsx_output_column_format
29
+ }
30
+ }
31
+ end
32
+
33
+ def xlsx_output_row_values(entry)
34
+ entry.values_at(*xlsx_output_column_headers.keys)
35
+ end
36
+
40
37
  def xlsx_output_column_format
41
38
  # These columns are used to set the headers, also
42
39
  # to set the row values depending on your implementation.
@@ -51,15 +48,7 @@ module WorkerTools
51
48
  {}
52
49
  end
53
50
 
54
- def xlsx_output_target_folder
55
- @xlsx_output_target_folder ||= File.dirname(xlsx_output_target)
56
- end
57
-
58
- def xlsx_ensure_output_target_folder
59
- FileUtils.mkdir_p(xlsx_output_target_folder) unless File.directory?(xlsx_output_target_folder)
60
- end
61
-
62
- def xlsx_insert_headers(spreadsheet, headers)
51
+ def xlsx_output_insert_headers(spreadsheet, headers)
63
52
  return unless headers
64
53
 
65
54
  iterator =
@@ -73,15 +62,15 @@ module WorkerTools
73
62
  end
74
63
  end
75
64
 
76
- def xlsx_insert_rows(spreadsheet, rows, headers)
65
+ def xlsx_output_insert_rows(spreadsheet, rows, headers)
77
66
  rows.each_with_index do |row, row_index|
78
- xlsx_iterators(row, headers).each_with_index do |value, col_index|
67
+ xlsx_output_iterators(row, headers).each_with_index do |value, col_index|
79
68
  spreadsheet.add_cell(row_index + 1, col_index, value.to_s)
80
69
  end
81
70
  end
82
71
  end
83
72
 
84
- def xlsx_iterators(iterable, compare_hash = nil)
73
+ def xlsx_output_iterators(iterable, compare_hash = nil)
85
74
  if iterable.is_a? Hash
86
75
  raise 'parameter compare_hash should be a hash, too.' if compare_hash.nil? || !compare_hash.is_a?(Hash)
87
76
 
@@ -91,10 +80,10 @@ module WorkerTools
91
80
  end
92
81
  end
93
82
 
94
- def xlsx_style_columns(spreadsheet, styles, headers)
83
+ def xlsx_output_style_columns(spreadsheet, styles, headers)
95
84
  return false unless headers
96
85
 
97
- xlsx_iterators(styles, headers).each_with_index do |format, index|
86
+ xlsx_output_iterators(styles, headers).each_with_index do |format, index|
98
87
  next unless format
99
88
 
100
89
  spreadsheet.change_column_width(index, format[:width])
@@ -103,25 +92,41 @@ module WorkerTools
103
92
  true
104
93
  end
105
94
 
106
- def xlsx_write_sheet(workbook, sheet_content, index)
95
+ def xlsx_output_tmp_file
96
+ @xlsx_output_tmp_file ||= Tempfile.new(['output', '.xlsx'])
97
+ end
98
+
99
+ def xlsx_output_file_name
100
+ "#{model_kind}.xlsx"
101
+ end
102
+
103
+ def xlsx_output_add_attachment
104
+ model.add_attachment(
105
+ xlsx_output_tmp_file,
106
+ file_name: xlsx_output_file_name,
107
+ content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
108
+ )
109
+ end
110
+
111
+ def xlsx_output_write_sheet(workbook, sheet_content, index)
107
112
  sheet = workbook.worksheets[index]
108
113
  sheet = workbook.add_worksheet(sheet_content[:label]) if sheet.nil?
109
114
 
110
115
  sheet.sheet_name = sheet_content[:label]
111
- xlsx_style_columns(sheet, sheet_content[:column_style], sheet_content[:headers])
112
- xlsx_insert_headers(sheet, sheet_content[:headers])
113
- xlsx_insert_rows(sheet, sheet_content[:rows], sheet_content[:headers])
116
+ xlsx_output_style_columns(sheet, sheet_content[:column_style], sheet_content[:headers])
117
+ xlsx_output_insert_headers(sheet, sheet_content[:headers])
118
+ xlsx_output_insert_rows(sheet, sheet_content[:rows], sheet_content[:headers])
114
119
  end
115
120
 
116
- def xlsx_write_output_target
117
- xlsx_ensure_output_target_folder
118
-
121
+ def xlsx_output_write_file
119
122
  book = RubyXL::Workbook.new
120
123
  xlsx_output_content.each_with_index do |(_, object), index|
121
- xlsx_write_sheet(book, object, index)
124
+ xlsx_output_write_sheet(book, object, index)
122
125
  end
123
126
 
124
- book.write xlsx_output_target
127
+ book.write xlsx_output_tmp_file
128
+
129
+ xlsx_output_add_attachment
125
130
  end
126
131
  end
127
132
  end
data/lib/worker_tools.rb CHANGED
@@ -1,4 +1,4 @@
1
- Dir[File.join(__dir__, 'worker_tools/*.rb')].each { |path| require path }
1
+ Dir[File.join(__dir__, 'worker_tools/**/*.rb')].sort.each { |path| require path }
2
2
 
3
3
  module WorkerTools
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: worker_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fsainz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-21 00:00:00.000000000 Z
11
+ date: 2022-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -230,6 +230,7 @@ files:
230
230
  - ".gitignore"
231
231
  - ".rubocop.yml"
232
232
  - ".travis.yml"
233
+ - CHANGELOG.md
233
234
  - Gemfile
234
235
  - LICENSE
235
236
  - README.md
@@ -242,8 +243,11 @@ files:
242
243
  - lib/worker_tools/counters.rb
243
244
  - lib/worker_tools/csv_input.rb
244
245
  - lib/worker_tools/csv_output.rb
246
+ - lib/worker_tools/errors.rb
245
247
  - lib/worker_tools/recorder.rb
246
248
  - lib/worker_tools/slack_error_notifier.rb
249
+ - lib/worker_tools/utils/hash_with_indifferent_access_type.rb
250
+ - lib/worker_tools/utils/serialized_array_type.rb
247
251
  - lib/worker_tools/version.rb
248
252
  - lib/worker_tools/xlsx_input.rb
249
253
  - lib/worker_tools/xlsx_output.rb
@@ -268,7 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
268
272
  - !ruby/object:Gem::Version
269
273
  version: '0'
270
274
  requirements: []
271
- rubygems_version: 3.1.4
275
+ rubygems_version: 3.1.2
272
276
  signing_key:
273
277
  specification_version: 4
274
278
  summary: A collection of modules to help writing common worker tasks)