The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/205
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === A common API issue that users report is an easier way to detect when an object just doesn't exist. This is becoming more and more apparent as I update methods in nova-lxd. Add `NotFound` in the case of 404, which inherits from `LXDAPIException`, so it doesn't break backwards compatibility. `Container`, `Image`, and `Profile` now have a `exists` class method, so that one can do `client.containers.exists('an-container')` as a wrapper around this new functionality.
From 1fe83e913c9a054fc5e413be485ab7644a7596b3 Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@canonical.com> Date: Thu, 24 Nov 2016 11:25:15 -0700 Subject: [PATCH 1/2] Add NotFound exception in the case of 404 --- pylxd/client.py | 2 ++ pylxd/exceptions.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pylxd/client.py b/pylxd/client.py index 606f11f..b4470d0 100644 --- a/pylxd/client.py +++ b/pylxd/client.py @@ -64,6 +64,8 @@ def _assert_response( user. """ if response.status_code not in allowed_status_codes: + if response.status_code == 404: + raise exceptions.NotFound(response) raise exceptions.LXDAPIException(response) # In the case of streaming, we can't validate the json the way we diff --git a/pylxd/exceptions.py b/pylxd/exceptions.py index d5b6b5e..a8b27f6 100644 --- a/pylxd/exceptions.py +++ b/pylxd/exceptions.py @@ -26,5 +26,9 @@ def __str__(self): return self.response.content.decode('utf-8') +class NotFound(LXDAPIException): + """An exception raised when an object is not found.""" + + class ClientConnectionFailed(Exception): """An exception raised when the Client connection fails.""" From 4514e8f551d57c63b86662445d8180b0935f3d7d Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@canonical.com> Date: Thu, 24 Nov 2016 11:26:25 -0700 Subject: [PATCH 2/2] Add `exists` to `Container`, `Image`, and `Profile` --- pylxd/models/_model.py | 2 ++ pylxd/models/container.py | 9 +++++++++ pylxd/models/image.py | 16 ++++++++++++++++ pylxd/models/profile.py | 9 +++++++++ pylxd/tests/models/test_container.py | 24 ++++++++++++++++++++++++ pylxd/tests/models/test_image.py | 29 +++++++++++++++++++++++++++++ pylxd/tests/models/test_profile.py | 22 ++++++++++++++++++++++ 7 files changed, 111 insertions(+) diff --git a/pylxd/models/_model.py b/pylxd/models/_model.py index 7307a0d..24900ae 100644 --- a/pylxd/models/_model.py +++ b/pylxd/models/_model.py @@ -15,6 +15,7 @@ import six +from pylxd import exceptions from pylxd.models.operation import Operation @@ -96,6 +97,7 @@ class Model(object): the instance is marked as dirty. `save` will save the changes to the server. """ + NotFound = exceptions.NotFound __slots__ = ['client', '__dirty__'] def __init__(self, client, **kwargs): diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 27552f8..dbedc6c 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -91,6 +91,15 @@ def get(self, filepath): return response.content @classmethod + def exists(cls, client, name): + """Determine whether a container exists.""" + try: + client.containers.get(name) + return True + except cls.NotFound: + return False + + @classmethod def get(cls, client, name): """Get a container by name.""" response = client.api.containers[name].get() diff --git a/pylxd/models/image.py b/pylxd/models/image.py index ac1c4fa..6c54e9f 100644 --- a/pylxd/models/image.py +++ b/pylxd/models/image.py @@ -58,6 +58,22 @@ def api(self): return self.client.api.images[self.fingerprint] @classmethod + def exists(cls, client, fingerprint, alias=False): + """Determine whether an image exists. + + If `alias` is True, look up the image by its alias, + rather than its fingerprint. + """ + try: + if alias: + client.images.get_by_alias(fingerprint) + else: + client.images.get(fingerprint) + return True + except cls.NotFound: + return False + + @classmethod def get(cls, client, fingerprint): """Get an image.""" response = client.api.images[fingerprint].get() diff --git a/pylxd/models/profile.py b/pylxd/models/profile.py index c5d564c..13f01e7 100644 --- a/pylxd/models/profile.py +++ b/pylxd/models/profile.py @@ -23,6 +23,15 @@ class Profile(model.Model): devices = model.Attribute() @classmethod + def exists(cls, client, name): + """Determine whether a profile exists.""" + try: + client.profiles.get(name) + return True + except cls.NotFound: + return False + + @classmethod def get(cls, client, name): """Get a profile.""" response = client.api.profiles[name].get() diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index a32eb84..4fd15ac 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -72,6 +72,30 @@ def test_create(self): self.assertEqual(config['name'], an_new_container.name) + def test_exists(self): + """A container exists.""" + name = 'an-container' + + self.assertTrue(models.Container.exists(self.client, name)) + + def test_not_exists(self): + """A container exists.""" + def not_found(request, context): + context.status_code = 404 + return json.dumps({ + 'type': 'error', + 'error': 'Not found', + 'error_code': 404}) + self.add_rule({ + 'text': not_found, + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA + }) + + name = 'an-missing-container' + + self.assertFalse(models.Container.exists(self.client, name)) + def test_fetch(self): """A sync updates the properties of a container.""" an_container = models.Container( diff --git a/pylxd/tests/models/test_image.py b/pylxd/tests/models/test_image.py index 40cf853..7040a2d 100644 --- a/pylxd/tests/models/test_image.py +++ b/pylxd/tests/models/test_image.py @@ -62,6 +62,35 @@ def test_get_by_alias(self): self.assertEqual(fingerprint, a_image.fingerprint) + def test_exists(self): + """An image is fetched.""" + fingerprint = hashlib.sha256(b'').hexdigest() + + self.assertTrue(models.Image.exists(self.client, fingerprint)) + + def test_exists_by_alias(self): + """An image is fetched.""" + self.assertTrue(models.Image.exists( + self.client, 'an-alias', alias=True)) + + def test_not_exists(self): + """LXDAPIException is raised when the image isn't found.""" + def not_found(request, context): + context.status_code = 404 + return json.dumps({ + 'type': 'error', + 'error': 'Not found', + 'error_code': 404}) + self.add_rule({ + 'text': not_found, + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA + }) + + fingerprint = hashlib.sha256(b'').hexdigest() + + self.assertFalse(models.Image.exists(self.client, fingerprint)) + def test_all(self): """A list of all images is returned.""" images = models.Image.all(self.client) diff --git a/pylxd/tests/models/test_profile.py b/pylxd/tests/models/test_profile.py index 6a357aa..58b01bd 100644 --- a/pylxd/tests/models/test_profile.py +++ b/pylxd/tests/models/test_profile.py @@ -50,6 +50,28 @@ def error(request, context): exceptions.LXDAPIException, models.Profile.get, self.client, 'an-profile') + def test_exists(self): + name = 'an-profile' + + self.assertTrue(models.Profile.exists(self.client, name)) + + def test_not_exists(self): + def not_found(request, context): + context.status_code = 404 + return json.dumps({ + 'type': 'error', + 'error': 'Not found', + 'error_code': 404}) + self.add_rule({ + 'text': not_found, + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', + }) + + name = 'an-profile' + + self.assertFalse(models.Profile.exists(self.client, name)) + def test_all(self): """A list of all profiles is returned.""" profiles = models.Profile.all(self.client)
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel