kitcat 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f6c930e9f26d08132fc15fa533107ebd5d8f9c43
4
+ data.tar.gz: 898f7a3fa58d3b38cf85692aee414b0b9dab82d3
5
+ SHA512:
6
+ metadata.gz: 16cec3058a3b816289dfc6b2cf7adc8bea0b210a07103596c4096e66b436012056cfee8d8ae55d5019a27966d1e7951510d7f764e557a5bfc952235d5a21f1db
7
+ data.tar.gz: d3af7d89498d41509cd026c0ee3f72c1294584d28a2f18e5e8daf19fafa1834ff8777b8ee775072bae413499dcf61b5c4336942a87aa6a3fb5bbcf7a08ac5a16
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,11 @@
1
+ # Contributing
2
+
3
+ 1. Submit a ticket with your issue here at Github. Make sure your ticket clearly describes the bug (with steps to reproduce it) or desired feature.
4
+ 1. Fork the repository.
5
+ 1. Create a topic branch based on master branch if you want your feature/bugfix to be released in the next versions. Alternatively, you can base your branch on
6
+ a release branch if you want your bugfix to be part of a particular version.
7
+ 1. Make sure you split your work into reasonable commits (`git rebase` can be your friend)
8
+ 1. Write a test for your new feature/bugfix
9
+ 1. Run whole test suite to make sure you didn't break anything.
10
+ 1. Create a PR to the main repository.
11
+ 1. Link the PR to the Github Issue ticket.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Simply Business
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,37 @@
1
+ [![Build Status](https://travis-ci.org/simplybusiness/kitcat.svg?branch=master)](https://travis-ci.org/simplybusiness/kitcat)
2
+ [![Coverage Status](https://coveralls.io/repos/github/simplybusiness/kitcat/badge.svg?branch=develop)](https://coveralls.io/github/simplybusiness/kitcat?branch=develop)
3
+ [![Code Climate](https://codeclimate.com/github/simplybusiness/kitcat/badges/gpa.svg)](https://codeclimate.com/github/simplybusiness/kitcat)
4
+
5
+ # KitCat
6
+
7
+ *Sometimes schema migrations are just not enough.*
8
+
9
+ Data migration framework written in plain Ruby. Although originally created for migrating data in MongoDb, currently it is pretty **generic**, since it **does not depend** on any web framework.
10
+
11
+ ## Features of Framework
12
+
13
+ This is a small migration framework that offers the following functionality for free:
14
+
15
+ 1. Logging
16
+ 2. Progress Bar
17
+ 3. Gracefully handling when user interrupts (Ctrl+C or `kill <process_id>`)
18
+
19
+ ## Example Usage
20
+
21
+ Assuming that the migration strategy is implemented with a class `MigrationStrategy`, then executing a migration is quite simple:
22
+
23
+ ``` ruby
24
+ migration_strategy = MigrationStrategy.new
25
+ migration_framework = KitCat::Framework.new(migration_strategy)
26
+ migration_framework.execute
27
+ ```
28
+
29
+ The above will run the migration and will create a log file inside the `log` directory (which is created if not present)
30
+
31
+ [How to implement MigrationStrategy](./docs/STRATEGY.md)
32
+
33
+ # Testing
34
+
35
+ ```bash
36
+ bundle exec rake
37
+ ```
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
3
+ require 'bundler/audit/task'
4
+
5
+ RSpec::Core::RakeTask.new :spec
6
+ RuboCop::RakeTask.new :rubocop
7
+ Bundler::Audit::Task.new
8
+
9
+ task default: [:spec, :rubocop, 'bundle:audit']
@@ -0,0 +1,191 @@
1
+ require 'ruby-progressbar'
2
+ require 'active_model'
3
+ require 'active_support/core_ext'
4
+
5
+ module KitCat
6
+ class Framework
7
+ attr_reader :last_item_processed,
8
+ :migration_name,
9
+ :number_of_items_processed,
10
+ :migration_strategy
11
+
12
+ # @params migration_strategy {Object}
13
+ # Instance implementing the methods of +KitCat::Callbacks+
14
+ #
15
+ # migration_name {String} Optional.
16
+ # The name of the migration. Used as tag in log file name. If not given, a random/unique one is used.
17
+ #
18
+ # number_of_items_to_process {Integer} Optional.
19
+ # If given, the processing will stop after processing that many number of items.
20
+ #
21
+ # progress_bar {Boolean} Optional.
22
+ # When +True+. it will instantiate a use a progress bar, incrementing that by 1 every time
23
+ # the +migration_strategy+ finishes processing an item. The total load will be calculated based on the
24
+ # result of +migration_strategy#criteria#count+.
25
+ # When +False+, progress bar will not be used
26
+ #
27
+ # progress_bar_output Optional. Defaults to STDOUT. Anything that responds to
28
+ # #print, #flush, #tty? and #puts.
29
+ # It is taken into account only if progress bar is enabled.
30
+ #
31
+ def initialize(migration_strategy,
32
+ migration_name: nil,
33
+ number_of_items_to_process: nil,
34
+ progress_bar: true,
35
+ progress_bar_output: STDOUT)
36
+ @migration_strategy = migration_strategy
37
+ @migration_name = build_migration_name(migration_name)
38
+ @number_of_items_to_process = number_of_items_to_process
39
+ @last_item_processed = nil
40
+ @progress_bar = initialize_progress_bar(progress_bar, progress_bar_output)
41
+ end
42
+
43
+ def execute
44
+ @interrupted = false
45
+ Signal.trap('TERM') { @interrupted = true }
46
+ Signal.trap('INT') { @interrupted = true }
47
+ start_logging
48
+
49
+ @number_of_items_processed = 0
50
+
51
+ items.each do |item|
52
+ if migration_strategy.process(item)
53
+
54
+ log_success(item)
55
+
56
+ @number_of_items_processed += 1
57
+
58
+ increment_progress_bar
59
+
60
+ @last_item_processed = item
61
+
62
+ break unless process_more?
63
+ else
64
+ log_failure(item)
65
+
66
+ break
67
+ end
68
+ if @interrupted
69
+ handle_user_interrupt
70
+ break
71
+ end
72
+ end
73
+ end_logging
74
+ end
75
+
76
+ def number_of_items_to_process
77
+ @number_of_items_to_process ||= migration_strategy.criteria.count
78
+ end
79
+
80
+ def log_file_path
81
+ @log_file_path ||= File.join(log_dir, build_log_file_name)
82
+ end
83
+
84
+ def progress_bar?
85
+ !@progress_bar.nil?
86
+ end
87
+
88
+ def progress
89
+ return -1 unless progress_bar?
90
+ @progress_bar.progress
91
+ end
92
+
93
+ private
94
+
95
+ def items
96
+ return enum_for(:items) unless block_given?
97
+
98
+ enum = migration_strategy.criteria.each
99
+
100
+ loop do
101
+ yield enum.next
102
+ end
103
+ end
104
+
105
+ def log_dir
106
+ @log_dir ||= FileUtils.mkdir_p(File.join(Dir.pwd, 'log'))
107
+ end
108
+
109
+ def build_log_file_name
110
+ "migration-#{migration_name}-#{timestamp}.log"
111
+ end
112
+
113
+ def timestamp
114
+ Time.now.to_s(:number)
115
+ end
116
+
117
+ def build_migration_name(migration_name)
118
+ result = migration_name || migration_strategy.class.name.delete(':').underscore.upcase
119
+ result.gsub(/\W/, '').upcase
120
+ end
121
+
122
+ def initialize_progress_bar(progress_bar_flag, output)
123
+ create_progress_bar(output) if progress_bar_flag || progress_bar_flag.nil?
124
+ end
125
+
126
+ def create_progress_bar(output)
127
+ @progress_bar = ProgressBar.create(total: migration_strategy.criteria.count,
128
+ output: output,
129
+ progress_mark: ' ',
130
+ remainder_mark: '-',
131
+ length: terminal_width,
132
+ format: "%a %bᗧ%i %p%% %e")
133
+ end
134
+
135
+ def start_logging
136
+ logger.info 'Start Processing...'
137
+ end
138
+
139
+ def end_logging
140
+ logger.info '...end of processing'
141
+ end
142
+
143
+ def logger
144
+ @logger ||= Logger.new(log_file_path)
145
+ end
146
+
147
+ def log_success(item)
148
+ log_line(item) { |method| logger.info "...successfully processed item: #{item.try(method)}" }
149
+ end
150
+
151
+ def log_failure(item)
152
+ log_line(item) { |method| logger.error "...error while processing item: #{item.try(method)}" }
153
+ end
154
+
155
+ def log_line(item)
156
+ method = item.respond_to?(:to_log) ? :to_log : :to_s
157
+ yield method
158
+ end
159
+
160
+ def log_interrupt_callback_start
161
+ logger.info '...user interrupted, calling interrupt callback on migration strategy...'
162
+ end
163
+
164
+ def log_interrupt_callback_finish
165
+ logger.info '......end of interrupt callback after user interruption'
166
+ end
167
+
168
+ def process_more?
169
+ @number_of_items_to_process.nil? || @number_of_items_processed < @number_of_items_to_process
170
+ end
171
+
172
+ def increment_progress_bar
173
+ return unless progress_bar?
174
+ @progress_bar.increment
175
+ end
176
+
177
+ def handle_user_interrupt
178
+ log_interrupt_callback_start
179
+ migration_strategy.interrupt_callback if migration_strategy.respond_to?(:interrupt_callback)
180
+ log_interrupt_callback_finish
181
+ true
182
+ end
183
+
184
+ # The following is to correctly calculate the width of the terminal
185
+ # so that the progress bar occupies the whole width
186
+ #
187
+ def terminal_width
188
+ TerminalWidthCalculator.calculate
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,39 @@
1
+ module KitCat
2
+ module TerminalWidthCalculator
3
+ class << self
4
+ def calculate
5
+ default_width = 80
6
+
7
+ term_width = calculate_term_width
8
+
9
+ term_width > 0 ? term_width : default_width
10
+ end
11
+
12
+ private
13
+
14
+ def calculate_term_width
15
+ if ENV['COLUMNS'] =~ /^\d+$/
16
+ ENV['COLUMNS'].to_i
17
+ elsif tput_case?
18
+ `tput cols`.to_i
19
+ elsif stty_case?
20
+ `stty size`.scan(/\d+/).map(&:to_i)[1]
21
+ end
22
+ rescue
23
+ 0
24
+ end
25
+
26
+ def tput_case?
27
+ (RUBY_PLATFORM =~ /java/ || !STDIN.tty? && ENV['TERM']) && shell_command_exists?('tput')
28
+ end
29
+
30
+ def stty_case?
31
+ STDIN.tty? && shell_command_exists?('stty')
32
+ end
33
+
34
+ def shell_command_exists?(command)
35
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? { |d| File.exist? File.join(d, command) }
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/kitcat.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'kitcat/framework'
2
+ require 'kitcat/terminal_width_calculator'
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kitcat
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Simply Business
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-progressbar
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: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
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: timecop
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: coveralls
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: rake
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: rubocop
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: bundler-audit
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
+ description: initially created for data migrations. Provides logging, progess bar
126
+ and graceful handling
127
+ email:
128
+ - tech@simplybusiness.co.uk
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - Rakefile
134
+ - lib/kitcat/framework.rb
135
+ - lib/kitcat/terminal_width_calculator.rb
136
+ - lib/kitcat.rb
137
+ - LICENSE
138
+ - CONTRIBUTING.md
139
+ - README.md
140
+ homepage: https://github.com/simplybusiness/kitcat
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.0.14
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: a framework to support data processing
164
+ test_files: []