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.
Files changed (119) hide show
  1. data/.bumpversion.cfg +6 -0
  2. data/.gitignore +5 -4
  3. data/.travis.yml +1 -1
  4. data/Gemfile +3 -0
  5. data/README.md +34 -34
  6. data/Rakefile +1 -18
  7. data/bin/solvebio.rb +14 -16
  8. data/installer +64 -0
  9. data/lib/solvebio.rb +50 -11
  10. data/lib/solvebio/acccount.rb +4 -0
  11. data/lib/solvebio/annotation.rb +11 -0
  12. data/lib/solvebio/api_operations.rb +147 -0
  13. data/lib/solvebio/api_resource.rb +32 -0
  14. data/lib/solvebio/cli.rb +75 -0
  15. data/lib/solvebio/cli/auth.rb +106 -0
  16. data/lib/solvebio/cli/credentials.rb +54 -0
  17. data/lib/{cli → solvebio/cli}/irb.rb +0 -23
  18. data/lib/solvebio/cli/irbrc.rb +48 -0
  19. data/lib/solvebio/cli/tutorial.rb +12 -0
  20. data/lib/solvebio/client.rb +149 -0
  21. data/lib/solvebio/dataset.rb +60 -0
  22. data/lib/solvebio/dataset_field.rb +12 -0
  23. data/lib/solvebio/depository.rb +38 -0
  24. data/lib/solvebio/depository_version.rb +40 -0
  25. data/lib/solvebio/errors.rb +64 -0
  26. data/lib/solvebio/filter.rb +315 -0
  27. data/lib/solvebio/list_object.rb +73 -0
  28. data/lib/solvebio/locale.rb +43 -0
  29. data/lib/solvebio/query.rb +341 -0
  30. data/lib/solvebio/sample.rb +54 -0
  31. data/lib/solvebio/singleton_api_resource.rb +25 -0
  32. data/lib/solvebio/solve_object.rb +164 -0
  33. data/lib/solvebio/tabulate.rb +589 -0
  34. data/lib/solvebio/user.rb +4 -0
  35. data/lib/solvebio/util.rb +59 -0
  36. data/lib/solvebio/version.rb +3 -0
  37. data/solvebio.gemspec +10 -18
  38. data/test/helper.rb +6 -2
  39. data/test/solvebio/data/.gitignore +1 -0
  40. data/test/solvebio/data/.netrc +6 -0
  41. data/test/{data → solvebio/data}/netrc-save +0 -0
  42. data/test/solvebio/data/sample.vcf.gz +0 -0
  43. data/test/solvebio/data/test_creds +3 -0
  44. data/test/solvebio/test_annotation.rb +45 -0
  45. data/test/solvebio/test_client.rb +29 -0
  46. data/test/solvebio/test_conversion.rb +14 -0
  47. data/test/solvebio/test_credentials.rb +67 -0
  48. data/test/solvebio/test_dataset.rb +52 -0
  49. data/test/solvebio/test_depository.rb +24 -0
  50. data/test/solvebio/test_depositoryversion.rb +22 -0
  51. data/test/solvebio/test_error.rb +31 -0
  52. data/test/solvebio/test_filter.rb +86 -0
  53. data/test/solvebio/test_query.rb +282 -0
  54. data/test/solvebio/test_query_batch.rb +38 -0
  55. data/test/solvebio/test_query_init.rb +30 -0
  56. data/test/solvebio/test_query_tabulate.rb +73 -0
  57. data/test/solvebio/test_ratelimit.rb +31 -0
  58. data/test/solvebio/test_resource.rb +29 -0
  59. data/test/solvebio/test_sample_access.rb +60 -0
  60. data/test/solvebio/test_sample_download.rb +20 -0
  61. data/test/solvebio/test_tabulate.rb +129 -0
  62. data/test/solvebio/test_util.rb +39 -0
  63. metadata +100 -85
  64. data/Makefile +0 -17
  65. data/demo/README.md +0 -14
  66. data/demo/cheatsheet.rb +0 -31
  67. data/demo/dataset/facets.rb +0 -13
  68. data/demo/dataset/field.rb +0 -13
  69. data/demo/depository/README.md +0 -24
  70. data/demo/depository/all.rb +0 -13
  71. data/demo/depository/retrieve.rb +0 -13
  72. data/demo/depository/versions-all.rb +0 -13
  73. data/demo/query/query-filter.rb +0 -30
  74. data/demo/query/query.rb +0 -13
  75. data/demo/query/range-filter.rb +0 -18
  76. data/demo/test-api.rb +0 -98
  77. data/lib/cli/auth.rb +0 -122
  78. data/lib/cli/help.rb +0 -13
  79. data/lib/cli/irbrc.rb +0 -54
  80. data/lib/cli/options.rb +0 -75
  81. data/lib/client.rb +0 -154
  82. data/lib/credentials.rb +0 -67
  83. data/lib/errors.rb +0 -81
  84. data/lib/filter.rb +0 -312
  85. data/lib/locale.rb +0 -47
  86. data/lib/main.rb +0 -46
  87. data/lib/query.rb +0 -414
  88. data/lib/resource/annotation.rb +0 -23
  89. data/lib/resource/apiresource.rb +0 -241
  90. data/lib/resource/dataset.rb +0 -91
  91. data/lib/resource/datasetfield.rb +0 -37
  92. data/lib/resource/depository.rb +0 -50
  93. data/lib/resource/depositoryversion.rb +0 -69
  94. data/lib/resource/main.rb +0 -123
  95. data/lib/resource/sample.rb +0 -75
  96. data/lib/resource/solveobject.rb +0 -122
  97. data/lib/resource/user.rb +0 -5
  98. data/lib/tabulate.rb +0 -706
  99. data/lib/util.rb +0 -29
  100. data/test/Makefile +0 -9
  101. data/test/data/sample.vcf.gz +0 -0
  102. data/test/test-annotation.rb +0 -46
  103. data/test/test-auth.rb +0 -58
  104. data/test/test-client.rb +0 -27
  105. data/test/test-conversion.rb +0 -13
  106. data/test/test-dataset.rb +0 -42
  107. data/test/test-depository.rb +0 -35
  108. data/test/test-error.rb +0 -36
  109. data/test/test-filter.rb +0 -70
  110. data/test/test-netrc.rb +0 -52
  111. data/test/test-query-batch.rb +0 -40
  112. data/test/test-query-init.rb +0 -29
  113. data/test/test-query-paging.rb +0 -102
  114. data/test/test-query.rb +0 -71
  115. data/test/test-resource.rb +0 -40
  116. data/test/test-sample-access.rb +0 -59
  117. data/test/test-sample-download.rb +0 -20
  118. data/test/test-tabulate.rb +0 -131
  119. 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