solvebio 1.6.1 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.bumpversion.cfg +6 -0
- data/.gitignore +5 -4
- data/.travis.yml +1 -1
- data/Gemfile +3 -0
- data/README.md +34 -34
- data/Rakefile +1 -18
- data/bin/solvebio.rb +14 -16
- data/installer +64 -0
- data/lib/solvebio.rb +50 -11
- data/lib/solvebio/acccount.rb +4 -0
- data/lib/solvebio/annotation.rb +11 -0
- data/lib/solvebio/api_operations.rb +147 -0
- data/lib/solvebio/api_resource.rb +32 -0
- data/lib/solvebio/cli.rb +75 -0
- data/lib/solvebio/cli/auth.rb +106 -0
- data/lib/solvebio/cli/credentials.rb +54 -0
- data/lib/{cli → solvebio/cli}/irb.rb +0 -23
- data/lib/solvebio/cli/irbrc.rb +48 -0
- data/lib/solvebio/cli/tutorial.rb +12 -0
- data/lib/solvebio/client.rb +149 -0
- data/lib/solvebio/dataset.rb +60 -0
- data/lib/solvebio/dataset_field.rb +12 -0
- data/lib/solvebio/depository.rb +38 -0
- data/lib/solvebio/depository_version.rb +40 -0
- data/lib/solvebio/errors.rb +64 -0
- data/lib/solvebio/filter.rb +315 -0
- data/lib/solvebio/list_object.rb +73 -0
- data/lib/solvebio/locale.rb +43 -0
- data/lib/solvebio/query.rb +341 -0
- data/lib/solvebio/sample.rb +54 -0
- data/lib/solvebio/singleton_api_resource.rb +25 -0
- data/lib/solvebio/solve_object.rb +164 -0
- data/lib/solvebio/tabulate.rb +589 -0
- data/lib/solvebio/user.rb +4 -0
- data/lib/solvebio/util.rb +59 -0
- data/lib/solvebio/version.rb +3 -0
- data/solvebio.gemspec +10 -18
- data/test/helper.rb +6 -2
- data/test/solvebio/data/.gitignore +1 -0
- data/test/solvebio/data/.netrc +6 -0
- data/test/{data → solvebio/data}/netrc-save +0 -0
- data/test/solvebio/data/sample.vcf.gz +0 -0
- data/test/solvebio/data/test_creds +3 -0
- data/test/solvebio/test_annotation.rb +45 -0
- data/test/solvebio/test_client.rb +29 -0
- data/test/solvebio/test_conversion.rb +14 -0
- data/test/solvebio/test_credentials.rb +67 -0
- data/test/solvebio/test_dataset.rb +52 -0
- data/test/solvebio/test_depository.rb +24 -0
- data/test/solvebio/test_depositoryversion.rb +22 -0
- data/test/solvebio/test_error.rb +31 -0
- data/test/solvebio/test_filter.rb +86 -0
- data/test/solvebio/test_query.rb +282 -0
- data/test/solvebio/test_query_batch.rb +38 -0
- data/test/solvebio/test_query_init.rb +30 -0
- data/test/solvebio/test_query_tabulate.rb +73 -0
- data/test/solvebio/test_ratelimit.rb +31 -0
- data/test/solvebio/test_resource.rb +29 -0
- data/test/solvebio/test_sample_access.rb +60 -0
- data/test/solvebio/test_sample_download.rb +20 -0
- data/test/solvebio/test_tabulate.rb +129 -0
- data/test/solvebio/test_util.rb +39 -0
- metadata +100 -85
- data/Makefile +0 -17
- data/demo/README.md +0 -14
- data/demo/cheatsheet.rb +0 -31
- data/demo/dataset/facets.rb +0 -13
- data/demo/dataset/field.rb +0 -13
- data/demo/depository/README.md +0 -24
- data/demo/depository/all.rb +0 -13
- data/demo/depository/retrieve.rb +0 -13
- data/demo/depository/versions-all.rb +0 -13
- data/demo/query/query-filter.rb +0 -30
- data/demo/query/query.rb +0 -13
- data/demo/query/range-filter.rb +0 -18
- data/demo/test-api.rb +0 -98
- data/lib/cli/auth.rb +0 -122
- data/lib/cli/help.rb +0 -13
- data/lib/cli/irbrc.rb +0 -54
- data/lib/cli/options.rb +0 -75
- data/lib/client.rb +0 -154
- data/lib/credentials.rb +0 -67
- data/lib/errors.rb +0 -81
- data/lib/filter.rb +0 -312
- data/lib/locale.rb +0 -47
- data/lib/main.rb +0 -46
- data/lib/query.rb +0 -414
- data/lib/resource/annotation.rb +0 -23
- data/lib/resource/apiresource.rb +0 -241
- data/lib/resource/dataset.rb +0 -91
- data/lib/resource/datasetfield.rb +0 -37
- data/lib/resource/depository.rb +0 -50
- data/lib/resource/depositoryversion.rb +0 -69
- data/lib/resource/main.rb +0 -123
- data/lib/resource/sample.rb +0 -75
- data/lib/resource/solveobject.rb +0 -122
- data/lib/resource/user.rb +0 -5
- data/lib/tabulate.rb +0 -706
- data/lib/util.rb +0 -29
- data/test/Makefile +0 -9
- data/test/data/sample.vcf.gz +0 -0
- data/test/test-annotation.rb +0 -46
- data/test/test-auth.rb +0 -58
- data/test/test-client.rb +0 -27
- data/test/test-conversion.rb +0 -13
- data/test/test-dataset.rb +0 -42
- data/test/test-depository.rb +0 -35
- data/test/test-error.rb +0 -36
- data/test/test-filter.rb +0 -70
- data/test/test-netrc.rb +0 -52
- data/test/test-query-batch.rb +0 -40
- data/test/test-query-init.rb +0 -29
- data/test/test-query-paging.rb +0 -102
- data/test/test-query.rb +0 -71
- data/test/test-resource.rb +0 -40
- data/test/test-sample-access.rb +0 -59
- data/test/test-sample-download.rb +0 -20
- data/test/test-tabulate.rb +0 -131
- data/test/test-util.rb +0 -42
@@ -0,0 +1,54 @@
|
|
1
|
+
module SolveBio
|
2
|
+
class Sample < APIResource
|
3
|
+
# Samples are VCF files uploaded to the SolveBio API. We currently
|
4
|
+
# support uncompressed, extension `.vcf`, and gzip-compressed, extension
|
5
|
+
# `.vcf.gz`, VCF files. Any other extension will be rejected.
|
6
|
+
include SolveBio::APIOperations::List
|
7
|
+
include SolveBio::APIOperations::Delete
|
8
|
+
include SolveBio::APIOperations::Download
|
9
|
+
include SolveBio::APIOperations::Help
|
10
|
+
|
11
|
+
def annotate
|
12
|
+
Annotation.create :sample_id => self.id
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create(genome_build, params={})
|
16
|
+
if params.member?(:vcf_url)
|
17
|
+
if params.member?(:vcf_file)
|
18
|
+
raise TypeError,
|
19
|
+
'Specified both vcf_url and vcf_file; use only one'
|
20
|
+
end
|
21
|
+
|
22
|
+
self.create_from_url(genome_build, params[:vcf_url])
|
23
|
+
elsif params.member?(:vcf_file)
|
24
|
+
return self.create_from_file(genome_build, params[:vcf_file])
|
25
|
+
else
|
26
|
+
raise TypeError,
|
27
|
+
'Must specify exactly one of vcf_url or vcf_file parameter'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates from the specified file. The data of the should be in
|
32
|
+
# VCF format.
|
33
|
+
def self.create_from_file(genome_build, vcf_file)
|
34
|
+
data = {
|
35
|
+
:genome_build => genome_build,
|
36
|
+
:vcf_file => File.open(vcf_file, 'rb')
|
37
|
+
}
|
38
|
+
response = Client.post(url, data, :no_json => true)
|
39
|
+
Util.to_solve_object(response)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates from the specified URL. The data of the should be in
|
43
|
+
# VCF format.
|
44
|
+
def self.create_from_url(genome_build, vcf_url)
|
45
|
+
params = {:genome_build => genome_build,
|
46
|
+
:vcf_url => vcf_url}
|
47
|
+
begin
|
48
|
+
response = Client.post(url, params)
|
49
|
+
rescue SolveError => response
|
50
|
+
end
|
51
|
+
Util.to_solve_object(response)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SolveBio
|
2
|
+
class SingletonAPIResource < APIResource
|
3
|
+
# def self.class_to_api_name(cls)
|
4
|
+
# cls_name = cls.to_s.sub('SolveBio::', '')
|
5
|
+
# Util.camelcase_to_underscore(cls_name)
|
6
|
+
# end
|
7
|
+
|
8
|
+
def self.retrieve
|
9
|
+
instance = self.new(nil)
|
10
|
+
instance.refresh
|
11
|
+
instance
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.url
|
15
|
+
if self == SingletonAPIResource
|
16
|
+
raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (User, Account, etc.)')
|
17
|
+
end
|
18
|
+
"/v1/#{CGI.escape(class_name.downcase)}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def url
|
22
|
+
self.class.url
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module SolveBio
|
2
|
+
class SolveObject
|
3
|
+
include Enumerable
|
4
|
+
@@permanent_attributes = Set.new([:id])
|
5
|
+
|
6
|
+
# The default :id method is deprecated and isn't useful to us
|
7
|
+
if method_defined?(:id)
|
8
|
+
undef :id
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id=nil)
|
12
|
+
@values = {}
|
13
|
+
# store manually updated values for partial updates
|
14
|
+
@unsaved_values = Set.new
|
15
|
+
|
16
|
+
@values[:id] = id if id
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.construct_from(values)
|
20
|
+
self.new(values[:id]).refresh_from(values)
|
21
|
+
end
|
22
|
+
|
23
|
+
def refresh_from(values, partial=false)
|
24
|
+
removed = partial ? Set.new : Set.new(@values.keys - values.keys)
|
25
|
+
added = Set.new(values.keys - @values.keys)
|
26
|
+
|
27
|
+
instance_eval do
|
28
|
+
remove_accessors(removed)
|
29
|
+
add_accessors(added)
|
30
|
+
end
|
31
|
+
|
32
|
+
removed.each do |k|
|
33
|
+
@values.delete(k)
|
34
|
+
@unsaved_values.delete(k)
|
35
|
+
end
|
36
|
+
|
37
|
+
values.each do |k, v|
|
38
|
+
@values[k] = Util.to_solve_object(v)
|
39
|
+
@unsaved_values.delete(k)
|
40
|
+
end
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect
|
46
|
+
ident = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
|
47
|
+
|
48
|
+
if self.respond_to?(:full_name) && !self.full_name.nil?
|
49
|
+
ident += " full_name=#{self.full_name}"
|
50
|
+
end
|
51
|
+
|
52
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}#{ident}> JSON: " + JSON.pretty_generate(@values)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
if self.respond_to?(:tabulate)
|
57
|
+
self.tabulate(@values)
|
58
|
+
else
|
59
|
+
# No equivalent of Python's json sort_keys?
|
60
|
+
JSON.pretty_generate(@values, :indent => ' ')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def [](k)
|
65
|
+
@values[k.to_sym]
|
66
|
+
end
|
67
|
+
|
68
|
+
def []=(k, v)
|
69
|
+
send(:"#{k}=", v)
|
70
|
+
end
|
71
|
+
|
72
|
+
def keys
|
73
|
+
@values.keys
|
74
|
+
end
|
75
|
+
|
76
|
+
def values
|
77
|
+
@values.values
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_json(*a)
|
81
|
+
JSON.generate(@values)
|
82
|
+
end
|
83
|
+
|
84
|
+
def as_json(*a)
|
85
|
+
@values.as_json(*a)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_hash
|
89
|
+
@values.inject({}) do |acc, (key, value)|
|
90
|
+
acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
|
91
|
+
acc
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def each(&blk)
|
96
|
+
@values.each(&blk)
|
97
|
+
end
|
98
|
+
|
99
|
+
if RUBY_VERSION < '1.9.2'
|
100
|
+
def respond_to?(symbol)
|
101
|
+
@values.has_key?(symbol) || super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
|
107
|
+
def metaclass
|
108
|
+
class << self; self; end
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove_accessors(keys)
|
112
|
+
metaclass.instance_eval do
|
113
|
+
keys.each do |k|
|
114
|
+
next if @@permanent_attributes.include?(k)
|
115
|
+
k_eq = :"#{k}="
|
116
|
+
remove_method(k) if method_defined?(k)
|
117
|
+
remove_method(k_eq) if method_defined?(k_eq)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_accessors(keys)
|
123
|
+
metaclass.instance_eval do
|
124
|
+
keys.each do |k|
|
125
|
+
next if @@permanent_attributes.include?(k)
|
126
|
+
k_eq = :"#{k}="
|
127
|
+
define_method(k) { @values[k] }
|
128
|
+
define_method(k_eq) do |v|
|
129
|
+
if v == ""
|
130
|
+
raise ArgumentError.new(
|
131
|
+
"You cannot set #{k} to an empty string." +
|
132
|
+
"We interpret empty strings as nil in requests." +
|
133
|
+
"You may set #{self}.#{k} = nil to delete the property.")
|
134
|
+
end
|
135
|
+
@values[k] = v
|
136
|
+
@unsaved_values.add(k)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def method_missing(name, *args)
|
143
|
+
if name.to_s.end_with?('=')
|
144
|
+
attr = name.to_s[0...-1].to_sym
|
145
|
+
add_accessors([attr])
|
146
|
+
begin
|
147
|
+
mth = method(name)
|
148
|
+
rescue NameError
|
149
|
+
raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
|
150
|
+
end
|
151
|
+
return mth.call(args[0])
|
152
|
+
else
|
153
|
+
return @values[name] if @values.has_key?(name)
|
154
|
+
end
|
155
|
+
|
156
|
+
super
|
157
|
+
end
|
158
|
+
|
159
|
+
def respond_to_missing?(symbol, include_private = false)
|
160
|
+
@values && @values.has_key?(symbol) || super
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,589 @@
|
|
1
|
+
module SolveBio
|
2
|
+
module Tabulate
|
3
|
+
TYPES = {NilClass => 0, Fixnum => 1, Float => 2, String => 4}
|
4
|
+
|
5
|
+
INVISIBILE_CODES = %r{\\x1b\[\d*m} # ANSI color codes
|
6
|
+
|
7
|
+
Line = Struct.new(:start, :hline, :sep, :last)
|
8
|
+
|
9
|
+
DataRow = Struct.new(:start, :sep, :last)
|
10
|
+
|
11
|
+
TableFormat = Struct.new(:lineabove, :linebelowheader,
|
12
|
+
:linebetweenrows, :linebelow,
|
13
|
+
:headerrow, :datarow,
|
14
|
+
:padding, :usecolons,
|
15
|
+
:with_header_hide,
|
16
|
+
:without_header_hide)
|
17
|
+
|
18
|
+
FORMAT_DEFAULTS = {
|
19
|
+
:padding => 0,
|
20
|
+
:usecolons => false,
|
21
|
+
:with_header_hide => [],
|
22
|
+
:without_header_hide => []
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
INVTYPES = {
|
27
|
+
4 => String,
|
28
|
+
2 => Float,
|
29
|
+
1 => Fixnum,
|
30
|
+
0 => NilClass
|
31
|
+
}
|
32
|
+
|
33
|
+
SIMPLE_DATAROW = DataRow.new('', ' ', '')
|
34
|
+
PIPE_DATAROW = DataRow.new('|', '|', '|')
|
35
|
+
|
36
|
+
SIMPLE_LINE = Line.new('', '-', ' ', '')
|
37
|
+
GRID_LINE = Line.new('+', '-', '+', '+')
|
38
|
+
|
39
|
+
TABLE_FORMATS = {
|
40
|
+
:simple =>
|
41
|
+
TableFormat.new(lineabove = nil,
|
42
|
+
linebelowheader = SIMPLE_LINE,
|
43
|
+
linebetweenrows = nil,
|
44
|
+
linebelow = SIMPLE_LINE,
|
45
|
+
headerrow = SIMPLE_DATAROW,
|
46
|
+
datarow = SIMPLE_DATAROW,
|
47
|
+
padding = 0,
|
48
|
+
usecolons = false,
|
49
|
+
with_header_hide = ['linebelow'],
|
50
|
+
without_header_hide = []),
|
51
|
+
:grid =>
|
52
|
+
TableFormat.new(lineabove = SIMPLE_LINE,
|
53
|
+
linebelowheader = Line.new('+', '=', '+', '+'),
|
54
|
+
linebetweenrows = SIMPLE_LINE,
|
55
|
+
linebelow = SIMPLE_LINE,
|
56
|
+
headerrow = PIPE_DATAROW,
|
57
|
+
datarow = PIPE_DATAROW,
|
58
|
+
padding = 1,
|
59
|
+
usecolons = false,
|
60
|
+
with_header_hide = [],
|
61
|
+
without_header_hide = ['linebelowheader']),
|
62
|
+
|
63
|
+
:pipe =>
|
64
|
+
TableFormat.new(lineabove = nil,
|
65
|
+
linebelowheader = Line.new('|', '-', '|', '|'),
|
66
|
+
linebetweenrows = nil,
|
67
|
+
linebelow = nil,
|
68
|
+
headerrow = PIPE_DATAROW,
|
69
|
+
datarow = PIPE_DATAROW,
|
70
|
+
padding = 1,
|
71
|
+
usecolons = true,
|
72
|
+
with_header_hide = [],
|
73
|
+
without_header_hide = []),
|
74
|
+
|
75
|
+
:orgmode =>
|
76
|
+
TableFormat.new(lineabove=nil,
|
77
|
+
linebelowheader = Line.new('|', '-', '+', '|'),
|
78
|
+
linebetweenrows = nil,
|
79
|
+
linebelow = nil,
|
80
|
+
headerrow = PIPE_DATAROW,
|
81
|
+
datarow = PIPE_DATAROW,
|
82
|
+
padding = 1,
|
83
|
+
usecolons = false,
|
84
|
+
with_header_hide = [],
|
85
|
+
without_header_hide = ['linebelowheader'])
|
86
|
+
}
|
87
|
+
|
88
|
+
module_function
|
89
|
+
# Simulate Python's multi-parameter zip function. Ruby's zip
|
90
|
+
# function, like Perl's, expects each arg to have dimension 2.
|
91
|
+
def python_zip(args)
|
92
|
+
result = args.first.reduce([]){|r, i| r << []}
|
93
|
+
args.each_with_index do |ary, i|
|
94
|
+
ary.each_with_index {|v, j| result[j][i] = v}
|
95
|
+
end
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
def simple_separated_format(separator)
|
100
|
+
# FIXME? python code hard-codes separator = "\n" below.
|
101
|
+
return TableFormat
|
102
|
+
.new(
|
103
|
+
:lineabove => nil,
|
104
|
+
:linebelowheader => nil,
|
105
|
+
:linebetweenrows => nil,
|
106
|
+
:linebelow => nil,
|
107
|
+
:headerrow => nil,
|
108
|
+
:datarow => DataRow.new('', separator, ''),
|
109
|
+
:padding => 0,
|
110
|
+
:usecolons => false,
|
111
|
+
:with_header_hide => [],
|
112
|
+
:without_header_hide => [],
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
# The least generic type, one of NilClass, Fixnum, Float, or String.
|
117
|
+
# _type(nil) => NilClass
|
118
|
+
# _type("foo") => String
|
119
|
+
# _type("1") => Fixnum
|
120
|
+
# _type("\x1b[31m42\x1b[0m") => Fixnum
|
121
|
+
def _type(obj, has_invisible=true)
|
122
|
+
|
123
|
+
obj = obj.strip_invisible if obj.kind_of?(String) and has_invisible
|
124
|
+
|
125
|
+
if obj.nil?
|
126
|
+
return NilClass
|
127
|
+
elsif obj.kind_of?(Fixnum) or obj.int?
|
128
|
+
return Fixnum
|
129
|
+
elsif obj.kind_of?(Float) or obj.number?
|
130
|
+
return Float
|
131
|
+
else
|
132
|
+
return String
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# [string] -> [padded_string]
|
137
|
+
#
|
138
|
+
# align_column(
|
139
|
+
# ["12.345", "-1234.5", "1.23", "1234.5",
|
140
|
+
# "1e+234", "1.0e234"], "decimal") =>
|
141
|
+
# [' 12.345 ', '-1234.5 ', ' 1.23 ',
|
142
|
+
# ' 1234.5 ', ' 1e+234 ', ' 1.0e234']
|
143
|
+
def align_column(strings, alignment, minwidth=0, has_invisible=true)
|
144
|
+
if alignment == "right"
|
145
|
+
strings = strings.map{|s| s.to_s.strip}
|
146
|
+
padfn = :padleft
|
147
|
+
elsif alignment == 'center'
|
148
|
+
strings = strings.map{|s| s.to_s.strip}
|
149
|
+
padfn = :padboth
|
150
|
+
elsif alignment == 'decimal'
|
151
|
+
decimals = strings.map{|s| s.to_s.afterpoint}
|
152
|
+
maxdecimals = decimals.max
|
153
|
+
zipped = strings.zip(decimals)
|
154
|
+
strings = zipped.map{|s, decs|
|
155
|
+
s.to_s + " " * ((maxdecimals - decs))
|
156
|
+
}
|
157
|
+
padfn = :padleft
|
158
|
+
else
|
159
|
+
strings = strings.map{|s| s.to_s.strip}
|
160
|
+
padfn = :padright
|
161
|
+
end
|
162
|
+
|
163
|
+
if has_invisible
|
164
|
+
width_fn = :visible_width
|
165
|
+
else
|
166
|
+
width_fn = :size
|
167
|
+
end
|
168
|
+
|
169
|
+
maxwidth = [strings.map{|s| s.send(width_fn)}.max, minwidth].max
|
170
|
+
strings.map{|s| s.send(padfn, maxwidth, has_invisible) }
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
def more_generic(type1, type2)
|
175
|
+
moregeneric = [TYPES[type1] || 4, TYPES[type2] || 4].max
|
176
|
+
return INVTYPES[moregeneric]
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# The least generic type all column values are convertible to.
|
181
|
+
#
|
182
|
+
# column_type(["1", "2"]) => Fixnum
|
183
|
+
# column_type(["1", "2.3"]) => Float
|
184
|
+
# column_type(["1", "2.3", "four"]) => String
|
185
|
+
# column_type(["four", '\u043f\u044f\u0442\u044c']) => String
|
186
|
+
# column_type([nil, "brux"]) => String
|
187
|
+
# column_type([1, 2, nil]) => Fixnum
|
188
|
+
def column_type(strings, has_invisible=true)
|
189
|
+
types = strings.map{|s| _type(s, has_invisible)}
|
190
|
+
# require 'trepanning'; debugger
|
191
|
+
return types.reduce(Fixnum){
|
192
|
+
|t, result|
|
193
|
+
more_generic(result, t)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# Format a value accoding to its type.
|
199
|
+
#
|
200
|
+
# Unicode is supported:
|
201
|
+
#
|
202
|
+
# >>> hrow = ["\u0431\u0443\u043a\u0432\u0430",
|
203
|
+
# "\u0446\u0438\u0444\u0440\u0430"]
|
204
|
+
# tbl = [["\u0430\u0437", 2], ["\u0431\u0443\u043a\u0438", 4]]
|
205
|
+
# expected = "\\u0431\\u0443\\u043a\\u0432\\u0430 \n
|
206
|
+
# \\u0446\\u0438\\u0444\\u0440\\u0430\\n-------\n
|
207
|
+
# -------\\n\\u0430\\u0437 \n
|
208
|
+
# 2\\n\\u0431\\u0443\\u043a\\u0438 4'
|
209
|
+
# tabulate(tbl, hrow) => good_result
|
210
|
+
# true
|
211
|
+
def format(val, valtype, floatfmt, missingval="")
|
212
|
+
if val.nil?
|
213
|
+
return missingval
|
214
|
+
end
|
215
|
+
|
216
|
+
if [Fixnum, String, Fixnum].member?(valtype)
|
217
|
+
return "%s" % val.to_s
|
218
|
+
elsif valtype.kind_of?(Float)
|
219
|
+
return "%#{floatfmt}" % Float(val)
|
220
|
+
else
|
221
|
+
return "%s" % val
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
def align_header(header, alignment, width)
|
227
|
+
if alignment == "left"
|
228
|
+
return header.padright(width)
|
229
|
+
elsif alignment == "center"
|
230
|
+
return header.padboth(width)
|
231
|
+
else
|
232
|
+
return header.padleft(width)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
# Transform a supported data type to an Array of Arrays, and an
|
238
|
+
# Array of headers.
|
239
|
+
#
|
240
|
+
# Supported tabular data types:
|
241
|
+
#
|
242
|
+
# * Array-of-Arrays or another Enumerable of Enumerables
|
243
|
+
#
|
244
|
+
# * Hash of Enumerables
|
245
|
+
#
|
246
|
+
# The first row can be used as headers if headers="firstrow",
|
247
|
+
# column indices can be used as headers if headers="keys".
|
248
|
+
#
|
249
|
+
def normalize_tabular_data(tabular_data, headers)
|
250
|
+
if tabular_data.respond_to?(:keys) and tabular_data.respond_to?(:values)
|
251
|
+
# likely a Hash
|
252
|
+
keys = tabular_data.keys
|
253
|
+
## FIXME: what's different in the Python code?
|
254
|
+
# columns have to be transposed
|
255
|
+
# rows = list(izip_longest(*tabular_data.values()))
|
256
|
+
# rows = vals[0].zip(*vals[1..-1])
|
257
|
+
rows = tabular_data.values
|
258
|
+
if headers == "keys"
|
259
|
+
# headers should be strings
|
260
|
+
headers = keys.map{|k| k.to_s}
|
261
|
+
end
|
262
|
+
elsif tabular_data.kind_of?(Enumerable)
|
263
|
+
# Likely an Enumerable of Enumerables
|
264
|
+
rows = tabular_data.to_a
|
265
|
+
if headers == "keys" and not rows.empty? # keys are column indices
|
266
|
+
headers = (0..rows[0]).map {|i| i.to_s}
|
267
|
+
end
|
268
|
+
else
|
269
|
+
raise(ValueError, "tabular data doesn't appear to be a Hash" +
|
270
|
+
" or Array")
|
271
|
+
end
|
272
|
+
|
273
|
+
# take headers from the first row if necessary
|
274
|
+
if headers == "firstrow" and not rows.empty?
|
275
|
+
headers = rows[0].map{|row| [_text_type(row)]}
|
276
|
+
rows.shift
|
277
|
+
end
|
278
|
+
|
279
|
+
# pad with empty headers for initial columns if necessary
|
280
|
+
if not headers.empty? and not rows.empty?
|
281
|
+
nhs = headers.size
|
282
|
+
ncols = rows[0].size
|
283
|
+
if nhs < ncols
|
284
|
+
headers = [''] * (ncols - nhs) + headers
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
return rows, headers
|
289
|
+
end
|
290
|
+
|
291
|
+
TTY_COLS = ENV['COLUMNS'].to_i || 80 rescue 80
|
292
|
+
# Return a string which represents a row of data cells.
|
293
|
+
def build_row(cells, padding, first, sep, last)
|
294
|
+
|
295
|
+
pad = ' ' * padding
|
296
|
+
padded_cells = cells.map{|cell| pad + cell + pad }
|
297
|
+
rendered_cells = (first + padded_cells.join(sep) + last).rstrip
|
298
|
+
|
299
|
+
# Enforce that we don't wrap lines by setting a max
|
300
|
+
# limit on row width which is equal to TTY_COLS (see printing)
|
301
|
+
if rendered_cells.size > TTY_COLS
|
302
|
+
if not cells[-1].end_with?(' ') and not cells[-1].end_with?('-')
|
303
|
+
terminating_str = ' ... '
|
304
|
+
else
|
305
|
+
terminating_str = ''
|
306
|
+
end
|
307
|
+
prefix = rendered_cells[0..TTY_COLS - terminating_str.size - 2]
|
308
|
+
rendered_cells = "%s%s%s" % [prefix, terminating_str, last]
|
309
|
+
end
|
310
|
+
|
311
|
+
return rendered_cells
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
# Return a string which represents a horizontal line.
|
316
|
+
def build_line(colwidths, padding, first, fill, sep, last)
|
317
|
+
cells = colwidths.map{|w| fill * (w + 2 * padding)}
|
318
|
+
return build_row(cells, 0, first, sep, last)
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
# Return a segment of a horizontal line with optional colons which
|
323
|
+
# indicate column's alignment (as in `pipe` output format).
|
324
|
+
def _line_segment_with_colons(linefmt, align, colwidth)
|
325
|
+
fill = linefmt.hline
|
326
|
+
w = colwidth
|
327
|
+
if ['right', 'decimal'].member?(align)
|
328
|
+
return (fill[0] * (w - 1)) + ":"
|
329
|
+
elsif align == "center"
|
330
|
+
return ":" + (fill[0] * (w - 2)) + ":"
|
331
|
+
elsif align == "left"
|
332
|
+
return ":" + (fill[0] * (w - 1))
|
333
|
+
else
|
334
|
+
return fill[0] * w
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
# Produce a plain-text representation of the table.
|
340
|
+
def format_table(fmt, headers, rows, colwidths, colaligns)
|
341
|
+
lines = []
|
342
|
+
hidden = headers ? fmt.with_header_hide : fmt.without_header_hide
|
343
|
+
pad = fmt.padding || 0
|
344
|
+
datarow = fmt.datarow ? fmt.datarow : SIMPLE_DATAROW
|
345
|
+
headerrow = fmt.headerrow ? fmt.headerrow : fmt.datarow
|
346
|
+
|
347
|
+
if fmt.lineabove and hidden and hidden.member?("lineabove")
|
348
|
+
lines << build_line(colwidths, pad, *fmt.lineabove)
|
349
|
+
end
|
350
|
+
|
351
|
+
unless headers.empty?
|
352
|
+
lines << build_row(headers, pad, headerrow.start, headerrow.sep,
|
353
|
+
headerrow.last)
|
354
|
+
end
|
355
|
+
|
356
|
+
if fmt.linebelowheader and not hidden.member?("linebelowheader")
|
357
|
+
first, _, sep, last = fmt.linebelowheader
|
358
|
+
if fmt.usecolons
|
359
|
+
segs = [
|
360
|
+
colwidths.zip(colaligns).map do |w, a|
|
361
|
+
_line_segment_with_colons(fmt.linebelowheader, a, w + 2 * pad)
|
362
|
+
end ]
|
363
|
+
lines << build_row(segs, 0, first, sep, last)
|
364
|
+
else
|
365
|
+
lines << build_line(colwidths, pad, fmt.linebelowheader.start,
|
366
|
+
fmt.linebelowheader.hline,
|
367
|
+
fmt.linebelowheader.sep,
|
368
|
+
fmt.linebelowheader.last)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
if rows and fmt.linebetweenrows and hidden.member?('linebetweenrows')
|
373
|
+
# initial rows with a line below
|
374
|
+
rows[1..-1].each do |row|
|
375
|
+
lines << build_row(row, pad, fmt.datarow.start,
|
376
|
+
fmt.datarow.sep, fmt.datarow.last)
|
377
|
+
lines << build_line(colwidths, pad, fmt.linebetweenrows.start,
|
378
|
+
fmt.linebelowheader.hline,
|
379
|
+
fmt.linebetweenrows.sep,
|
380
|
+
fmt.linebetweenrows.last)
|
381
|
+
end
|
382
|
+
# the last row without a line below
|
383
|
+
lines << build_row(rows[-1], pad, datarow.start,
|
384
|
+
datarow.sep, datarow.last)
|
385
|
+
else
|
386
|
+
rows.each do |row|
|
387
|
+
lines << build_row(row, pad, datarow.start, datarow.sep,
|
388
|
+
datarow.last)
|
389
|
+
|
390
|
+
if fmt.linebelow and hidden.member?('linebelow')
|
391
|
+
lines << build_line(colwidths, pad, fmt.linebelow.start,
|
392
|
+
fmt.linebelowheader.hline,
|
393
|
+
fmt.linebelow.sep,
|
394
|
+
fmt.linebelow.last)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
return lines.join("\n")
|
399
|
+
end
|
400
|
+
|
401
|
+
# Construct a simple TableFormat with columns separated by a separator.
|
402
|
+
#
|
403
|
+
# tsv = simple_separated_format("\t")
|
404
|
+
# tabulate([["foo", 1], ["spam", 23]], [], true, tsv) =>
|
405
|
+
# "foo 1\nspam 23"
|
406
|
+
def tabulate(tabular_data, headers=[], aligns=[], sort=true,
|
407
|
+
tablefmt=TABLE_FORMATS[:orgmode], floatfmt="g", missingval='')
|
408
|
+
|
409
|
+
tabular_data = tabular_data.sort_by{|x| x[0]} if sort
|
410
|
+
list_of_lists, headers = normalize_tabular_data(tabular_data, headers)
|
411
|
+
|
412
|
+
# optimization: look for ANSI control codes once,
|
413
|
+
# enable smart width functions only if a control code is found
|
414
|
+
plain_rows = [headers.map{|h| h.to_s}.join("\t")]
|
415
|
+
row_text = list_of_lists.map{|row|
|
416
|
+
row.map{|r| r.to_s}.join("\t")
|
417
|
+
}
|
418
|
+
plain_rows += row_text
|
419
|
+
plain_text = plain_rows.join("\n")
|
420
|
+
|
421
|
+
has_invisible = INVISIBILE_CODES.match(plain_text)
|
422
|
+
if has_invisible
|
423
|
+
width_fn = :visible_width
|
424
|
+
else
|
425
|
+
width_fn = :size
|
426
|
+
end
|
427
|
+
|
428
|
+
# format rows and columns, convert numeric values to strings
|
429
|
+
cols = list_of_lists[0].zip(*list_of_lists[1..-1]) if
|
430
|
+
list_of_lists.size > 1
|
431
|
+
|
432
|
+
coltypes = cols.map{|c| column_type(c)}
|
433
|
+
|
434
|
+
cols = cols.zip(coltypes).map do |c, ct|
|
435
|
+
c.map{|v| format(v, ct, floatfmt, missingval)}
|
436
|
+
end
|
437
|
+
|
438
|
+
# align columns
|
439
|
+
if aligns.empty?
|
440
|
+
# dynamic alignment by col type
|
441
|
+
aligns = coltypes.map do |ct|
|
442
|
+
[Fixnum, Float].member?(ct) ? 'decimal' : 'left'
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
minwidths =
|
447
|
+
if headers.empty? then
|
448
|
+
[0] * cols.size
|
449
|
+
else
|
450
|
+
headers.map{|h| h.send(width_fn) + 2}
|
451
|
+
end
|
452
|
+
|
453
|
+
cols = cols.zip(aligns, minwidths).map do |c, a, minw|
|
454
|
+
align_column(c, a, minw, has_invisible)
|
455
|
+
end
|
456
|
+
|
457
|
+
if headers.empty?
|
458
|
+
minwidths = cols.map{|c| c[0].send(width_fn)}
|
459
|
+
else
|
460
|
+
# align headers and add headers
|
461
|
+
minwidths =
|
462
|
+
minwidths.zip(cols).map{|minw, c| [minw, c[0].send(width_fn)].max}
|
463
|
+
headers =
|
464
|
+
headers.zip(aligns, minwidths).map{|h, a, minw| align_header(h, a, minw)}
|
465
|
+
end
|
466
|
+
rows = python_zip(cols)
|
467
|
+
|
468
|
+
tablefmt = TABLE_FORMATS[:orgmode] unless
|
469
|
+
tablefmt.kind_of?(TableFormat)
|
470
|
+
|
471
|
+
# make sure values don't have newlines or tabs in them
|
472
|
+
rows.each do |r|
|
473
|
+
r.each_with_index do |c, i|
|
474
|
+
r[i] = c.gsub("\n", '').gsub("\t", '')
|
475
|
+
end
|
476
|
+
end
|
477
|
+
return format_table(tablefmt, headers, rows, minwidths, aligns)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
end
|
482
|
+
|
483
|
+
# Monkey patching
|
484
|
+
class Object
|
485
|
+
# "123.45".number? => true
|
486
|
+
# "123".number? => true
|
487
|
+
# "spam".number? => false
|
488
|
+
def number?
|
489
|
+
begin
|
490
|
+
Float(self)
|
491
|
+
return true
|
492
|
+
rescue
|
493
|
+
return false
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
# "123".int? => true
|
498
|
+
# "123.45".int? => false
|
499
|
+
def int?
|
500
|
+
begin
|
501
|
+
Integer(self)
|
502
|
+
return true
|
503
|
+
rescue
|
504
|
+
return false
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
class String
|
510
|
+
# Symbols after a decimal point, -1 if the string lacks the decimal point.
|
511
|
+
#
|
512
|
+
# "123.45".afterpoint => 2
|
513
|
+
# "1001".afterpoint => -1
|
514
|
+
# "eggs".afterpoint => -1
|
515
|
+
# "123e45".afterpoint => 2
|
516
|
+
def afterpoint
|
517
|
+
if self.number?
|
518
|
+
if self.int?
|
519
|
+
return -1
|
520
|
+
else
|
521
|
+
pos = self.rindex('.') || -1
|
522
|
+
pos = self.downcase().rindex('e') if pos < 0
|
523
|
+
if pos >= 0
|
524
|
+
return self.size - pos - 1
|
525
|
+
else
|
526
|
+
return -1 # no point
|
527
|
+
end
|
528
|
+
end
|
529
|
+
else
|
530
|
+
return -1 # not a number
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def adjusted_size(has_invisible)
|
535
|
+
return has_invisible ? self.strip_invisible.size : self.size
|
536
|
+
end
|
537
|
+
|
538
|
+
# Visible width of a printed string. ANSI color codes are removed.
|
539
|
+
#
|
540
|
+
# ['\x1b[31mhello\x1b[0m' "world"].map{|s| s.visible_width} =>
|
541
|
+
# [5, 5]
|
542
|
+
def visible_width
|
543
|
+
# if self.kind_of?(_text_type) or self.kind_of?(_binary_type)
|
544
|
+
return self.strip_invisible.size
|
545
|
+
# else
|
546
|
+
# return _text_type(s).size
|
547
|
+
# end
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
# Flush right.
|
552
|
+
#
|
553
|
+
# '\u044f\u0439\u0446\u0430'.padleft(6) =>
|
554
|
+
# ' \u044f\u0439\u0446\u0430'
|
555
|
+
# 'abc'.padleft(2) => 'abc'
|
556
|
+
def padleft(width, has_invisible=true)
|
557
|
+
s_width = self.adjusted_size(has_invisible)
|
558
|
+
s_width < width ? (' ' * (width - s_width)) + self : self
|
559
|
+
end
|
560
|
+
|
561
|
+
# Flush left.
|
562
|
+
#
|
563
|
+
# padright(6, '\u044f\u0439\u0446\u0430') => '\u044f\u0439\u0446\u0430 '
|
564
|
+
# padright(2, 'abc') => 'abc'
|
565
|
+
def padright(width, has_invisible=true)
|
566
|
+
s_width = self.adjusted_size(has_invisible)
|
567
|
+
s_width < width ? self + (' ' * (width - s_width)) : self
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
# Center string with uneven space on the right
|
572
|
+
#
|
573
|
+
# '\u044f\u0439\u0446\u0430'.padboth(6) => ' \u044f\u0439\u0446\u0430 '
|
574
|
+
# 'abc'.padboth(2) => 'abc'
|
575
|
+
# 'abc'.padboth(6) => ' abc '
|
576
|
+
def padboth(width, has_invisible=true)
|
577
|
+
s_width = self.adjusted_size(has_invisible)
|
578
|
+
return self if s_width >= width
|
579
|
+
pad_size = width - s_width
|
580
|
+
pad_left = ' ' * (pad_size/2)
|
581
|
+
pad_right = ' ' * ((pad_size + 1)/ 2)
|
582
|
+
pad_left + self + pad_right
|
583
|
+
end
|
584
|
+
|
585
|
+
# Remove invisible ANSI color codes.
|
586
|
+
def strip_invisible
|
587
|
+
return self.gsub(SolveBio::Tabulate::INVISIBILE_CODES, '')
|
588
|
+
end
|
589
|
+
end
|