Added MediaInMemoryUpload for uploads that are a byte stream, with tests.
diff --git a/apiclient/http.py b/apiclient/http.py
index 8e15e11..94eb266 100644
--- a/apiclient/http.py
+++ b/apiclient/http.py
@@ -26,6 +26,7 @@
]
import StringIO
+import base64
import copy
import gzip
import httplib2
@@ -216,6 +217,99 @@
d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
+class MediaInMemoryUpload(MediaUpload):
+ """MediaUpload for a chunk of bytes.
+
+ Construct a MediaFileUpload and pass as the media_body parameter of the
+ method. For example, if we had a service that allowed plain text:
+ """
+
+ def __init__(self, body, mimetype='application/octet-stream',
+ chunksize=256*1024, resumable=False):
+ """Create a new MediaBytesUpload.
+
+ Args:
+ body: string, Bytes of body content.
+ mimetype: string, Mime-type of the file or default of
+ 'application/octet-stream'.
+ chunksize: int, File will be uploaded in chunks of this many bytes. Only
+ used if resumable=True.
+ resumable: bool, True if this is a resumable upload. False means upload
+ in a single request.
+ """
+ self._body = body
+ self._mimetype = mimetype
+ self._resumable = resumable
+ self._chunksize = chunksize
+
+ def chunksize(self):
+ """Chunk size for resumable uploads.
+
+ Returns:
+ Chunk size in bytes.
+ """
+ return self._chunksize
+
+ def mimetype(self):
+ """Mime type of the body.
+
+ Returns:
+ Mime type.
+ """
+ return self._mimetype
+
+ def size(self):
+ """Size of upload.
+
+ Returns:
+ Size of the body.
+ """
+ return len(self.body)
+
+ def resumable(self):
+ """Whether this upload is resumable.
+
+ Returns:
+ True if resumable upload or False.
+ """
+ return self._resumable
+
+ def getbytes(self, begin, length):
+ """Get bytes from the media.
+
+ Args:
+ begin: int, offset from beginning of file.
+ length: int, number of bytes to read, starting at begin.
+
+ Returns:
+ A string of bytes read. May be shorter than length if EOF was reached
+ first.
+ """
+ return self._body[begin:begin + length]
+
+ def to_json(self):
+ """Create a JSON representation of a MediaInMemoryUpload.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ t = type(self)
+ d = copy.copy(self.__dict__)
+ del d['_body']
+ d['_class'] = t.__name__
+ d['_module'] = t.__module__
+ d['_b64body'] = base64.b64encode(self._body)
+ return simplejson.dumps(d)
+
+ @staticmethod
+ def from_json(s):
+ d = simplejson.loads(s)
+ return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
+ d['_mimetype'], d['_chunksize'],
+ d['_resumable'])
+
+
class HttpRequest(object):
"""Encapsulates a single HTTP request."""
diff --git a/tests/test_http.py b/tests/test_http.py
index 2744d9e..67e8aa6 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -32,6 +32,7 @@
from apiclient.http import HttpRequest
from apiclient.http import MediaFileUpload
from apiclient.http import MediaUpload
+from apiclient.http import MediaInMemoryUpload
from apiclient.http import set_user_agent
from apiclient.model import JsonModel
@@ -332,5 +333,24 @@
self.assertEqual(callbacks.responses['1'], {'foo': 42})
self.assertEqual(callbacks.responses['2'], {'baz': 'qux'})
+ def test_media_inmemory_upload(self):
+ media = MediaInMemoryUpload('abcdef', 'text/plain', chunksize=10,
+ resumable=True)
+ self.assertEqual('text/plain', media.mimetype())
+ self.assertEqual(10, media.chunksize())
+ self.assertTrue(media.resumable())
+ self.assertEqual('bc', media.getbytes(1, 2))
+
+ def test_media_inmemory_upload_json_roundtrip(self):
+ media = MediaInMemoryUpload(os.urandom(64), 'text/plain', chunksize=10,
+ resumable=True)
+ data = media.to_json()
+ newmedia = MediaInMemoryUpload.new_from_json(data)
+ self.assertEqual(media._body, newmedia._body)
+ self.assertEqual(media._chunksize, newmedia._chunksize)
+ self.assertEqual(media._resumable, newmedia._resumable)
+ self.assertEqual(media._mimetype, newmedia._mimetype)
+
+
if __name__ == '__main__':
unittest.main()