Merge pull request #191 from rustyrobot/master

Create scope for docker client specific errors
This commit is contained in:
Joffrey F 2014-04-08 20:21:49 +02:00
commit 85ea42ecb3
5 changed files with 78 additions and 48 deletions

View File

@ -15,4 +15,4 @@
__title__ = 'docker-py' __title__ = 'docker-py'
__version__ = '0.3.0' __version__ = '0.3.0'
from .client import Client, APIError # flake8: noqa from .client import Client # flake8: noqa

View File

@ -20,6 +20,7 @@ import os
import six import six
from ..utils import utils from ..utils import utils
from docker import errors
INDEX_URL = 'https://index.docker.io/v1/' INDEX_URL = 'https://index.docker.io/v1/'
DOCKER_CONFIG_FILENAME = '.dockercfg' DOCKER_CONFIG_FILENAME = '.dockercfg'
@ -45,18 +46,19 @@ def expand_registry_url(hostname):
def resolve_repository_name(repo_name): def resolve_repository_name(repo_name):
if '://' in repo_name: if '://' in repo_name:
raise ValueError('Repository name cannot contain a ' raise errors.InvalidRepository(
'scheme ({0})'.format(repo_name)) 'Repository name cannot contain a scheme ({0})'.format(repo_name))
parts = repo_name.split('/', 1) parts = repo_name.split('/', 1)
if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost': if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost':
# This is a docker index repo (ex: foo/bar or ubuntu) # This is a docker index repo (ex: foo/bar or ubuntu)
return INDEX_URL, repo_name return INDEX_URL, repo_name
if len(parts) < 2: if len(parts) < 2:
raise ValueError('Invalid repository name ({0})'.format(repo_name)) raise errors.InvalidRepository(
'Invalid repository name ({0})'.format(repo_name))
if 'index.docker.io' in parts[0]: if 'index.docker.io' in parts[0]:
raise ValueError('Invalid repository name,' raise errors.InvalidRepository(
'try "{0}" instead'.format(parts[1])) 'Invalid repository name, try "{0}" instead'.format(parts[1]))
return expand_registry_url(parts[0]), parts[1] return expand_registry_url(parts[0]), parts[1]
@ -147,7 +149,8 @@ def load_config(root=None):
data.append(line.strip().split(' = ')[1]) data.append(line.strip().split(' = ')[1])
if len(data) < 2: if len(data) < 2:
# Not enough data # Not enough data
raise Exception('Invalid or empty configuration file!') raise errors.InvalidConfigFile(
'Invalid or empty configuration file!')
username, password = decode_auth(data[0]) username, password = decode_auth(data[0])
conf[INDEX_URL] = { conf[INDEX_URL] = {

View File

@ -24,6 +24,7 @@ import six
from .auth import auth from .auth import auth
from .unixconn import unixconn from .unixconn import unixconn
from .utils import utils from .utils import utils
from docker import errors
if not six.PY3: if not six.PY3:
import websocket import websocket
@ -33,41 +34,6 @@ DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8 STREAM_HEADER_SIZE_BYTES = 8
class APIError(requests.exceptions.HTTPError):
def __init__(self, message, response, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 doesn't
super(APIError, self).__init__(message)
self.response = response
self.explanation = explanation
if self.explanation is None and response.content:
self.explanation = response.content.strip()
def __str__(self):
message = super(APIError, self).__str__()
if self.is_client_error():
message = '%s Client Error: %s' % (
self.response.status_code, self.response.reason)
elif self.is_server_error():
message = '%s Server Error: %s' % (
self.response.status_code, self.response.reason)
if self.explanation:
message = '%s ("%s")' % (message, self.explanation)
return message
def is_client_error(self):
return 400 <= self.response.status_code < 500
def is_server_error(self):
return 500 <= self.response.status_code < 600
class Client(requests.Session): class Client(requests.Session):
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
timeout=DEFAULT_TIMEOUT_SECONDS): timeout=DEFAULT_TIMEOUT_SECONDS):
@ -112,7 +78,7 @@ class Client(requests.Session):
try: try:
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise APIError(e, response, explanation=explanation) raise errors.APIError(e, response, explanation=explanation)
def _result(self, response, json=False, binary=False): def _result(self, response, json=False, binary=False):
assert not (json and binary) assert not (json and binary)
@ -341,7 +307,7 @@ class Client(requests.Session):
nocache=False, rm=False, stream=False, timeout=None): nocache=False, rm=False, stream=False, timeout=None):
remote = context = headers = None remote = context = headers = None
if path is None and fileobj is None: if path is None and fileobj is None:
raise Exception("Either path or fileobj needs to be provided.") raise TypeError("Either path or fileobj needs to be provided.")
if fileobj is not None: if fileobj is not None:
context = utils.mkbuildcontext(fileobj) context = utils.mkbuildcontext(fileobj)

61
docker/errors.py Normal file
View File

@ -0,0 +1,61 @@
# Copyright 2014 dotCloud inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
class APIError(requests.exceptions.HTTPError):
def __init__(self, message, response, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 doesn't
super(APIError, self).__init__(message)
self.response = response
self.explanation = explanation
if self.explanation is None and response.content:
self.explanation = response.content.strip()
def __str__(self):
message = super(APIError, self).__str__()
if self.is_client_error():
message = '%s Client Error: %s' % (
self.response.status_code, self.response.reason)
elif self.is_server_error():
message = '%s Server Error: %s' % (
self.response.status_code, self.response.reason)
if self.explanation:
message = '%s ("%s")' % (message, self.explanation)
return message
def is_client_error(self):
return 400 <= self.response.status_code < 500
def is_server_error(self):
return 500 <= self.response.status_code < 600
class DockerException(Exception):
pass
class InvalidRepository(DockerException):
pass
class InvalidConfigFile(DockerException):
pass

View File

@ -41,13 +41,13 @@ class BaseTestCase(unittest.TestCase):
for img in self.tmp_imgs: for img in self.tmp_imgs:
try: try:
self.client.remove_image(img) self.client.remove_image(img)
except docker.APIError: except docker.errors.APIError:
pass pass
for container in self.tmp_containers: for container in self.tmp_containers:
try: try:
self.client.stop(container, timeout=1) self.client.stop(container, timeout=1)
self.client.remove_container(container) self.client.remove_container(container)
except docker.APIError: except docker.errors.APIError:
pass pass
######################### #########################
@ -641,7 +641,7 @@ class TestPull(BaseTestCase):
try: try:
self.client.remove_image('joffrey/test001') self.client.remove_image('joffrey/test001')
self.client.remove_image('376968a23351') self.client.remove_image('376968a23351')
except docker.APIError: except docker.errors.APIError:
pass pass
info = self.client.info() info = self.client.info()
self.assertIn('Images', info) self.assertIn('Images', info)
@ -660,7 +660,7 @@ class TestPullStream(BaseTestCase):
try: try:
self.client.remove_image('joffrey/test001') self.client.remove_image('joffrey/test001')
self.client.remove_image('376968a23351') self.client.remove_image('376968a23351')
except docker.APIError: except docker.errors.APIError:
pass pass
info = self.client.info() info = self.client.info()
self.assertIn('Images', info) self.assertIn('Images', info)