solvebio 1.6.1 → 1.7.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.
- 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
|