Add preliminary support for uploading media.

Reviewed in http://codereview.appspot.com/4515144/
diff --git a/apiclient/discovery.py b/apiclient/discovery.py
index 825604a..08ddc26 100644
--- a/apiclient/discovery.py
+++ b/apiclient/discovery.py
@@ -29,17 +29,21 @@
 import uritemplate
 import urllib
 import urlparse
+import mimetypes
+
 try:
     from urlparse import parse_qsl
 except ImportError:
     from cgi import parse_qsl
 
-from http import HttpRequest
 from anyjson import simplejson
-from model import JsonModel
-from errors import UnknownLinkType
+from email.mime.multipart import MIMEMultipart
+from email.mime.nonmultipart import MIMENonMultipart
 from errors import HttpError
 from errors import InvalidJsonError
+from errors import UnknownLinkType
+from http import HttpRequest
+from model import JsonModel
 
 URITEMPLATE = re.compile('{[^}]*}')
 VARNAME = re.compile('[a-zA-Z0-9_-]+')
@@ -52,6 +56,11 @@
   'userip', 'strict']
 
 
+def _write_headers(self):
+  # Utility no-op method for multipart media handling
+  pass
+
+
 def key2param(key):
   """Converts key names into parameter names.
 
@@ -250,6 +259,11 @@
           'type': 'object',
           'required': True,
           }
+      methodDesc['parameters']['media_body'] = {
+          'description': 'The filename of the media request body.',
+          'type': 'string',
+          'required': False,
+          }
 
     argmap = {} # Map from method parameter name to query parameter name
     required_params = [] # Required parameters
@@ -310,6 +324,7 @@
                 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
                 (name, kwargs[name], str(enums)))
 
+      media_filename = kwargs.pop('media_body', None)
       actual_query_params = {}
       actual_path_params = {}
       for key, value in kwargs.iteritems():
@@ -332,16 +347,49 @@
       headers, params, query, body = self._model.request(headers,
           actual_path_params, actual_query_params, body_value)
 
-      # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
-      # document.  Base URLs should not contain any path elements. If they do
-      # then urlparse.urljoin will strip them out This results in an incorrect
-      # URL which returns a 404
-      url_result = urlparse.urlsplit(self._baseUrl)
-      new_base_url = url_result[0] + '://' + url_result[1]
-
       expanded_url = uritemplate.expand(pathUrl, params)
-      url = urlparse.urljoin(self._baseUrl,
-                             url_result[2] + expanded_url + query)
+      url = urlparse.urljoin(self._baseUrl, expanded_url + query)
+
+      if media_filename:
+        (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
+        if media_mime_type is None:
+          raise UnknownFileType(media_filename)
+
+        # modify the path to prepend '/upload'
+        parsed = list(urlparse.urlparse(url))
+        parsed[2] = '/upload' + parsed[2]
+        url = urlparse.urlunparse(parsed)
+
+        if body is None:
+          headers['content-type'] = media_mime_type
+          # make the body the contents of the file
+          f = file(media_filename, 'rb')
+          body = f.read()
+          f.close()
+        else:
+          msgRoot = MIMEMultipart('related')
+          # msgRoot should not write out it's own headers
+          setattr(msgRoot, '_write_headers', lambda self: None)
+
+          # attach the body as one part
+          msg = MIMENonMultipart(*headers['content-type'].split('/'))
+          msg.set_payload(body)
+          msgRoot.attach(msg)
+
+          # attach the media as the second part
+          msg = MIMENonMultipart(*media_mime_type.split('/'))
+          msg['Content-Transfer-Encoding'] = 'binary'
+
+          f = file(media_filename, 'rb')
+          msg.set_payload(f.read())
+          f.close()
+          msgRoot.attach(msg)
+
+          body = msgRoot.as_string()
+
+          # must appear after the call to as_string() to get the right boundary
+          headers['content-type'] = ('multipart/related; '
+                                     'boundary="%s"') % msgRoot.get_boundary()
 
       logging.info('URL being requested: %s' % url)
       return self._requestBuilder(self._http,
@@ -358,6 +406,8 @@
     for arg in argmap.iterkeys():
       if arg in STACK_QUERY_PARAMETERS:
         continue
+      if arg == 'media_body':
+        continue
       repeated = ''
       if arg in repeated_params:
         repeated = ' (repeated)'
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index 208a3f5..964092d 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -98,10 +98,12 @@
                                          debug=True)
     self.app = TestApp(application)
     users.get_current_user = UserMock
+    self.httplib2_orig = httplib2.Http
     httplib2.Http = Http2Mock
 
   def tearDown(self):
     self.testbed.deactivate()
+    httplib2.Http = self.httplib2_orig
 
   def test_required(self):
     # An initial request to an oauth_required decorated path should be a