Repository: docker/docker-py

Issue:
push fails when string includes a tag
I'm trying to push a tagged image to my private repository and I'm running into an error when the tag is part of the string passed to the `repository` argument.

if I were doing this in the command line I would run

```
docker push <PRIVATE_REGISTRY_HOST>:5000/hello_92e2b00:accepted
```

This works as I expect, but using the `push` method in python does not work as expected and I don't see any way to push any tag through the python client. I would expect the behavior to be similar to the `pull` method which has a tag optional argument, and parses the repository tag when passed in as part of the `repository` argument.

I see the following when trying to push with the library

```
>>> c = docker.Client(os.environ['DOCKER_HOST'])
>>> c.push("<PRIVATE_REGISTRY_HOST>:5000/hello_92e2b00:accepted")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/site-packages/docker/client.py", line 674, in push
    or self._result(response)
  File "/usr/local/lib/python2.7/site-packages/docker/client.py", line 119, in _result
    self._raise_for_status(response)
  File "/usr/local/lib/python2.7/site-packages/docker/client.py", line 115, in _raise_for_status
    raise APIError(e, response, explanation=explanation)
docker.client.APIError: 500 Server Error: Internal Server Error ("Invalid repository name (hello_92e2b00:accepted), only [a-z0-9-_.] are allowed")
```

I'd be happy to take a crack at implementing the optional tag argument and handling a tag passed in the `repository` string if this is the intended behavior.

I hope I'm not missing something stupid. :)

Thanks!



Discussion:
Can you do `pip freeze | grep docker` and copy the output?

`docker-py==0.3.1`

So first off, you should upgrade to 0.4.0 or at least 0.3.2 =)

But I checked and you're correct, docker-py doesn't support pushing a single tag at the moment (it was a recent addition to docker itself) -- it should be pretty easy to implement though, if you want to try your hand at a patch.

Thank you for the report!

Sorry I disappeared, went camping then got sick. I'll try my hand at a patch this evening. Do you have documentation of the process you like to go through? This will be my first OSS contribution.

We're pretty lax on this project in particular. Pretty much:
- Fork the repo and submit a PR when it's ready (or before if you need help finishing it)
- Make sure the tests pass (just run `tox` in the project's root folder).
- There are flake8 tests, you might find them a bit annoying but they ensure we stay consistent on a project like that with tens of contributors.

Also, you should look into the `Client.pull` implementation which already does that kind of parsing, make sure you don't rewrite code that already exists. =)

Thanks!


Resolving patch:
diff --git a/docker/client.py b/docker/client.py
--- a/docker/client.py
+++ b/docker/client.py
@@ -747,9 +747,14 @@ def pull(self, repository, tag=None, stream=False):
         else:
             return self._result(response)
 
-    def push(self, repository, stream=False):
+    def push(self, repository, tag=None, stream=False):
+        if not tag:
+            repository, tag = utils.parse_repository_tag(repository)
         registry, repo_name = auth.resolve_repository_name(repository)
         u = self._url("/images/{0}/push".format(repository))
+        params = {
+            'tag': tag
+        }
         headers = {}
 
         if utils.compare_version('1.5', self._version) >= 0:
@@ -765,9 +770,10 @@ def push(self, repository, stream=False):
             if authcfg:
                 headers['X-Registry-Auth'] = auth.encode_header(authcfg)
 
-            response = self._post_json(u, None, headers=headers, stream=stream)
+            response = self._post_json(u, None, headers=headers,
+                                       stream=stream, params=params)
         else:
-            response = self._post_json(u, None, stream=stream)
+            response = self._post_json(u, None, stream=stream, params=params)
 
         return stream and self._stream_helper(response) \
             or self._result(response)


Test patch:
diff --git a/tests/test.py b/tests/test.py
--- a/tests/test.py
+++ b/tests/test.py
@@ -1306,6 +1306,28 @@ def test_push_image(self):
 
         fake_request.assert_called_with(
             url_prefix + 'images/test_image/push',
+            params={
+                'tag': None
+            },
+            data='{}',
+            headers={'Content-Type': 'application/json'},
+            stream=False,
+            timeout=docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_push_image_with_tag(self):
+        try:
+            with mock.patch('docker.auth.auth.resolve_authconfig',
+                            fake_resolve_authconfig):
+                self.client.push(fake_api.FAKE_IMAGE_NAME, tag=fake_api.FAKE_TAG_NAME)
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        fake_request.assert_called_with(
+            url_prefix + 'images/test_image/push',
+            params={
+                'tag': fake_api.FAKE_TAG_NAME,
+            },
             data='{}',
             headers={'Content-Type': 'application/json'},
             stream=False,
@@ -1322,6 +1344,9 @@ def test_push_image_stream(self):
 
         fake_request.assert_called_with(
             url_prefix + 'images/test_image/push',
+            params={
+                'tag': None
+            },
             data='{}',
             headers={'Content-Type': 'application/json'},
             stream=True,