rev-api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +20 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +42 -0
  7. data/LICENSE +191 -0
  8. data/README.md +124 -0
  9. data/Rakefile +14 -0
  10. data/examples/cli.rb +200 -0
  11. data/lib/rev-api.rb +26 -0
  12. data/lib/rev-api/api.rb +311 -0
  13. data/lib/rev-api/api_serializable.rb +30 -0
  14. data/lib/rev-api/exceptions.rb +108 -0
  15. data/lib/rev-api/http_client.rb +97 -0
  16. data/lib/rev-api/models/order.rb +113 -0
  17. data/lib/rev-api/models/order_request.rb +183 -0
  18. data/lib/rev-api/version.rb +3 -0
  19. data/rev-api.gemspec +33 -0
  20. data/spec/fixtures/api_cassettes/cancel_order.yml +38 -0
  21. data/spec/fixtures/api_cassettes/cancel_order_not_allowed.yml +40 -0
  22. data/spec/fixtures/api_cassettes/get_attachment_content.yml +399 -0
  23. data/spec/fixtures/api_cassettes/get_attachment_content_as_pdf.yml +399 -0
  24. data/spec/fixtures/api_cassettes/get_attachment_content_as_text.yml +65 -0
  25. data/spec/fixtures/api_cassettes/get_attachment_content_as_youtube_transcript.yml +66 -0
  26. data/spec/fixtures/api_cassettes/get_attachment_content_unacceptable_representation.yml +42 -0
  27. data/spec/fixtures/api_cassettes/get_attachment_content_with_invalid_id.yml +42 -0
  28. data/spec/fixtures/api_cassettes/get_attachment_metadata.yml +42 -0
  29. data/spec/fixtures/api_cassettes/get_attachment_with_invalid_id.yml +40 -0
  30. data/spec/fixtures/api_cassettes/get_orders.yml +122 -0
  31. data/spec/fixtures/api_cassettes/get_tc_order.yml +44 -0
  32. data/spec/fixtures/api_cassettes/get_third_page_of_orders.yml +58 -0
  33. data/spec/fixtures/api_cassettes/get_tr_order.yml +44 -0
  34. data/spec/fixtures/api_cassettes/link_input.yml +44 -0
  35. data/spec/fixtures/api_cassettes/link_input_with_all_attributes.yml +44 -0
  36. data/spec/fixtures/api_cassettes/not_found_order.yml +42 -0
  37. data/spec/fixtures/api_cassettes/submit_tc_order_with_account_balance.yml +45 -0
  38. data/spec/fixtures/api_cassettes/submit_tc_order_with_cc_and_all_attributes.yml +46 -0
  39. data/spec/fixtures/api_cassettes/submit_tc_order_with_invalid_request.yml +45 -0
  40. data/spec/fixtures/api_cassettes/submit_tc_order_with_saved_cc.yml +45 -0
  41. data/spec/fixtures/api_cassettes/submit_tr_order.yml +44 -0
  42. data/spec/fixtures/api_cassettes/unauthorized.yml +42 -0
  43. data/spec/fixtures/api_cassettes/upload_input.yml +130 -0
  44. data/spec/fixtures/api_cassettes/upload_input_with_invalid_content_type.yml +131 -0
  45. data/spec/fixtures/sourcedocument.png +0 -0
  46. data/spec/lib/rev/api_spec.rb +24 -0
  47. data/spec/lib/rev/cancel_order_spec.rb +25 -0
  48. data/spec/lib/rev/get_attachment_content_spec.rb +79 -0
  49. data/spec/lib/rev/get_attachment_metadata_spec.rb +33 -0
  50. data/spec/lib/rev/get_order_spec.rb +68 -0
  51. data/spec/lib/rev/get_orders_spec.rb +39 -0
  52. data/spec/lib/rev/http_client_spec.rb +32 -0
  53. data/spec/lib/rev/post_inputs_spec.rb +75 -0
  54. data/spec/lib/rev/post_order_spec.rb +207 -0
  55. data/spec/spec_helper.rb +31 -0
  56. metadata +248 -0
