Add media_mime_type keyword argument

Sometimes the Python mimetypes module cannot automatically detect the
MIME type of a media upload file, and the user would want to explicitly
specify it. An example is audio/x-raw.

This commit adds a media_mime_type keyword argument to media upload
methods. If the caller does not specify this argument, a warning is
logged to teach the user about it in case they need to explicitly
specify a MIME type.
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index c451b47..4e7b736 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -108,6 +108,12 @@
     'type': 'string',
     'required': False,
 }
+MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
+    'description': ('The MIME type of the media request body, or an instance '
+                    'of a MediaUpload object.'),
+    'type': 'string',
+    'required': False,
+}
 
 # Parameters accepted by the stack, but not visible via discovery.
 # TODO(dhermes): Remove 'userip' in 'v2'.
@@ -481,7 +487,7 @@
 
 
 def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
-  """Updates parameters of API by adding 'media_body' if supported by method.
+  """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
 
   SIDE EFFECTS: If the method supports media upload and has a required body,
   sets body to be optional (required=False) instead. Also, if there is a
@@ -518,6 +524,7 @@
   if media_upload:
     media_path_url = _media_path_url_from_info(root_desc, path_url)
     parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
+    parameters['media_mime_type'] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
     if 'body' in parameters:
       parameters['body']['required'] = False
 
@@ -751,6 +758,7 @@
         actual_path_params[parameters.argmap[key]] = cast_value
     body_value = kwargs.get('body', None)
     media_filename = kwargs.get('media_body', None)
+    media_mime_type = kwargs.get('media_mime_type', None)
 
     if self._developerKey:
       actual_query_params['key'] = self._developerKey
@@ -774,7 +782,11 @@
     if media_filename:
       # Ensure we end up with a valid MediaUpload object.
       if isinstance(media_filename, six.string_types):
-        (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
+        if media_mime_type is None:
+          logger.warning(
+              'media_mime_type argument not specified: trying to auto-detect for %s',
+              media_filename)
+          media_mime_type, _ = mimetypes.guess_type(media_filename)
         if media_mime_type is None:
           raise UnknownFileType(media_filename)
         if not mimeparse.best_match([media_mime_type], ','.join(accept)):
diff --git a/tests/data/small-png b/tests/data/small-png
new file mode 100644
index 0000000..5446bc2
--- /dev/null
+++ b/tests/data/small-png
Binary files differ
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index c43d9e4..210717d 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -50,6 +50,7 @@
 from googleapiclient.discovery import DISCOVERY_URI
 from googleapiclient.discovery import key2param
 from googleapiclient.discovery import MEDIA_BODY_PARAMETER_DEFAULT_VALUE
+from googleapiclient.discovery import MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE
 from googleapiclient.discovery import ResourceMethodParameters
 from googleapiclient.discovery import STACK_QUERY_PARAMETERS
 from googleapiclient.discovery import STACK_QUERY_PARAMETER_DEFAULT_VALUE
@@ -61,6 +62,7 @@
 from googleapiclient.errors import ResumableUploadError
 from googleapiclient.errors import UnacceptableMimeTypeError
 from googleapiclient.errors import UnknownApiNameOrVersion
+from googleapiclient.errors import UnknownFileType
 from googleapiclient.http import BatchHttpRequest
 from googleapiclient.http import HttpMock
 from googleapiclient.http import HttpMockSequence
@@ -212,14 +214,16 @@
 
   def test_fix_up_media_upload_no_initial_valid_minimal(self):
     valid_method_desc = {'mediaUpload': {'accept': []}}
-    final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE}
+    final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
+                        'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE}
     self._base_fix_up_method_description_test(
         valid_method_desc, {}, final_parameters, [], 0,
         'http://root/upload/fake/fake-path/')
 
   def test_fix_up_media_upload_no_initial_valid_full(self):
     valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}}
-    final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE}
+    final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
+                        'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE}
     ten_gb = 10 * 2**30
     self._base_fix_up_method_description_test(
         valid_method_desc, {}, final_parameters, ['*/*'],
@@ -236,7 +240,8 @@
     valid_method_desc = {'mediaUpload': {'accept': []}}
     initial_parameters = {'body': {}}
     final_parameters = {'body': {'required': False},
-                        'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE}
+                        'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
+                        'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE}
     self._base_fix_up_method_description_test(
         valid_method_desc, initial_parameters, final_parameters, [], 0,
         'http://root/upload/fake/fake-path/')
@@ -245,7 +250,8 @@
     valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}}
     initial_parameters = {'body': {}}
     final_parameters = {'body': {'required': False},
-                        'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE}
+                        'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
+                        'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE}
     ten_gb = 10 * 2**30
     self._base_fix_up_method_description_test(
         valid_method_desc, initial_parameters, final_parameters, ['*/*'],
@@ -775,6 +781,24 @@
         'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json',
         request.uri)
 
+  def test_simple_media_unknown_mimetype(self):
+    self.http = HttpMock(datafile('zoo.json'), {'status': '200'})
+    zoo = build('zoo', 'v1', http=self.http)
+
+    try:
+      zoo.animals().insert(media_body=datafile('small-png'))
+      self.fail("should throw exception if mimetype is unknown.")
+    except UnknownFileType:
+      pass
+
+    request = zoo.animals().insert(media_body=datafile('small-png'),
+                                   media_mime_type='image/png')
+    self.assertEquals('image/png', request.headers['content-type'])
+    self.assertEquals(b'PNG', request.body[1:4])
+    assertUrisEqual(self,
+        'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json',
+        request.uri)
+
   def test_multipart_media_raise_correct_exceptions(self):
     self.http = HttpMock(datafile('zoo.json'), {'status': '200'})
     zoo = build('zoo', 'v1', http=self.http)