Enable streaming responses in download_image
Previously, the openstack.image.image_download method would place the contents of a remote image into a Python variable. With this change, the download_image method can optionally return the requests.Response object returned by session.get(), which permits the caller to download the image in chunks using the iter_content method. This can prevent performance issues when dealing with large images. Change-Id: Ie62ebcc895ca893321a10def18ac5d74c7c843b9
This commit is contained in:
committed by
Brian Curtin
parent
f4e4d1496f
commit
5b2df7e724
@@ -119,7 +119,8 @@ latex_documents = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
intersphinx_mapping = {'https://docs.python.org/3/': None,
|
||||||
|
'http://docs.python-requests.org/en/master/': None}
|
||||||
|
|
||||||
# Include both the class and __init__ docstrings when describing the class
|
# Include both the class and __init__ docstrings when describing the class
|
||||||
autoclass_content = "both"
|
autoclass_content = "both"
|
||||||
|
|||||||
@@ -32,6 +32,40 @@ Create an image by uploading its data and setting its attributes.
|
|||||||
|
|
||||||
Full example: `image resource create`_
|
Full example: `image resource create`_
|
||||||
|
|
||||||
|
.. _download_image-stream-true:
|
||||||
|
|
||||||
|
Downloading an Image with stream=True
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
As images are often very large pieces of data, storing their entire contents
|
||||||
|
in the memory of your application can be less than desirable. A more
|
||||||
|
efficient method may be to iterate over a stream of the response data.
|
||||||
|
|
||||||
|
By choosing to stream the response content, you determine the ``chunk_size``
|
||||||
|
that is appropriate for your needs, meaning only that many bytes of data are
|
||||||
|
read for each iteration of the loop until all data has been consumed.
|
||||||
|
See :meth:`requests.Response.iter_content` for more information, as well
|
||||||
|
as Requests' :ref:`body-content-workflow`.
|
||||||
|
|
||||||
|
When you choose to stream an image download, openstacksdk is no longer
|
||||||
|
able to compute the checksum of the response data for you. This example
|
||||||
|
shows how you might do that yourself, in a very similar manner to how
|
||||||
|
the library calculates checksums for non-streamed responses.
|
||||||
|
|
||||||
|
.. literalinclude:: ../examples/image/download.py
|
||||||
|
:pyobject: download_image_stream
|
||||||
|
|
||||||
|
Downloading an Image with stream=False
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
If you wish to download an image's contents all at once and to memory,
|
||||||
|
simply set ``stream=False``, which is the default.
|
||||||
|
|
||||||
|
.. literalinclude:: ../examples/image/download.py
|
||||||
|
:pyobject: download_image
|
||||||
|
|
||||||
|
Full example: `image resource download`_
|
||||||
|
|
||||||
Delete Image
|
Delete Image
|
||||||
------------
|
------------
|
||||||
|
|
||||||
@@ -45,3 +79,4 @@ Full example: `image resource delete`_
|
|||||||
.. _image resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/create.py
|
.. _image resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/create.py
|
||||||
.. _image resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/delete.py
|
.. _image resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/delete.py
|
||||||
.. _image resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/list.py
|
.. _image resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/list.py
|
||||||
|
.. _image resource download: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/download.py
|
||||||
|
|||||||
@@ -60,16 +60,40 @@ class Proxy(proxy2.BaseProxy):
|
|||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def download_image(self, image):
|
def download_image(self, image, stream=False):
|
||||||
"""Download an image
|
"""Download an image
|
||||||
|
|
||||||
|
This will download an image to memory when ``stream=False``, or allow
|
||||||
|
streaming downloads using an iterator when ``stream=True``.
|
||||||
|
For examples of working with streamed responses, see
|
||||||
|
:ref:`download_image-stream-true` and the Requests documentation
|
||||||
|
:ref:`body-content-workflow`.
|
||||||
|
|
||||||
:param image: The value can be either the ID of an image or a
|
:param image: The value can be either the ID of an image or a
|
||||||
:class:`~openstack.image.v2.image.Image` instance.
|
:class:`~openstack.image.v2.image.Image` instance.
|
||||||
|
|
||||||
:returns: The bytes comprising the given Image.
|
:param bool stream: When ``True``, return a :class:`requests.Response`
|
||||||
|
instance allowing you to iterate over the
|
||||||
|
response data stream instead of storing its entire
|
||||||
|
contents in memory. See
|
||||||
|
:meth:`requests.Response.iter_content` for more
|
||||||
|
details. *NOTE*: If you do not consume
|
||||||
|
the entirety of the response you must explicitly
|
||||||
|
call :meth:`requests.Response.close` or otherwise
|
||||||
|
risk inefficiencies with the ``requests``
|
||||||
|
library's handling of connections.
|
||||||
|
|
||||||
|
|
||||||
|
When ``False``, return the entire
|
||||||
|
contents of the response.
|
||||||
|
|
||||||
|
:returns: The bytes comprising the given Image when stream is
|
||||||
|
False, otherwise a :class:`requests.Response`
|
||||||
|
instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
image = self._get_resource(_image.Image, image)
|
image = self._get_resource(_image.Image, image)
|
||||||
return image.download(self._session)
|
return image.download(self._session, stream=stream)
|
||||||
|
|
||||||
def delete_image(self, image, ignore_missing=True):
|
def delete_image(self, image, ignore_missing=True):
|
||||||
"""Delete an image
|
"""Delete an image
|
||||||
|
|||||||
@@ -246,12 +246,12 @@ class Image(resource2.Resource):
|
|||||||
headers={"Content-Type": "application/octet-stream",
|
headers={"Content-Type": "application/octet-stream",
|
||||||
"Accept": ""})
|
"Accept": ""})
|
||||||
|
|
||||||
def download(self, session):
|
def download(self, session, stream=False):
|
||||||
"""Download the data contained in an image"""
|
"""Download the data contained in an image"""
|
||||||
# TODO(briancurtin): This method should probably offload the get
|
# TODO(briancurtin): This method should probably offload the get
|
||||||
# operation into another thread or something of that nature.
|
# operation into another thread or something of that nature.
|
||||||
url = utils.urljoin(self.base_path, self.id, 'file')
|
url = utils.urljoin(self.base_path, self.id, 'file')
|
||||||
resp = session.get(url, endpoint_filter=self.service)
|
resp = session.get(url, endpoint_filter=self.service, stream=stream)
|
||||||
|
|
||||||
# See the following bug report for details on why the checksum
|
# See the following bug report for details on why the checksum
|
||||||
# code may sometimes depend on a second GET call.
|
# code may sometimes depend on a second GET call.
|
||||||
@@ -265,6 +265,14 @@ class Image(resource2.Resource):
|
|||||||
details = self.get(session)
|
details = self.get(session)
|
||||||
checksum = details.checksum
|
checksum = details.checksum
|
||||||
|
|
||||||
|
# if we are returning the repsonse object, ensure that it
|
||||||
|
# has the content-md5 header so that the caller doesn't
|
||||||
|
# need to jump through the same hoops through which we
|
||||||
|
# just jumped.
|
||||||
|
if stream:
|
||||||
|
resp.headers['content-md5'] = checksum
|
||||||
|
return resp
|
||||||
|
|
||||||
if checksum is not None:
|
if checksum is not None:
|
||||||
digest = hashlib.md5(resp.content).hexdigest()
|
digest = hashlib.md5(resp.content).hexdigest()
|
||||||
if digest != checksum:
|
if digest != checksum:
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ class TestImage(testtools.TestCase):
|
|||||||
|
|
||||||
rv = sot.download(self.sess)
|
rv = sot.download(self.sess)
|
||||||
self.sess.get.assert_called_with('images/IDENTIFIER/file',
|
self.sess.get.assert_called_with('images/IDENTIFIER/file',
|
||||||
endpoint_filter=sot.service)
|
endpoint_filter=sot.service,
|
||||||
|
stream=False)
|
||||||
|
|
||||||
self.assertEqual(rv, resp.content)
|
self.assertEqual(rv, resp.content)
|
||||||
|
|
||||||
@@ -242,7 +243,8 @@ class TestImage(testtools.TestCase):
|
|||||||
|
|
||||||
rv = sot.download(self.sess)
|
rv = sot.download(self.sess)
|
||||||
self.sess.get.assert_has_calls(
|
self.sess.get.assert_has_calls(
|
||||||
[mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service),
|
[mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service,
|
||||||
|
stream=False),
|
||||||
mock.call('images/IDENTIFIER', endpoint_filter=sot.service)])
|
mock.call('images/IDENTIFIER', endpoint_filter=sot.service)])
|
||||||
|
|
||||||
self.assertEqual(rv, resp1.content)
|
self.assertEqual(rv, resp1.content)
|
||||||
@@ -270,7 +272,23 @@ class TestImage(testtools.TestCase):
|
|||||||
log.records[0].msg)
|
log.records[0].msg)
|
||||||
|
|
||||||
self.sess.get.assert_has_calls(
|
self.sess.get.assert_has_calls(
|
||||||
[mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service),
|
[mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service,
|
||||||
|
stream=False),
|
||||||
mock.call('images/IDENTIFIER', endpoint_filter=sot.service)])
|
mock.call('images/IDENTIFIER', endpoint_filter=sot.service)])
|
||||||
|
|
||||||
self.assertEqual(rv, resp1.content)
|
self.assertEqual(rv, resp1.content)
|
||||||
|
|
||||||
|
def test_download_stream(self):
|
||||||
|
sot = image.Image(**EXAMPLE)
|
||||||
|
|
||||||
|
resp = mock.Mock()
|
||||||
|
resp.content = b"abc"
|
||||||
|
resp.headers = {"Content-MD5": "900150983cd24fb0d6963f7d28e17f72"}
|
||||||
|
self.sess.get.return_value = resp
|
||||||
|
|
||||||
|
rv = sot.download(self.sess, stream=True)
|
||||||
|
self.sess.get.assert_called_with('images/IDENTIFIER/file',
|
||||||
|
endpoint_filter=sot.service,
|
||||||
|
stream=True)
|
||||||
|
|
||||||
|
self.assertEqual(rv, resp)
|
||||||
|
|||||||
Reference in New Issue
Block a user