google-cloud-bigquery 0.20.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.
@@ -0,0 +1,238 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/bigquery/service"
17
+ require "google/cloud/bigquery/data"
18
+
19
+ module Google
20
+ module Cloud
21
+ module Bigquery
22
+ ##
23
+ # # QueryData
24
+ #
25
+ # Represents Data returned from a query a a list of name/value pairs.
26
+ class QueryData < Data
27
+ ##
28
+ # @private The Service object.
29
+ attr_accessor :service
30
+
31
+ # @private
32
+ def initialize arr = []
33
+ @job = nil
34
+ super
35
+ end
36
+
37
+ # The total number of bytes processed for this query.
38
+ def total_bytes
39
+ Integer @gapi.total_bytes_processed
40
+ rescue
41
+ nil
42
+ end
43
+
44
+ # Whether the query has completed or not. When data is present this will
45
+ # always be `true`. When `false`, `total` will not be available.
46
+ def complete?
47
+ @gapi.job_complete
48
+ end
49
+
50
+ # Whether the query result was fetched from the query cache.
51
+ def cache_hit?
52
+ @gapi.cache_hit
53
+ end
54
+
55
+ ##
56
+ # The schema of the data.
57
+ def schema
58
+ Schema.from_gapi(@gapi.schema).freeze
59
+ end
60
+
61
+ ##
62
+ # The fields of the data.
63
+ def fields
64
+ f = schema.fields
65
+ f = f.to_hash if f.respond_to? :to_hash
66
+ f = [] if f.nil?
67
+ f
68
+ end
69
+
70
+ ##
71
+ # The name of the columns in the data.
72
+ def headers
73
+ fields.map(&:name)
74
+ end
75
+
76
+ ##
77
+ # Whether there is a next page of query data.
78
+ #
79
+ # @return [Boolean]
80
+ #
81
+ # @example
82
+ # require "google/cloud"
83
+ #
84
+ # gcloud = Google::Cloud.new
85
+ # bigquery = gcloud.bigquery
86
+ # job = bigquery.job "my_job"
87
+ #
88
+ # data = job.query_results
89
+ # if data.next?
90
+ # next_data = data.next
91
+ # end
92
+ #
93
+ def next?
94
+ !token.nil?
95
+ end
96
+
97
+ ##
98
+ # Retrieve the next page of query data.
99
+ #
100
+ # @return [QueryData]
101
+ #
102
+ # @example
103
+ # require "google/cloud"
104
+ #
105
+ # gcloud = Google::Cloud.new
106
+ # bigquery = gcloud.bigquery
107
+ # job = bigquery.job "my_job"
108
+ #
109
+ # data = job.query_results
110
+ # if data.next?
111
+ # next_data = data.next
112
+ # end
113
+ #
114
+ def next
115
+ return nil unless next?
116
+ ensure_service!
117
+ gapi = service.job_query_results job_id, token: token
118
+ QueryData.from_gapi gapi, service
119
+ end
120
+
121
+ ##
122
+ # Retrieves all rows by repeatedly loading {#next} until {#next?}
123
+ # returns `false`. Calls the given block once for each row, which is
124
+ # passed as the parameter.
125
+ #
126
+ # An Enumerator is returned if no block is given.
127
+ #
128
+ # This method may make several API calls until all rows are retrieved.
129
+ # Be sure to use as narrow a search criteria as possible. Please use
130
+ # with caution.
131
+ #
132
+ # @param [Integer] request_limit The upper limit of API requests to make
133
+ # to load all data. Default is no limit.
134
+ # @yield [row] The block for accessing each row of data.
135
+ # @yieldparam [Hash] row The row object.
136
+ #
137
+ # @return [Enumerator]
138
+ #
139
+ # @example Iterating each row by passing a block:
140
+ # require "google/cloud"
141
+ #
142
+ # gcloud = Google::Cloud.new
143
+ # bigquery = gcloud.bigquery
144
+ # job = bigquery.job "my_job"
145
+ #
146
+ # data = job.query_results
147
+ # data.all do |row|
148
+ # puts row["word"]
149
+ # end
150
+ #
151
+ # @example Using the enumerator by not passing a block:
152
+ # require "google/cloud"
153
+ #
154
+ # gcloud = Google::Cloud.new
155
+ # bigquery = gcloud.bigquery
156
+ # job = bigquery.job "my_job"
157
+ #
158
+ # data = job.query_results
159
+ # words = data.all.map do |row|
160
+ # row["word"]
161
+ # end
162
+ #
163
+ # @example Limit the number of API calls made:
164
+ # require "google/cloud"
165
+ #
166
+ # gcloud = Google::Cloud.new
167
+ # bigquery = gcloud.bigquery
168
+ # job = bigquery.job "my_job"
169
+ #
170
+ # data = job.query_results
171
+ # data.all(request_limit: 10) do |row|
172
+ # puts row["word"]
173
+ # end
174
+ #
175
+ def all request_limit: nil
176
+ request_limit = request_limit.to_i if request_limit
177
+ unless block_given?
178
+ return enum_for(:all, request_limit: request_limit)
179
+ end
180
+ results = self
181
+ loop do
182
+ results.each { |r| yield r }
183
+ if request_limit
184
+ request_limit -= 1
185
+ break if request_limit < 0
186
+ end
187
+ break unless results.next?
188
+ results = results.next
189
+ end
190
+ end
191
+
192
+ ##
193
+ # The BigQuery {Job} that was created to run the query.
194
+ def job
195
+ return @job if @job
196
+ return nil unless job?
197
+ ensure_service!
198
+ gapi = service.get_job job_id
199
+ @job = Job.from_gapi gapi, service
200
+ rescue Google::Cloud::NotFoundError
201
+ nil
202
+ end
203
+
204
+ ##
205
+ # @private New Data from a response object.
206
+ def self.from_gapi gapi, service
207
+ if gapi.schema.nil?
208
+ formatted_rows = []
209
+ else
210
+ formatted_rows = format_rows gapi.rows,
211
+ gapi.schema.fields
212
+ end
213
+
214
+ data = new formatted_rows
215
+ data.gapi = gapi
216
+ data.service = service
217
+ data
218
+ end
219
+
220
+ protected
221
+
222
+ ##
223
+ # Raise an error unless an active connection is available.
224
+ def ensure_service!
225
+ fail "Must have active connection" unless service
226
+ end
227
+
228
+ def job?
229
+ @gapi.job_reference && @gapi.job_reference.job_id
230
+ end
231
+
232
+ def job_id
233
+ @gapi.job_reference.job_id
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,139 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/bigquery/service"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Bigquery
21
+ ##
22
+ # # QueryJob
23
+ #
24
+ # A {Job} subclass representing a query operation that may be performed
25
+ # on a {Table}. A QueryJob instance is created when you call
26
+ # {Project#query_job}, {Dataset#query_job}, or {View#data}.
27
+ #
28
+ # @see https://cloud.google.com/bigquery/querying-data Querying Data
29
+ # @see https://cloud.google.com/bigquery/docs/reference/v2/jobs Jobs API
30
+ # reference
31
+ #
32
+ class QueryJob < Job
33
+ ##
34
+ # Checks if the priority for the query is `BATCH`.
35
+ def batch?
36
+ val = @gapi.configuration.query.priority
37
+ val == "BATCH"
38
+ end
39
+
40
+ ##
41
+ # Checks if the priority for the query is `INTERACTIVE`.
42
+ def interactive?
43
+ val = @gapi.configuration.query.priority
44
+ return true if val.nil?
45
+ val == "INTERACTIVE"
46
+ end
47
+
48
+ ##
49
+ # Checks if the the query job allows arbitrarily large results at a
50
+ # slight cost to performance.
51
+ def large_results?
52
+ val = @gapi.configuration.query.allow_large_results
53
+ return false if val.nil?
54
+ val
55
+ end
56
+
57
+ ##
58
+ # Checks if the query job looks for an existing result in the query
59
+ # cache. For more information, see [Query
60
+ # Caching](https://cloud.google.com/bigquery/querying-data#querycaching).
61
+ def cache?
62
+ val = @gapi.configuration.query.use_query_cache
63
+ return false if val.nil?
64
+ val
65
+ end
66
+
67
+ ##
68
+ # Checks if the query job flattens nested and repeated fields in the
69
+ # query results. The default is `true`. If the value is `false`,
70
+ # #large_results? should return `true`.
71
+ def flatten?
72
+ val = @gapi.configuration.query.flatten_results
73
+ return true if val.nil?
74
+ val
75
+ end
76
+
77
+ ##
78
+ # Checks if the query results are from the query cache.
79
+ def cache_hit?
80
+ @gapi.statistics.query.cache_hit
81
+ end
82
+
83
+ ##
84
+ # The number of bytes processed by the query.
85
+ def bytes_processed
86
+ Integer @gapi.statistics.query.total_bytes_processed
87
+ rescue
88
+ nil
89
+ end
90
+
91
+ ##
92
+ # The table in which the query results are stored.
93
+ def destination
94
+ table = @gapi.configuration.query.destination_table
95
+ return nil unless table
96
+ retrieve_table table.project_id,
97
+ table.dataset_id,
98
+ table.table_id
99
+ end
100
+
101
+ ##
102
+ # Retrieves the query results for the job.
103
+ #
104
+ # @param [String] token Page token, returned by a previous call,
105
+ # identifying the result set.
106
+ # @param [Integer] max Maximum number of results to return.
107
+ # @param [Integer] start Zero-based index of the starting row to read.
108
+ # @param [Integer] timeout How long to wait for the query to complete,
109
+ # in milliseconds, before returning. Default is 10,000 milliseconds
110
+ # (10 seconds).
111
+ #
112
+ # @return [Google::Cloud::Bigquery::QueryData]
113
+ #
114
+ # @example
115
+ # require "google/cloud"
116
+ #
117
+ # gcloud = Google::Cloud.new
118
+ # bigquery = gcloud.bigquery
119
+ #
120
+ # q = "SELECT word FROM publicdata:samples.shakespeare"
121
+ # job = bigquery.query_job q
122
+ #
123
+ # job.wait_until_done!
124
+ # data = job.query_results
125
+ # data.each do |row|
126
+ # puts row["word"]
127
+ # end
128
+ # data = data.next if data.next?
129
+ #
130
+ def query_results token: nil, max: nil, start: nil, timeout: nil
131
+ ensure_service!
132
+ options = { token: token, max: max, start: start, timeout: timeout }
133
+ gapi = service.job_query_results job_id, options
134
+ QueryData.from_gapi gapi, service
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,361 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Bigquery
19
+ ##
20
+ # # Table Schema
21
+ #
22
+ # A builder for BigQuery table schemas, passed to block arguments to
23
+ # {Dataset#create_table} and {Table#schema}. Supports nested and
24
+ # repeated fields via a nested block.
25
+ #
26
+ # @see https://cloud.google.com/bigquery/preparing-data-for-bigquery
27
+ # Preparing Data for BigQuery
28
+ #
29
+ # @example
30
+ # require "google/cloud"
31
+ #
32
+ # gcloud = Google::Cloud.new
33
+ # bigquery = gcloud.bigquery
34
+ # dataset = bigquery.dataset "my_dataset"
35
+ # table = dataset.create_table "my_table"
36
+ #
37
+ # table.schema do |schema|
38
+ # schema.string "first_name", mode: :required
39
+ # schema.record "cities_lived", mode: :repeated do |cities_lived|
40
+ # cities_lived.string "place", mode: :required
41
+ # cities_lived.integer "number_of_years", mode: :required
42
+ # end
43
+ # end
44
+ #
45
+ class Schema
46
+ def initialize
47
+ @nested = nil
48
+ end
49
+
50
+ def fields
51
+ @fields ||= @gapi.fields.map { |f| Field.from_gapi f }
52
+ end
53
+
54
+ def fields= new_fields
55
+ @gapi.fields = Array(new_fields).map(&:to_gapi)
56
+ @fields = @gapi.fields.map { |f| Field.from_gapi f }
57
+ end
58
+
59
+ def empty?
60
+ fields.empty?
61
+ end
62
+
63
+ # @private
64
+ def changed?
65
+ return false if frozen?
66
+ check_for_mutated_schema!
67
+ @original_json != @gapi.to_json
68
+ end
69
+
70
+ # @private
71
+ def freeze
72
+ @gapi = @gapi.dup.freeze
73
+ @gapi.fields.freeze
74
+ @fields = @gapi.fields.map { |f| Field.from_gapi(f).freeze }
75
+ @fields.freeze
76
+ super
77
+ end
78
+
79
+ ##
80
+ # @private Make sure any changes are saved.
81
+ def check_for_mutated_schema!
82
+ return if frozen?
83
+ return if @gapi.frozen?
84
+ return if @fields.nil?
85
+ gapi_fields = Array(@fields).map(&:to_gapi)
86
+ @gapi.update! fields: gapi_fields
87
+ end
88
+
89
+ # @private
90
+ def self.from_gapi gapi
91
+ gapi ||= Google::Apis::BigqueryV2::TableSchema.new fields: []
92
+ gapi.fields ||= []
93
+ new.tap do |s|
94
+ s.instance_variable_set :@gapi, gapi
95
+ s.instance_variable_set :@original_json, gapi.to_json
96
+ end
97
+ end
98
+
99
+ # @private
100
+ def to_gapi
101
+ check_for_mutated_schema!
102
+ @gapi
103
+ end
104
+
105
+ # @private
106
+ def == other
107
+ return false unless other.is_a? Schema
108
+ to_gapi.to_h == other.to_gapi.to_h
109
+ end
110
+
111
+ ##
112
+ # Adds a string field to the schema.
113
+ #
114
+ # @param [String] name The field name. The name must contain only
115
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
116
+ # start with a letter or underscore. The maximum length is 128
117
+ # characters.
118
+ # @param [String] description A description of the field.
119
+ # @param [Symbol] mode The field's mode. The possible values are
120
+ # `:nullable`, `:required`, and `:repeated`. The default value is
121
+ # `:nullable`.
122
+ def string name, description: nil, mode: :nullable
123
+ add_field name, :string, nil, description: description, mode: mode
124
+ end
125
+
126
+ ##
127
+ # Adds an integer field to the schema.
128
+ #
129
+ # @param [String] name The field name. The name must contain only
130
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
131
+ # start with a letter or underscore. The maximum length is 128
132
+ # characters.
133
+ # @param [String] description A description of the field.
134
+ # @param [Symbol] mode The field's mode. The possible values are
135
+ # `:nullable`, `:required`, and `:repeated`. The default value is
136
+ # `:nullable`.
137
+ def integer name, description: nil, mode: :nullable
138
+ add_field name, :integer, nil, description: description, mode: mode
139
+ end
140
+
141
+ ##
142
+ # Adds a floating-point number field to the schema.
143
+ #
144
+ # @param [String] name The field name. The name must contain only
145
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
146
+ # start with a letter or underscore. The maximum length is 128
147
+ # characters.
148
+ # @param [String] description A description of the field.
149
+ # @param [Symbol] mode The field's mode. The possible values are
150
+ # `:nullable`, `:required`, and `:repeated`. The default value is
151
+ # `:nullable`.
152
+ def float name, description: nil, mode: :nullable
153
+ add_field name, :float, nil, description: description, mode: mode
154
+ end
155
+
156
+ ##
157
+ # Adds a boolean field to the schema.
158
+ #
159
+ # @param [String] name The field name. The name must contain only
160
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
161
+ # start with a letter or underscore. The maximum length is 128
162
+ # characters.
163
+ # @param [String] description A description of the field.
164
+ # @param [Symbol] mode The field's mode. The possible values are
165
+ # `:nullable`, `:required`, and `:repeated`. The default value is
166
+ # `:nullable`.
167
+ def boolean name, description: nil, mode: :nullable
168
+ add_field name, :boolean, nil, description: description, mode: mode
169
+ end
170
+
171
+ ##
172
+ # Adds a timestamp field to the schema.
173
+ #
174
+ # @param [String] name The field name. The name must contain only
175
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
176
+ # start with a letter or underscore. The maximum length is 128
177
+ # characters.
178
+ # @param [String] description A description of the field.
179
+ # @param [Symbol] mode The field's mode. The possible values are
180
+ # `:nullable`, `:required`, and `:repeated`. The default value is
181
+ # `:nullable`.
182
+ def timestamp name, description: nil, mode: :nullable
183
+ add_field name, :timestamp, nil, description: description, mode: mode
184
+ end
185
+
186
+ ##
187
+ # Adds a record field to the schema. A block must be passed describing
188
+ # the nested fields of the record. For more information about nested
189
+ # and repeated records, see [Preparing Data for BigQuery
190
+ # ](https://cloud.google.com/bigquery/preparing-data-for-bigquery).
191
+ #
192
+ # @param [String] name The field name. The name must contain only
193
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
194
+ # start with a letter or underscore. The maximum length is 128
195
+ # characters.
196
+ # @param [String] description A description of the field.
197
+ # @param [Symbol] mode The field's mode. The possible values are
198
+ # `:nullable`, `:required`, and `:repeated`. The default value is
199
+ # `:nullable`.
200
+ # @yield [nested_schema] a block for setting the nested schema
201
+ # @yieldparam [Schema] nested_schema the object accepting the
202
+ # nested schema
203
+ #
204
+ # @example
205
+ # require "google/cloud"
206
+ #
207
+ # gcloud = Google::Cloud.new
208
+ # bigquery = gcloud.bigquery
209
+ # dataset = bigquery.dataset "my_dataset"
210
+ # table = dataset.create_table "my_table"
211
+ #
212
+ # table.schema do |schema|
213
+ # schema.string "first_name", mode: :required
214
+ # schema.record "cities_lived", mode: :repeated do |cities_lived|
215
+ # cities_lived.string "place", mode: :required
216
+ # cities_lived.integer "number_of_years", mode: :required
217
+ # end
218
+ # end
219
+ #
220
+ def record name, description: nil, mode: nil
221
+ fail ArgumentError, "nested RECORD type is not permitted" if @nested
222
+ fail ArgumentError, "a block is required" unless block_given?
223
+ empty_schema = Google::Apis::BigqueryV2::TableSchema.new fields: []
224
+ nested_schema = self.class.from_gapi(empty_schema).tap do |s|
225
+ s.instance_variable_set :@nested, true
226
+ end
227
+ yield nested_schema
228
+ add_field name, :record, nested_schema.fields,
229
+ description: description, mode: mode
230
+ end
231
+
232
+ protected
233
+
234
+ def add_field name, type, nested_fields, description: nil,
235
+ mode: :nullable
236
+ # Remove any existing field of this name
237
+ fields.reject! { |f| f.name == name }
238
+ fields << Field.new(name, type, description: description,
239
+ mode: mode, fields: nested_fields)
240
+ end
241
+
242
+ class Field
243
+ # @private
244
+ MODES = %w( NULLABLE REQUIRED REPEATED )
245
+
246
+ # @private
247
+ TYPES = %w( STRING INTEGER FLOAT BOOLEAN TIMESTAMP RECORD )
248
+
249
+ def initialize name, type, description: nil,
250
+ mode: :nullable, fields: nil
251
+ @gapi = Google::Apis::BigqueryV2::TableFieldSchema.new
252
+ @gapi.update! name: name
253
+ @gapi.update! type: verify_type(type)
254
+ @gapi.update! description: description if description
255
+ @gapi.update! mode: verify_mode(mode) if mode
256
+ if fields
257
+ @fields = fields
258
+ check_for_changed_fields!
259
+ end
260
+ @original_json = @gapi.to_json
261
+ end
262
+
263
+ def name
264
+ @gapi.name
265
+ end
266
+
267
+ def name= new_name
268
+ @gapi.update! name: new_name
269
+ end
270
+
271
+ def type
272
+ @gapi.type
273
+ end
274
+
275
+ def type= new_type
276
+ @gapi.update! type: verify_type(new_type)
277
+ end
278
+
279
+ def description
280
+ @gapi.description
281
+ end
282
+
283
+ def description= new_description
284
+ @gapi.update! description: new_description
285
+ end
286
+
287
+ def mode
288
+ @gapi.mode
289
+ end
290
+
291
+ def mode= new_mode
292
+ @gapi.update! mode: verify_mode(new_mode)
293
+ end
294
+
295
+ def fields
296
+ @fields ||= Array(@gapi.fields).map { |f| Field.from_gapi f }
297
+ end
298
+
299
+ def fields= new_fields
300
+ @fields = new_fields
301
+ end
302
+
303
+ ##
304
+ # @private Make sure any fields are saved.
305
+ def check_for_changed_fields!
306
+ return if frozen?
307
+ fields.each(&:check_for_changed_fields!)
308
+ gapi_fields = Array(fields).map(&:to_gapi)
309
+ gapi_fields = nil if gapi_fields.empty?
310
+ @gapi.update! fields: gapi_fields
311
+ end
312
+
313
+ # @private
314
+ def changed?
315
+ @original_json == to_gapi.to_json
316
+ end
317
+
318
+ # @private
319
+ def self.from_gapi gapi
320
+ new("to-be-replaced", "STRING").tap do |f|
321
+ f.instance_variable_set :@gapi, gapi
322
+ f.instance_variable_set :@original_json, gapi.to_json
323
+ end
324
+ end
325
+
326
+ # @private
327
+ def to_gapi
328
+ # make sure any changes are saved.
329
+ check_for_changed_fields!
330
+ @gapi
331
+ end
332
+
333
+ # @private
334
+ def == other
335
+ return false unless other.is_a? Field
336
+ to_gapi.to_h == other.to_gapi.to_h
337
+ end
338
+
339
+ protected
340
+
341
+ def verify_type type
342
+ upcase_type = type.to_s.upcase
343
+ unless TYPES.include? upcase_type
344
+ fail ArgumentError,
345
+ "Type '#{upcase_type}' not found in #{TYPES.inspect}"
346
+ end
347
+ upcase_type
348
+ end
349
+
350
+ def verify_mode mode
351
+ upcase_mode = mode.to_s.upcase
352
+ unless MODES.include? upcase_mode
353
+ fail ArgumentError "Unable to determine mode for '#{mode}'"
354
+ end
355
+ upcase_mode
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end