flat_kit 0.1.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +46 -0
  3. data/HISTORY.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/Manifest.txt +66 -0
  6. data/README.md +80 -0
  7. data/Rakefile +20 -0
  8. data/bin/fk +4 -0
  9. data/lib/flat_kit.rb +23 -0
  10. data/lib/flat_kit/cli.rb +80 -0
  11. data/lib/flat_kit/command.rb +53 -0
  12. data/lib/flat_kit/command/cat.rb +93 -0
  13. data/lib/flat_kit/command/merge.rb +88 -0
  14. data/lib/flat_kit/command/sort.rb +88 -0
  15. data/lib/flat_kit/descendant_tracker.rb +27 -0
  16. data/lib/flat_kit/error.rb +5 -0
  17. data/lib/flat_kit/format.rb +34 -0
  18. data/lib/flat_kit/input.rb +32 -0
  19. data/lib/flat_kit/input/file.rb +53 -0
  20. data/lib/flat_kit/input/io.rb +54 -0
  21. data/lib/flat_kit/internal_node.rb +84 -0
  22. data/lib/flat_kit/jsonl.rb +8 -0
  23. data/lib/flat_kit/jsonl/format.rb +25 -0
  24. data/lib/flat_kit/jsonl/reader.rb +30 -0
  25. data/lib/flat_kit/jsonl/record.rb +84 -0
  26. data/lib/flat_kit/jsonl/writer.rb +45 -0
  27. data/lib/flat_kit/leaf_node.rb +71 -0
  28. data/lib/flat_kit/logger.rb +39 -0
  29. data/lib/flat_kit/merge.rb +35 -0
  30. data/lib/flat_kit/merge_tree.rb +104 -0
  31. data/lib/flat_kit/output.rb +32 -0
  32. data/lib/flat_kit/output/file.rb +55 -0
  33. data/lib/flat_kit/output/io.rb +73 -0
  34. data/lib/flat_kit/reader.rb +61 -0
  35. data/lib/flat_kit/record.rb +83 -0
  36. data/lib/flat_kit/sentinel_internal_node.rb +37 -0
  37. data/lib/flat_kit/sentinel_leaf_node.rb +37 -0
  38. data/lib/flat_kit/sort.rb +35 -0
  39. data/lib/flat_kit/writer.rb +38 -0
  40. data/lib/flat_kit/xsv.rb +8 -0
  41. data/lib/flat_kit/xsv/format.rb +25 -0
  42. data/lib/flat_kit/xsv/reader.rb +45 -0
  43. data/lib/flat_kit/xsv/record.rb +90 -0
  44. data/lib/flat_kit/xsv/writer.rb +70 -0
  45. data/tasks/default.rake +242 -0
  46. data/tasks/extension.rake +38 -0
  47. data/tasks/man.rake +7 -0
  48. data/tasks/this.rb +208 -0
  49. data/test/device_dataset.rb +117 -0
  50. data/test/input/test_file.rb +73 -0
  51. data/test/input/test_io.rb +93 -0
  52. data/test/jsonl/test_format.rb +22 -0
  53. data/test/jsonl/test_reader.rb +49 -0
  54. data/test/jsonl/test_record.rb +61 -0
  55. data/test/jsonl/test_writer.rb +68 -0
  56. data/test/output/test_file.rb +60 -0
  57. data/test/output/test_io.rb +104 -0
  58. data/test/test_conversions.rb +45 -0
  59. data/test/test_format.rb +24 -0
  60. data/test/test_helper.rb +26 -0
  61. data/test/test_merge.rb +40 -0
  62. data/test/test_merge_tree.rb +64 -0
  63. data/test/test_version.rb +11 -0
  64. data/test/xsv/test_format.rb +22 -0
  65. data/test/xsv/test_reader.rb +61 -0
  66. data/test/xsv/test_record.rb +69 -0
  67. data/test/xsv/test_writer.rb +68 -0
  68. metadata +237 -0
