solvebio 1.6.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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