Repository: docker/docker-py

Issue:
Low-level API container export
Hi together,

I've got an issue to export a container with the low level API:

```
docker version
Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:42:18 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.09.0-ce
 API version:  1.32 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:56 2017
 OS/Arch:      linux/amd64
 Experimental: false
```

The documentation says:
```
Parameters: | container (str) – The container to export -- | --The filesystem tar archive (str)
docker.errors.APIError – If the server returns an error.
```
But I don't get a string, instead I get a `HTTPresponse object.` But when I do object.text, .json(), .content it doesn't work.

This is my export function:
```python
from docker import APIClient
...
self.cli = APIClient(base_url=self.base_url, timeout=self.timeout)
....
def archive(self):
        """Archive the container"""
        # Returns the path to the exported file
        try:
            return self.cli.export(container=self.container.get('Id'))
        except errors.APIError as ex:
            raise ex
```

If you need more information, let me know. :)

Gino


Discussion:
Yeah... That's definitely not the most well-tested method in the library. Sorry about that!
As a workaround for the time being, you should be able to use the methods on the HTTPResponse object to read the response data.

```python
from docker import APIClient
...
self.cli = APIClient(base_url=self.base_url, timeout=self.timeout)
....
def archive(self):
        """Archive the container"""
        # Returns the path to the exported file
        try:
            resp = self.cli.export(container=self.container.get('Id'))
            for data in resp.stream():
                yield data
        except errors.APIError as ex:
            raise ex
```
Thank you very response! :) 
So technically I could stream the response to a tarball? 
Yes, just `f.write` the data you receive into a new file.

Resolving patch:
diff --git a/docker/api/container.py b/docker/api/container.py
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -698,7 +698,7 @@ def export(self, container):
             container (str): The container to export
 
         Returns:
-            (str): The filesystem tar archive
+            (generator): The archived filesystem data stream
 
         Raises:
             :py:class:`docker.errors.APIError`
@@ -707,8 +707,7 @@ def export(self, container):
         res = self._get(
             self._url("/containers/{0}/export", container), stream=True
         )
-        self._raise_for_status(res)
-        return res.raw
+        return self._stream_raw_result(res)
 
     @utils.check_resource('container')
     @utils.minimum_version('1.20')
@@ -737,7 +736,7 @@ def get_archive(self, container, path):
         self._raise_for_status(res)
         encoded_stat = res.headers.get('x-docker-container-path-stat')
         return (
-            res.raw,
+            self._stream_raw_result(res),
             utils.decode_json_header(encoded_stat) if encoded_stat else None
         )
 
diff --git a/docker/api/image.py b/docker/api/image.py
--- a/docker/api/image.py
+++ b/docker/api/image.py
@@ -21,8 +21,7 @@ def get_image(self, image):
             image (str): Image name to get
 
         Returns:
-            (urllib3.response.HTTPResponse object): The response from the
-            daemon.
+            (generator): A stream of raw archive data.
 
         Raises:
             :py:class:`docker.errors.APIError`
@@ -30,14 +29,14 @@ def get_image(self, image):
 
         Example:
 
-            >>> image = cli.get_image("fedora:latest")
-            >>> f = open('/tmp/fedora-latest.tar', 'w')
-            >>> f.write(image.data)
+            >>> image = cli.get_image("busybox:latest")
+            >>> f = open('/tmp/busybox-latest.tar', 'w')
+            >>> for chunk in image:
+            >>>   f.write(chunk)
             >>> f.close()
         """
         res = self._get(self._url("/images/{0}/get", image), stream=True)
-        self._raise_for_status(res)
-        return res.raw
+        return self._stream_raw_result(res)
 
     @utils.check_resource('image')
     def history(self, image):
diff --git a/docker/models/images.py b/docker/models/images.py
--- a/docker/models/images.py
+++ b/docker/models/images.py
@@ -61,8 +61,7 @@ def save(self):
         Get a tarball of an image. Similar to the ``docker save`` command.
 
         Returns:
-            (urllib3.response.HTTPResponse object): The response from the
-            daemon.
+            (generator): A stream of raw archive data.
 
         Raises:
             :py:class:`docker.errors.APIError`
@@ -70,11 +69,10 @@ def save(self):
 
         Example:
 
-            >>> image = cli.images.get("fedora:latest")
-            >>> resp = image.save()
-            >>> f = open('/tmp/fedora-latest.tar', 'w')
-            >>> for chunk in resp.stream():
-            >>>     f.write(chunk)
+            >>> image = cli.get_image("busybox:latest")
+            >>> f = open('/tmp/busybox-latest.tar', 'w')
+            >>> for chunk in image:
+            >>>   f.write(chunk)
             >>> f.close()
         """
         return self.client.api.get_image(self.id)


Test patch:
diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py
--- a/tests/integration/api_image_test.py
+++ b/tests/integration/api_image_test.py
@@ -329,7 +329,7 @@ def test_prune_images(self):
         img_id = self.client.inspect_image('hello-world')['Id']
         result = self.client.prune_images()
         assert img_id not in [
-            img.get('Deleted') for img in result['ImagesDeleted']
+            img.get('Deleted') for img in result.get('ImagesDeleted') or []
         ]
         result = self.client.prune_images({'dangling': False})
         assert result['SpaceReclaimed'] > 0
@@ -339,3 +339,25 @@ def test_prune_images(self):
         assert img_id in [
             img.get('Deleted') for img in result['ImagesDeleted']
         ]
+
+
+class SaveLoadImagesTest(BaseAPIIntegrationTest):
+    @requires_api_version('1.23')
+    def test_get_image_load_image(self):
+        with tempfile.TemporaryFile() as f:
+            stream = self.client.get_image(BUSYBOX)
+            for chunk in stream:
+                f.write(chunk)
+
+            f.seek(0)
+            result = self.client.load_image(f.read())
+
+        success = False
+        result_line = 'Loaded image: {}\n'.format(BUSYBOX)
+        for data in result:
+            print(data)
+            if 'stream' in data:
+                if data['stream'] == result_line:
+                    success = True
+                    break
+        assert success is True
diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py
--- a/tests/integration/models_images_test.py
+++ b/tests/integration/models_images_test.py
@@ -1,9 +1,10 @@
 import io
+import tempfile
 
 import docker
 import pytest
 
-from .base import BaseIntegrationTest, TEST_API_VERSION
+from .base import BaseIntegrationTest, BUSYBOX, TEST_API_VERSION
 
 
 class ImageCollectionTest(BaseIntegrationTest):
@@ -76,6 +77,20 @@ def test_load_error(self):
         with pytest.raises(docker.errors.ImageLoadError):
             client.images.load('abc')
 
+    def test_save_and_load(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        image = client.images.get(BUSYBOX)
+        with tempfile.TemporaryFile() as f:
+            stream = image.save()
+            for chunk in stream:
+                f.write(chunk)
+
+            f.seek(0)
+            result = client.images.load(f.read())
+
+        assert len(result) == 1
+        assert result[0].id == image.id
+
 
 class ImageTest(BaseIntegrationTest):