@@ -0,0 +1,8 @@
1
+ module FlatKit
2
+ module Xsv
3
+ end
4
+ end
5
+ require 'flat_kit/xsv/record'
6
+ require 'flat_kit/xsv/reader'
7
+ require 'flat_kit/xsv/writer'
8
+ require 'flat_kit/xsv/format'
@@ -0,0 +1,25 @@
1
+ module FlatKit
2
+ module Xsv
3
+ class Format < ::FlatKit::Format
4
+ def self.format_name
5
+ "xsv"
6
+ end
7
+
8
+ def self.handles?(filename)
9
+ parts = filename.split(".")
10
+ %w[ csv tsv txt ].each do |ext|
11
+ return true if parts.include?(ext)
12
+ end
13
+ return false
14
+ end
15
+
16
+ def self.reader
17
+ ::FlatKit::Xsv::Reader
18
+ end
19
+
20
+ def self.writer
21
+ ::FlatKit::Xsv::Writer
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ require 'csv'
2
+
3
+ module FlatKit
4
+ module Xsv
5
+ class Reader < ::FlatKit::Reader
6
+ attr_reader :input
7
+ attr_reader :count
8
+ attr_reader :fields
9
+
10
+ def self.format_name
11
+ ::FlatKit::Xsv::Format.format_name
12
+ end
13
+
14
+ def self.default_csv_options
15
+ {
16
+ headers: :first_row,
17
+ converters: :numeric,
18
+ return_headers: false
19
+ }
20
+ end
21
+
22
+ def initialize(source:, compare_fields: :none, **csv_options)
23
+ super(source: source, compare_fields: compare_fields)
24
+ @input = ::FlatKit::Input.from(source)
25
+ @count = 0
26
+ @csv_options = Reader.default_csv_options.merge(csv_options)
27
+ @fields = nil
28
+ @csv = CSV.new(input.io, **@csv_options)
29
+ end
30
+
31
+ def each
32
+ @csv.each do |row|
33
+ @fields = row.headers if @fields.nil?
34
+ record = ::FlatKit::Xsv::Record.new(data: row, compare_fields: compare_fields)
35
+ @count += 1
36
+ yield record
37
+ end
38
+ input.close
39
+ rescue => e
40
+ ::FlatKit.logger.error "Error reading xsv records from #{input.name}: #{e}"
41
+ raise ::FlatKit::Error, e
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,90 @@
1
+ require 'csv'
2
+ require 'flat_kit/record'
3
+
4
+ module FlatKit
5
+ module Xsv
6
+ class Record < ::FlatKit::Record
7
+ attr_reader :ordered_fields
8
+
9
+ def self.format_name
10
+ ::FlatKit::Xsv::Format.format_name
11
+ end
12
+
13
+ def self.from_record(record, ordered_fields: nil)
14
+ if record.instance_of?(FlatKit::Xsv::Record) then
15
+ new(data: record.data, compare_fields: record.compare_fields)
16
+ else
17
+ new(data: nil, compare_fields: record.compare_fields,
18
+ ordered_fields: nil,
19
+ complete_structured_data: record.to_hash)
20
+ end
21
+ end
22
+
23
+ def initialize(data:, compare_fields: :none,
24
+ ordered_fields: :auto,
25
+ complete_structured_data: nil)
26
+ super(data: data, compare_fields: compare_fields)
27
+
28
+ @complete_structured_data = complete_structured_data
29
+ @ordered_fields = ordered_fields
30
+
31
+ if data.nil? && (complete_structured_data.nil? || complete_structured_data.empty?) then
32
+ raise FlatKit::Error,
33
+ "#{self.class} requires initialization from data: or complete_structured_data:"
34
+ end
35
+
36
+ resolve_ordered_fields
37
+ end
38
+
39
+ def [](key)
40
+ return nil unless @compare_fields.include?(key)
41
+ if data.nil? && !@complete_structured_data.nil? then
42
+ @complete_structured_data[key]
43
+ else
44
+ data[key]
45
+ end
46
+ end
47
+
48
+ def complete_structured_data
49
+ @complete_structured_data ||= data.to_hash
50
+ end
51
+ alias to_hash complete_structured_data
52
+
53
+ def to_a
54
+ return data.fields unless data.nil?
55
+
56
+ Array.new.tap do |a|
57
+ @ordered_fields.each do |field|
58
+ a << @complete_structured_data[field]
59
+ end
60
+ end
61
+ end
62
+
63
+ # convert to a csv line,
64
+ #
65
+ # First we use data if it is there since that should be a CSV::Row
66
+ #
67
+ # Next, if that doesn't work - iterate over the ordered fields and use the
68
+ # yield the values from complete_structured_data in that order
69
+ #
70
+ # And finally, if that doesn'twork, then just use complete structured data
71
+ # values in that order.
72
+ def to_s
73
+ return data.to_csv unless data.nil?
74
+ CSV.generate_line(to_a)
75
+ end
76
+
77
+ private
78
+
79
+ def resolve_ordered_fields
80
+ if (@ordered_fields == :auto) || (@ordered_fields.nil? || @ordered_fields.empty?) then
81
+ if @data.nil? || @data.empty? then
82
+ @ordered_fields = complete_structured_data.keys
83
+ else
84
+ @ordered_fields = @data.headers
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,70 @@
1
+ module FlatKit
2
+ module Xsv
3
+ class Writer < ::FlatKit::Writer
4
+ attr_reader :output
5
+ attr_reader :count
6
+ attr_reader :fields
7
+
8
+ def self.format_name
9
+ ::FlatKit::Xsv::Format.format_name
10
+ end
11
+
12
+ def self.default_csv_options
13
+ {
14
+ headers: nil,
15
+ write_headers: true
16
+ }
17
+ end
18
+
19
+ def initialize(destination:, fields: :auto, **csv_options)
20
+ super(destination: destination)
21
+ @fields = fields
22
+ @output = ::FlatKit::Output.from(@destination)
23
+ @count = 0
24
+ @we_write_the_header = nil
25
+ @csv_options = Writer.default_csv_options.dup
26
+
27
+ if @fields == :auto then
28
+ @we_write_the_header = true
29
+ else
30
+ @csv_options.merge!(headers: fields)
31
+ @we_write_the_header = false
32
+ end
33
+
34
+ @csv_options.merge!(csv_options)
35
+ @csv = CSV.new(output.io, **@csv_options)
36
+ end
37
+
38
+ def write(record)
39
+ case record
40
+ when FlatKit::Xsv::Record
41
+ write_record(record)
42
+ when FlatKit::Record
43
+ converted_record = ::FlatKit::Xsv::Record.from_record(record, ordered_fields: @fields)
44
+ write_record(converted_record)
45
+ else
46
+ raise FlatKit::Error, "Unable to write records of type #{record.class}"
47
+ end
48
+ rescue FlatKit::Error => fe
49
+ raise fe
50
+ rescue => e
51
+ ::FlatKit.logger.error "Error reading jsonl records from #{output.name}: #{e}"
52
+ raise ::FlatKit::Error, e
53
+ end
54
+
55
+ def close
56
+ @output.close
57
+ end
58
+
59
+ private
60
+
61
+ def write_record(record)
62
+ if @we_write_the_header && @count == 0 then
63
+ @csv << record.ordered_fields
64
+ end
65
+ @count += 1
66
+ @csv << record.to_a
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,242 @@
1
+ # vim: syntax=ruby
2
+ require 'rake/clean'
3
+ require 'digest'
4
+ #------------------------------------------------------------------------------
5
+ # If you want to Develop on this project just run 'rake develop' and you'll
6
+ # have all you need to get going. If you want to use bundler for development,
7
+ # then run 'rake develop:using_bundler'
8
+ #------------------------------------------------------------------------------
9
+ namespace :develop do
10
+
11
+ # Install all the development and runtime dependencies of this gem using the
12
+ # gemspec.
13
+ task :default => 'Gemfile' do
14
+ require 'rubygems/dependency_installer'
15
+ installer = ::Gem::DependencyInstaller.new
16
+ puts "Installing bundler..."
17
+ installer.install 'bundler'
18
+ sh 'bundle install'
19
+ puts "\n\nNow run 'rake test'"
20
+ end
21
+
22
+ # Create a Gemfile that just references the gemspec
23
+ file 'Gemfile' => :gemspec do
24
+ File.open( "Gemfile", "w+" ) do |f|
25
+ f.puts "# DO NOT EDIT - This file is automatically generated"
26
+ f.puts "# Make changes to Manifest.txt and/or Rakefile and regenerate"
27
+ f.puts 'source "https://rubygems.org"'
28
+ f.puts 'gemspec'
29
+ end
30
+ end
31
+ end
32
+ desc "Bootstrap development"
33
+ task :develop => "develop:default"
34
+
35
+ #------------------------------------------------------------------------------
36
+ # Minitest - standard TestTask
37
+ #------------------------------------------------------------------------------
38
+ begin
39
+ require 'rake/testtask'
40
+ Rake::TestTask.new( :test ) do |t|
41
+ t.ruby_opts = %w[ -w ]
42
+ t.libs = %w[ lib spec test ]
43
+ t.pattern = "{test,spec}/**/{test_*,*_spec}.rb"
44
+ end
45
+
46
+ task :test_requirements
47
+ task :test => :test_requirements
48
+ task :default => :test
49
+ rescue LoadError
50
+ This.task_warning( 'test' )
51
+ end
52
+
53
+ #------------------------------------------------------------------------------
54
+ # RDoc - standard rdoc rake task, although we must make sure to use a more
55
+ # recent version of rdoc since it is the one that has 'tomdoc' markup
56
+ #------------------------------------------------------------------------------
57
+ begin
58
+ gem 'rdoc' # otherwise we get the wrong task from stdlib
59
+ require 'rdoc/task'
60
+ RDoc::Task.new do |t|
61
+ t.markup = 'tomdoc'
62
+ t.rdoc_dir = 'doc'
63
+ t.main = 'README.md'
64
+ t.title = "#{This.name} #{This.version}"
65
+ t.rdoc_files.include( FileList['*.{rdoc,md,txt}'], FileList['ext/**/*.c'],
66
+ FileList['lib/**/*.rb'] )
67
+ end
68
+ rescue StandardError, LoadError
69
+ This.task_warning( 'rdoc' )
70
+ end
71
+
72
+ #------------------------------------------------------------------------------
73
+ # Coverage - optional code coverage, rcov for 1.8 and simplecov for 1.9, so
74
+ # for the moment only rcov is listed.
75
+ #------------------------------------------------------------------------------
76
+ begin
77
+ require 'simplecov'
78
+ desc 'Run tests with code coverage'
79
+ task :coverage do
80
+ ENV['COVERAGE'] = 'true'
81
+ Rake::Task[:test].execute
82
+ end
83
+ CLOBBER << 'coverage' if File.directory?( 'coverage' )
84
+ rescue LoadError
85
+ This.task_warning( 'simplecov' )
86
+ end
87
+
88
+ #------------------------------------------------------------------------------
89
+ # Manifest - We want an explicit list of thos files that are to be packaged in
90
+ # the gem. Most of this is from Hoe.
91
+ #------------------------------------------------------------------------------
92
+ namespace 'manifest' do
93
+ desc "Check the manifest"
94
+ task :check => :clean do
95
+ files = FileList["**/*", ".*"].exclude( This.exclude_from_manifest ).to_a.sort
96
+ files = files.select{ |f| File.file?( f ) }
97
+
98
+ tmp = "Manifest.tmp"
99
+ File.open( tmp, 'w' ) do |f|
100
+ f.puts files.join("\n")
101
+ end
102
+
103
+ begin
104
+ sh "diff -du Manifest.txt #{tmp}"
105
+ ensure
106
+ rm tmp
107
+ end
108
+ puts "Manifest looks good"
109
+ end
110
+
111
+ desc "Generate the manifest"
112
+ task :generate => :clean do
113
+ files = %x[ git ls-files ].split("\n").sort
114
+ files.reject! { |f| f =~ This.exclude_from_manifest }
115
+ File.open( "Manifest.txt", "w" ) do |f|
116
+ f.puts files.join("\n")
117
+ end
118
+ end
119
+ end
120
+
121
+ #------------------------------------------------------------------------------
122
+ # Fixme - look for fixmes and report them
123
+ #------------------------------------------------------------------------------
124
+ namespace :fixme do
125
+ task :default => 'manifest:check' do
126
+ This.manifest.each do |file|
127
+ next if file == __FILE__
128
+ next unless file =~ %r/(txt|rb|md|rdoc|css|html|xml|css)\Z/
129
+ puts "FIXME: Rename #{file}" if file =~ /fixme/i
130
+ IO.readlines( file ).each_with_index do |line, idx|
131
+ prefix = "FIXME: #{file}:#{idx+1}".ljust(42)
132
+ puts "#{prefix} => #{line.strip}" if line =~ /fixme/i
133
+ end
134
+ end
135
+ end
136
+
137
+ def fixme_project_root
138
+ This.project_path( '../fixme' )
139
+ end
140
+
141
+ def fixme_project_path( subtree )
142
+ fixme_project_root.join( subtree )
143
+ end
144
+
145
+ def local_fixme_files
146
+ This.manifest.select { |p| p =~ %r|^tasks/| }
147
+ end
148
+
149
+ def outdated_fixme_files
150
+ local_fixme_files.select do |local|
151
+ upstream = fixme_project_path( local )
152
+ upstream.exist? &&
153
+ ( Digest::SHA256.file( local ) != Digest::SHA256.file( upstream ) )
154
+ end
155
+ end
156
+
157
+ def fixme_up_to_date?
158
+ outdated_fixme_files.empty?
159
+ end
160
+
161
+ desc "See if the fixme tools are outdated"
162
+ task :outdated do
163
+ if fixme_up_to_date? then
164
+ puts "Fixme files are up to date."
165
+ else
166
+ outdated_fixme_files.each do |f|
167
+ puts "#{f} is outdated"
168
+ end
169
+ end
170
+ end
171
+
172
+ desc "Update outdated fixme files"
173
+ task :update do
174
+ if fixme_up_to_date? then
175
+ puts "Fixme files are already up to date."
176
+ else
177
+ puts "Updating fixme files:"
178
+ outdated_fixme_files.each do |local|
179
+ upstream = fixme_project_path( local )
180
+ puts " * #{local}"
181
+ FileUtils.cp( upstream, local )
182
+ end
183
+ puts "Use your git commands as appropriate."
184
+ end
185
+ end
186
+ end
187
+ desc "Look for fixmes and report them"
188
+ task :fixme => "fixme:default"
189
+
190
+ #------------------------------------------------------------------------------
191
+ # Gem Specification
192
+ #------------------------------------------------------------------------------
193
+ # Really this is only here to support those who use bundler
194
+ desc "Build the #{This.name}.gemspec file"
195
+ task :gemspec do
196
+ File.open( This.gemspec_file, "wb+" ) do |f|
197
+ f.puts "# DO NOT EDIT - This file is automatically generated"
198
+ f.puts "# Make changes to Manifest.txt and/or Rakefile and regenerate"
199
+ f.write This.platform_gemspec.to_ruby
200
+ end
201
+ end
202
+
203
+ # .rbc files from ruby 2.0
204
+ CLOBBER << FileList["**/*.rbc"]
205
+
206
+ # The standard gem packaging task, everyone has it.
207
+ require 'rubygems/package_task'
208
+ ::Gem::PackageTask.new( This.platform_gemspec ) do
209
+ # nothing
210
+ end
211
+
212
+ #------------------------------------------------------------------------------
213
+ # Release - the steps we go through to do a final release, this is pulled from
214
+ # a compbination of mojombo's rakegem, hoe and hoe-git
215
+ #
216
+ # 1) make sure we are on the main branch
217
+ # 2) make sure there are no uncommitted items
218
+ # 3) check the manifest and make sure all looks good
219
+ # 4) build the gem
220
+ # 5) do an empty commit to have the commit message of the version
221
+ # 6) tag that commit as the version
222
+ # 7) push main
223
+ # 8) push the tag
224
+ # 7) pus the gem
225
+ #------------------------------------------------------------------------------
226
+ task :release_check do
227
+ unless `git branch` =~ /^\* main/
228
+ abort "You must be on the main branch to release!"
229
+ end
230
+ unless `git status` =~ /^nothing to commit/m
231
+ abort "Nope, sorry, you have unfinished business"
232
+ end
233
+ end
234
+
235
+ desc "Create tag v#{This.version}, build and push #{This.platform_gemspec.full_name} to rubygems.org"
236
+ task :release => [ :release_check, 'manifest:check', :gem ] do
237
+ sh "git commit --allow-empty -a -m 'Release #{This.version}'"
238
+ sh "git tag -a -m 'v#{This.version}' v#{This.version}"
239
+ sh "git push origin main"
240
+ sh "git push origin v#{This.version}"
241
+ sh "gem push pkg/#{This.platform_gemspec.full_name}.gem"
242
+ end