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 +7 -0
- data/CONTRIBUTING.md +11 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/Rakefile +9 -0
- data/lib/kitcat/framework.rb +191 -0
- data/lib/kitcat/terminal_width_calculator.rb +39 -0
- data/lib/kitcat.rb +2 -0
- metadata +164 -0
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
|
+
[](https://travis-ci.org/simplybusiness/kitcat)
|
2
|
+
[](https://coveralls.io/github/simplybusiness/kitcat?branch=develop)
|
3
|
+
[](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,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
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: []
|