Repository: docker/docker-py

Issue:
RFE: try to figure out version automatically
### Story

I have a docker daemon with version "1.16", my docker-py client has default version "1.17". As soon as I try to do some api calls, it fails with:

```
APIError: 404 Client Error: Not Found ("client and server don't have same version (client : 1.17, server: 1.16)")
```

Nothing new.
### Solution

I would prefer if docker-py could try to change the version so it would match server's api version, if possible.
### Workflow
1. `d = docker.Client()`
2. `d.images()`
3. Exception is thrown
4. docker-py retries request with different version (the one from exception)
   - if it succeeds, warn user that version should be changed explicitely
   - if it fails, it fails

What do you think? If it makes sense, I can create a PR.



Discussion:
I'd like that. You can actually hit the unversioned API endpoint `/version` to fetch the server's version preemptively. We had a start here: #281 but the PR was abandoned by its author.

okay, will try to create a PR (no promises)

@shin- just checked the original PR. Are you fine with detecting api version in Client's constructor? That's gonna be a change in behavior. Right now there are no requests in constructor. I was actually thinking about hooking api discovery before first request (downside: first request will take longer). 

I think it is not good to automatic check the versions, the right ways, should the client specify what version of API to talk.

@HackToday see discussion on #505, but you will always be able to specify the API version manually no matter what.


Resolving patch:
diff --git a/docker/__init__.py b/docker/__init__.py
--- a/docker/__init__.py
+++ b/docker/__init__.py
@@ -17,4 +17,4 @@
 __version__ = version
 __title__ = 'docker-py'
 
-from .client import Client # flake8: noqa
+from .client import Client, AutoVersionClient # flake8: noqa
diff --git a/docker/client.py b/docker/client.py
--- a/docker/client.py
+++ b/docker/client.py
@@ -40,7 +40,7 @@
 
 
 class Client(requests.Session):
-    def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
+    def __init__(self, base_url=None, version=None,
                  timeout=DEFAULT_TIMEOUT_SECONDS, tls=False):
         super(Client, self).__init__()
         base_url = utils.parse_host(base_url)
@@ -50,15 +50,8 @@ def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
             raise errors.TLSParameterError(
                 'If using TLS, the base_url argument must begin with '
                 '"https://".')
-        if not isinstance(version, six.string_types):
-            raise errors.DockerException(
-                'version parameter must be a string. Found {0}'.format(
-                    type(version).__name__
-                )
-            )
         self.base_url = base_url
         self.timeout = timeout
-        self._version = version
         self._auth_configs = auth.load_config()
 
         # Use SSLAdapter for the ability to specify SSL version
@@ -69,6 +62,29 @@ def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
         else:
             self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
 
+        # version detection needs to be after unix adapter mounting
+        if version is None:
+            self._version = DEFAULT_DOCKER_API_VERSION
+        elif isinstance(version, six.string_types):
+            if version.lower() == "auto":
+                self._version = self.retrieve_server_version()
+            else:
+                self._version = version
+        else:
+            raise errors.DockerException(
+                'Version parameter must be a string or None. Found {0}'.format(
+                    type(version).__name__
+                )
+            )
+
+    def retrieve_server_version(self):
+        response = self.version(api_version=False)
+        try:
+            return response["ApiVersion"]
+        except KeyError:
+            raise ValueError("Invalid response from docker daemon: "
+                             "key \"ApiVersion\" is missing.")
+
     def _set_request_timeout(self, kwargs):
         """Prepare the kwargs for an HTTP request by inserting the timeout
         parameter, if not already present."""
@@ -84,8 +100,11 @@ def _get(self, url, **kwargs):
     def _delete(self, url, **kwargs):
         return self.delete(url, **self._set_request_timeout(kwargs))
 
-    def _url(self, path):
-        return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
+    def _url(self, path, versioned_api=True):
+        if versioned_api:
+            return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
+        else:
+            return '{0}{1}'.format(self.base_url, path)
 
     def _raise_for_status(self, response, explanation=None):
         """Raises stored :class:`APIError`, if one occurred."""
@@ -914,8 +933,9 @@ def top(self, container):
         u = self._url("/containers/{0}/top".format(container))
         return self._result(self._get(u), True)
 
-    def version(self):
-        return self._result(self._get(self._url("/version")), True)
+    def version(self, api_version=True):
+        url = self._url("/version", versioned_api=api_version)
+        return self._result(self._get(url), json=True)
 
     def unpause(self, container):
         if isinstance(container, dict):
@@ -934,3 +954,9 @@ def wait(self, container, timeout=None):
         if 'StatusCode' in json_:
             return json_['StatusCode']
         return -1
+
+
+class AutoVersionClient(Client):
+    def __init__(self, *args, **kwargs):
+        kwargs['version'] = 'auto'
+        super(AutoVersionClient, self).__init__(*args, **kwargs)


Test patch:
diff --git a/tests/fake_api.py b/tests/fake_api.py
--- a/tests/fake_api.py
+++ b/tests/fake_api.py
@@ -30,6 +30,17 @@
 # for clarity and readability
 
 
+def get_fake_raw_version():
+    status_code = 200
+    response = {
+        "ApiVersion": "1.17",
+        "GitCommit": "fake-commit",
+        "GoVersion": "go1.3.3",
+        "Version": "1.5.0"
+    }
+    return status_code, response
+
+
 def get_fake_version():
     status_code = 200
     response = {'GoVersion': '1', 'Version': '1.1.1',
@@ -347,6 +358,8 @@ def get_fake_stats():
 # Maps real api url to fake response callback
 prefix = 'http+unix://var/run/docker.sock'
 fake_responses = {
+    '{0}/version'.format(prefix):
+    get_fake_raw_version,
     '{1}/{0}/version'.format(CURRENT_VERSION, prefix):
     get_fake_version,
     '{1}/{0}/info'.format(CURRENT_VERSION, prefix):
diff --git a/tests/test.py b/tests/test.py
--- a/tests/test.py
+++ b/tests/test.py
@@ -130,7 +130,7 @@ def test_ctor(self):
             if not six.PY3:
                 self.assertEqual(
                     str(e),
-                    'version parameter must be a string. Found float'
+                    'Version parameter must be a string or None. Found float'
                 )
 
     #########################
@@ -147,6 +147,19 @@ def test_version(self):
             timeout=docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
+    def test_retrieve_server_version(self):
+        client = docker.Client(version="auto")
+        self.assertTrue(isinstance(client._version, six.string_types))
+        self.assertFalse(client._version == "auto")
+
+    def test_auto_retrieve_server_version(self):
+        try:
+            version = self.client.retrieve_server_version()
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+        else:
+            self.assertTrue(isinstance(version, six.string_types))
+
     def test_info(self):
         try:
             self.client.info()