@@ -0,0 +1,26 @@
1
+ require 'httparty'
2
+
3
+ # These three are the only classes that should be accessed directly
4
+ require 'rev-api/api'
5
+
6
+ module Rev
7
+ class << self
8
+ # Alias for Rev::Api.new
9
+ #
10
+ # @return [Rev::Api]
11
+ def new(client_api_key, user_api_key, host)
12
+ Rev::Api.new(client_api_key, user_api_key, host)
13
+ end
14
+
15
+ # Delegate to Rev::Api
16
+ #
17
+ def method_missing(method, *args, &block)
18
+ return super unless new.respond_to?(method)
19
+ new.send(method, *args, &block)
20
+ end
21
+
22
+ def respond_to?(method, include_private = false)
23
+ new.respond_to?(method, include_private) || super(method, include_private)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,311 @@
1
+ require 'rev-api/version'
2
+ require 'rev-api/http_client'
3
+ require 'rev-api/exceptions'
4
+ require 'json'
5
+
6
+ # automatically include business logic objects
7
+ Dir[File.dirname(__FILE__) + '/models/*.rb'].each do |file|
8
+ require file
9
+ end
10
+
11
+ # Rev API Ruby SDK
12
+ module Rev
13
+ # Main point of interaction with API.
14
+ # Wraps common REST operations, returning plain objects.
15
+ # Internally utilizes JSON resource representation.
16
+ class Api
17
+
18
+ # Production host. Used by default for new Rev::Api client
19
+ PRODUCTION_HOST = 'www.rev.com'
20
+
21
+ # Sandbox domain - pass 'Rev::Api::SANDBOX_HOST' as third param
22
+ # into Rev::Api ctor
23
+ SANDBOX_HOST = 'api-sandbox.rev.com'
24
+
25
+ # @note http://www.rev.com/api/security
26
+ # @param client_api_key [String] secret key specific to each partner that wishes to use the Rev API
27
+ # @param user_api_key [String] secret key specific to a Rev user, which identifies the user account under whose privileges the requested operation executes
28
+ # @param host [String] use {Rev::Api::PRODUCTION_HOST} or {Rev::Api::SANDBOX_HOST}. Production is default value
29
+ # @return [HttpClient] client obj
30
+ def initialize(client_api_key, user_api_key, host = PRODUCTION_HOST)
31
+ @client = HttpClient.new(client_api_key, user_api_key, host)
32
+ end
33
+
34
+ # Loads single page of existing orders for current client
35
+ #
36
+ # @note http://www.rev.com/api/ordersget
37
+ # @param page [Int, nil] 0-based page number, defaults to 0
38
+ # @return [OrdersListPage] paged result cointaining 'orders'
39
+ def get_orders_page(page = 0)
40
+ response = @client.get("/orders?page=#{page.to_i}")
41
+ Api.verify_get_response(response)
42
+ OrdersListPage.new(Api.parse(response))
43
+ end
44
+
45
+ # Loads all orders for current client. Works by calling get_orders_page multiple times.
46
+ # Use with caution if your order list might be large.
47
+ #
48
+ # @note http://www.rev.com/api/ordersget
49
+ # @return [Array of Order] list of orders
50
+ def get_all_orders
51
+ orders = []
52
+ page = 0
53
+ loop do
54
+ orders_page = self.get_orders_page page
55
+ page += 1
56
+ orders.push *orders_page.orders
57
+ break if (page * orders_page.results_per_page >= orders_page.total_count)
58
+ end
59
+ orders
60
+ end
61
+
62
+ # Returns Order given an order number.
63
+ #
64
+ # @note http://www.rev.com/api/ordersgetone
65
+ # @param number [String] order number, like 'TCXXXXXXXX'
66
+ # @return [Order] order obj
67
+ def get_order(number)
68
+ response = @client.get("/orders/#{number}")
69
+ Api.verify_get_response(response)
70
+ Order.new(Api.parse(response))
71
+ end
72
+
73
+ # Cancel an order by number. If cancellation is not allowed, Rev::Api::BadRequestError is raised.
74
+ #
75
+ # @note http://www.rev.com/api/orderscancel
76
+ # @param number [String] order number
77
+ # @return [Boolean] true on success, raised Exception from Rev::Api namespace otherwise
78
+ def cancel_order(number)
79
+ data = { :order_num => number }
80
+ response = @client.post("/orders/#{number}/cancel", data)
81
+ Api.verify_post_response(response)
82
+ end
83
+
84
+ # Get metadata about an order attachment.
85
+ # Use this method to retrieve information about an order attachment (either transcript,
86
+ # translation, or source file).
87
+ #
88
+ # @note http://www.rev.com/api/attachmentsget
89
+ # @param id [String] attachment id, as returned in info about an order
90
+ # @return [Attachment] attachment object
91
+ def get_attachment_metadata(id)
92
+ response = @client.get("/attachments/#{id}")
93
+ Api.verify_get_response(response)
94
+ Attachment.new(Api.parse(response))
95
+ end
96
+
97
+ # Get the raw data for the attachment with given id.
98
+ # Download the contents of an attachment. Use this method to download either a finished transcript,
99
+ # finished translation or a source file for an order.
100
+ # For transcript and translation attachments, you may request to get the contents in a specific
101
+ # representation, specified via a mime-type.
102
+ #
103
+ # See {Rev::Order::Attachment::REPRESENTATIONS} hash, which contains symbols for currently supported mime types.
104
+ # The authoritative list is in the API documentation at http://www.rev.com/api/attachmentsgetcontent
105
+ #
106
+ # If a block is given, the response is passed to the block directly, to allow progressive reading of the data.
107
+ # In this case, the block must itself check for error responses, using Api.verify_get_response.
108
+ # If no block is given, the full response is returned. In that case, if the response is an error, an appropriate
109
+ # error is raised.
110
+ #
111
+ # @param id [String] attachment id
112
+ # @param mime_type [String, nil] mime-type for the desired format in which the content should be retrieved.
113
+ # @yieldparam resp [Net::HTTP::Response] the response, ready to be read
114
+ # @return [Net::HTTP::Response] the response containing raw data
115
+ def get_attachment_content(id, mime_type = nil, &block)
116
+ headers = {}
117
+
118
+ unless mime_type.nil?
119
+ headers['Accept'] = mime_type
120
+ headers['Accept-Charset'] = 'utf-8' if mime_type.start_with? 'text/'
121
+ end
122
+
123
+ if block_given?
124
+ @client.get_binary("/attachments/#{id}/content", headers, &block)
125
+ else
126
+ response = @client.get_binary("/attachments/#{id}/content", headers)
127
+ Api.verify_get_response(response)
128
+ response
129
+ end
130
+ end
131
+
132
+ # Get the raw data for the attachment with given id.
133
+ # Download the contents of an attachment and save it into a file. Use this method to download either a finished transcript,
134
+ # finished translation or a source file for an order.
135
+ # For transcript and translation attachments, you may request to get the contents in a specific
136
+ # representation, specified via a mime-type.
137
+ #
138
+ # See {Rev::Order::Attachment::REPRESENTATIONS} hash, which contains symbols for currently supported mime types.
139
+ # The authoritative list is in the API documentation at http://www.rev.com/api/attachmentsgetcontent
140
+ #
141
+ # @param id [String] attachment id
142
+ # @param path [String, nil] path to file into which the content is to be saved.
143
+ # @param mime_type [String, nil] mime-type for the desired format in which the content should be retrieved.
144
+ # @return [String] filepath content has been saved to. Might raise standard IO exception if file creation files
145
+ def save_attachment_content(id, path, mime_type = nil)
146
+ headers = {}
147
+
148
+ unless mime_type.nil?
149
+ headers['Accept'] = mime_type
150
+ headers['Accept-Charset'] = 'utf-8' if mime_type.start_with? 'text/'
151
+ end
152
+
153
+ # same simple approach as box-api does for now: return response.body as-is if path for saving is nil
154
+ File.open(path, 'wb') do |file|
155
+ response = @client.get_binary("/attachments/#{id}/content", headers) do |resp|
156
+ resp.read_body do |segment|
157
+ file.write(segment)
158
+ end
159
+ end
160
+ Api.verify_get_response(response)
161
+ end
162
+
163
+ # we don't handle IO-related exceptions
164
+ path
165
+ end
166
+
167
+ # Get the content of the attachment with given id as a string. Use this method to grab the contents of a finished transcript
168
+ # or translation as a string. This method should generally not be used for source attachments, as those are typically
169
+ # binary files like MP3s, which cannot be converted to a string.
170
+ #
171
+ # May raise Rev::Api::NotAcceptableError if the attachment cannot be converted into a text representation.
172
+ #
173
+ # @param id [String] attachment id
174
+ # @return [String] the content of the attachment as a string
175
+ def get_attachment_content_as_string(id)
176
+ response = self.get_attachment_content(id, Attachment::REPRESENTATIONS[:txt])
177
+ response.body
178
+ end
179
+
180
+ # Submit a new order using {Rev::OrderRequest}.
181
+ # @note http://www.rev.com/api/ordersposttranscription - for full information
182
+ #
183
+ # @param order_request [OrderRequest] object specifying payment, inputs, options and notification info.
184
+ # inputs must previously be uploaded using upload_input or create_input_from_link
185
+ # @return [String] order number for the new order
186
+ # Raises {Rev::BadRequestError} on failure (.code attr exposes API error code -
187
+ # see {Rev::OrderRequestError}).
188
+ def submit_order(order_request)
189
+ response = @client.post("/orders", order_request.to_json, { 'Content-Type' => 'application/json' })
190
+ Api.verify_post_response(response)
191
+
192
+ new_order_uri = response.headers['Location']
193
+ return new_order_uri.split('/')[-1]
194
+ end
195
+
196
+ # Upload given local file directly as source input for order.
197
+ # @note http://www.rev.com/api/inputspost
198
+ #
199
+ # @param path [String] mandatory, path to local file (relative or absolute) to upload
200
+ # @param content_type [String] mandatory, content-type of the file you're uploading
201
+ # @return [String] URI identifying newly uploaded media. This URI can be used to identify the input
202
+ # when constructing a OrderRequest object to submit an order.
203
+ # {Rev::BadRequestError} is raised on failure (.code attr exposes API error code -
204
+ # see {Rev::InputRequestError}).
205
+ def upload_input(path, content_type)
206
+ filename = Pathname.new(path).basename
207
+ headers = {
208
+ 'Content-Disposition' => "attachment; filename=#{filename}",
209
+ 'Content-Type' => content_type
210
+ }
211
+
212
+ File.open(path) do |data|
213
+ response = @client.post_binary("/inputs", data, headers)
214
+ Api.verify_post_response(response)
215
+
216
+ headers = HTTParty::Response::Headers.new(response.to_hash)
217
+ return headers['Location']
218
+ end
219
+ end
220
+
221
+ # Request creation of a source input based on an external URL which the server will attempt to download.
222
+ # @note http://www.rev.com/api/inputspost
223
+ #
224
+ # @param url [String] mandatory, URL where the media can be retrieved. Must be publicly accessible.
225
+ # HTTPS urls are ok as long as the site in question has a valid certificate
226
+ # @param filename [String, nil] optional, the filename for the media. If not specified, we will
227
+ # determine it from the URL
228
+ # @param content_type [String, nil] optional, the content type of the media to be retrieved.
229
+ # If not specified, we will try to determine it from the server response
230
+ # @return [String] URI identifying newly uploaded media. This URI can be used to identify the input
231
+ # when constructing a OrderRequest object to submit an order.
232
+ # {Rev::BadRequestError} is raised on failure (.code attr exposes API error code -
233
+ # see {Rev::InputRequestError}).
234
+ def create_input_from_link(url, filename = nil, content_type = nil)
235
+ request = { :url => url }
236
+ request[:filename] = filename unless filename.nil?
237
+ request[:content_type] = content_type unless content_type.nil?
238
+
239
+ response = @client.post("/inputs", request.to_json, { 'Content-Type' => 'application/json' })
240
+ Api.verify_post_response(response)
241
+
242
+ response.headers['Location']
243
+ end
244
+
245
+ private
246
+ # Below are utility helper methods for handling response
247
+ class << self
248
+
249
+ # Parse given response's body JSON into Hash, so that it might be
250
+ # easily mapped onto business logic object.
251
+ #
252
+ # @param response [Response] HTTParty response obj
253
+ # @return [Hash] hash of values parsed from JSON
254
+ def parse(response)
255
+ JSON.load response.body.to_s
256
+ end
257
+
258
+ # Raises exception if response is not considered as success
259
+ #
260
+ # @param response [HTTPParty::Response] HTTParty response obj. Net::HTTPResponse represented by .response
261
+ # @return [Boolean] true if response is considered as successful
262
+ def verify_get_response(response)
263
+ # HTTP response codes are handled here and propagated up to the caller, since caller should be able
264
+ # to handle all types of errors the same - using exceptions
265
+ unless response.response.instance_of? Net::HTTPOK
266
+ Api.handle_error(response)
267
+ end
268
+
269
+ true
270
+ end
271
+
272
+ # (see #verify_get_response)
273
+ def verify_post_response(response)
274
+ # see http://www.rev.com/api/errorhandling
275
+ unless response.response.instance_of?(Net::HTTPCreated) || response.response.instance_of?(Net::HTTPNoContent)
276
+ Api.handle_error(response)
277
+ end
278
+
279
+ true
280
+ end
281
+
282
+ # Given a response, raises a corresponding Exception.
283
+ # Full response is given for the sake of BadRequest reporting,
284
+ # which usually contains validation errors.
285
+ #
286
+ # @param response [Response] containing failing status to look for
287
+ def handle_error(response)
288
+ case response.response
289
+ when Net::HTTPBadRequest
290
+ # Bad request - response contains error code and details. Usually means failed validation
291
+ body = JSON.load response.body.to_s
292
+ msg = "API responded with code #{body['code']}: #{body['message']}"
293
+ msg += " Details: #{body['detail'].to_s}" if body['detail']
294
+ raise BadRequestError.new msg, body['code']
295
+ when Net::HTTPUnauthorized
296
+ raise NotAuthorizedError
297
+ when Net::HTTPForbidden
298
+ raise ForbiddenError
299
+ when Net::HTTPNotFound
300
+ raise NotFoundError
301
+ when Net::HTTPNotAcceptable
302
+ raise NotAcceptableError
303
+ when Net::HTTPServerError
304
+ raise ServerError, "Status code: #{response.code}"
305
+ else
306
+ raise UnknownError
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,30 @@
1
+ module Rev
2
+ # Utility class with instance methods for hash/JSON conversion
3
+ class ApiSerializable
4
+
5
+ # Map given hash to instance properties
6
+ #
7
+ # @param fields [Hash] of fields to initialize instance. See instance attributes for available fields.
8
+ def initialize(fields = {})
9
+ fields.each { |k,v| self.instance_variable_set("@#{k.to_sym}", v) if self.methods.include? k.to_sym }
10
+ end
11
+
12
+ # Recursively convert object to hash
13
+ # @note http://stackoverflow.com/questions/1684588/how-to-do-ruby-object-serialization-using-json
14
+ #
15
+ # @return [Hash] hash map of the object including all nested children
16
+ def to_hash
17
+ h = {}
18
+ instance_variables.each do |e|
19
+ o = instance_variable_get e.to_sym
20
+ h[e[1..-1]] = (o.respond_to? :to_hash) ? o.to_hash : o;
21
+ end
22
+ h
23
+ end
24
+
25
+ # Recursively convert object to JSON (internally utilizing hash)
26
+ def to_json *args
27
+ to_hash.to_json *args
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,108 @@
1
+ module Rev
2
+ class ApiError < StandardError; end
3
+
4
+ # 400 BadRequest. Response body contains API error code and optional details
5
+ class BadRequestError < ApiError
6
+
7
+ # Code of the validation error
8
+ attr_reader :code
9
+
10
+ # @param message [String] custom message, usually includes API validation error code and it's meaning
11
+ # @param code [Integer] API validation code is passed separately to be evaluated in consumer's app
12
+ def initialize(message, code)
13
+ super message
14
+ @code = code
15
+ end
16
+ end
17
+
18
+ # 401 Unauthorized
19
+ class NotAuthorizedError < ApiError; end
20
+
21
+ # 403 Forbidden (not allowed)
22
+ class ForbiddenError < ApiError; end
23
+
24
+ # 404 Not Found
25
+ class NotFoundError < ApiError; end
26
+
27
+ # 406 NotAcceptable (used when requested representation is not supported by attachment)
28
+ class NotAcceptableError < ApiError; end
29
+
30
+ # 500 ServerError (internal error on API server)
31
+ class ServerError < ApiError; end
32
+
33
+ # have no idea what's going on - used in 'pokemon' rescue
34
+ class UnknownError < ApiError; end
35
+
36
+ # Constants for validation error codes in OrderRequest response
37
+ module OrderRequestErrorCodes
38
+ # 10001 Missing Inputs - if the order request did not contain any input media
39
+ MISSING_INPUTS = 10001
40
+
41
+ # 10002 Invalid Input - if one of the input media URIs is invalid, eg does not identify a valid media uploaded via a POST to /inputs
42
+ INVALID_INPUTS = 10002
43
+
44
+ # 10003 Transcription and Translation Specified - only one of the translation option and transcription option sections can be included
45
+ TC_AND_TR_OPTIONS_SPECIFIED = 10003
46
+
47
+ # 10001 Missing Inputs - if the order request did not contain any input media
48
+ TC_OR_TR_OPTIONS_NOT_SPECIFIED = 10004
49
+
50
+ # 10005 External Link and URI specified - only External Link or URI should be set for input media
51
+ EXTERNAL_LINK_AND_URI_SPECIFIED = 10005
52
+
53
+ # 10006 Input Location is not specified - neither of External Link and URI set for input media
54
+ EXTERNAL_LINK_OR_URI_NOT_SPECIFIED = 10006
55
+
56
+ # 20001 Invalid Audio Length - If one of the input medias has a specified length that is not a positive integer
57
+ INVALID_AUDIO_LENGTH = 20001
58
+
59
+ # 20002 Invalid Word Count - word counts for translation are missing or inaccurate
60
+ INVALID_WORD_COUNT = 20002
61
+
62
+ # 20003 Invalid Language Code - the language codes provided for translation are invalid
63
+ INVALID_LANGUAGE_CODE = 20003
64
+
65
+ # 20010 Reference Number Too Long Code - the reference number provided longer than 40 characters
66
+ REFERENCE_NUMBER_TOO_LONG = 20010
67
+
68
+ # 30001 Missing Payment Info - if the order request did not contain a payment information element
69
+ MISSING_PAYMENT_INFO = 30001
70
+
71
+ # 30002 Missing Payment Type - if the order request did not contain a payment kind element
72
+ MISSING_PAYMENT_TYPE = 30002
73
+
74
+ # 30010 Ineligible For Balance Payments - if the user on whose behalf the order request was made is not eligible for paying using account balance
75
+ INELIGIBLE_FOR_BALANCE_PAYMENT = 30010
76
+
77
+ # 30011 Account Balance Limit Exceeded - if the order request specified payment using account balance, but doing so would exceed the user's balance limit
78
+ ACCOUNT_BALANCE_LIMIT_EXCEEDED = 30011
79
+
80
+ # 30020 Missing Credit Card Info - if the order request specified payment using credit card, but did not provide the credit card info element
81
+ MISSING_CREDIT_CARD_INFO = 30020
82
+
83
+ # 30021 Invalid Saved Credit Card - if the order request specified payment using a saved credit card, but the specified credit card id was invalid
84
+ INVALID_SAVED_CREDIT_CARD = 30021
85
+
86
+ # 30023 Invalid Credit Card Details - if the order request specified payment using credit card, but some required credit card data elements were missing or had invalid values
87
+ INVALID_CREDIT_CARD_DETAILS = 30023
88
+
89
+ # 30024 Credit Card Authorization Failed - if the order request specified payment using credit card, but we could not charge the card
90
+ CREDIT_CARD_AUTHORIZATION_FAILED = 30024
91
+ end
92
+
93
+ module InputRequestErrorCodes
94
+ # 10001 Unsupported Content Type – if the content type of the media is not currently supported by our system.
95
+ # Supported media types for inputs are listed in http://www.rev.com/api/inputspost
96
+ UNSUPPORTED_CONTENT_TYPE = 10001
97
+
98
+ # 10002 Could not retrieve file – if we could not retrieve the file from the specified location.
99
+ COULD_NOT_RETRIEVE_MEDIA = 10002
100
+
101
+ # 10003 Invalid multipart request – If the multipart request did not contain exactly one file part, or was otherwise malformed.
102
+ INVALID_MULTIPART_REQUEST = 10003
103
+
104
+ # 10004 Unspecified filename - If the filename for the media was not specified explicitly and could not be determined automatically.
105
+ UNSPECIFIED_FILENAME = 10004
106
+ end
107
+
108
+ end