rev-api 1.0.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 (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